[
  {
    "path": ".editorconfig",
    "content": "# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\nindent_style = space\nindent_size = 4"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/bug-report.yml",
    "content": "title: \"[Bug report] \"\nlabels: [\"Thank you for reporting a bug!\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Before reporting a bug, please explore the existing topics in our [GitHub Discussions](https://github.com/GetPublii/Publii/discussions). If you can't find a discussion that addresses your concern, feel free to initiate a new discussion.\n        If you encounter issues with premium products purchased on the [Publii Marketplace](https://marketplace.getpublii.com/), please use the dedicated support platform provided on the marketplace for assistance.\n        \n        ---\n        \n        Thank you for reporting a bug. We need some information to assist us in promptly investigating and resolving the issue.\n  - type: input\n    id: os\n    attributes:\n      label: Operating system\n      description: \"Which operating system do you use to run the Publii app? Please provide the version as well.\"\n      placeholder: \"macOS Monterey 12.2\"\n    validations:\n      required: true  \n  - type: input\n    id: publii\n    attributes:\n      label: Publii version\n      description: \"Which Publii version do you use?\"\n      placeholder: \"0.38.3 (build 14239)\"\n    validations:\n      required: true\n  - type: dropdown\n    id: issue_type\n    attributes:\n      label: Issue type\n      description: \"What does the problem relate to?\"\n      options:\n        - Application\n        - Free theme\n        - Free plugin\n        - Something else\n    validations:\n      required: true\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Bug description\n      description: What happened?\n    validations:\n      required: true\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to reproduce\n      description: Which steps do we need to take to reproduce this error?\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: If applicable, provide relevant log output that can be generated with the Publii \"Log Viewer\" tool.\n      render: shell\n\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: GetPublii\nopen_collective: publii\ncustom: https://getpublii.com/donate/\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: Bug\ndescription: File a bug report\ntitle: \"[Bug]: \"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Before opening a bug report, please search for the behavior in the existing issues. If you can't find what you're looking for, then please open a new issue. \n        For questions about Publii functionality, **themes**, **plugins**, or other general queries, please contact our development team via the [community forum](https://forum.getpublii.com/).\n        \n        ---\n        \n        Thank you for taking the time to file a bug report. In order to help us investigate and fix the issue as quickly as possible, we need some information.\n  - type: input\n    id: os\n    attributes:\n      label: Operating system\n      description: \"Which operating system do you use to run Publii app? Please provide the version as well.\"\n      placeholder: \"macOS Monterey 12.2\"\n    validations:\n      required: true  \n  - type: input\n    id: publii\n    attributes:\n      label: Publii version\n      description: \"Which Publii version do you use?\"\n      placeholder: \"0.38.3 (build 14239)\"\n    validations:\n      required: true\n  - type: dropdown\n    id: editor\n    attributes:\n      label: Post editor\n      description: If you're reporting a bug with a post editor, please specify which one.\n      options:\n        - WYSIWYG editor\n        - Block editor\n        - Markdown editor\n    validations:\n      required: false\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Bug description\n      description: What happened?\n    validations:\n      required: true\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to reproduce\n      description: Which steps do we need to take to reproduce this error?\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: If applicable, provide relevant log output that can be generated with the Publii \"Log Viewer\" tool.\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Check the docs\n    url: https://getpublii.com/docs/\n    about: 'This repository is for reporting bugs and proposing new features. If you need help getting started with Publii, check out the docs!'\n  - name: Visit the community forum\n    url: https://forum.getpublii.com/\n    about: 'For questions about Publii functionality, themes, plugins, or other general queries, please contact our development team via the community forum.'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: Feature Request\ndescription: Propose a new feature for Publii\ntitle: \"[Feature Request]: \"\nlabels: [feature request]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for proposing a feature for Publii!\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Feature Description\n      description: How should this feature look like?\n    validations:\n      required: true\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\n.idea\n.DS_Store\n.coveralls.yml\nnpm-debug.log\n/node_modules/\n/app/node_modules/\n/coverage\n/app/dist/css\n/app/dist/vendor\n/app/dist/*.js\n/app/dist/*.txt\n/app/dist/*.js.map\n/app/dist/*.png\n/app/dist/*.svg\n/app/dist/*.node\n!/app/dist/index.html\n/dist\n/cache/\n/StaticBlog-darwin-x64/\n/StaticBlog-win32-x64/\n/Publii-darwin-x64/\n/Publii-win32-x64/\napp/Publii-win32-x64/\n/Publii-win32-ia32/\n/Publii-linux-x64/\n/Publii-win32-x64-backup/\napp/licenses.txt\napp/Publii-darwin-x64/\n/Publii-darwin-x64/\n/dmg-release/\n/create-dmg/\n\nlicenses.txt\nrelease/RELEASES\n*.nupkg\nrelease/Setup.exe\n\nPublii.dmg\nrelease/PubliiSetup.exe\n*.msi\nrelease/Setup.wixobj\n*.wixpdb\nrelease/Setup.wxs\n.vscode\n"
  },
  {
    "path": ".nvmrc",
    "content": "v22.18.0\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# Publii - Static CMS for privacy-focused, SEO-optimized websites.\n\n[![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://github.com/GetPublii/Publii/blob/master/LICENSE)\n [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/GetPublii/Publii/graphs/commit-activity) [![OpenCollective](https://opencollective.com/publii/backers/badge.svg)](https://opencollective.com/publii/) ![Open Source Love svg1](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)\n\n\n\n\n[Publii](https://getpublii.com/) is a desktop-based CMS for Windows, Mac and Linux that makes creating static websites fast\nand hassle-free, even for beginners.\n\n**Current version: 0.47.5 (build 17411)**\n\n## Why Publii?\nUnlike static-site generators that are often unwieldy and difficult to use, Publii provides an\neasy-to-understand UI much like server-based CMSs such as WordPress or Joomla!, where users\ncan create posts and other site content, and style their site using a variety of built-in themes and\noptions. Users can enjoy the benefits of a super-fast and secure static website, with all the\nconvenience that a CMS provides.\n\nWhat makes Publii even more unique is that the app runs locally on your desktop rather\nthan on the site&#39;s server. Available for Windows, Mac, Linux once the app has been installed\nyou can create a site in minutes, even without internet access; since Publii is a desktop app you\ncan create, update and modify your site offline, then upload the site changes to your server at\nthe click of a button. Publii supports multiple upload options, including standard HTTP/HTTPS\nservers, Netlify, Amazon S3, GitHub Pages and Google Cloud or SFTP.\n\n\n![Publii Open Source Static CMS](https://getpublii.com/assets/images/publii-cms.webp)\n\n## Download\n\nPublii is available for Mac, Windows, and Linux and can be downloaded from our website:\n\n[Download Publii (.exe, .dmg, .deb, .rpm, .AppImage)](https://getpublii.com/download/)\n\n## Developing\n\nIf you want to build newest version of Publii or contribute to the Publii code, please read about [app build process](https://github.com/GetPublii/Publii/wiki/App-build-process). \n\n## Getting Started\nYou can learn more about getting started in our [User documentation](https://getpublii.com/docs/) or [Developer documentation](https://getpublii.com/dev/).\nIf you have any questions or suggestions, or just need some help with using Publii, you can\nvisit our [Community Hub](https://github.com/GetPublii/Publii/discussions) or follow us on [Twitter](https://twitter.com/GetPublii)\n\n### Learn More\n\n* [User docs](https://getpublii.com/docs/)\n* [Developer docs](https://getpublii.com/dev/)\n* [Wiki](https://github.com/GetPublii/Publii/wiki/)\n* [Issues](https://github.com/GetPublii/Publii/issues/)\n* [Community forum](https://forum.getpublii.com/)\n\n## Contributors\n\nThis project exists thanks to all the people who contribute.\n<a href=\"https://github.com/GetPublii/Publii/graphs/contributors\"><img src=\"https://opencollective.com/Publii/contributors.svg?width=890&button=false\" /></a>\n\n\n## Backers\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Publii#backer)]\n\n<a href=\"https://opencollective.com/Publii#backers\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/backers.svg?width=890\"></a>\n\n\n## Sponsors\n\nSupport this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Publii#sponsor)]\n\n<a href=\"https://opencollective.com/Publii/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/1/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/1/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/2/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/2/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/3/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/3/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/4/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/4/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/5/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/5/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/6/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/6/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/7/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/7/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/8/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/8/avatar.svg\"></a>\n<a href=\"https://opencollective.com/Publii/sponsor/9/website\" target=\"_blank\"><img src=\"https://opencollective.com/Publii/sponsor/9/avatar.svg\"></a>\n\n\n\n## License\nCopyright (c) 2026 TidyCustoms. General Public License v3.0, read [LICENSE](https://getpublii.com/license.html) for details.\n"
  },
  {
    "path": "app/back-end/app-preload.js",
    "content": "const { contextBridge, ipcRenderer, webUtils } = require('electron');\n\ncontextBridge.exposeInMainWorld('mainProcessAPI', {\n    shellShowItemInFolder: (url) => ipcRenderer.invoke('publii-shell-show-item-in-folder', url),\n    shellOpenPath: (filePath) => ipcRenderer.invoke('publii-shell-open-path', filePath),\n    shellOpenExternal: (url) => ipcRenderer.invoke('publii-shell-open-external', url),\n    existsSync: (pathToCheck) => ipcRenderer.invoke('publii-native-exists-sync', pathToCheck),\n    normalizePath: (pathToNormalize) => ipcRenderer.invoke('publii-native-normalize-path', pathToNormalize),\n    createMD5: (value) => ipcRenderer.invoke('publii-native-md5', value),\n    getPathForFile: (value) => webUtils.getPathForFile(value),\n    getEnv: () => ({\n        name: process.env.NODE_ENV,\n        nodeVersion: process.versions.node,\n        chromeVersion: process.versions.chrome,\n        electronVersion: process.versions.electron,\n        platformName: process.platform\n    }),\n    send: (channel, ...data) => {\n        const validChannels = [\n            'app-save-color-theme',\n            'app-license-load',\n            'app-config-save',\n            'app-backup-set-location',\n            'app-theme-upload',\n            'app-author-save',\n            'app-author-cancel',\n            'app-authors-load',\n            'app-author-delete',\n            'app-backups-list-load',\n            'app-backup-remove',\n            'app-backup-rename',\n            'app-backup-create',\n            'app-backup-restore',\n            'app-site-reload',\n            'app-site-css-load',\n            'app-site-css-save',\n            'app-site-config-save',\n            'app-site-check-website-to-restore',\n            'app-site-check-website-catalog-availability',\n            'app-site-restore-from-backup',\n            'app-site-remove-temporary-backup-files',\n            'app-site-restore-from-backup',\n            'app-file-manager-list',\n            'app-file-manager-delete',\n            'app-file-manager-create',\n            'app-file-manager-upload',\n            'app-log-files-load',\n            'app-log-file-load',\n            'app-menu-update',\n            'publii-set-spellchecker-language',\n            'app-post-load',\n            'app-post-save',\n            'app-post-cancel',\n            'app-page-load',\n            'app-page-save',\n            'app-page-cancel',\n            'app-pages-hierarchy-load',\n            'app-pages-hierarchy-save',\n            'app-image-upload',\n            'app-image-upload-remove',\n            'app-post-delete',\n            'app-post-duplicate',\n            'app-post-status-change',\n            'app-page-delete',\n            'app-page-duplicate',\n            'app-page-status-change',\n            'app-site-regenerate-thumbnails',\n            'app-site-abort-regenerate-thumbnails',\n            'app-preview-render',\n            'app-deploy-test',\n            'app-site-regenerate-thumbnails-required',\n            'app-site-switch',\n            'app-site-create',\n            'app-site-clone',\n            'app-site-delete',\n            'app-license-accept',\n            'app-deploy-render-abort',\n            'app-deploy-abort',\n            'app-deploy-continue',\n            'app-deploy-render',\n            'app-deploy-upload',\n            'app-sync-is-done',\n            'app-tag-save',\n            'app-tag-cancel',\n            'app-tags-load',\n            'app-tags-status-change',\n            'app-tag-delete',\n            'app-site-theme-config-save',\n            'app-theme-delete',\n            'app-notifications-retrieve',\n            'app-wxr-check',\n            'app-wxr-import',\n            'app-language-upload',\n            'app-language-delete',\n            'app-plugin-upload',\n            'app-plugin-delete',\n            'app-site-get-plugins-state',\n            'app-site-plugin-activate',\n            'app-site-plugin-deactivate',\n            'app-site-get-plugin-config',\n            'app-site-save-plugin-config',\n            'app-close',\n            'app-set-ui-zoom-level',\n            'app-set-notifications-center-state',\n            'app-get-notifications-file',\n            'app-pages-hierarchy-update',\n            'app-content-fields-update'\n        ];\n\n        if (validChannels.includes(channel)) {\n            ipcRenderer.send(channel, ...data);\n        } else {\n            console.info('Event: ', channel, ' is not supported in send');\n        }\n    },\n    receive: (channel, func) => {\n        const validChannels = [\n            'app-data-loaded',\n            'app-deploy-render-error',\n            'app-theme-mode:changed',\n            'app-files-selected',\n            'app-site-regenerate-thumbnails-progress',\n            'app-rendering-progress',\n            'app-deploy-rendered',\n            'app-connection-in-progress',\n            'app-connection-error',\n            'app-connection-success',\n            'app-uploading-progress',\n            'app-wxr-import-progress',\n            'app-show-search-form',\n            'block-editor-undo',\n            'block-editor-redo',\n            'no-remote-files'\n        ];\n\n        if (validChannels.includes(channel)) {\n            // Strip event as it includes `sender` \n            ipcRenderer.on(channel, (event, ...args) => func(...args));\n        } else {\n            console.info('Event: ', channel, ' is not supported in receive');\n        }\n    },\n    receiveOnce: (channel, func) => {\n        const validChannels = [\n            'app-license-loaded',\n            'app-config-saved',\n            'app-file-selected',\n            'app-theme-uploaded',\n            'app-author-saved',\n            'app-authors-loaded',\n            'app-author-deleted',\n            'app-backups-list-loaded',\n            'app-backup-removed',\n            'app-backup-renamed',\n            'app-backup-created',\n            'app-backup-restored',\n            'app-site-reloaded',\n            'app-site-css-loaded',\n            'app-site-css-saved',\n            'app-site-config-saved',\n            'app-site-backup-checked',\n            'app-site-restored-from-backup',\n            'app-site-website-catalog-availability-checked',\n            'app-site-restored-from-backup',\n            'app-file-manager-listed',\n            'app-file-manager-deleted',\n            'app-file-manager-created',\n            'app-file-manager-uploaded',\n            'app-log-files-loaded',\n            'app-log-file-loaded',\n            'app-post-loaded',\n            'app-post-saved',\n            'app-post-deleted',\n            'app-post-duplicated',\n            'app-post-status-changed',\n            'app-page-loaded',\n            'app-page-saved',\n            'app-page-deleted',\n            'app-page-duplicated',\n            'app-page-status-changed',\n            'app-pages-hierarchy-loaded',\n            'app-site-regenerate-thumbnails-error',\n            'app-site-regenerate-thumbnails-success',\n            'app-preview-rendered',\n            'app-preview-render-error',\n            'app-deploy-test-success',\n            'app-deploy-test-write-error',\n            'app-deploy-test-error',\n            'app-site-regenerate-thumbnails-required-status',\n            'app-site-switched',\n            'app-site-creation-error',\n            'app-site-creation-duplicate',\n            'app-site-creation-db-error',\n            'app-site-created',\n            'app-site-cloned',\n            'app-site-deleted',\n            'app-license-accepted',\n            'app-deploy-aborted',\n            'app-deploy-uploaded',\n            'app-sync-is-done-saved',\n            'app-tag-saved',\n            'app-tags-loaded',\n            'app-tags-status-changed',\n            'app-tag-deleted',\n            'app-site-theme-config-saved',\n            'app-theme-deleted',\n            'app-notifications-retrieved',\n            'app-wxr-imported',\n            'app-wxr-checked',\n            'app-directory-selected',\n            'app-image-uploaded',\n            'app-files-selected',\n            'app-language-uploaded',\n            'app-language-deleted',\n            'app-plugin-uploaded',\n            'app-plugin-deleted',\n            'app-site-plugin-config-saved',\n            'app-site-plugins-state-loaded',\n            'app-site-plugin-activated',\n            'app-site-plugin-deactivated',\n            'app-site-get-plugin-config-retrieved',\n            'app-content-fields-updated'\n        ];\n\n        if (validChannels.includes(channel)) {\n            // Strip event as it includes `sender` \n            ipcRenderer.once(channel, (event, ...args) => func(...args));\n        } else {\n            console.info('Event: ', channel, ' is not supported in receiveOnce');\n        }\n    },\n    invoke: (command, ...data) => {\n        const validCommands = [\n            'app-theme-mode:set-light',\n            'app-theme-mode:set-dark',\n            'app-theme-mode:get-theme',\n            'app-theme-mode:set-system',\n            'app-credits-list:get-app-path',\n            'app-main-process-is-osx11-or-higher',\n            'app-main-process-select-file',\n            'app-main-process-create-slug',\n            'app-main-process-select-files',\n            'publii-get-spellchecker-language',\n            'app-main-get-spellchecker-languages',\n            'app-main-set-spellchecker-language-for-webview',\n            'app-main-process-load-password',\n            'app-window:minimize',\n            'app-window:maximize',\n            'app-window:unmaximize',\n            'app-window:close',\n            'app-main-process-select-directory',\n            'app-main-webview-search-find-in-page',\n            'app-main-webview-search-stop-find-in-page', \n            'app-main-load-language',\n            'app-plugins-api:save-config-file',\n            'app-plugins-api:save-language-file',\n            'app-plugins-api:read-config-file',\n            'app-plugins-api:read-language-file',\n            'app-plugins-api:read-theme-file',\n            'app-plugins-api:delete-config-file',\n            'app-plugins-api:delete-language-file'\n        ];\n\n        if (validCommands.includes(command)) {\n            return ipcRenderer.invoke(command, ...data);\n        } else {\n            console.info('Event: ', channel, ' is not supported in invoke');\n        }\n\n        return false;\n    },\n    stopReceive: (channel, func) => {\n        const validChannels = [\n            'app-preview-render-error',\n            'app-connection-error',\n            'app-wxr-imported',\n            'app-wxr-import-progress'\n        ];\n\n        if (validChannels.includes(channel)) {\n            ipcRenderer.removeListener(channel, func);\n        } else {\n            console.info('Event: ', channel, ' is not supported in stopReceive');\n        }\n    },\n    stopReceiveAll: (channel) => {\n        const validChannels = [\n            'app-license-accepted',\n            'app-files-selected',\n            'app-site-regenerate-thumbnails-error',\n            'app-site-regenerate-thumbnails-progress',\n            'app-site-regenerate-thumbnails-success',\n            'app-preview-render-error',\n            'app-rendering-progress',\n            'app-site-created',\n            'app-site-creation-duplicate',\n            'app-site-creation-db-error',\n            'app-site-creation-error',\n            'app-connection-error',\n            'app-show-search-form',\n            'block-editor-undo',\n            'block-editor-redo'\n        ];\n\n        if (validChannels.includes(channel)) {\n            ipcRenderer.removeAllListeners(channel);\n        } else {\n            console.info('Event: ', channel, ' is not supported in stopReceiveAll');\n        }\n    }\n});\n"
  },
  {
    "path": "app/back-end/app.js",
    "content": "/*\n * Main Application class\n */\n\n// Necessary packages\nconst fs = require('fs-extra');\nconst os = require('os');\nconst path = require('path');\nconst Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3');\nconst compare = require('node-version-compare');\nconst normalizePath = require('normalize-path');\nconst url = require('url');\n// Electron classes\nconst { screen, shell, nativeTheme, Menu, dialog, BrowserWindow } = require('electron');\n// Collection classes\nconst Posts = require('./posts.js');\nconst Pages = require('./pages.js');\nconst Tags = require('./tags.js');\nconst Authors = require('./authors.js');\nconst Themes = require('./themes.js');\nconst Languages = require('./languages.js');\nconst Plugins = require('./plugins.js');\n// Helper classes\nconst DBUtils = require('./helpers/db.utils.js');\nconst Site = require('./site.js');\nconst Utils = require('./helpers/utils.js');\nconst FileHelper = require('./helpers/file.js');\n// List of the Event classes\nconst EventClasses = require('./events/_modules.js');\n// Migration classes\nconst SiteConfigMigrator = require('./migrators/site-config.js');\n// Default config\nconst defaultAstAppConfig = require('./../config/AST.app.config');\nconst defaultAstCurrentSiteConfig = require('./../config/AST.currentSite.config');\n// Plugins packages\nconst PluginsAPI = require('./modules/plugins/plugins-api.js')\n\n/**\n * Main app class\n */\nclass App {\n    /**\n     * Constructor\n     *\n     * @param startupSettings\n     */\n    constructor(startupSettings) {\n        this.mainWindow = startupSettings.mainWindow;\n        this.app = startupSettings.app;\n        this.basedir = startupSettings.basedir;\n        this.appDir = path.join(this.app.getPath('documents'), 'Publii');\n        this.app.appDir = this.appDir;\n        this.initPath = path.join(this.appDir, 'config', 'window-config.json');\n        this.appConfigPath = path.join(this.appDir, 'config', 'app-config.json');\n        this.tinymceOverridedConfigPath = path.join(this.appDir, 'config', 'tinymce.override.json');\n        this.versionData = JSON.parse(FileHelper.readFileSync(__dirname + '/builddata.json', 'utf8'));\n        this.versionData.os = os.platform() === 'darwin' ? 'mac' : os.platform() === 'linux' ? 'linux' : 'win';\n        this.windowBounds = null;\n        this.appConfig = null;\n        this.tinymceOverridedConfig = {};\n        this.sites = {};\n        this.sitesDir = null;\n        this.app.sitesDir = null;\n        this.db = false;\n        this.pluginsAPI = new PluginsAPI();\n\n        /*\n         * Run the app\n         */\n        this.checkDirs();\n        let loadConfigResult = this.loadConfig();\n\n        if (!loadConfigResult) {\n            this.app.quit();\n            return;\n        }\n\n        this.loadAdditionalConfig();\n        this.checkThemes();\n\n        let loadingSitesResult = this.loadSites();\n\n        if (!loadingSitesResult) {\n            this.app.quit();\n            return;\n        }\n\n        this.loadThemes();\n        this.loadLanguages();\n        this.loadPlugins();\n        this.initWindow();\n        this.initWindowEvents();\n    }\n\n    /**\n     * Create the application dir if not exists\n     */\n    checkDirs() {\n        if (!fs.existsSync(this.appDir)) {\n            fs.mkdirSync(this.appDir);\n\n            // Create also other dirs\n            fs.mkdirSync(path.join(this.appDir, 'sites'));\n            fs.mkdirSync(path.join(this.appDir, 'config'));\n            fs.mkdirSync(path.join(this.appDir, 'themes'));\n            fs.copySync(\n                path.join(__dirname, '..', 'default-files', 'default-themes').replace('app.asar', 'app.asar.unpacked'),\n                path.join(this.appDir, 'themes'),\n                {\n                    filter: this.skipSystemFiles,\n                    dereference: true\n                }\n            );\n            fs.mkdirSync(path.join(this.appDir, 'languages'));\n            fs.mkdirSync(path.join(this.appDir, 'plugins'));\n        }\n\n        if (!fs.existsSync(path.join(this.appDir, 'backups'))) {\n            fs.mkdirSync(path.join(this.appDir, 'backups'));\n        }\n\n        if (!fs.existsSync(path.join(this.appDir, 'languages'))) {\n            fs.mkdirSync(path.join(this.appDir, 'languages'));\n        }\n\n        if (!fs.existsSync(path.join(this.appDir, 'plugins'))) {\n            fs.mkdirSync(path.join(this.appDir, 'plugins'));\n        }\n    }\n\n    /**\n     * Check if some themes should be updated\n     */\n    checkThemes() {\n        let appThemesPath = path.join(__dirname, '..', 'default-files', 'default-themes');\n        let userThemesPath = path.join(this.appDir, 'themes');\n\n        // Merge themes directory\n        let appThemeDirs = fs.readdirSync(appThemesPath);\n\n        for (let file of appThemeDirs) {\n            // Skip files and hidden files\n            if (file.indexOf('.') > -1) {\n                continue;\n            }\n\n            // Detect missing themes\n            if (!fs.existsSync(path.join(userThemesPath, file))) {\n                fs.mkdirSync(path.join(userThemesPath, file), { recursive: true });\n\n                try {\n                    fs.copySync(\n                        path.join(appThemesPath, file).replace('app.asar', 'app.asar.unpacked'),\n                        path.join(userThemesPath, file),\n                        {\n                            filter: this.skipSystemFiles,\n                            dereference: true\n                        }\n                    );\n                } catch (err) {\n                    fs.appendFile(this.app.getPath('logs') + '/themes-copy-errors.txt', JSON.stringify(err));\n                }\n            } else {\n                // For existing themes - compare versions\n                let appThemeConfig = path.join(appThemesPath, file, 'config.json');\n                let userThemeConfig = path.join(userThemesPath, file, 'config.json');\n\n                // Check if both config.json files exists\n                if (fs.existsSync(appThemeConfig) && fs.existsSync(userThemeConfig)) {\n                    let appThemeData = JSON.parse(FileHelper.readFileSync(appThemeConfig, 'utf8'));\n                    let userThemeData = JSON.parse(FileHelper.readFileSync(userThemeConfig, 'utf8'));\n\n                    // If app theme is newer version than the existing one\n                    if(compare(appThemeData.version, userThemeData.version) === 1) {\n                        // Remove all files from the theme dir\n                        fs.emptyDirSync(path.join(userThemesPath, file));\n\n                        // Copy updated theme files\n                        fs.copySync(\n                            path.join(appThemesPath, file).replace('app.asar', 'app.asar.unpacked'),\n                            path.join(userThemesPath, file),\n                            {\n                                filter: this.skipSystemFiles,\n                                dereference: true\n                            }\n                        );\n                    }\n                }\n            }\n        }\n    }\n\n    // Reload website data\n    reloadSite (siteName) {\n        let siteData = this.switchSite(siteName);\n        let siteConfig = this.loadSite(siteName);\n\n        return {\n            data: siteData,\n            config: siteConfig\n        };\n    }\n\n    // Load website and their config and database\n    switchSite (site) {\n        if (!site) {\n            return { status: false };\n        }\n\n        const siteDir = path.join(this.sitesDir, site);\n        const menuConfigPath = path.join(siteDir, 'input', 'config', 'menu.config.json');\n        const themeConfigPath = path.join(siteDir, 'input', 'config', 'theme.config.json');\n        const dbPath = path.join(siteDir, 'input', 'db.sqlite');\n\n        if (!Utils.fileExists(dbPath)) {\n            return { status: false };\n        }\n\n        if (this.db) {\n            try {\n                this.db.close();\n            } catch (e) {\n                console.log('[SWITCH WEBSITE] DB already closed');\n            }\n        }\n\n        this.db = new DBUtils(new Database(dbPath));\n        let tags = new Tags(this, {site});\n        let posts = new Posts(this, {site});\n        let pages = new Pages(this, {site});\n        let authors = new Authors(this, {site});\n        let themes = new Themes(this, {site});\n        let themeDir = path.join(siteDir, 'input', 'themes', themes.currentTheme(true));\n        let themeOverridesDir = path.join(siteDir, 'input', 'themes', themes.currentTheme(true) + '-override');\n        let themeConfig = Themes.loadThemeConfig(themeConfigPath, themeDir);\n        let menuStructure = FileHelper.readFileSync(menuConfigPath, 'utf8');\n        let parsedMenuStructure = {};\n\n        try {\n            parsedMenuStructure = JSON.parse(menuStructure);\n        } catch (e) {\n            return { status: false };\n        }\n\n        return {\n            status: true,\n            posts: posts.load(),\n            pages: pages.load(),\n            tags: tags.load(),\n            authors: authors.load(),\n            postsTags: posts.loadTagsXRef(),\n            postsAuthors: posts.loadAuthorsXRef(),\n            pagesAuthors: pages.loadAuthorsXRef(),\n            postTemplates: themes.loadPostTemplates(),\n            pageTemplates: themes.loadPageTemplates(),\n            tagTemplates: themes.loadTagTemplates(),\n            authorTemplates: themes.loadAuthorTemplates(),\n            themes: themes.load(),\n            themeHasOverrides: Utils.dirExists(themeOverridesDir),\n            themeSettings: themeConfig,\n            menuStructure: parsedMenuStructure,\n            siteDir: siteDir\n        };\n    }\n\n    // Load specific website\n    loadSite (siteName) {\n        let dirPath = path.join(this.sitesDir, siteName);\n        let fileStat = fs.statSync(dirPath);\n\n        // check directories only\n        if (!fileStat.isDirectory()) {\n            return;\n        }\n\n        // check if the config file exists\n        let configFilePath = path.join(dirPath, 'input', 'config', 'site.config.json');\n\n        if (!Utils.fileExists(configFilePath)) {\n            return;\n        }\n\n        // check if all necessary files exists\n        Site.checkFilesConsistency(this, siteName);\n\n        // Load the config\n        let defaultSiteConfig = JSON.parse(JSON.stringify(defaultAstCurrentSiteConfig));\n        let siteConfig = FileHelper.readFileSync(configFilePath);\n\n        try {\n            siteConfig = JSON.parse(siteConfig);\n        } catch (e) {\n            dialog.showErrorBox('Publii cannot read site config', 'There is an issue with file: ' + configFilePath + \"\\n\\nError details: \" + e.message);\n            return;\n        }\n\n        if (siteConfig.name !== siteName) {\n            siteConfig.name = siteName;\n            fs.writeFileSync(configFilePath, JSON.stringify(siteConfig, null, 4));\n        }\n\n        siteConfig = Utils.mergeObjects(defaultSiteConfig, siteConfig);\n\n        // Migrate old author data if necessary\n        siteConfig = SiteConfigMigrator.moveOldAuthorData(this, siteConfig);\n\n        // set site data\n        this.sites[siteConfig.name] = JSON.parse(JSON.stringify(siteConfig));\n\n        if (this.sites[siteConfig.name].logo.icon.indexOf('#') > -1) {\n            this.sites[siteConfig.name].logo.icon = this.sites[siteConfig.name].logo.icon.split('#')[1];\n        }\n\n        // Fill displayName fields for old websites without it\n        if (!this.sites[siteConfig.name].displayName) {\n            this.sites[siteConfig.name].displayName = siteConfig.name;\n        }\n\n        return siteConfig;\n    }\n\n    // Load websites\n    loadSites() {\n        if (!fs.existsSync(this.sitesDir)) {\n            dialog.showErrorBox('Publii cannot find your sites folder.', 'Please check if the directory ' + this.sitesDir + ' exists or create it manually, then reopen the application.');\n            return false;\n        }\n\n        let files = fs.readdirSync(this.sitesDir);\n        this.sites = {};\n\n        for (let siteName of files) {\n            this.loadSite(siteName);\n        }\n\n        return true;\n    }\n\n    // Load themes\n    loadThemes() {\n        let themesLoader = new Themes(this);\n        this.themes = themesLoader.loadThemes();\n        this.themesPath = normalizePath(path.join(this.appDir, 'themes'));\n        this.dirPaths = {\n            sites: normalizePath(path.join(this.appDir, 'sites')),\n            temp: normalizePath(path.join(this.appDir, 'temp')),\n            logs: normalizePath(this.app.getPath('logs'))\n        };\n    }\n\n    // Load languages\n    loadLanguages() {\n        let languagesLoader = new Languages(this);\n        this.languages = languagesLoader.loadLanguages();\n        this.languagesPath = normalizePath(path.join(this.appDir, 'languages'));\n        this.languagesDefaultPath = normalizePath(path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked'));\n        this.languageLoadingError = false;\n\n        if (this.appConfig.language && this.appConfig.languageType) {\n            this.currentLanguageName = this.appConfig.language;\n            this.currentLanguageType = this.appConfig.languageType;\n            this.currentLanguageTranslations = languagesLoader.loadTranslations(this.appConfig.language, this.appConfig.languageType);\n            let languageConfig = languagesLoader.loadLanguageConfig(this.appConfig.language, this.appConfig.languageType);\n\n            if (languageConfig) {\n                this.currentLanguageMomentLocale = languageConfig.momentLocale;\n                this.currentWysiwygTranslation = languagesLoader.loadWysiwygTranslation(this.appConfig.language, this.appConfig.languageType);\n            }\n        }\n\n        this.loadDefaultLanguage(languagesLoader, false);\n    }\n\n    // Load plugins\n    loadPlugins() {\n        let pluginsLoader = new Plugins(this.appDir, this.sitesDir);\n        this.plugins = pluginsLoader.loadPlugins();\n        this.pluginsPath = normalizePath(path.join(this.appDir, 'plugins'));\n    }\n\n    // Load default language\n    loadDefaultLanguage (languagesLoader, errorOccurred = false) {\n        this.defaultLanguageName = 'en-gb';\n        this.defaultLanguageType = 'default';\n        this.defaultLanguageTranslations = languagesLoader.loadTranslations('en-gb', 'default');\n        let languageConfig = languagesLoader.loadLanguageConfig('en-gb', 'default');\n        this.defaultLanguageMomentLocale = languageConfig.momentLocale;\n        this.defaultWysiwygTranslation = languagesLoader.loadWysiwygTranslation('en-gb', 'default');\n\n        if (errorOccurred) {\n            this.defaultLanguageLoadingError = true;\n        }\n    }\n\n    // Load language\n    loadLanguage (lang, type) {\n        if (type !== 'default' && type !== 'installed') {\n            type = 'default';\n            lang = 'en-gb';\n        }\n\n        let languagesLoader = new Languages(this);\n        this.currentLanguageName = lang.replace(/[^a-z\\-\\_\\.]/gmi, '');\n        this.currentLanguageType = type;\n        this.currentLanguageTranslations = languagesLoader.loadTranslations(lang, type);\n        this.languageLoadingError = false;\n        let languageConfig = languagesLoader.loadLanguageConfig(lang, type);\n\n        if (languageConfig) {\n            this.currentLanguageMomentLocale = languageConfig.momentLocale;\n            this.currentWysiwygTranslation = languagesLoader.loadWysiwygTranslation(lang, type);\n        }\n\n        if (\n            !this.currentLanguageTranslations ||\n            !languageConfig ||\n            (!this.currentWysiwygTranslation && lang !== 'en-gb')\n        ) {\n            this.languageLoadingError = true;\n        }\n    }\n\n    // Set language\n    setLanguage (lang, type) {\n        if (type !== 'default' && type !== 'installed') {\n            type = 'default';\n            lang = 'en-gb';\n        }\n\n        this.appConfig.language = lang.replace(/[^a-z\\-\\_\\.]/gmi, '');\n        this.appConfig.languageType = type;\n\n        try {\n            fs.writeFileSync(this.appConfigPath, JSON.stringify(this.appConfig, null, 4), {'flags': 'w'});\n        } catch (e) {\n            if (this.hasPermissionsErrors(e)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    // Read or create the application config\n    loadConfig () {\n        // Try to get window bounds\n        try {\n            this.windowBounds = JSON.parse(FileHelper.readFileSync(this.initPath, 'utf8'));\n        } catch (e) {\n            console.log('The window-config.json file will be created');\n        }\n\n        if (!this.windowBounds) {\n            let screens = screen.getAllDisplays();\n            let width = screens[0].workAreaSize.width;\n            let height = screens[0].workAreaSize.height;\n\n            for (let i = 0; i < screens.length; i++) {\n                if (screens[i].width < width) {\n                    width = screens[i].width;\n                }\n\n                if (screens[i].height < height) {\n                    height = screens[i].height;\n                }\n            }\n\n            this.windowBounds = {\n                width: width,\n                height: height\n            };\n        } else {\n            let screens = screen.getAllDisplays();\n            let isInsideScreenBounds = false;\n\n            for (let monitor of screens) {\n                if (\n                    this.windowBounds.x >= monitor.bounds.x && \n                    this.windowBounds.y >= monitor.bounds.y && \n                    this.windowBounds.x + this.windowBounds.width <= monitor.bounds.x + monitor.bounds.width && \n                    this.windowBounds.y + this.windowBounds.height <= monitor.bounds.y + monitor.bounds.height\n                ) {\n                    isInsideScreenBounds = true;\n                    break\n                }\n            }\n\n            if (!isInsideScreenBounds) {\n                let width = screens[0].workAreaSize.width;\n                let height = screens[0].workAreaSize.height;\n                \n                this.windowBounds = {\n                    width: width,\n                    height: height\n                };\n            }\n        }\n\n        // Try to get application config\n        try {\n            this.appConfig = JSON.parse(FileHelper.readFileSync(this.appConfigPath, 'utf8'));\n            this.appConfig = Utils.mergeObjects(JSON.parse(JSON.stringify(defaultAstAppConfig)), this.appConfig);\n        } catch (e) {\n            if (this.hasPermissionsErrors(e)) {\n                return false;\n            }\n\n            console.log('The app-config.json file will be created');\n            this.appConfig = JSON.parse(JSON.stringify(defaultAstAppConfig));\n\n            try {\n                fs.writeFileSync(this.appConfigPath, JSON.stringify(this.appConfig, null, 4), {'flags': 'w'});\n            } catch (e) {\n                if (this.hasPermissionsErrors(e)) {\n                    return false;\n                }\n            }\n\n            return true;\n        }\n\n        return true;\n    }\n\n    // Load additional config data\n    loadAdditionalConfig () {\n        // Try to get TinyMCE overrided config\n        try {\n            this.tinymceOverridedConfig = JSON.parse(FileHelper.readFileSync(this.tinymceOverridedConfigPath, 'utf8'));\n        } catch (e) {}\n\n        if (this.appConfig.sitesLocation) {\n            this.sitesDir = this.appConfig.sitesLocation;\n            this.app.sitesDir = this.appConfig.sitesLocation;\n        } else {\n            this.appConfig.sitesLocation = path.join(this.appDir, 'sites');\n            this.sitesDir = path.join(this.appDir, 'sites');\n            this.app.sitesDir = path.join(this.appDir, 'sites');\n        }\n\n        this.pluginsHelper = new Plugins(this.appDir, this.sitesDir);\n    }\n\n    // Check permissions errors\n    hasPermissionsErrors (error) {\n        if (error.code === 'EACCES') {\n            dialog.showErrorBox('Publii has no read/write access to the config folder', 'Please check the permissions of the Publii config folder and try to reopen the application.');\n            return true;\n        }\n\n        if (error.code === 'EPERM') {\n            dialog.showErrorBox('Publii has no read/write access to the config folder', 'If you are using macOS 10.15+ - please open \"System Preferences\", go to \"Security & Privacy\" and under \"Privacy Tab\" please check if Publii has proper permissions for the \"Files and Documents\". For other operating systems - please check the file permissions for the Publii configuration folder.');\n            return true;\n        }\n\n        return false;\n    }\n\n    // Create the window\n    initWindow() {\n        let self = this;\n        let windowParams = this.windowBounds;\n\n        windowParams.minWidth = 1200;\n        windowParams.minHeight = 700;\n        windowParams.webPreferences = {\n            nodeIntegration: false,\n            contextIsolation: true,\n            spellcheck: true,\n            preload: path.join(__dirname, 'app-preload.js'),\n            icon: path.join(__dirname, 'assets', 'icon.png')\n        };\n\n        if (this.appConfig.appTheme === 'dark' || (this.appConfig.appTheme === 'system' && nativeTheme.shouldUseDarkColors)) {\n            windowParams.backgroundColor = '#202128';\n        }\n\n        let displays = screen.getAllDisplays();\n        let externalDisplay = displays.find((display) => {\n            return display.bounds.x !== 0 || display.bounds.y !== 0;\n        });\n\n        // Detect case when Publii was displayed on the external display which is now unavailable\n        if (\n            !externalDisplay &&\n            (\n                windowParams.x < 0 ||\n                windowParams.x > screen.getPrimaryDisplay().workAreaSize.width ||\n                windowParams.y < 0 ||\n                windowParams.y > screen.getPrimaryDisplay().workAreaSize.height\n            )\n        ) {\n            windowParams.x = 0;\n            windowParams.y = 0;\n        }\n\n        if((/^darwin/).test(process.platform)) {\n            windowParams.titleBarStyle = 'hidden';\n        }\n\n        if((/^win/).test(process.platform)) {\n            windowParams.frame = false;\n        }\n\n        Menu.setApplicationMenu(null);\n        this.mainWindow = new BrowserWindow(windowParams);\n        this.mainWindow.setMenu(null);\n        this.mainWindow.loadURL('file:///' + this.basedir + '/dist/index.html');\n        this.mainWindow.removeMenu();\n\n        // Register search shortcut listener\n        this.mainWindow.webContents.on('before-input-event', (event, input) => {\n            if (input.type === 'mouseDown' && (input.button === 'back' || input.button === 'forward')) {\n                event.preventDefault();\n            }\n\n            if (input.key === 'f' && (input.meta || input.control)) {\n                this.mainWindow.webContents.send('app-show-search-form');\n            } else if (input.key === 'z' && (input.meta || input.control) && !input.shift) {\n                this.mainWindow.webContents.send('block-editor-undo');\n            } else if (\n                (input.key === 'z' && (input.meta || input.control) && input.shift) || \n                (input.key === 'y' && (input.meta || input.control) && !input.shift)\n            ) {\n                this.mainWindow.webContents.send('block-editor-redo');\n            }\n        });\n\n        this.mainWindow.webContents.setWindowOpenHandler(({ url }) => {\n            if (typeof url !== 'string') {\n                return { action: 'deny' };\n            }\n\n            let urlToOpen;\n            let allowedProtocols = ['http:', 'https:', 'file:', 'dat:', 'ipfs:', 'dweb:'];\n\n            try {\n                urlToOpen = new URL(url);\n            } catch (e) {\n                return { action: 'deny' };\n            }\n\n            if (allowedProtocols.indexOf(urlToOpen.protocol) > -1) {\n                urlToOpen = urlToOpen.href.replace(/\\s/gmi, '');\n                shell.openExternal(url);\n            }\n            \n            return { action: 'deny' };\n        });\n\n        this.mainWindow.webContents.on('app-command', (e, cmd) => {\n            // disable back/forward mouse buttons\n            if (cmd === 'browser-backward' || cmd === 'browser-forward') {\n                e.preventDefault();\n            }\n        });\n\n        this.mainWindow.webContents.on('did-finish-load', function() {\n            let appData = {\n                version: self.versionData,\n                config: self.appConfig,\n                customConfig: {\n                    tinymce: self.tinymceOverridedConfig\n                },\n                currentLanguage: {\n                    name: self.currentLanguageName,\n                    translations: self.currentLanguageTranslations,\n                    wysiwygTranslation: self.currentWysiwygTranslation,\n                    momentLocale: self.currentLanguageMomentLocale,\n                    languageLoadingError: self.languageLoadingError\n                },\n                defaultLanguage: {\n                    name: self.defaultLanguageName,\n                    translations: self.defaultLanguageTranslations,\n                    wysiwygTranslation: self.defaultWysiwygTranslation,\n                    momentLocale: self.defaultLanguageMomentLocale,\n                    languageLoadingError: self.languageLoadingError\n                },\n                languages: self.languages,\n                languagesPath: self.languagesPath,\n                languagesDefaultPath: self.languagesDefaultPath,\n                plugins: self.plugins,\n                pluginsPath: self.pluginsPath,\n                sites: self.sites,\n                themes: self.themes,\n                themesPath: self.themesPath,\n                dirs: self.dirPaths,\n                vendorPath: normalizePath(path.join(__dirname, '..', 'default-files', 'vendor').replace('app.asar', 'app.asar.unpacked'))\n            };\n\n            self.mainWindow.webContents.send('app-data-loaded', appData);\n            \n            // Open Dev Tools\n            if (self.appConfig.openDevToolsInMain) {\n                self.mainWindow.webContents.openDevTools();\n            }\n\n            self.setCurrentZoomLevel();\n        });\n\n        this.mainWindow.on('resize', () => this.setCurrentZoomLevel());\n        this.mainWindow.on('maximize', () => this.setCurrentZoomLevel());\n        this.mainWindow.on('unmaximize', () => this.setCurrentZoomLevel());\n        this.mainWindow.on('restore', () => this.setCurrentZoomLevel());\n\n        if (process.platform === 'linux') {\n            this.mainWindow.webContents.on('before-input-event', (event, input) => {\n                if (input.control && input.key === 'q') {\n                    this.app.quit();\n                }\n            });\n        }\n\n        // Create context menu\n        const ContextMenuBuilder = require('./helpers/context-menu-builder.js');\n        let contextMenuBuilder = new ContextMenuBuilder(this.mainWindow.webContents);\n\n        this.mainWindow.webContents.on('context-menu', (event, params) => {\n            event.preventDefault();\n            contextMenuBuilder.showPopupMenu(params);\n        });\n    }\n\n    // Add events to the window\n    initWindowEvents() {\n        this.mainWindow.on('close', () => {\n            let windowBounds = this.mainWindow.getBounds();\n            fs.writeFileSync(this.initPath, JSON.stringify(windowBounds, null, 4), {'flags': 'w'});\n        });\n\n        this.mainWindow.on('closed', () => {\n            this.mainWindow = null;\n        });\n\n        this.initializeCustomIpcMainEvents();\n    }\n\n    // Initializes all custom events for IPC Main thread\n    initializeCustomIpcMainEvents () {\n        // Create instances for all custom event classes\n        let classNames = Object.keys(EventClasses);\n\n        for (let className of classNames) {\n            new EventClasses[className](this);\n        }\n    }\n\n    // Getter for the main window object\n    getMainWindow() {\n        return this.mainWindow;\n    }\n\n    // Function used to filter unnecessary files\n    skipSystemFiles (src, dest) {\n        return src.indexOf('.DS_Store') > -1 ? false : true;\n    }\n\n    // Function used to add sites to the back-end sites list\n    addSite (siteCatalog, siteData) {\n        this.sites[siteCatalog] = siteData;\n    }\n\n    // Function used to restore current zoom level of window, because it is lost if zoom is changed after windo load\n    setCurrentZoomLevel () {\n        let zoom = parseFloat(this.appConfig.uiZoomLevel);\n        \n        if (zoom && zoom > 0 && zoom <= 2.5) {\n            this.mainWindow.webContents.setZoomFactor(zoom);\n        }\n    }\n}\n\nmodule.exports = App;\n"
  },
  {
    "path": "app/back-end/author.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst Model = require('./model.js');\nconst Authors = require('./authors.js');\nconst Pages = require('./pages.js');\nconst Posts = require('./posts.js');\nconst slug = require('./helpers/slug');\nconst ImageHelper = require('./helpers/image.helper.js');\nconst Themes = require('./themes.js');\nconst Utils = require('./helpers/utils.js');\nconst FileHelper = require('./helpers/file.js');\n\n/**\n * Author Model - used for operations connected with author management\n */\nclass Author extends Model {\n    /**\n     * Creates an instance of the model\n     *\n     * @param appInstance {object} - instance of the application\n     * @param authorData {object} - object with author data\n     */\n    constructor(appInstance, authorData, storeMode = true) {\n        super(appInstance, authorData);\n        this.id = parseInt(authorData.id, 10);\n        this.authorsData = new Authors(appInstance, authorData);\n        this.postsData = new Posts(appInstance, authorData);\n        this.pagesData = new Pages(appInstance, authorData);\n        this.storeMode = storeMode;\n\n        if (authorData.additionalData) {\n            this.additionalData = authorData.additionalData;\n        }\n\n        if (authorData.imageConfigFields) {\n            this.imageConfigFields = authorData.imageConfigFields;\n        }\n\n        if(authorData.name || authorData.name === '') {\n            this.name = authorData.name;\n            this.username = authorData.username;\n            this.config = authorData.config;\n            this.additionalData = authorData.additionalData;\n            this.prepareAuthorName();\n        }\n\n        if (typeof this.additionalData === 'string') {\n            try {\n                this.additionalData = JSON.parse(this.additionalData);\n            } catch (e) {\n                console.log('(!) An issue occurred during initial parsing author additional data', this.id);\n            }\n        }\n    }\n\n    /**\n     * Saves new/existing author data\n     *\n     * @returns {object} - object with created/edited author data\n     */\n    save () {\n        if (this.name === '') {\n            return {\n                status: false,\n                message: 'author-empty-name'\n            };\n        }\n\n        if (this.username === '' || slug(this.username) === '') {\n            this.username = slug(this.name);\n        }\n\n        if (slug(this.username).trim() === '') {\n            return {\n                status: false,\n                message: 'author-empty-username'\n            };\n        }\n\n        if (!this.isAuthorNameUnique()) {\n            return {\n                status: false,\n                message: 'author-duplicate-name',\n                authors: this.authorsData.load()\n            };\n        }\n\n        if (!this.isAuthorUsernameUnique()) {\n            return {\n                status: false,\n                message: 'author-duplicate-username',\n                authors: this.authorsData.load()\n            };\n        }\n\n        if (this.id !== 0) {\n            return this.updateAuthor();\n        }\n\n        return this.addAuthor();\n    }\n\n    /**\n     * Stores new author in the DB\n     *\n     * @returns {{status: boolean, message: string, authors: *}}\n     */\n    addAuthor() {\n        let sqlQuery = this.db.prepare(`INSERT INTO authors VALUES(null, @name, @slug, '', @config, @additionalData)`);\n        sqlQuery.run({\n            name: this.name,\n            slug: slug(this.username),\n            config: this.config,\n            additionalData: JSON.stringify(this.additionalData)\n        });\n\n        // Get the newly added item ID if necessary\n        if (this.id === 0) {\n            this.id = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n\n            // Move images from the temp directory\n            let tempDirectoryExists = true;\n            let tempImagesDir = path.join(this.siteDir, 'input', 'media', 'authors', 'temp');\n\n            try {\n                fs.statSync(tempImagesDir).isDirectory();\n            } catch (err) {\n                tempDirectoryExists = false;\n            }\n\n            if (tempDirectoryExists) {\n                let finalImagesDir = path.join(this.siteDir, 'input', 'media', 'authors', (this.id).toString());\n                fs.copySync(tempImagesDir, finalImagesDir);\n                fs.removeSync(tempImagesDir);\n            }\n        }\n\n        this.checkAndCleanImages();\n\n        return {\n            status: true,\n            message: 'author-added',\n            authorID: this.id,\n            postsAuthors: this.postsData.loadAuthorsXRef(),\n            pagesAuthors: this.pagesData.loadAuthorsXRef(),\n            authors: this.authorsData.load()\n        };\n    }\n\n    /**\n     * Updates existing author in the DB\n     *\n     * @returns {{status: boolean, message: string}}\n     */\n    updateAuthor() {\n        let sqlQuery = this.db.prepare(`UPDATE authors\n                        SET\n                            name = @name,\n                            username = @slug,\n                            password = '',\n                            config = @config,\n                            additional_data = @additionalData\n                        WHERE\n                            id = @id`);\n        sqlQuery.run({\n            name: this.name,\n            slug: slug(this.username),\n            config: this.config,\n            additionalData: JSON.stringify(this.additionalData),\n            id: this.id\n        });\n\n        this.checkAndCleanImages();\n\n        return {\n            status: true,\n            message: 'author-updated',\n            postsAuthors: this.postsData.loadAuthorsXRef(),\n            authors: this.authorsData.load()\n        };\n    }\n\n    /**\n     * Creates author name without leading/ending spaces\n     */\n    prepareAuthorName() {\n        if(typeof this.name == 'undefined') {\n            this.name = '';\n        }\n        // Remove leading and ending spaces (trim it)\n        // it will also exclude case when author name contains only\n        // whitespaces\n        this.name = this.name.replace(/^\\s+/, '').replace(/\\s+$/, '');\n    }\n\n    /**\n     * Check if the author name is unique\n     *\n     * @returns {boolean}\n     */\n    isAuthorNameUnique() {\n        let query = this.db.prepare('SELECT * FROM authors WHERE name LIKE @name AND id != @id');\n        let queryParams = {\n            name: this.escape(this.name),\n            id: this.id\n        };\n        let foundedAuthors = query.all(queryParams);\n\n        if (foundedAuthors.length) {\n            for (const author of foundedAuthors) {\n                if (author.name === this.name) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Checks if author username (slug) is unique\n     *\n     * @returns {boolean}\n     */\n    isAuthorUsernameUnique() {\n        let query = this.db.prepare('SELECT username FROM authors WHERE id != @id');\n        let queryParams = {\n            id: this.id\n        };\n        let foundedAuthors = query.all(queryParams);\n\n        if (foundedAuthors.length) {\n            for (const author of foundedAuthors) {\n                if (slug(this.username) === slug(author.username)) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Removes current author\n     *\n     * @returns {{status: boolean, message: string}}\n     */\n    delete() {\n        if(this.id === 1) {\n            return {\n                status: false,\n                message: 'cannot-delete-main-author'\n            };\n        }\n\n        this.db.exec(`DELETE FROM authors WHERE id = ${parseInt(this.id, 10)}`);\n        this.db.prepare(`UPDATE posts SET authors = '1' WHERE authors LIKE @id`).run({ id: this.id.toString() });\n        ImageHelper.deleteImagesDirectory(this.siteDir, 'authors', this.id);\n\n        return {\n            status: true,\n            message: 'author-deleted',\n            posts: this.postsData.load(),\n            postsAuthors: this.postsData.loadAuthorsXRef(),\n            authors: this.authorsData.load()\n        };\n    }\n\n    /*\n     * Remove unused images\n     */\n    checkAndCleanImages (cancelEvent = false) {\n        let authorDir = this.id;\n\n        if(this.id === 0) {\n            authorDir = 'temp';\n        }\n\n        let imagesDir = path.join(this.siteDir, 'input', 'media', 'authors', (authorDir).toString());\n        let authorDirectoryExists = true;\n\n        try {\n            fs.statSync(imagesDir).isDirectory();\n        } catch (err) {\n            authorDirectoryExists = false;\n        }\n\n        if(!authorDirectoryExists) {\n            return;\n        }\n\n        let images = fs.readdirSync(imagesDir);\n        this.cleanImages(images, imagesDir, cancelEvent);\n    }\n\n    /*\n     * Removes images from a given image dir\n     */\n    cleanImages(images, imagesDir, cancelEvent) {\n        let authorDir = this.id;\n        let featuredImage = '';\n        let viewConfig = {};\n        \n        if (this.additionalData && this.additionalData.featuredImage) {\n            featuredImage = path.parse(this.additionalData.featuredImage).base;\n        }\n\n        if (this.additionalData && this.additionalData.viewConfig) {\n            viewConfig = this.additionalData.viewConfig;\n        }\n\n        // If author is cancelled - get the previous featured image\n        if (cancelEvent && this.id !== 0) {\n            let additionalDataQuery = `SELECT additional_data FROM authors WHERE id = @id`;\n            let additionalDataResult = this.db.prepare(additionalDataQuery).all({ id: this.id });\n\n            if (additionalDataResult) {\n                try {\n                    featuredImage = JSON.parse(additionalDataResult[0].additional_data).featuredImage;\n                    viewConfig = JSON.parse(additionalDataResult[0].additional_data).viewConfig;\n                } catch (e) {\n                    console.log('(!) An issue occurred during parsing author additional data', this.id);\n                }\n            }\n        }\n\n        if (this.id === 0) {\n            authorDir = 'temp';\n        }\n\n        let imagesInViewSettings = [];\n        \n        imagesInViewSettings = Object.keys(viewConfig).filter((fieldName) => {\n            return this.imageConfigFields.indexOf(fieldName) !== -1 && viewConfig[fieldName] !== '';\n        }).map((fieldName) => {\n            return viewConfig[fieldName];\n        });\n\n        // Iterate through images\n        for (let i in images) {\n            let imagePath = images[i];\n            let fullPath = path.join(imagesDir, imagePath);\n\n            // Skip dirs and symlinks\n            if (imagePath === '.' || imagePath === '..' || imagePath === 'responsive') {\n                continue;\n            }\n\n            // Remove files which does not exist as featured image and authorViewSettings\n            if (\n                (cancelEvent && authorDir === 'temp') ||\n                (\n                    imagesInViewSettings.indexOf(imagePath) === -1 &&\n                    featuredImage !== imagePath\n                )\n            ) {\n                try {\n                    fs.unlinkSync(fullPath);\n                } catch(e) {\n                    console.error(e);\n                }\n\n                this.removeResponsiveImages(fullPath);\n            }\n        }\n\n        // Clean unused avatar images\n        let themesHelper = new Themes(this.application, { site: this.site });\n        let themeConfigPath = path.join(this.application.sitesDir, this.site, 'input', 'config', 'theme.config.json');\n\n        if (fs.existsSync(themeConfigPath)) {\n            let themeConfigString = FileHelper.readFileSync(themeConfigPath, 'utf8');\n            themesHelper.checkAndCleanImages(themeConfigString);\n        }\n    }\n\n    /*\n     * Remove unused responsive images\n     */\n    removeResponsiveImages(originalPath) {\n        let themesHelper = new Themes(this.application, { site: this.site });\n        let currentTheme = themesHelper.currentTheme();\n\n        // If there is no selected theme\n        if (currentTheme === 'not selected') {\n            return;\n        }\n\n        // Load theme config\n        let themeConfig = Utils.loadThemeConfig(path.join(this.siteDir, 'input'), currentTheme);\n\n        // check if responsive images config exists\n        if(Utils.responsiveImagesConfigExists(themeConfig)) {\n            let dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages');\n            let featuredDimensions = Utils.responsiveImagesDimensions(themeConfig, 'authorImages');\n\n            if (featuredDimensions !== false) {\n                featuredDimensions.forEach(item => {\n                    if (dimensions.indexOf(item) === -1) {\n                        dimensions.push(item);\n                    }\n                });\n            }\n\n            let responsiveImagesDir = path.parse(originalPath).dir;\n            responsiveImagesDir = path.join(responsiveImagesDir, 'responsive');\n\n            if (typeof dimensions === \"boolean\") {\n                return;\n            }\n\n            let forceWebp = !!this.application.sites[this.site]?.advanced?.forceWebp;\n\n            // Remove responsive images of each size\n            for(let dimensionName of dimensions) {\n                let filename = path.parse(originalPath).name;\n                let extension = path.parse(originalPath).ext;\n            \n                if (forceWebp && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) {\n                    extension = '.webp'; \n                }\n\n                let responsiveImagePath = path.join(responsiveImagesDir, filename + '-' + dimensionName + extension);\n\n                if(Utils.fileExists(responsiveImagePath)){\n                    fs.unlinkSync(responsiveImagePath);\n                }\n            }\n        }\n    }\n}\n\nmodule.exports = Author;\n"
  },
  {
    "path": "app/back-end/authors.js",
    "content": "/*\n * Authors instance\n */\n\nconst Model = require('./model.js');\n\nclass Authors extends Model {\n    /**\n     * Authors constructor\n     *\n     * @param appInstance\n     * @param authorsData\n     */\n    constructor(appInstance, authorsData) {\n        super(appInstance, authorsData);\n    }\n\n    /**\n     * Load authors\n     */\n    load() {\n        let sqlQuery = `SELECT\n            id,\n            name,\n            username,\n            config,\n            additional_data AS additionalData\n        FROM\n            authors\n        GROUP BY\n            id\n        ORDER BY\n            id ASC`;\n            \n        return this.db.prepare(sqlQuery).all();\n    }\n}\n\nmodule.exports = Authors;\n"
  },
  {
    "path": "app/back-end/builddata.json",
    "content": "{\n  \"version\": \"0.47.5\",\n  \"build\": 17411\n}\n"
  },
  {
    "path": "app/back-end/events/_modules.js",
    "content": "/*\n * Module which loads all Event classes\n */\nmodule.exports = {\n    AppEvents: require('./app.js'),\n    ContentEvents: require('./content.js'),\n    CreditsEvents: require('./credits'),\n    ImageUploaderEvents: require('./image-uploader.js'),\n    PageEvents: require('./page.js'),\n    PostEvents: require('./post.js'),\n    SiteEvents: require('./site.js'),\n    TagEvents: require('./tag.js'),\n    TagsEvents: require('./tags.js'),\n    DeployEvents: require('./deploy.js'),\n    SyncEvents: require('./sync.js'),\n    MenuEvents: require('./menu.js'),\n    PreviewEvents: require('./preview.js'),\n    NotificationsEvents: require('./notifications.js'),\n    BackupEvents: require('./backup.js'),\n    AuthorEvents: require('./author.js'),\n    AuthorsEvents: require('./authors.js'),\n    ImportEvents: require('./import.js'),\n    FileManagerEvents: require('./file-manager.js'),\n    PluginEvents: require('./plugin.js'),\n    PluginsApiEvents: require('./plugins-api.js')\n};\n"
  },
  {
    "path": "app/back-end/events/app.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('../helpers/file.js');\nconst ipcMain = require('electron').ipcMain;\nconst Themes = require('../themes.js');\nconst Languages = require('../languages.js');\nconst Plugins = require('../plugins.js');\nconst AppFiles = require('../helpers/app-files.js');\nconst AdmZip = require(\"adm-zip\");\n\n/*\n * Events for the IPC communication regarding app\n */\n\nclass AppEvents {\n    constructor(appInstance) {\n        /*\n         * Close app\n         */\n        ipcMain.on('app-close', function(event, config) {\n            appInstance.app.quit();\n        });\n        \n        /*\n         * Save licence acceptance\n         */\n        ipcMain.on('app-license-accept', function(event, config) {\n            fs.writeFileSync(appInstance.appConfigPath, JSON.stringify({licenseAccepted: true}, null, 4));\n            appInstance.appConfig = config;\n\n            event.sender.send('app-license-accepted', true);\n        });\n\n        /*\n         * Save app config\n         */\n        ipcMain.on('app-config-save', function (event, config) {\n            if (config.sitesLocation === '') {\n                config.sitesLocation = appInstance.dirPaths.sites;\n            }\n\n            if (config.sitesLocation !== appInstance.appConfig.sitesLocation) {\n                let result = true;\n\n                if (appInstance.appConfig.sitesLocation) {\n                    let appFilesHelper = new AppFiles(appInstance);\n                    \n                    if (appInstance.db) {\n                        try {\n                            appInstance.db.close();\n                        } catch (e) {\n                            console.log('[SITE LOCATION CHANGE] DB already closed');\n                        }\n                    }\n\n                    setTimeout(() => {\n                        if (config.changeSitesLocationWithoutCopying) {\n                            fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(config, null, 4));\n                            appInstance.appConfig = config;\n                            appInstance.sitesDir = config.sitesLocation;\n                        } else {\n                            result = appFilesHelper.relocateSites(\n                                appInstance.appConfig.sitesLocation,\n                                config.sitesLocation,\n                                event\n                            );\n\n                            if (result) {\n                                fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(config, null, 4));\n                                appInstance.appConfig = config;\n                                appInstance.sitesDir = config.sitesLocation;\n                            }\n                        }\n        \n                        appInstance.loadSites();\n                        \n                        event.sender.send('app-config-saved', {\n                            status: true,\n                            message: 'success-save',\n                            sites: appInstance.sites\n                        });\n                    }, 500);\n\n                    return;\n                }\n            }\n\n            event.sender.send('app-config-saved', {\n                status: true,\n                message: 'success-save'\n            });\n\n            fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(config, null, 4));\n            appInstance.appConfig = config;\n        });\n\n        /*\n         * Save app color theme config\n         */\n        ipcMain.on('app-save-color-theme', function (event, theme) {\n            let appConfig = FileHelper.readFileSync(appInstance.appConfigPath, 'utf8');\n\n            try {\n                appConfig = JSON.parse(appConfig);\n                appConfig.appTheme = theme;\n                fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(appConfig, null, 4));\n            } catch (e) {\n                console.log('(!) App was unable to save the color theme');\n            }\n        });\n\n        /*\n         * Delete theme\n         */\n        ipcMain.on('app-theme-delete', function(event, config) {\n            let themesLoader = new Themes(appInstance);\n\n            if(config.directory !== '') {\n                themesLoader.removeTheme(config.directory);\n\n                appInstance.themes = appInstance.themes.filter(function (theme) {\n                    return theme.name !== config.name;\n                });\n\n                event.sender.send('app-theme-deleted', {\n                    status: true,\n                    themes: appInstance.themes\n                });\n            }\n        });\n\n        /*\n         * Delete language\n         */\n        ipcMain.on('app-language-delete', function(event, config) {\n            let languagesLoader = new Languages(appInstance);\n\n            if(config.directory !== '') {\n                languagesLoader.removeLanguage(config.directory);\n\n                appInstance.languages = appInstance.languages.filter(function (language) {\n                    return language.name !== config.name;\n                });\n\n                event.sender.send('app-language-deleted', {\n                    status: true,\n                    languages: appInstance.languages\n                });\n            }\n        });\n\n        /*\n         * Delete plugin\n         */\n        ipcMain.on('app-plugin-delete', function(event, config) {\n            let pluginsLoader = new Plugins(appInstance.appDir, appInstance.sitesDir);\n\n            if(config.directory !== '') {\n                pluginsLoader.removePlugin(config.directory);\n\n                appInstance.plugins = appInstance.plugins.filter(function (plugin) {\n                    return plugin.name !== config.name;\n                });\n\n                event.sender.send('app-plugin-deleted', {\n                    status: true,\n                    plugins: appInstance.plugins\n                });\n            }\n        });\n\n        /*\n         * Add new theme\n         */\n        ipcMain.on('app-theme-upload', function(event, config) {\n            let themesLoader = new Themes(appInstance);\n            let newThemeDir = path.parse(config.sourcePath).name;\n            let extension = path.parse(config.sourcePath).ext;\n            let status = '';\n\n            if (extension === '.zip' || extension === '') {\n                if (extension === '.zip') {\n                    let zipPath = path.join(themesLoader.themesPath, '__TEMP__');\n                    let zip = new AdmZip(config.sourcePath);\n                    fs.mkdirSync(zipPath, { recursive: true });\n                    zip.extractAllTo(zipPath, true);\n\n                    let dirs = fs.readdirSync(zipPath).filter(function(file) {\n                        if(file.substr(0,1) === '_' || file.substr(0,1) === '.') {\n                            return false;\n                        }\n\n                        return fs.statSync(path.join(zipPath, file)).isDirectory();\n                    });\n\n                    if (dirs.length !== 1) {\n                        event.sender.send('app-theme-uploaded', {\n                            status: 'wrong-format',\n                            themes: appInstance.themes\n                        });\n\n                        fs.removeSync(zipPath);\n\n                        return;\n                    }\n\n                    newThemeDir = dirs[0];\n                    let directoryPath = path.join(themesLoader.themesPath, newThemeDir);\n\n                    try {\n                        fs.statSync(directoryPath);\n                        status = 'updated';\n                        fs.removeSync(directoryPath);\n                    } catch (e) {\n                        status = 'added';\n                    }\n\n                    fs.copySync(path.join(zipPath, newThemeDir), directoryPath);\n                    fs.removeSync(zipPath);\n                    appInstance.themes = themesLoader.loadThemes();\n\n                    event.sender.send('app-theme-uploaded', {\n                        status: status,\n                        themes: appInstance.themes\n                    });\n\n                    return;\n                } else {\n                    let directoryPath = path.join(themesLoader.themesPath, newThemeDir);\n\n                    try {\n                        fs.statSync(directoryPath);\n                        status = 'updated';\n                        fs.removeSync(directoryPath);\n                    } catch (e) {\n                        status = 'added';\n                    }\n\n                    fs.copySync(config.sourcePath, directoryPath);\n                    appInstance.themes = themesLoader.loadThemes();\n                }\n            } else {\n                status = 'wrong-format';\n            }\n\n            event.sender.send('app-theme-uploaded', {\n                status: status,\n                themes: appInstance.themes\n            });\n        });\n\n        /*\n         * Add new language\n         */\n        ipcMain.on('app-language-upload', function(event, config) {\n            let languagesLoader = new Languages(appInstance);\n            let newLanguageDir = path.parse(config.sourcePath).name;\n            let extension = path.parse(config.sourcePath).ext;\n            let status = '';\n\n            if (extension === '.zip' || extension === '') {\n                if (extension === '.zip') {\n                    let zipPath = path.join(languagesLoader.languagesPath, '__TEMP__');\n                    let zip = new AdmZip(config.sourcePath);\n                    fs.mkdirSync(zipPath, { recursive: true });\n                    zip.extractAllTo(zipPath, true);\n\n                    let dirs = fs.readdirSync(zipPath).filter(function(file) {\n                        if(file.substr(0,1) === '_' || file.substr(0,1) === '.') {\n                            return false;\n                        }\n\n                        return fs.statSync(path.join(zipPath, file)).isDirectory();\n                    });\n\n                    if (dirs.length !== 1) {\n                        event.sender.send('app-language-uploaded', {\n                            status: 'wrong-format',\n                            languages: appInstance.languages\n                        });\n\n                        fs.removeSync(zipPath);\n\n                        return;\n                    }\n\n                    newLanguageDir = dirs[0];\n\n                    let directoryPath = path.join(languagesLoader.languagesPath, newLanguageDir);\n\n                    try {\n                        fs.statSync(directoryPath);\n                        status = 'updated';\n                        fs.removeSync(directoryPath);\n                    } catch (e) {\n                        status = 'added';\n                    }\n\n                    fs.copySync(path.join(zipPath, newLanguageDir), directoryPath);\n                    fs.removeSync(zipPath);\n                    appInstance.languages = languagesLoader.loadLanguages();\n\n                    event.sender.send('app-language-uploaded', {\n                        status: status,\n                        languages: appInstance.languages\n                    });\n\n                    return;\n                } else {\n                    let directoryPath = path.join(languagesLoader.languagesPath, newLanguageDir);\n\n                    try {\n                        fs.statSync(directoryPath);\n                        status = 'updated';\n                        fs.removeSync(directoryPath);\n                    } catch (e) {\n                        status = 'added';\n                    }\n\n                    fs.copySync(config.sourcePath, directoryPath);\n                    appInstance.languages = languagesLoader.loadLanguages();\n                }\n            } else {\n                status = 'wrong-format';\n            }\n\n            event.sender.send('app-language-uploaded', {\n                status: status,\n                languages: appInstance.languages\n            });\n        });\n\n        /*\n         * Add new plugin\n         */\n        ipcMain.on('app-plugin-upload', function(event, config) {\n            let pluginsLoader = new Plugins(appInstance.appDir, appInstance.sitesDir);\n            let newPluginDir = path.parse(config.sourcePath).name;\n            let extension = path.parse(config.sourcePath).ext;\n            let status = '';\n\n            if (extension === '.zip' || extension === '') {\n                if (extension === '.zip') {\n                    let zipPath = path.join(pluginsLoader.pluginsPath, '__TEMP__');\n                    fs.mkdirSync(zipPath, { recursive: true });\n                    let zip = new AdmZip(config.sourcePath);\n                    zip.extractAllTo(zipPath, true);\n\n                    let dirs = fs.readdirSync(zipPath).filter(function(file) {\n                        if(file.substr(0,1) === '_' || file.substr(0,1) === '.') {\n                            return false;\n                        }\n\n                        return fs.statSync(path.join(zipPath, file)).isDirectory();\n                    });\n\n                    if (dirs.length !== 1) {\n                        event.sender.send('app-plugin-uploaded', {\n                            status: 'wrong-format',\n                            plugins: appInstance.plugins\n                        });\n\n                        fs.removeSync(zipPath);\n\n                        return;\n                    }\n\n                    newPluginDir = dirs[0];\n\n                    let directoryPath = path.join(pluginsLoader.pluginsPath, newPluginDir);\n\n                    try {\n                        fs.statSync(directoryPath);\n                        status = 'updated';\n                        fs.removeSync(directoryPath);\n                    } catch (e) {\n                        status = 'added';\n                    }\n\n                    fs.copySync(path.join(zipPath, newPluginDir), directoryPath);\n                    fs.removeSync(zipPath);\n                    appInstance.plugins = pluginsLoader.loadPlugins();\n\n                    event.sender.send('app-plugin-uploaded', {\n                        status: status,\n                        plugins: appInstance.plugins\n                    });\n\n                    return;\n                } else {\n                    let directoryPath = path.join(pluginsLoader.pluginsPath, newPluginDir);\n\n                    try {\n                        fs.statSync(directoryPath);\n                        status = 'updated';\n                        fs.removeSync(directoryPath);\n                    } catch (e) {\n                        status = 'added';\n                    }\n\n                    fs.copySync(config.sourcePath, directoryPath);\n                    appInstance.plugins = pluginsLoader.loadPlugins();\n                }\n            } else {\n                status = 'wrong-format';\n            }\n\n            event.sender.send('app-plugin-uploaded', {\n                status: status,\n                plugins: appInstance.plugins\n            });\n        });\n\n        /*\n         * Load log files list\n         */\n        ipcMain.on('app-log-files-load', function(event) {\n            let logPath = appInstance.app.getPath('logs');\n            let files = fs.readdirSync(logPath).filter(function(file) {\n                return file.substr(-4) === '.txt' || file.substr(-4) === '.log';\n            });\n\n            event.sender.send('app-log-files-loaded', {\n                files: files\n            });\n        });\n\n        /*\n         * Load specific log file\n         */\n        ipcMain.on('app-log-file-load', function(event, filename) {\n            let logPath = appInstance.app.getPath('logs');\n            let logFiles = fs.readdirSync(logPath).filter(function(file) {\n                return file.substr(-4) === '.txt' || file.substr(-4) === '.log';\n            });\n\n            if (logFiles.indexOf(filename) === -1) {\n                event.sender.send('app-log-file-loaded', {\n                    fileContent: 'File not found!'\n                });\n            }\n\n            let filePath = path.join(appInstance.app.getPath('logs'), filename);\n            let fileContent = FileHelper.readFileSync(filePath, 'utf8');\n\n            event.sender.send('app-log-file-loaded', {\n                fileContent: fileContent\n            });\n        });\n\n        /*\n         * Set zoom level \n         */\n        ipcMain.on('app-set-ui-zoom-level', function(event, zoomLevel) {\n            zoomLevel = parseFloat(zoomLevel);\n\n            if (!zoomLevel || zoomLevel < 0 || zoomLevel > 2.5) {\n                console.log('(!) Invalid zoom level: ', parseFloat(zoomLevel));\n                return;\n            }\n\n            let appConfig = FileHelper.readFileSync(appInstance.appConfigPath, 'utf8');\n\n            try {\n                appConfig = JSON.parse(appConfig);\n                appConfig.uiZoomLevel = zoomLevel;\n                fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(appConfig, null, 4));\n            } catch (e) {\n                console.log('(!) App was unable to save the UI zoom level');\n            }\n\n            appInstance.mainWindow.webContents.setZoomFactor(zoomLevel);\n        });\n\n        /**\n         * Set notifications center state\n         */\n        ipcMain.on('app-set-notifications-center-state', function(event, state) {\n            let appConfig = fs.readFileSync(appInstance.appConfigPath, 'utf8');\n\n            try {\n                appConfig = JSON.parse(appConfig);\n                appConfig.notificationsStatus = state;\n                fs.writeFileSync(appInstance.appConfigPath, JSON.stringify(appConfig, null, 4));\n            } catch (e) {\n                console.log('(!) App was unable to save the notifications center state');\n            }\n        });\n    }\n}\n\nmodule.exports = AppEvents;\n"
  },
  {
    "path": "app/back-end/events/author.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst Author = require('../author.js');\n\n/*\n * Events for the IPC communication regarding single tags\n */\n\nclass AuthorEvents {\n    constructor(appInstance) {\n        // Save\n        ipcMain.on('app-author-save', function (event, authorData) {\n            let author = new Author(appInstance, authorData);\n            let result = author.save();\n            event.sender.send('app-author-saved', result);\n        });\n\n        // Delete\n        ipcMain.on('app-author-delete', function (event, authorData) {\n            let result = false;\n\n            for(let i = 0; i < authorData.ids.length; i++) {\n                let author = new Author(appInstance, {\n                    site: authorData.site,\n                    id: authorData.ids[i]\n                });\n\n                result = author.delete();\n            }\n\n            event.sender.send('app-author-deleted', result);\n        });\n\n        // Cancelled edition\n        ipcMain.on('app-author-cancel', function(event, authorData) {\n            let author = new Author(appInstance, authorData);\n            let result = author.checkAndCleanImages(true);\n            event.sender.send('app-author-cancelled', result);\n        });\n    }\n}\n\nmodule.exports = AuthorEvents;\n"
  },
  {
    "path": "app/back-end/events/authors.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst Posts = require('../posts.js');\nconst Authors = require('../authors.js');\n\n/*\n * Events for the IPC communication regarding authors list\n */\n\nclass AuthorsEvents {\n    constructor(appInstance) {\n        // Load\n        ipcMain.on('app-tags-load', function (event, siteData) {\n            let postsData = new Posts(appInstance, siteData);\n            let authorsData = new Authors(appInstance, siteData);\n\n            event.sender.send('app-tags-loaded', {\n                authors: authorsData.load(),\n                postsAuthors: postsData.loadAuthorsXRef()\n            });\n        });\n    }\n}\n\nmodule.exports = AuthorsEvents;\n"
  },
  {
    "path": "app/back-end/events/backup.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst ipcMain = require('electron').ipcMain;\nconst Backup = require('../modules/backup/backup.js');\n\nclass BackupEvents {\n    constructor(appInstance) {\n        let self = this;\n        this.app = appInstance;\n        this.backupsLocation = this.app.appConfig.backupsLocation;\n\n        if (this.backupsLocation === '') {\n            this.backupsLocation = path.join(this.app.appDir, 'backups');\n        }\n\n        ipcMain.on('app-backups-list-load', function (event, siteData) {\n            if(siteData.site) {\n                self.loadBackupsList(siteData.site, event);\n            } else {\n                event.sender.send('app-backups-list-loaded', {\n                    status: false\n                });\n            }\n        });\n\n        ipcMain.on('app-backup-create', function(event, siteData) {\n            if(siteData.site) {\n                self.createBackup(siteData.site, siteData.filename, event);\n            } else {\n                event.sender.send('app-backup-created', {\n                    status: false\n                });\n            }\n        });\n\n        ipcMain.on('app-backup-remove', function(event, siteData) {\n            if(siteData.site && siteData.backupsNames) {\n                self.removeBackups(siteData.site, siteData.backupsNames, event);\n            } else {\n                event.sender.send('app-backup-removed', {\n                    status: false\n                });\n            }\n        });\n\n        ipcMain.on('app-backup-rename', function(event, siteData) {\n            if(siteData.site && siteData.oldBackupName && siteData.newBackupName) {\n                self.renameBackup(siteData.site, siteData.oldBackupName, siteData.newBackupName, event);\n            } else {\n                event.sender.send('app-backup-renamed', {\n                    status: false\n                });\n            }\n        });\n\n        ipcMain.on('app-backup-restore', function(event, siteData) {\n            if(siteData.site && siteData.backupName) {\n                self.restoreBackup(siteData.site, siteData.backupName, event);\n            } else {\n                event.sender.send('app-backup-restored', {\n                    status: false\n                });\n            }\n        });\n\n        ipcMain.on('app-backup-set-location', (event, newLocation) => {\n            this.backupsLocation = newLocation;\n\n            if (this.backupsLocation === '') {\n                this.backupsLocation = path.join(this.app.appDir, 'backups');\n            }\n        }); \n    }\n\n    loadBackupsList(siteName, event) {\n        let backups = Backup.loadList(siteName, this.backupsLocation);\n\n        event.sender.send('app-backups-list-loaded', {\n            status: true,\n            backups: backups\n        });\n    }\n\n    async removeBackups(siteName, backupsNames, event) {\n        let result = await Backup.remove(siteName, backupsNames, this.backupsLocation);\n\n        event.sender.send('app-backup-removed', {\n            status: result.status,\n            backups: result.backups\n        });\n    }\n\n    renameBackup(siteName, oldBackupName, newBackupName, event) {\n        let result = Backup.rename(siteName, oldBackupName, newBackupName, this.backupsLocation);\n\n        event.sender.send('app-backup-renamed', {\n            status: result.status,\n            backups: result.backups\n        });\n    }\n\n    async createBackup(siteName, filename, event) {\n        let backupsDir = this.backupsLocation;\n        let sourceDir = path.join(this.app.sitesDir, siteName);\n        let backupResult = await Backup.create(siteName, filename, backupsDir, sourceDir);\n\n        if (backupResult.type === 'app-backup-create-success') {\n            event.sender.send('app-backup-created', {\n                status: true,\n                backups: backupResult.backups\n            });\n        } else {\n            event.sender.send('app-backup-created', {\n                status: false,\n                error: backupResult.error\n            });\n        }\n    }\n\n    async restoreBackup(siteName, backupName, event) {\n        let backupsDir = this.backupsLocation;\n        let destinationDir = this.app.sitesDir;\n        let tempDir = path.join(this.app.appDir, 'temp');\n        let backupResult = await Backup.restore(siteName, backupName, backupsDir, destinationDir, tempDir, this.app);\n\n        if (backupResult.type === 'app-backup-restore-success') {\n            event.sender.send('app-backup-restored', {\n                status: true\n            });\n        } else {\n            event.sender.send('app-backup-restored', {\n                status: false,\n                error: backupResult.error\n            });\n        }\n    }\n}\n\nmodule.exports = BackupEvents;\n"
  },
  {
    "path": "app/back-end/events/content.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst Model = require('../model.js');\n\n/*\n * Events for the IPC communication regarding content items\n */\n\nclass ContentEvents {\n    constructor(appInstance) {\n        // Save\n        ipcMain.on('app-content-fields-update', function (event, contentData) {\n            let model = new Model(appInstance, {\n                site: contentData.site\n            });\n            \n            let result = model.updateField(contentData.table, contentData.itemID, contentData.fieldsToUpdate);\n            event.sender.send('app-content-fields-updated', result);\n        });\n    }\n}\n\nmodule.exports = ContentEvents;\n"
  },
  {
    "path": "app/back-end/events/credits.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst ipcMain = require('electron').ipcMain;\nconst FileHelper = require('../helpers/file.js');\n\n/*\n * Events for the IPC communication regarding credits\n */\n\nclass CreditsEvents {\n    constructor(appInstance) {\n        /*\n         * Load license text\n         */\n        ipcMain.on('app-license-load', function(event, config) {\n            let filePath = path.join(__dirname, '../../', config.url);\n            let licenseText = {\n                translation: 'core.credits.errorLoadingLicenseMsg'\n            }\n\n            if(fs.existsSync(filePath)) {\n                licenseText = FileHelper.readFileSync(filePath, 'utf-8');\n            }\n\n            event.sender.send('app-license-loaded', licenseText);\n        });\n    }\n}\n\nmodule.exports = CreditsEvents;\n"
  },
  {
    "path": "app/back-end/events/deploy.js",
    "content": "const fs = require('fs-extra');\nconst ipcMain = require('electron').ipcMain;\nconst Deployment = require('../modules/deploy/deployment.js');\nconst childProcess = require('child_process');\nconst stripTags = require('striptags');\n\nclass DeployEvents {\n    constructor(appInstance) {\n        let self = this;\n        this.app = appInstance;\n        this.deploymentProcess = false;\n        this.rendererProcess = false;\n\n        ipcMain.on('app-deploy-render', function (event, siteData) {\n            if(siteData.site && siteData.theme) {\n                self.renderSite(siteData.site, event);\n            } else {\n                event.sender.send('app-deploy-rendered', {\n                    status: false\n                });\n            }\n        });\n\n        ipcMain.on('app-deploy-render-abort', function(event, siteData) {\n            if(self.rendererProcess) {\n                try {\n                    self.rendererProcess.send({\n                        type: 'abort'\n                    });\n\n                    self.rendererProcess = false;\n                } catch(e) {\n                    console.log(e);\n                    self.rendererProcess = false;\n                }\n            }\n\n            event.sender.send('app-deploy-aborted', true);\n        });\n\n        ipcMain.on('app-deploy-upload', function(event, siteData) {\n            if(siteData.site) {\n                self.deploySite(siteData.site, siteData.password, event.sender);\n            } else {\n                event.sender.send('app-deploy-uploaded', {\n                    status: false\n                });\n            }\n        });\n\n        ipcMain.on('app-deploy-abort', function(event, siteData) {\n            if(self.deploymentProcess) {\n                try {\n                    self.deploymentProcess.send({\n                        type: 'abort'\n                    });\n\n                    self.deploymentProcess = false;\n                } catch(e) {\n                    console.log(e);\n                    self.deploymentProcess = false;\n                }\n            }\n\n            event.sender.send('app-deploy-aborted', true);\n        });\n\n        ipcMain.on('app-deploy-continue', function() {\n            if (self.deploymentProcess) {\n                try {\n                    self.deploymentProcess.send({\n                        type: 'continue-sync'\n                    });\n\n                    self.deploymentProcess = false;\n                } catch(e) {\n                    console.log(e);\n                    self.deploymentProcess = false;\n                }\n            }\n        });\n\n        ipcMain.on('app-deploy-test', async (event, data) => {\n            try {\n                await this.testConnection(data.deploymentConfig, data.siteName, data.uuid);\n            } catch (err) {\n                console.log('Test connection error:', err);\n            }\n        });\n    }\n\n    renderSite(site, event) {\n        this.rendererProcess = childProcess.fork(__dirname + '/../workers/renderer/preview', {\n            stdio: [\n                null,\n                fs.openSync(this.app.app.getPath('logs') + \"/rendering-deployment-process.log\", \"w\"),\n                fs.openSync(this.app.app.getPath('logs') + \"/rendering-deployment-errors.log\", \"w\"),\n                'ipc'\n            ]\n        });\n\n        this.rendererProcess.send({\n            type: 'dependencies',\n            appDir: this.app.appDir,\n            sitesDir: this.app.sitesDir,\n            siteConfig: this.app.sites[site],\n            itemID: false,\n            postData: false,\n            previewMode: false,\n            singlePageMode: false,\n            homepageOnlyMode: false,\n            tagOnlyMode: false,\n            authorOnlyMode: false,\n            previewLocation: this.app.appConfig.previewLocation\n        });\n\n        this.rendererProcess.on('message', function(data) {\n            if(data.type === 'app-rendering-results') {\n                if(data.result === true) {\n                    event.sender.send('app-deploy-rendered', {\n                        status: true\n                    });\n                } else {\n                    let errorDesc = {\n                        translation: 'core.rendering.renderingProcessCrashedMsg'\n                    };\n\n                    let errorTitle = {\n                        translation: 'core.rendering.renderingProcessCrashed'\n                    };\n\n                    if (data.result && data.result[0] && data.result[0].message) {\n                        errorTitle = {\n                            translation: 'core.rendering.renderingProcessFailed'\n                        };\n                        errorDesc = data.result[0].message + \"\\n\\n\" + data.result[0].desc;\n                    }\n\n                    event.sender.send('app-deploy-render-error', {\n                        message: [{\n                            message: errorTitle,\n                            desc: stripTags((errorDesc).toString())\n                        }]\n                    });\n                }\n            } else {\n                event.sender.send(data.type, {\n                    progress: data.progress,\n                    message: stripTags((data.message).toString())\n                });\n            }\n        });\n    }\n\n    deploySite(site, password, sender) {\n        let self = this;\n        let deploymentConfig = this.app.sites[site];\n        this.deploymentProcess = childProcess.fork(__dirname + '/../workers/deploy/deployment', {\n            stdio: [\n                null,\n                fs.openSync(this.app.app.getPath('logs') + \"/deployment-process.log\", \"w\"),\n                fs.openSync(this.app.app.getPath('logs') + \"/deployment-errors.log\", \"w\"),\n                'ipc'\n            ]\n        });\n\n        if(password !== false) {\n            deploymentConfig.deployment.password = password;\n        }\n\n        this.deploymentProcess.send({\n            type: 'dependencies',\n            appDir: this.app.appDir,\n            sitesDir: this.app.sitesDir,\n            siteConfig: deploymentConfig,\n            useFtpAlt: this.app.appConfig.experimentalFeatureAppFtpAlt\n        });\n\n        this.deploymentProcess.on('message', function(data) {\n            if (data.type === 'web-contents') {\n                if(data.value) {\n                    self.app.mainWindow.webContents.send(data.message, data.value);\n                } else {\n                    self.app.mainWindow.webContents.send(data.message);\n                }\n            }\n\n            if(data.type === 'sender') {\n                sender.send(data.message, data.value);\n            }\n        });\n    }\n\n    async testConnection(deploymentConfig, siteName, uuid) {\n        let deployment = new Deployment(\n            this.app.app.getPath('logs'), \n            this.app.sitesDir, \n            deploymentConfig, \n            this.app.appConfig.experimentalFeatureAppFtpAlt\n        );\n        await deployment.testConnection(this.app, deploymentConfig, siteName, uuid);\n    }\n}\n\nmodule.exports = DeployEvents;\n"
  },
  {
    "path": "app/back-end/events/file-manager.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst ipcMain = require('electron').ipcMain;\nconst isBinaryFileSync = require('isbinaryfile').isBinaryFileSync;\n\n/*\n * Events for the IPC communication regarding file manager\n */\n\nclass FileManagerEvents {\n    /**\n     * Creating an events instance\n     *\n     * @param appInstance\n     */\n    constructor (appInstance) {\n        let self = this;\n        this.app = appInstance;\n\n        /*\n         * List files in a specific directory\n         */\n        ipcMain.on('app-file-manager-list', function(event, config) {\n            self.listFiles(config, event.sender);\n        });\n\n        /*\n         * Upload file\n         */\n        ipcMain.on('app-file-manager-upload', function(event, config) {\n            self.uploadFile(config, event.sender);\n        });\n\n        /*\n         * Create file\n         */\n        ipcMain.on('app-file-manager-create', function(event, config) {\n            self.createFile(config, event.sender);\n        });\n\n        /*\n         * Delete files\n         */\n        ipcMain.on('app-file-manager-delete', function(event, config) {\n            self.deleteFiles(config, event.sender);\n        });\n\n        /*\n         * Check filename\n         */\n        ipcMain.on('app-file-manager-check-name', function(event, config) {\n            self.checkFilename(config, event.sender);\n        });\n    }\n\n    /**\n     * Listing files from a specific directory\n     *\n     * @param config\n     * @param sender\n     */\n    listFiles(config, sender) {\n        let siteName = config.siteName;\n        let dirPath = config.dirPath;\n        let basePath = path.join(this.app.sitesDir, siteName, 'input', dirPath);\n        let files = fs.readdirSync(basePath);\n        let output = [];\n        let iterator = 0;\n\n        for(let file of files) {\n            if(file === '.DS_Store' || file === 'Thumbs.db') {\n                continue;\n            }\n\n            let fullPath = path.join(basePath, file);\n            let fileStats = fs.statSync(fullPath);\n            let isDirectory = fileStats.isDirectory();\n\n            output.push({\n                name: file,\n                fullPath: fullPath,\n                icon: this.getIcon(path.parse(file).ext, isDirectory),\n                size: fileStats.size,\n                isBinary: false,\n                isCatalog: isDirectory,\n                createdAt: fileStats.ctime,\n                modifiedAt: fileStats.mtime\n            });\n        }\n\n        this.checkIfIsBinaryFile(output, 0, sender);\n    }\n\n    /**\n     * Checks if files are binary\n     *\n     * @param output\n     * @param iterator\n     * @param sender\n     */\n    checkIfIsBinaryFile(output, iterator, sender) {\n        if(!output.length || iterator >= output.length) {\n            sender.send('app-file-manager-listed', output);\n            return;\n        }\n\n        if (output[iterator] && output[iterator].fullPath && fs.lstatSync(output[iterator].fullPath).isFile()) {\n            output[iterator].isBinary = isBinaryFileSync(output[iterator].fullPath);\n        }\n\n        iterator++;\n        this.checkIfIsBinaryFile(output, iterator, sender);\n    }\n\n    /**\n     * Returns icon file string according to given extension\n     *\n     * @param extension\n     * @return icon string\n     */\n    getIcon(extension, isDirectory = false) {\n        if (isDirectory) {\n            return 'catalog';\n        }\n\n        extension = extension.replace('.', '');\n\n        switch(extension) {\n            case '':\n            case 'txt':\n            case 'rdf':\n                return 'txt';\n            case 'doc':\n                return 'doc';\n            case 'docx':\n                return 'docx';\n            case 'xls':\n                return 'xls';\n            case 'xlsx':\n                return 'xlsx';\n            case 'pdf':\n                return 'pdf';\n            case 'asp':\n            case 'aspx': \n            case 'cfm':\n            case 'cgi':\n            case 'pl':\n            case 'jsp':\n            case 'php':\n            case 'py':\n            case 'rss':\n            case 'xhtml':\n            case 'vue':\n            case 'scss':\n                return 'code';\n            case 'js':\n                return 'js'; \n            case 'css':\n                return 'css';\n            case 'html':\n                return 'html';\n            case 'htm':\n                return 'htm';\n            case 'xml':\n                return 'xml';\n            case 'webp':\n                return 'webp';\n            case 'bmp':\n                return 'img';\n            case 'tiff':\n                return 'tiff';\n            case 'avif':\n                return 'avif';\n            case 'jpg':\n                return 'jpg';\n            case 'jpeg':\n                return 'jpeg';\n            case 'png':\n                return 'png';\n            case 'svg':\n                return 'svg';\n            case 'gif':\n                return 'gif';\n            case 'webm':\n            case 'ogg': \n            case 'flv':\n            case 'wmv':\n            case 'm4v':\n            case '3gp':\n            case '3g2':\n            case 'mkv':\n            case 'mpg':\n            case 'mpeg':\n            case 'rm':\n            case 'swf':\n            case 'vob':\n                return 'video';\n            case 'avi':\n                return 'avi';\n            case 'mov':\n                return 'mov';\n            case 'mp4':\n                return 'mp4';\n            case 'tar':\n            case 'zip':            \n            case 'gz':\n            case 'iso':\n            case 'dmg':\n            case 'bz2':\n            case 'lz':          \n            case 'ace':\n            case 'apk':\n            case 'jar':\n                return 'zip';\n            case '7z':\n                return '7z';\n            case 'rar':\n                return 'rar';\n            case 'mp3':\n            case '3gp':\n            case 'aac':\n            case 'aax':\n            case 'flac':\n            case 'm4p':\n            case 'ogg':\n            case 'wav':\n            case 'wma':\n            case 'vox':\n                return 'music';            \n            default:\n                return 'unknown';\n        }\n    }\n\n    /**\n     * Move file from a given location to specified catalog\n     *\n     * @param config\n     * @param sender\n     */\n    uploadFile(config, sender) {\n        let siteName = config.siteName;\n        let dirPath = config.dirPath;\n        let fileToMove = config.fileToMove;\n        let fileName = path.parse(fileToMove).base;\n        let destinationPath = path.join(this.app.sitesDir, siteName, 'input', dirPath);\n        let fullPath = path.join(destinationPath, fileName);\n\n        if(fs.existsSync(fullPath)) {\n            sender.send('app-file-manager-uploaded', false);\n        }\n\n        fs.copySync(fileToMove, fullPath);\n        sender.send('app-file-manager-uploaded', true);\n    }\n\n    /**\n     * Create new file\n     *\n     * @param config\n     * @param sender\n     */\n    createFile(config, sender) {\n        let siteName = config.siteName;\n        let dirPath = config.dirPath;\n        let fileToSave = config.fileToSave;\n        let filePath = path.join(this.app.sitesDir, siteName, 'input', dirPath, fileToSave);\n\n        if(fs.existsSync(filePath)) {\n            sender.send('app-file-manager-created', false);\n            return;\n        }\n\n        fs.writeFileSync(filePath, '', {'encoding': 'utf8'})\n        sender.send('app-file-manager-created', true);\n    }\n\n    /**\n     * Delete files\n     *\n     * @param config\n     * @param sender\n     */\n    deleteFiles(config, sender) {\n        let siteName = config.siteName;\n        let dirPath = config.dirPath;\n        let filesToDelete = config.filesToDelete;\n\n        for(let file of filesToDelete) {\n            let fullPath = path.join(this.app.sitesDir, siteName, 'input', dirPath, file);\n\n            if(fs.existsSync(fullPath)) {\n                fs.unlinkSync(fullPath);\n            }\n        }\n\n        sender.send('app-file-manager-deleted', true);\n    }\n\n    /**\n     * Check if filename exists\n     *\n     * @param config\n     * @param sender\n     */\n    checkFilename(config, sender) {\n        let siteName = config.siteName;\n        let dirPath = config.dirPath;\n        let filenameToCheck = config.filenameToCheck;\n        let fullPath = path.join(this.app.sitesDir, siteName, 'input', dirPath, filenameToCheck);\n        let result = fs.existsSync(fullPath);\n\n        sender.send('app-file-manager-checked-name', result);\n    }\n}\n\nmodule.exports = FileManagerEvents;\n"
  },
  {
    "path": "app/back-end/events/image-uploader.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst ipcMain = require('electron').ipcMain;\nconst Image = require('../image.js');\nconst childProcess = require('child_process');\n\n/*\n * Events for the IPC communication regarding post images\n */\n\nclass ImageUploaderEvents {\n    constructor(appInstance) {\n        // Upload\n        ipcMain.on('app-image-upload', function (event, imageData) {\n            let imageProcess = childProcess.fork(__dirname + '/../workers/thumbnails/post-images');\n            imageProcess.send({\n                type: 'dependencies',\n                appInstance: {\n                    appConfig: appInstance.appConfig,\n                    appDir: appInstance.appDir,\n                    sitesDir: appInstance.sitesDir,\n                    db: appInstance.db\n                },\n                imageData: imageData\n            });\n\n            imageProcess.on('message', function(data) {\n                if(data.type === 'image-copied') {\n                    imageProcess.send({\n                        type: 'start-regenerating'\n                    });\n                } else if(data.type === 'finished') {\n                    event.sender.send('app-image-uploaded', data.result);\n                }\n            });\n        });\n\n        // Remove\n        ipcMain.on('app-image-upload-remove', function (event, filePath, siteName) {\n            let sitePath = path.join(appInstance.sitesDir, siteName);\n\n            if (filePath.indexOf('media/plugins/') === 0) {\n                filePath = path.join(sitePath, 'input', filePath);\n            }\n\n            if (filePath.indexOf(sitePath) !== 0) {\n                return;\n            }\n\n            if (fs.existsSync(filePath)) {\n                fs.unlinkSync(filePath);\n            }\n        });\n    }\n}\n\nmodule.exports = ImageUploaderEvents;\n"
  },
  {
    "path": "app/back-end/events/import.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst ipcMain = require('electron').ipcMain;\nconst Import = require('../modules/import/import.js');\nconst childProcess = require('child_process');\n\n/*\n * Events for the IPC communication regarding imports\n */\n\nclass ImportEvents {\n    /**\n     * Creating an events instance\n     *\n     * @param appInstance\n     */\n    constructor(appInstance) {\n        let self = this;\n        this.app = appInstance;\n\n        /*\n         * Import WXR file\n         */\n        ipcMain.on('app-wxr-check', function(event, config) {\n            self.checkFile(config.siteName, config.filePath, event.sender);\n        });\n\n        ipcMain.on('app-wxr-import', function(event, config) {\n            self.importFile(appInstance, config, event.sender);\n        });\n    }\n\n    /**\n     * Checking the WXR file\n     *\n     * @param siteName\n     * @param filePath\n     */\n    checkFile(siteName, filePath, sender) {\n        let importProcess = childProcess.fork(__dirname + '/../workers/import/check', {\n            stdio: [\n                null,\n                fs.openSync(this.app.app.getPath('logs') + \"/import-check-process.log\", \"w\"),\n                fs.openSync(this.app.app.getPath('logs') + \"/import-check-errors.log\", \"w\"),\n                'ipc'\n            ]\n        });\n\n        importProcess.send({\n            type: 'dependencies',\n            siteName: siteName,\n            filePath: filePath\n        });\n\n        importProcess.on('message', function(data) {\n            sender.send('app-wxr-checked', data);\n        });\n    }\n\n    /**\n     * Imports data from the WXR file\n     *\n     * @param appInstance\n     * @param config\n     */\n    importFile(appInstance, config, sender) {\n        let importProcess = childProcess.fork(__dirname + '/../workers/import/import', {\n            stdio: [\n                null,\n                fs.openSync(this.app.app.getPath('logs') + \"/import-process.log\", \"w\"),\n                fs.openSync(this.app.app.getPath('logs') + \"/import-errors.log\", \"w\"),\n                'ipc'\n            ]\n        });\n\n        importProcess.send({\n            type: 'dependencies',\n            appInstance: {\n                appDir: appInstance.appDir,\n                sitesDir: appInstance.sitesDir,\n                sites: appInstance.sites\n            },\n            siteName: config.siteName,\n            filePath: config.filePath,\n            importAuthors: config.importAuthors,\n            usedTaxonomy: config.usedTaxonomy,\n            autop: config.autop,\n            postTypes: config.postTypes\n        });\n\n        importProcess.on('message', function(data) {\n            if(data.type === 'result') {\n                sender.send('app-wxr-imported', data);\n            } else {\n                sender.send('app-wxr-import-progress', data);\n            }\n        });\n    }\n}\n\nmodule.exports = ImportEvents;\n"
  },
  {
    "path": "app/back-end/events/menu.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst ipcMain = require('electron').ipcMain;\n\n/*\n * Events for the IPC communication regarding menu events\n */\n\nclass MenuEvents {\n    constructor(appInstance) {\n        let self = this;\n        this.app = appInstance;\n\n        /*\n         * Save information about menu\n         */\n        ipcMain.on('app-menu-update', function (event, data) {\n            self.saveNewMenuStructure(data.menuStructure, data.siteName);\n            event.sender.send('app-menu-updated', true);\n        });\n    }\n\n    saveNewMenuStructure(menuStructure, siteName) {\n        let menuFile = path.join(this.app.sitesDir, siteName, 'input', 'config', 'menu.config.json');\n        fs.writeFileSync(menuFile, JSON.stringify(menuStructure, null, 4), { encoding: 'utf8' });\n    }\n}\n\nmodule.exports = MenuEvents;\n"
  },
  {
    "path": "app/back-end/events/notifications.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst path = require('path');\nconst UpdatesHelper = require('../helpers/updates.helper.js');\n\n/*\n * Events for the IPC communication regarding notifications\n */\n\nclass NotificationsEvents {\n    constructor(appInstance) {\n        // Save\n        ipcMain.on('app-notifications-retrieve', function(event, downloadNotifications) {\n            let platform;\n            \n            if (process.platform === 'darwin') {\n                platform = process.arch === 'arm64' ? 'mac-arm64' : 'mac-x86';\n            } else if (process.platform === 'win32') {\n                platform = 'win';\n            } else {\n                platform = 'linux';\n            }\n\n            let updatesHelper = new UpdatesHelper({\n                event: event,\n                filePath: path.join(appInstance.app.getPath('logs'), 'updates.json'),\n                url: 'https://notifications.getpublii.com/updates-' + platform + '.json',\n                forceDownload: downloadNotifications\n            });\n\n            updatesHelper.retrieve();\n        });\n\n        // Get notifications file\n        ipcMain.handle('app-get-notifications-file', async (event, fileName) => {\n            let filePath = path.join(appInstance.app.getPath('logs'), fileName);\n\n            try {\n                let data = await appInstance.app.readFile(filePath, 'utf8');\n                return JSON.parse(data);\n            } catch (error) {\n                console.error(`Error reading notifications file: ${error.message}`);\n                return false;\n            }\n        });\n    }\n}\n\nmodule.exports = NotificationsEvents;\n"
  },
  {
    "path": "app/back-end/events/page.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst FileHelper = require('../helpers/file.js');\nconst ipcMain = require('electron').ipcMain;\nconst Page = require('../page.js');\n\n/*\n * Events for the IPC communication regarding pages\n */\n\nclass PageEvents {\n    constructor(appInstance) {\n        this.app = appInstance;\n\n        // Load\n        ipcMain.on('app-page-load', function (event, pageData) {\n            let page = new Page(appInstance, pageData);\n            let result = page.load();\n            event.sender.send('app-page-loaded', result);\n        });\n\n        // Save\n        ipcMain.on('app-page-save', function (event, pageData) {\n            let page = new Page(appInstance, pageData);\n            let result = page.save();\n            event.sender.send('app-page-saved', result);\n        });\n\n        // Delete\n        ipcMain.on('app-page-delete', function (event, pageData) {\n            let result = false;\n\n            for(let i = 0; i < pageData.ids.length; i++) {\n                let page = new Page(appInstance, {\n                    site: pageData.site,\n                    id: pageData.ids[i]\n                });\n\n                result = page.delete();\n            }\n\n            event.sender.send('app-page-deleted', result);\n        });\n\n        // Delete\n        ipcMain.on('app-page-duplicate', function (event, pageData) {\n            let result = false;\n\n            for(let i = 0; i < pageData.ids.length; i++) {\n                let page = new Page(appInstance, {\n                    site: pageData.site,\n                    id: pageData.ids[i]\n                });\n\n                result = page.duplicate();\n            }\n\n            event.sender.send('app-page-duplicated', result);\n        });\n\n        // Status change\n        ipcMain.on('app-page-status-change', function (event, pageData) {\n            let result = false;\n\n            for(let i = 0; i < pageData.ids.length; i++) {\n                let page = new Page(appInstance, {\n                    site: pageData.site,\n                    id: pageData.ids[i]\n                });\n\n                result = page.changeStatus(pageData.status, pageData.inverse);\n            }\n\n            event.sender.send('app-page-status-changed', result);\n        });\n\n        // Cancelled edition\n        ipcMain.on('app-page-cancel', function(event, pageData) {\n            let page = new Page(appInstance, pageData);\n            let result = page.checkAndCleanImages(true);\n            event.sender.send('app-page-cancelled', result);\n        });\n\n        // Load pages hierarchy\n        ipcMain.on('app-pages-hierarchy-load', (event, siteName) => {\n            let pagesFile = path.join(this.app.sitesDir, siteName, 'input', 'config', 'pages.config.json');\n\n            if (fs.existsSync(pagesFile)) {\n                let pagesHierarchy = JSON.parse(FileHelper.readFileSync(pagesFile, { encoding: 'utf8' }));\n                pagesHierarchy = this.removeDuplicatedDataFromHierarchy(pagesHierarchy);\n                event.sender.send('app-pages-hierarchy-loaded', pagesHierarchy);\n            } else {\n                event.sender.send('app-pages-hierarchy-loaded', null);\n            }\n        });\n\n        // Save pages hierarchy\n        ipcMain.on('app-pages-hierarchy-save', (event, pagesData) => {\n            let pagesFile = path.join(this.app.sitesDir, pagesData.siteName, 'input', 'config', 'pages.config.json');\n            pagesData.hierarchy = this.removeNullDataFromHierarchy(pagesData.hierarchy);\n            pagesData.hierarchy = this.removeDuplicatedDataFromHierarchy(pagesData.hierarchy);\n            fs.writeFileSync(pagesFile, JSON.stringify(pagesData.hierarchy, null, 4), { encoding: 'utf8' });\n        });\n\n        // Update pages hierarchy during post conversion\n        ipcMain.on('app-pages-hierarchy-update', (event, conversionData) => {\n            let pagesFile = path.join(this.app.sitesDir, conversionData.siteName, 'input', 'config', 'pages.config.json');\n            let pagesHierarchy = JSON.parse(FileHelper.readFileSync(pagesFile, { encoding: 'utf8' }));\n\n            for (let i = 0; i < conversionData.postIDs.length; i++) {\n                pagesHierarchy.push({\n                    id: conversionData.postIDs[i],\n                    subpages: []\n                });\n            }\n            \n            pagesHierarchy = this.removeNullDataFromHierarchy(pagesHierarchy);\n            pagesHierarchy = this.removeDuplicatedDataFromHierarchy(pagesHierarchy);\n            fs.writeFileSync(pagesFile, JSON.stringify(pagesHierarchy, null, 4), { encoding: 'utf8' });\n        });\n    }\n\n    removeNullDataFromHierarchy (data) {\n        return data\n            .filter(item => item !== null)\n            .map(item => ({\n                ...item,\n                subpages: item.subpages ? this.removeNullDataFromHierarchy(item.subpages) : []\n            }));\n    }\n\n    removeDuplicatedDataFromHierarchy (data) {\n        let existingIds = new Set();\n\n        return data.filter(item => {\n            if (existingIds.has(item.id)) {\n                return false;\n            }\n\n            existingIds.add(item.id);\n            return true;\n        });      \n    }\n}\n\nmodule.exports = PageEvents;\n"
  },
  {
    "path": "app/back-end/events/plugin.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst Plugins = require('../plugins.js');\n\n/*\n * Events for the IPC communication regarding plugins\n */\n\nclass PluginEvents {\n    constructor(appInstance) {\n        // Get plugins status\n        ipcMain.on('app-site-get-plugins-state', function (event, data) {\n            let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir);\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginsStatus = pluginsInstance.getSiteSpecificPluginsState(siteName);\n            event.sender.send('app-site-plugins-state-loaded', pluginsStatus);\n        });\n\n        // Activate\n        ipcMain.on('app-site-plugin-activate', function (event, data) {\n            let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir);\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginName = data.pluginName.replace(/[\\/\\\\]/gmi, '');\n            let result = pluginsInstance.activatePlugin(siteName, pluginName);\n            event.sender.send('app-site-plugin-activated', result);\n        });\n\n        // Deactivate\n        ipcMain.on('app-site-plugin-deactivate', function (event, data) {\n            let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir);\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginName = data.pluginName.replace(/[\\/\\\\]/gmi, '');\n            let result = pluginsInstance.deactivatePlugin(siteName, pluginName);\n            event.sender.send('app-site-plugin-deactivated', result);\n        });\n\n        // Get plugin info and config\n        ipcMain.on('app-site-get-plugin-config', function (event, data) {\n            let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir);\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginName = data.pluginName.replace(/[\\/\\\\]/gmi, '');\n            let result = pluginsInstance.getPluginConfig(siteName, pluginName);\n            event.sender.send('app-site-get-plugin-config-retrieved', result);\n        });\n\n        // Save plugin config\n        ipcMain.on('app-site-save-plugin-config', function (event, data) {\n            let pluginsInstance = new Plugins(appInstance.appDir, appInstance.sitesDir);\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginName = data.pluginName.replace(/[\\/\\\\]/gmi, '');\n            let result = pluginsInstance.savePluginConfig(siteName, pluginName, data.newConfig);\n            event.sender.send('app-site-plugin-config-saved', result);\n        });\n    }\n}\n\nmodule.exports = PluginEvents;\n"
  },
  {
    "path": "app/back-end/events/plugins-api.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst fs = require('fs');\nconst path = require('path');\nconst FileHelper = require('../helpers/file.js');\n\n/*\n * Events for the IPC communication regarding plugins\n */\n\nclass PluginsApiEvents {\n    constructor (appInstance) {\n        // Read file in site\n        ipcMain.handle('app-plugins-api:read-config-file', function (event, data) {\n            let fileName = data.fileName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginName = data.pluginName.replace(/[\\/\\\\]/gmi, '');\n            let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName, fileName);\n\n            if (!fs.existsSync(filePath)) {\n                return false;  \n            }\n\n            let fileContent = FileHelper.readFileSync(filePath);\n            fileContent = fileContent.toString();\n            return fileContent;\n        });\n\n        // Read file in the languages\n        ipcMain.handle('app-plugins-api:read-language-file', function (event, data) {\n            let fileName = data.fileName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'languages', fileName);\n\n            if (!fs.existsSync(filePath)) { \n                return false;\n            }\n\n            let fileContent = FileHelper.readFileSync(filePath);\n            fileContent = fileContent.toString();\n            return fileContent;\n        });\n\n        // Read file in the themes\n        ipcMain.handle('app-plugins-api:read-theme-file', function (event, data) {\n            let fileName = data.fileName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let themeName = data.themeName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'themes', themeName, fileName);\n\n            if (!fs.existsSync(filePath)) {\n                return false;  \n            }\n\n            let fileContent = FileHelper.readFileSync(filePath);\n            fileContent = fileContent.toString();\n            return fileContent;\n        });\n\n        // Save file in site\n        ipcMain.handle('app-plugins-api:save-config-file', function (event, data) {\n            let fileName = data.fileName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginName = data.pluginName.replace(/[\\/\\\\]/gmi, '');\n            let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName, fileName);\n           \n            if (!fs.existsSync(path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName))) {\n                fs.mkdirSync(path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName), { recursive: true });\n            }\n\n            try {\n                fs.writeFileSync(filePath, data.fileContent);\n                return { status: 'FILE_SAVED' };\n            } catch (e) {\n                return { status: 'FILE_NOT_SAVED' };\n            }\n        });\n\n        // Save file in languages\n        ipcMain.handle('app-plugins-api:save-language-file', function (event, data) {\n            let fileName = data.fileName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let dirPath = path.join(appInstance.sitesDir, siteName, 'input', 'languages');\n            let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'languages', fileName);\n\n            if (!fs.existsSync(dirPath)) {\n                fs.mkdirSync(dirPath, { recursive: true });\n            }\n\n            try {\n                fs.writeFileSync(filePath, data.fileContent);\n                return { status: 'FILE_SAVED' };\n            } catch (e) {\n                return { status: 'FILE_NOT_SAVED' };\n            }\n        });\n\n        // Delete file in site\n        ipcMain.handle('app-plugins-api:delete-config-file', function (event, data) {\n            let fileName = data.fileName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let pluginName = data.pluginName.replace(/[\\/\\\\]/gmi, '');\n            let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'plugins', pluginName, fileName);\n           \n            if (!fs.existsSync(filePath)) {  \n                return { status: 'FILE_TO_REMOVE_NOT_EXISTS' };\n            }\n\n            try {\n                fs.unlinkSync(filePath);\n                return { status: 'FILE_REMOVED' };\n            } catch (e) {\n                return { status: 'FILE_NOT_REMOVED' };\n            }\n        });\n\n        // Delete file in site\n        ipcMain.handle('app-plugins-api:delete-language-file', function (event, data) {\n            let fileName = data.fileName.replace(/a-zA-Z0-9\\-\\_\\.\\*\\@\\+/gmi, '');\n            let siteName = data.siteName.replace(/[\\/\\\\]/gmi, '');\n            let filePath = path.join(appInstance.sitesDir, siteName, 'input', 'languages', fileName);\n           \n            if (!fs.existsSync(filePath)) {  \n                return { status: 'FILE_TO_REMOVE_NOT_EXISTS' };\n            }\n\n            try {\n                fs.unlinkSync(filePath);\n                return { status: 'FILE_REMOVED' };\n            } catch (e) {\n                return { status: 'FILE_NOT_REMOVED' };\n            }\n        });\n    }\n}\n\nmodule.exports = PluginsApiEvents;\n"
  },
  {
    "path": "app/back-end/events/post.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst Post = require('../post.js');\n\n/*\n * Events for the IPC communication regarding single tags\n */\n\nclass PostEvents {\n    constructor(appInstance) {\n        // Load\n        ipcMain.on('app-post-load', function (event, postData) {\n            let post = new Post(appInstance, postData);\n            let result = post.load();\n            event.sender.send('app-post-loaded', result);\n        });\n\n        // Save\n        ipcMain.on('app-post-save', function (event, postData) {\n            let post = new Post(appInstance, postData);\n            let result = post.save();\n            event.sender.send('app-post-saved', result);\n        });\n\n        // Delete\n        ipcMain.on('app-post-delete', function (event, postData) {\n            let result = false;\n\n            for(let i = 0; i < postData.ids.length; i++) {\n                let post = new Post(appInstance, {\n                    site: postData.site,\n                    id: postData.ids[i]\n                });\n\n                result = post.delete();\n            }\n\n            event.sender.send('app-post-deleted', result);\n        });\n\n        // Delete\n        ipcMain.on('app-post-duplicate', function (event, postData) {\n            let result = false;\n\n            for(let i = 0; i < postData.ids.length; i++) {\n                let post = new Post(appInstance, {\n                    site: postData.site,\n                    id: postData.ids[i]\n                });\n\n                result = post.duplicate();\n            }\n\n            event.sender.send('app-post-duplicated', result);\n        });\n\n        // Status change\n        ipcMain.on('app-post-status-change', function (event, postData) {\n            let result = false;\n\n            for(let i = 0; i < postData.ids.length; i++) {\n                let post = new Post(appInstance, {\n                    site: postData.site,\n                    id: postData.ids[i]\n                });\n\n                result = post.changeStatus(postData.status, postData.inverse);\n            }\n\n            event.sender.send('app-post-status-changed', result);\n        });\n\n        // Cancelled edition\n        ipcMain.on('app-post-cancel', function(event, postData) {\n            let post = new Post(appInstance, postData);\n            let result = post.checkAndCleanImages(true);\n            event.sender.send('app-post-cancelled', result);\n        });\n    }\n}\n\nmodule.exports = PostEvents;\n"
  },
  {
    "path": "app/back-end/events/preview.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst electron = require('electron');\nconst shell = electron.shell;\nconst ipcMain = electron.ipcMain;\nconst childProcess = require('child_process');\nconst UtilsHelper = require('../helpers/utils.js');\nconst stripTags = require('striptags');\n\nclass PreviewEvents {\n    /**\n     * Creates preview events\n     *\n     * @param appInstance\n     */\n    constructor(appInstance) {\n        let self = this;\n        this.app = appInstance;\n\n        ipcMain.on('app-preview-render', function (event, siteData) {\n            if (siteData.site && siteData.theme) {\n                let itemID = false;\n                let mode = false;\n                let postData = false;\n                let showPreview = true;\n\n                if (siteData.itemID !== false && typeof siteData.itemID !== 'undefined') {\n                    itemID = siteData.itemID;\n                }\n\n                if (siteData.mode !== false && typeof siteData.mode !== 'undefined') {\n                    mode = siteData.mode;\n                }\n\n                if (siteData.postData) {\n                    postData = siteData.postData;\n                }\n\n                if (typeof siteData.showPreview !== 'undefined') {\n                    showPreview = siteData.showPreview;\n                }\n\n                self.renderSite(siteData.site, itemID, postData, mode, event, showPreview);\n            } else {\n                event.sender.send('app-preview-rendered', {\n                    status: false\n                });\n            }\n        });\n    }\n\n    /**\n     * Renders website\n     *\n     * @param site\n     * @param pageToRender\n     * @param postData\n     * @param event\n     */\n    renderSite(site, itemID, postData, mode, event, showPreview) {\n        let self = this;\n        let previewMode = true;\n        let resultsRetrieved = false;\n        let rendererProcess = childProcess.fork(__dirname + '/../workers/renderer/preview', {\n            stdio: [\n                null,\n                fs.openSync(this.app.app.getPath('logs') + \"/rendering-process.log\", \"w\"),\n                fs.openSync(this.app.app.getPath('logs') + \"/rendering-errors.log\", \"w\"),\n                'ipc'\n            ]\n        });\n\n        rendererProcess.on('disconnect', function(data) {\n            setTimeout(function() {\n                if(!resultsRetrieved) {\n                    let errorDesc = {\n                        translation: 'core.rendering.renderingProcessCrashedMsg'\n                    };\n\n                    let errorTitle = {\n                        translation: 'core.rendering.renderingProcessCrashed'\n                    };\n\n                    if (data && data.result && data.result[0] && data.result[0].message) {\n                        errorTitle = {\n                            translation: 'core.rendering.renderingProcessFailed'\n                        };\n                        errorDesc = stripTags((data.result[0].message + \"\\n\\n\" + data.result[0].desc).toString());\n                    }\n\n                    event.sender.send('app-preview-render-error', {\n                        message: [{\n                            message: errorTitle,\n                            desc: errorDesc\n                        }]\n                    });\n                }\n            }, 1000);\n        });\n\n        rendererProcess.send({\n            type: 'dependencies',\n            appDir: this.app.appDir,\n            sitesDir: this.app.sitesDir,\n            siteConfig: this.app.sites[site],\n            itemID: itemID,\n            postData: postData,\n            previewMode: previewMode,\n            mode: mode,\n            previewLocation: this.app.appConfig.previewLocation\n        });\n\n        rendererProcess.on('message', function(data) {\n            resultsRetrieved = true;\n\n            if(data.type === 'app-rendering-results') {\n                if(data.result === true) {\n                    event.sender.send('app-preview-rendered', {\n                        status: true\n                    });\n\n                    if (showPreview) {\n                        self.showPreview(site, mode);\n                    }\n                } else {\n                    let errorDesc = 'core.rendering.renderingProcessCrashedMsg';\n                    let errorTitle = 'core.rendering.renderingProcessCrashed';\n\n                    if (data.result && data.result[0] && data.result[0].message) {\n                        errorTitle = {\n                            translation: 'core.rendering.renderingProcessFailed'\n                        };\n                        errorDesc = stripTags((data.result[0].message + \"\\n\\n\" + data.result[0].desc).toString());\n                    }\n\n                    event.sender.send('app-preview-render-error', {\n                        message: [{\n                            message: errorTitle,\n                            desc: errorDesc\n                        }]\n                    });\n                }\n            } else {\n                event.sender.send(data.type, {\n                    progress: data.progress,\n                    message: stripTags((data.message).toString())\n                });\n            }\n        });\n    }\n\n    /**\n     * Displays preview\n     *\n     * @param siteData\n     */\n    showPreview (siteName, mode) {\n        let basePath = path.join(this.app.sitesDir, siteName, 'preview');\n        let previewLocation = '';\n\n        if(this.app.appConfig.previewLocation) {\n            previewLocation = this.app.appConfig.previewLocation.trim();\n        }\n\n        let url = '';\n\n        if(previewLocation !== '' && UtilsHelper.dirExists(previewLocation)) {\n            basePath = previewLocation;\n        }\n\n        url = path.join(basePath, 'index.html');\n\n        if (mode === 'tag' || mode === 'post' || mode === 'page' || mode === 'author') {\n            url = path.join(basePath, 'preview.html');\n        }\n\n        setTimeout(function() {\n            shell.openExternal('file:///' + url);\n        }, 1000);\n    }\n}\n\nmodule.exports = PreviewEvents;\n"
  },
  {
    "path": "app/back-end/events/site.js",
    "content": "const fs = require('fs-extra');\nconst os = require('os');\nconst path = require('path');\nconst FileHelper = require('../helpers/file.js');\nconst slug = require('./../helpers/slug');\nconst passwordSafeStorage = require('keytar');\nconst ipcMain = require('electron').ipcMain;\nconst Site = require('../site.js');\nconst Themes = require('../themes.js');\nconst Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3');\nconst DBUtils = require('../helpers/db.utils.js');\nconst UtilsHelper = require('../helpers/utils.js');\nconst normalizePath = require('normalize-path');\nconst URLHelper = require('../modules/render-html/helpers/url.js');\n\n/*\n * Events for the IPC communication regarding single sites\n */\n\nclass SiteEvents {\n    constructor(appInstance) {\n        let self = this;\n        this.regenerateProcess = false;\n\n        /*\n         * Reload site config and data\n         */\n        ipcMain.on('app-site-reload', (event, config) => {\n            let result = appInstance.reloadSite(config.siteName);\n            let language = this.getSiteLanguage(appInstance, config.siteName);\n            this.setSpellcheckerLanguage (appInstance, language);\n            event.sender.send('app-site-reloaded', result);\n        });\n\n        /*\n         * Save site config\n         */\n        ipcMain.on('app-site-config-save', async function (event, config) {\n            let siteName = '';\n            let siteNames = Object.keys(appInstance.sites);\n            let thumbnailsRegenerateRequired = false;\n\n            if (siteNames.indexOf(config.site) !== -1) {\n                siteName = config.site;\n            } else {\n                event.sender.send('app-site-config-saved', {\n                    status: false,\n                    message: 'site-not-exists'\n                });\n            }\n\n            if (config.source === 'server') {\n                self.removeGitConfigDirectory(appInstance, config.site);\n            }\n\n            // Prepare settings\n            config.settings.name = slug(config.settings.name);\n            config.settings.advanced.urls.postsPrefix = slug(config.settings.advanced.urls.postsPrefix);\n            config.settings.advanced.urls.tagsPrefix = slug(config.settings.advanced.urls.tagsPrefix);\n            config.settings.advanced.urls.authorsPrefix = slug(config.settings.advanced.urls.authorsPrefix);\n            config.settings.advanced.urls.pageName = slug(config.settings.advanced.urls.pageName);\n            config.settings.advanced.urls.errorPage = slug(config.settings.advanced.urls.errorPage, true);\n            config.settings.advanced.urls.searchPage = slug(config.settings.advanced.urls.searchPage, true);\n\n            // If user changed the site name\n            if (\n                config.settings.name !== '' &&\n                siteName !== '' &&\n                slug(config.settings.name) !== slug(config.site)\n            ) {\n                // Check if new site name is unique\n                if (\n                    !fs.existsSync(path.join(appInstance.sitesDir, config.settings.name)) &&\n                    slug(config.settings.displayName) === config.settings.name\n                ) {\n                    if (appInstance.db) {\n                        try {\n                            appInstance.db.close();\n                        } catch (e) {\n                            console.log('[SITE NAME CHANGE] DB already closed');\n                        }\n                    }\n\n                    // If yes - rename the dir\n                    delete appInstance.sites[siteName];\n                    siteName = config.settings.name;\n                    fs.renameSync(\n                        path.join(appInstance.sitesDir, config.site),\n                        path.join(appInstance.sitesDir, config.settings.name)\n                    );\n\n                    let dbPath = path.join(appInstance.sitesDir, config.settings.name, 'input', 'db.sqlite');\n                    appInstance.db = new DBUtils(new Database(dbPath));\n\n                    // Rename also the backups directory\n                    let backupsDir = appInstance.appConfig.backupsLocation;\n\n                    if(backupsDir) {\n                        let backupsLocation = path.join(backupsDir, config.site);\n                        let newBackupsLocation = path.join(backupsDir, config.settings.name);\n\n                        if (UtilsHelper.dirExists(backupsLocation)) {\n                            fs.renameSync(\n                                backupsLocation,\n                                newBackupsLocation\n                            );\n                        }\n                    }\n                } else {\n                    // If no - return error\n                    event.sender.send('app-site-config-saved', {\n                        status: false,\n                        message: 'duplicated-name'\n                    });\n\n                    return;\n                }\n            }\n\n            // Check for empty names\n            if (\n                siteName === '' ||\n                config.settings.name === ''\n            ) {\n                event.sender.send('app-site-config-saved', {\n                    status: false,\n                    message: 'empty-name'\n                });\n\n                return;\n            }\n\n            let configFile = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'site.config.json');\n            let oldConfig = FileHelper.readFileSync(configFile, 'utf8');\n            let themesPath = path.join(appInstance.sitesDir, siteName, 'input', 'themes');\n            let newThemeConfig = {};\n            oldConfig = JSON.parse(oldConfig);\n\n            if (config.settings.theme === '' && oldConfig.theme) {\n                config.settings.theme = oldConfig.theme;\n            } else {\n                let themes = new Themes(appInstance, {\n                    site: siteName\n                });\n\n                if(self.prepareThemeName(config.settings.theme) !== oldConfig.theme) {\n                    thumbnailsRegenerateRequired = true;\n                }\n\n                config.settings.theme = themes.changeTheme(config.settings.theme, oldConfig.theme);\n                let themeConfigPath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'theme.config.json');\n                let themePath = path.join(themesPath, config.settings.theme);\n                newThemeConfig = Themes.loadThemeConfig(themeConfigPath, themePath);\n            }\n\n\n            if (\n                oldConfig.advanced && (\n                    (oldConfig.advanced.responsiveImages !== config.settings.advanced.responsiveImages) ||\n                    (oldConfig.advanced.imagesQuality !== config.settings.advanced.imagesQuality) || \n                    (oldConfig.advanced.alphaQuality !== config.settings.advanced.alphaQuality) || \n                    (oldConfig.advanced.forceWebp !== config.settings.advanced.forceWebp) ||\n                    (oldConfig.advanced.webpLossless !== config.settings.advanced.webpLossless)\n                )\n            ) {\n                thumbnailsRegenerateRequired = true;\n            }\n\n            if(\n                config.settings.advanced &&\n                config.settings.advanced.openGraphImage &&\n                config.settings.advanced.openGraphImage !== ''\n            ) {\n                let filename = path.parse(config.settings.advanced.openGraphImage);\n                config.settings.advanced.openGraphImage = filename.base;\n            }\n\n            let passwordData = false;\n            let passwordGitData = false;\n            let passphraseData = false;\n            let s3IdData = false;\n            let s3KeyData = false;\n            let ghTokenData = false;\n            let glTokenData = false;\n            let netlifyIdData = false;\n            let netlifyTokenData = false;\n            let siteID = slug(config.settings.name);\n\n            if (config.settings.uuid) {\n                siteID = config.settings.uuid;\n            }\n\n            // Save the password in the keychain\n            if (passwordSafeStorage) {\n                try {\n                    if (\n                        config.settings.deployment.password !== '' && \n                        config.settings.deployment.password !== 'publii ' + siteID\n                    ) {\n                        passwordData = await self.loadPassword(\n                            config.settings,\n                            'publii',\n                            config.settings.deployment.password\n                        );\n                        config.settings.deployment.password = passwordData.toSave;\n                    }\n\n                    if (\n                        config.settings.deployment.git.password !== '' && \n                        config.settings.deployment.git.password !== 'publii-git-password ' + siteID\n                    ) {\n                        passwordGitData = await self.loadPassword(\n                            config.settings,\n                            'publii-git-password',\n                            config.settings.deployment.git.password\n                        );\n                        config.settings.deployment.git.password = passwordGitData.toSave;\n                    }\n\n                    if (\n                        config.settings.deployment.passphrase !== '' && \n                        config.settings.deployment.passphrase !== 'publii-passphrase ' + siteID\n                    ) {\n                        passphraseData = await self.loadPassword(\n                            config.settings,\n                            'publii-passphrase',\n                            config.settings.deployment.passphrase\n                        );\n                        config.settings.deployment.passphrase = passphraseData.toSave;\n                    }\n\n                    if (\n                        config.settings.deployment.s3.id !== '' && \n                        config.settings.deployment.s3.key !== '' && \n                        config.settings.deployment.s3.id !== 'publii-s3-id ' + siteID\n                    ) {\n                        s3IdData = await self.loadPassword(\n                            config.settings,\n                            'publii-s3-id',\n                            config.settings.deployment.s3.id\n                        );\n                        s3KeyData = await self.loadPassword(\n                            config.settings,\n                            'publii-s3-key',\n                            config.settings.deployment.s3.key\n                        );\n                        config.settings.deployment.s3.id = s3IdData.toSave;\n                        config.settings.deployment.s3.key = s3KeyData.toSave;\n                    }\n\n                    if (\n                        config.settings.deployment.github.token !== '' &&\n                        config.settings.deployment.github.token !== 'publii-gh-token ' + siteID\n                    ) {\n                        ghTokenData = await self.loadPassword(\n                            config.settings,\n                            'publii-gh-token',\n                            config.settings.deployment.github.token\n                        );\n                        config.settings.deployment.github.token = ghTokenData.toSave;\n                    }\n\n                    if (\n                        config.settings.deployment.gitlab.token !== '' && \n                        config.settings.deployment.gitlab.token !== 'publii-gl-token ' + siteID\n                    ) {\n                        glTokenData = await self.loadPassword(\n                            config.settings,\n                            'publii-gl-token',\n                            config.settings.deployment.gitlab.token\n                        );\n                        config.settings.deployment.gitlab.token = glTokenData.toSave;\n                    }\n\n                    if (\n                        config.settings.deployment.netlify.id !== '' && \n                        config.settings.deployment.netlify.token !== '' &&\n                        config.settings.deployment.netlify.token !== 'publii-netlify-token ' + siteID\n                    ) {\n                        netlifyIdData = await self.loadPassword(\n                            config.settings,\n                            'publii-netlify-id',\n                            config.settings.deployment.netlify.id\n                        );\n                        netlifyTokenData = await self.loadPassword(\n                            config.settings,\n                            'publii-netlify-token',\n                            config.settings.deployment.netlify.token\n                        );\n                        config.settings.deployment.netlify.id = netlifyIdData.toSave;\n                        config.settings.deployment.netlify.token = netlifyTokenData.toSave;\n                    }\n                } catch (error) {\n                    event.sender.send('app-site-config-saved', {\n                        status: false,\n                        message: 'no-keyring'\n                    });\n\n                    return;\n                }\n            }\n\n            // Save config\n            fs.writeFileSync(configFile, JSON.stringify(config.settings, null, 4));\n\n            if(passwordData && passwordData.newPassword) {\n                config.settings.deployment.password = passwordData.newPassword;\n            }\n\n            if(passwordGitData && passwordGitData.newPassword) {\n                config.settings.deployment.git.password = passwordGitData.newPassword;\n            }\n\n            if(passphraseData && passphraseData.newPassword) {\n                config.settings.deployment.passphrase = passphraseData.newPassword;\n            }\n\n            if(s3IdData && s3IdData.newPassword) {\n                config.settings.deployment.s3.id = s3IdData.newPassword;\n            }\n\n            if(s3KeyData && s3KeyData.newPassword) {\n                config.settings.deployment.s3.key = s3KeyData.newPassword;\n            }\n\n            if(ghTokenData && ghTokenData.newPassword) {\n                config.settings.deployment.github.token = ghTokenData.newPassword;\n            }\n\n            if(netlifyIdData && netlifyIdData.newPassword) {\n                config.settings.deployment.netlify.id = netlifyIdData.newPassword;\n            }\n\n            if(netlifyTokenData && netlifyTokenData.newPassword) {\n                config.settings.deployment.netlify.token = netlifyTokenData.newPassword;\n            }\n\n            appInstance.sites[config.settings.name] = config.settings;\n\n            let themesHelper = new Themes(appInstance, { site: siteName });\n            let themeConfigPath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'theme.config.json');\n\n            if (fs.existsSync(themeConfigPath)) {\n                let themeConfigString = FileHelper.readFileSync(themeConfigPath, 'utf8');\n                themesHelper.checkAndCleanImages(themeConfigString);\n            }\n\n            // Send success message\n            event.sender.send('app-site-config-saved', {\n                status: true,\n                siteName: siteName,\n                message: 'success-save',\n                themeName: config.settings.theme,\n                newThemeConfig: newThemeConfig,\n                thumbnailsRegenerateRequired: thumbnailsRegenerateRequired\n            });\n        });\n\n        /*\n         * Switch website\n         */\n        ipcMain.on('app-site-switch', (event, config) => {\n            let result = appInstance.switchSite(config.site);\n            let language = this.getSiteLanguage(appInstance, config.site);\n            this.setSpellcheckerLanguage (appInstance, language);\n            event.sender.send('app-site-switched', result);\n        });\n\n        /*\n         * Refresh website data\n         */\n        ipcMain.on('app-site-refresh', function (event, config) {\n            let result = appInstance.switchSite(config.site);\n            event.sender.send('app-site-refreshed', result);\n        });\n\n        /*\n         * Save site theme config\n         */\n        ipcMain.on('app-site-theme-config-save', function (event, data) {\n            let siteData = {\n                site: data.site\n            };\n            let newConfig = data.config;\n            let themeName = data.theme;\n            let themePath = path.join(appInstance.sitesDir, data.site, 'input', 'themes', themeName);\n            let themeConfigPath = path.join(appInstance.sitesDir, data.site, 'input', 'config', 'theme.config.json');\n            let themesHelper = new Themes(appInstance, siteData);\n            themesHelper.updateThemeConfig(newConfig);\n            let themeConfig = Themes.loadThemeConfig(themeConfigPath, themePath);\n\n            event.sender.send('app-site-theme-config-saved', {\n                status: true,\n                newConfig: {\n                    config: themeConfig.config,\n                    customConfig: themeConfig.customConfig,\n                    pageConfig: themeConfig.pageConfig,\n                    postConfig: themeConfig.postConfig,\n                    tagConfig: themeConfig.tagConfig,\n                    authorConfig: themeConfig.authorConfig,\n                    defaultTemplates: themeConfig.defaultTemplates\n                }\n            });\n        });\n\n        /*\n         * Create new website\n         */\n        ipcMain.on('app-site-create', function (event, config, authorName) {\n            config.name = slug(config.name);\n\n            if (config.name.trim() === '') {\n                event.sender.send('app-site-creation-error', {\n                    name: config.name.trim() === '',\n                    author: slug(authorName).trim() === ''\n                });\n\n                return;\n            }\n\n            let site = new Site(appInstance, config);\n            let result = site.create(authorName);\n\n            if (result === 'db-error') {\n                event.sender.send('app-site-creation-db-error');\n                return;\n            }\n\n            if (result === 'duplicate') {\n                event.sender.send('app-site-creation-duplicate');\n                return;\n            }\n\n            config.uuid = site.uuid;\n            config.theme = 'simple';\n            appInstance.sites[config.name] = config;\n\n            // Load newly created db\n            let siteDir = path.join(appInstance.sitesDir, config.name);\n            let dbPath = path.join(siteDir, 'input', 'db.sqlite');\n\n            if (appInstance.db) {\n                try {\n                    appInstance.db.close();\n                } catch (e) {\n                    console.log('[SITE CREATION] DB already closed');\n                }\n            }\n\n            appInstance.db = new DBUtils(new Database(dbPath));\n\n            result = {\n                siteConfig: appInstance.sites[config.name],\n                siteDir: siteDir,\n                authorName: authorName\n            };\n\n            event.sender.send('app-site-created', result);\n        });\n\n        /*\n         * Regenerate thumbnails\n         */\n        ipcMain.on('app-site-regenerate-thumbnails', function(event, config) {\n            let site = new Site(appInstance, config, true);\n            self.regenerateProcess = site.regenerateThumbnails(event.sender);\n        });\n\n        ipcMain.on('app-site-abort-regenerate-thumbnails', function(event, config) {\n            if (self.regenerateProcess) {\n                self.regenerateProcess.send({\n                    type: 'abort'\n                });\n                \n                self.regenerateProcess = false;\n            }\n        });\n\n        /*\n         * Regenerate thumbnails stauts\n         */\n        ipcMain.on('app-site-regenerate-thumbnails-required', function(event, config) {\n            let site = new Site(appInstance, config, true);\n            site.regenerateThumbnailsIsRequired(event.sender);\n        });\n\n        /*\n         * Delete website\n         */\n        ipcMain.on('app-site-delete', function (event, config) {\n            Site.delete(appInstance, config.site);\n            delete appInstance.sites[config.site];\n            event.sender.send('app-site-deleted', true);\n        });\n\n        /*\n         * Clone website\n         */\n        ipcMain.on('app-site-clone', function (event, config) {\n            let clonedWebsiteData = Site.clone(appInstance, config.catalogName, config.siteName);\n            event.sender.send('app-site-cloned', clonedWebsiteData);\n        });\n\n        /*\n         * Save custom CSS\n         */\n        ipcMain.on('app-site-css-save', function (event, config) {\n            Site.saveCustomCSS(appInstance, config.site, config.code);\n            event.sender.send('app-site-css-saved', true);\n        });\n\n        /*\n         * Load custom CSS\n         */\n        ipcMain.on('app-site-css-load', function (event, config) {\n            let customCSS = Site.loadCustomCSS(appInstance, config.site);\n            event.sender.send('app-site-css-loaded', customCSS);\n        });\n\n        /*\n         * Check website catalog name\n         */\n        ipcMain.on('app-site-check-website-to-restore', async function (event, config) {\n            let result = await Site.checkWebsiteBackup(appInstance, config.backupPath);\n            event.sender.send('app-site-backup-checked', result);\n        });\n\n        /*\n         * Check website catalog availability\n         */\n        ipcMain.on('app-site-check-website-catalog-availability', function (event, config) {\n            let result = Site.checkWebsiteCatalogAvailability(appInstance, config.siteName);\n            event.sender.send('app-site-website-catalog-availability-checked', result);\n        });\n\n        /*\n         * Remove temp backup files \n         */\n        ipcMain.on('app-site-remove-temporary-backup-files', function (event, config) {\n            let tempBackupDir = path.join(appInstance.appDir, 'temp', 'backup-to-restore');\n\n            if (fs.existsSync(tempBackupDir)) {\n                fs.emptyDirSync(tempBackupDir);\n            }\n        });\n\n        /*\n         * Restore website from backup\n         */\n        ipcMain.on('app-site-restore-from-backup', function (event, config) {\n            let result = Site.restoreFromBackup(appInstance, config.siteName);\n            event.sender.send('app-site-restored-from-backup', result);\n        });\n    }\n\n    prepareThemeName(themeName) {\n        if(!themeName) {\n            return false;\n        }\n\n        return themeName.replace('install-use-', '')\n                        .replace('uninstall-', '')\n                        .replace('use-', '');\n    }\n\n    async loadPassword(settings, type, newPassword) {\n        let account = slug(settings.name);\n\n        if (settings.uuid) {\n            account = settings.uuid;\n        }\n\n        if (!settings.deployment.askforpassword || type !== 'publii') {\n            let existingPassword = await passwordSafeStorage.getPassword(type, account);\n\n            if (newPassword !== '') {\n                if (newPassword === 'publii ' + account) {\n                    newPassword = existingPassword;\n                } else {\n                    await passwordSafeStorage.setPassword(type, account, newPassword);\n                }\n            } else if (existingPassword !== null) {\n                // Remove the password from the storage if it still exists\n                // and user provided an empty password now\n                await passwordSafeStorage.deletePassword(type, account);\n            }\n\n            return {\n                newPassword: newPassword,\n                toSave: type + ' ' + account\n            };\n        }\n\n        await passwordSafeStorage.deletePassword(type, account);\n\n        return {\n            newPassword: '',\n            toSave: ''\n        };\n    }\n\n    getSiteLanguage (appInstance, siteName) {\n        if (process.platform === 'darwin' || !siteName) {\n            return 'null';\n        }\n\n        let configPath = path.join(appInstance.sitesDir, siteName, 'input', 'config', 'site.config.json');\n        let config = FileHelper.readFileSync(configPath, 'utf8');\n        \n        try {\n            config = JSON.parse(config);\n\n            if (config.language) {\n                if (config.language === 'custom') {\n                    return config.customLanguage;\n                }\n\n                return config.language;\n            }\n        } catch (e) {\n            console.log('(!) An error occurred during detecting site language');\n        }\n\n        return 'null';\n    }\n\n    setSpellcheckerLanguage (appInstance, language) {\n        if (process.platform === 'darwin') {\n            return;\n        }\n\n        let availableLanguages = appInstance.mainWindow.webContents.session.availableSpellCheckerLanguages;\n        language = language.toLocaleLowerCase();\n        language = language.split('-');\n\n        if (language[1]) {\n            language = language[0] + '-' + language[1].toLocaleUpperCase();\n        } else {\n            language = language[0];\n        }\n\n        if (availableLanguages.indexOf(language) > -1) {\n            appInstance.mainWindow.webContents.session.setSpellCheckerLanguages([language]);\n            console.log('Set spellchecker to:', language);\n            return;\n        }\n\n        language = language.split('-');\n        language = language[0];\n\n        if (availableLanguages.indexOf(language) > -1) {\n            appInstance.mainWindow.webContents.session.setSpellCheckerLanguages([language]);\n            console.log('Set spellchecker to:', language);\n            return;\n        }\n\n        console.log('(!) Unable to set spellchecker to use selected language - ' + language);\n    }\n\n    removeGitConfigDirectory (appInstance, siteName) {\n        let gitDirPath = path.join(appInstance.sitesDir, siteName, 'output', '.git');\n\n        if (UtilsHelper.dirExists(gitDirPath)) {\n            fs.removeSync(gitDirPath);\n        }\n    }\n}\n\nmodule.exports = SiteEvents;\n"
  },
  {
    "path": "app/back-end/events/sync.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('../helpers/file.js');\nconst ipcMain = require('electron').ipcMain;\n\n/*\n * Events for the IPC communication regarding sync events\n */\n\nclass SyncEvents {\n    constructor(appInstance) {\n        let self = this;\n        this.app = appInstance;\n\n        /*\n         * Save information about site that there are no\n         * operations to sync\n         */\n        ipcMain.on('app-sync-is-done', function (event, config) {\n            self.saveSyncStatus('synced', config.site);\n            event.sender.send('app-sync-is-done-saved', true);\n        });\n    }\n\n    saveSyncStatus(status, siteName) {\n        let configFile = path.join(this.app.sitesDir, siteName, 'input', 'config', 'site.config.json');\n        let configContent = FileHelper.readFileSync(configFile, 'utf8');\n        configContent = JSON.parse(configContent);\n        configContent.synced = status;\n\n        if(status === 'synced') {\n            configContent.syncDate = Date.now();\n        }\n\n        fs.writeFileSync(configFile, JSON.stringify(configContent, null, 4));\n    }\n}\n\nmodule.exports = SyncEvents;\n"
  },
  {
    "path": "app/back-end/events/tag.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst Tag = require('../tag.js');\n\n/*\n * Events for the IPC communication regarding single tags\n */\n\nclass TagEvents {\n    constructor(appInstance) {\n        // Save\n        ipcMain.on('app-tag-save', function (event, tagData) {\n            let tag = new Tag(appInstance, tagData);\n            let result = tag.save();\n            event.sender.send('app-tag-saved', result);\n        });\n\n        // Delete\n        ipcMain.on('app-tag-delete', function (event, tagData) {\n            let result = false;\n\n            for(let i = 0; i < tagData.ids.length; i++) {\n                let tag = new Tag(appInstance, {\n                    site: tagData.site,\n                    id: tagData.ids[i]\n                });\n\n                result = tag.delete();\n            }\n\n            event.sender.send('app-tag-deleted', result);\n        });\n\n        // Status change\n        ipcMain.on('app-tags-status-change', function (event, tagData) {\n            let result = false;\n\n            for(let i = 0; i < tagData.ids.length; i++) {\n                let tag = new Tag(appInstance, {\n                    site: tagData.site,\n                    id: tagData.ids[i]\n                });\n\n                result = tag.changeStatus(tagData.status, tagData.inverse);\n            }\n\n            event.sender.send('app-tags-status-changed', result);\n        });\n\n        // Cancelled edition\n        ipcMain.on('app-tag-cancel', function(event, tagData) {\n            let tag = new Tag(appInstance, tagData);\n            let result = tag.checkAndCleanImages(true);\n            event.sender.send('app-tag-cancelled', result);\n        });\n    }\n}\n\nmodule.exports = TagEvents;\n"
  },
  {
    "path": "app/back-end/events/tags.js",
    "content": "const ipcMain = require('electron').ipcMain;\nconst Posts = require('../posts.js');\nconst Tags = require('../tags.js');\n\n/*\n * Events for the IPC communication regarding tags list\n */\n\nclass TagsEvents {\n    constructor(appInstance) {\n        // Load\n        ipcMain.on('app-tags-load', function (event, siteData) {\n            let postsData = new Posts(appInstance, siteData);\n            let tagsData = new Tags(appInstance, siteData);\n\n            event.sender.send('app-tags-loaded', {\n                tags: tagsData.load(),\n                postsTags: postsData.loadTagsXRef()\n            });\n        });\n    }\n}\n\nmodule.exports = TagsEvents;\n"
  },
  {
    "path": "app/back-end/helpers/app-files.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst Utils = require('./utils.js');\n\n/**\n * Helper class used to manage app-related files\n */\nclass AppFilesHelper {\n    /**\n     * Constructor of the helper\n     *\n     * @param appInstance\n     */\n    constructor(appInstance) {\n        this.application = appInstance;\n    }\n\n    /**\n     * Relocate all sites from old to new location\n     *\n     * @param oldLocation\n     * @param newLocation\n     * @param event\n     * @returns {boolean}\n     */\n    relocateSites(oldLocation, newLocation, event) {\n        if(!Utils.dirExists(oldLocation) || !Utils.dirExists(newLocation)) {\n            event.sender.send('app-config-saved', {\n                status: false,\n                message: 'error-save'\n            });\n\n            return false;\n        }\n\n        let sitesToMove = fs.readdirSync(oldLocation);\n        let copyErrorOccurred = false;\n        let catalogsToRemove = [];\n\n        for(let site of sitesToMove) {\n            let result = this.relocateSite(oldLocation, newLocation, site);\n\n            if(result === false) {\n                copyErrorOccurred = true;\n                break;\n            }\n\n            if(result !== '') {\n                catalogsToRemove.push(result);\n            }\n        }\n\n\n        if(copyErrorOccurred) {\n            fs.emptyDirSync(newLocation);\n\n            event.sender.send('app-config-saved', {\n                status: false,\n                message: 'error-save'\n            });\n\n            return false;\n        }\n\n        this.removeCatalogs(oldLocation, catalogsToRemove);\n        this.application.sitesDir = newLocation;\n        this.application.app.sitesDir = newLocation;\n\n        return true;\n    }\n\n    /**\n     * Moves specific site files from old to a new location\n     *\n     * @param oldLocation\n     * @param newLocation\n     * @param site\n     * @returns {boolean|string} - false on error, string with site directory name otherwise\n     */\n    relocateSite(oldLocation, newLocation, site) {\n        let siteLocation = path.join(oldLocation, site);\n\n        // Checks only for existing directories with Publii website\n        if(!Utils.dirExists(siteLocation) || !this.checkIfDirectoryIsSite(siteLocation)) {\n            return '';\n        }\n\n        try {\n            fs.mkdirSync(path.join(newLocation, site), { recursive: true });\n\n            fs.copySync(\n                path.join(oldLocation, site),\n                path.join(newLocation, site),\n                {\n                    overwrite: true,\n                    preserveTimestamps: true\n                }\n            );\n        } catch(e) {\n            console.log('ERROR OCCURRED: ' + site, e);\n            return false;\n        }\n\n        return site;\n    }\n\n    /**\n     * Check if specific directory is a Publii website catalog\n     *\n     * @param siteLocation\n     */\n    checkIfDirectoryIsSite(siteLocation) {\n        let inputDirPath = path.join(siteLocation, 'input');\n        let databasePath = path.join(inputDirPath, 'db.sqlite');\n        let inputDirExists = Utils.dirExists(inputDirPath);\n        let databaseExists = Utils.fileExists(databasePath);\n\n        return inputDirExists && databaseExists;\n    }\n\n    /**\n     * Removes specific catalogs\n     * removing all catalogs\n     *\n     * @param catalogsToRemove\n     */\n    removeCatalogs(location, catalogsToRemove) {\n        for(let catalogToRemove of catalogsToRemove) {\n            let catalogPath = path.join(location, catalogToRemove);\n            fs.removeSync(catalogPath);\n        }\n    }\n}\n\nmodule.exports = AppFilesHelper;\n"
  },
  {
    "path": "app/back-end/helpers/avatar.js",
    "content": "const sizeOf = require('image-size');\n\nclass AvatarHelper {\n    /*\n     * Check if the provided path is a local avatar file\n     *\n     * @param pathToCheck - avatar path\n     *\n     * @return bool\n     */\n    static isLocalAvatar(pathToCheck) {\n        return  typeof pathToCheck === 'string' &&\n                pathToCheck !== '' &&\n                pathToCheck.indexOf('http://') === -1 &&\n                pathToCheck.indexOf('https://') === -1 &&\n                pathToCheck.indexOf('media/website/') === -1;\n    }\n\n    /*\n     * Check if the provided path is a gravatar remote file\n     *\n     * @param pathToCheck - avatar path\n     *\n     * @return bool\n     */\n    static isGravatar(pathToCheck) {\n        return !AvatarHelper.isLocalAvatar(pathToCheck);\n    }\n\n    /*\n     * Returns avatar data object with:\n     * - alt - alternative text (author name)\n     * - url - URL to the avatar image\n     * - width - width of the avatar image\n     * - height - height of the avatar image\n     *\n     * @param authorObject - object with the author data\n     * @param avatarPath - (optional) path to the local avatar image\n     *\n     * @return object - object with the avatar data\n     */\n    static getAvatarData(authorObject, avatarPath) {\n        let avatarDimensions = {\n            height: 240,\n            width: 240\n        };\n\n        if(avatarPath === '') {\n            return false;\n        }\n\n        if(avatarPath) {\n            try {\n                avatarDimensions = sizeOf(avatarPath);\n            } catch(e) {\n                console.log('helpers/avatar.js - missing avatar image');\n\n                avatarDimensions = {\n                    height: false,\n                    width: false\n                };\n            }\n        }\n\n        return {\n            alt: authorObject.name,\n            height: avatarDimensions.height,\n            url: authorObject.avatar,\n            width: avatarDimensions.width\n        }\n    }\n}\n\nmodule.exports = AvatarHelper;\n"
  },
  {
    "path": "app/back-end/helpers/context-menu-builder.js",
    "content": "const electron = require('electron');\nconst shell = electron.shell;\nconst Menu = electron.Menu;\nconst MenuItem = electron.MenuItem;\nmodule.exports = class ContextMenuBuilder {\n    constructor (webContents) {\n        this.menu = null;\n        this.target = webContents;\n        this.translations = {\n            cut: 'Cut',\n            copy: 'Copy',\n            paste: 'Paste',\n            search: 'Search with Google'\n        };\n    }\n\n    async showPopupMenu(context) {\n        if (!context.misspelledWord || context.misspelledWord.length === 0) {\n            return;\n        }\n\n        let menu = await this.buildMenuForElement(context);\n        \n        if (!menu) {\n            return;\n        }\n\n        menu.popup({});\n    }\n\n    async buildMenuForElement(info) {\n        if (info.isEditable || (info.inputFieldType && info.inputFieldType !== 'none')) {\n            return await this.buildMenuForTextInput(info);\n        }\n    }\n\n    async buildMenuForTextInput(menuInfo) {\n        let menu = new Menu();\n\n        await this.addSpellingItems(menu, menuInfo);\n        this.addSearchItems(menu, menuInfo);\n        this.addCut(menu, menuInfo);\n        this.addCopy(menu, menuInfo);\n        this.addPaste(menu, menuInfo);\n\n        return menu;\n    }\n\n    async addSpellingItems(menu, menuInfo) {\n        if (!menuInfo.misspelledWord || menuInfo.misspelledWord.length === 0) {\n            return menu;\n        }\n\n        let corrections = menuInfo.dictionarySuggestions;\n\n        if (corrections && corrections.length) {\n            corrections.forEach((correction) => {\n                let item = new MenuItem({\n                    label: correction,\n                    click: () => this.target.replaceMisspelling(correction)\n                });\n\n                menu.append(item);\n            });\n\n            this.addSeparator(menu);\n        }\n\n        return menu;\n    }\n\n    addSearchItems(menu, menuInfo) {\n        if (!menuInfo.selectionText || menuInfo.selectionText.length < 1) {\n        return menu;\n        }\n\n        let search = new MenuItem({\n        label: this.translations.search,\n        click: () => {\n            let url = `https://www.google.com/#q=${encodeURIComponent(menuInfo.selectionText)}`;\n            shell.openExternal(url);\n        }\n        });\n\n        menu.append(search);\n        this.addSeparator(menu);\n\n        return menu;\n    }\n\n    addCut(menu, menuInfo) {        \n        menu.append(new MenuItem({\n            label: this.translations.cut,\n            accelerator: 'CommandOrControl+X',\n            enabled: menuInfo.editFlags.canCut,\n            click: () => this.target.cut()\n        }));\n\n        return menu;\n    }\n\n    addCopy(menu, menuInfo) {\n        menu.append(new MenuItem({\n            label: this.translations.copy,\n            accelerator: 'CommandOrControl+C',\n            enabled: menuInfo.editFlags.canCopy,\n            click: () => this.target.copy()\n        }));\n\n        return menu;\n    }\n\n    addPaste(menu, menuInfo) {\n        menu.append(new MenuItem({\n            label: this.translations.paste,\n            accelerator: 'CommandOrControl+V',\n            enabled: menuInfo.editFlags.canPaste,\n            click: () => this.target.paste()\n        }));\n\n        return menu;\n    }\n\n    addSeparator(menu) {\n        menu.append(new MenuItem({type: 'separator'}));\n        return menu;\n    }\n}\n"
  },
  {
    "path": "app/back-end/helpers/db.utils.js",
    "content": "const os = require('os');\n\n/*\n * Other helper functions\n */\nclass DBUtils {\n    constructor (dbInstance) {\n        this.DB = dbInstance; \n        this.statement = '';\n        this.useWASM = os.platform() === 'linux';\n    }\n\n    prepare (sqlStatement) {\n        this.statement = sqlStatement;\n        return this;\n    }\n\n    get (paramsObject = null) {\n        if (this.useWASM) {\n            if (paramsObject) {\n                paramsObject = this.transformParams(paramsObject);\n                return this.DB.get(this.statement, paramsObject);\n            }\n\n            return this.DB.get(this.statement);\n        }\n\n        if (paramsObject !== null) {\n            return this.DB.prepare(this.statement).get(paramsObject);\n        }\n\n        return this.DB.prepare(this.statement).get();\n    }\n\n    run (paramsObject = null) {\n        if (this.useWASM) {\n            if (paramsObject) {\n                paramsObject = this.transformParams(paramsObject);\n                return this.DB.run(this.statement, paramsObject);\n            }\n\n            return this.DB.run(this.statement);\n        }\n\n        if (paramsObject !== null) {\n            return this.DB.prepare(this.statement).run(paramsObject);\n        }\n\n        return this.DB.prepare(this.statement).run();\n    }\n\n    all (paramsObject = null) {\n        if (this.useWASM) {\n            if (paramsObject) {\n                paramsObject = this.transformParams(paramsObject);\n                return this.DB.all(this.statement, paramsObject);\n            }\n\n            return this.DB.all(this.statement);\n        }\n\n        if (paramsObject !== null) {\n            return this.DB.prepare(this.statement).all(paramsObject);\n        }\n\n        return this.DB.prepare(this.statement).all();\n    }\n\n    exec (sqlQueries) {\n        this.DB.exec(sqlQueries);\n    }\n\n    close () {\n        this.DB.close();\n    }\n\n    /**\n     * Prefix all params in object with \"@\"\n     */\n    transformParams (paramsObject) {\n        const newParamsObject = {};\n\n        for (const key in paramsObject) {\n            if (paramsObject.hasOwnProperty(key)) {\n                newParamsObject[\"@\" + key] = paramsObject[key];\n            }\n        }\n\n        return newParamsObject;\n    }\n}\n\nmodule.exports = DBUtils;\n"
  },
  {
    "path": "app/back-end/helpers/file.js",
    "content": "const fs = require('fs');\n\n/**\n * FileHelper wraps fs.readFileSync to avoid leaking file descriptors.\n */\nclass FileHelper {\n    /**\n     * Reads a file synchronously, ensuring the file descriptor is closed.\n     * @param {string|Buffer|URL|integer} path - filename or file descriptor\n     * @param {Object|string} [options] - options or encoding\n     * @returns {string|Buffer}\n     */\n    static readFileSync(path, options) {\n        let fd;\n        try {\n            fd = fs.openSync(path, 'r');\n            return fs.readFileSync(fd, options);\n        } finally {\n            if (fd !== undefined) {\n                try { fs.closeSync(fd); } catch (e) { /* ignore */ }\n            }\n        }\n    }\n}\n\nmodule.exports = FileHelper;\n"
  },
  {
    "path": "app/back-end/helpers/image.helper.js",
    "content": "/*\n * Image helper for posts\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst normalizePath = require('normalize-path');\nconst Utils = require('./utils.js');\nconst slug = require('./slug');\n\nclass ImageHelper {\n    constructor(postInstance) {\n        this.application = postInstance.application;\n        this.site = postInstance.site;\n        this.featuredImage = normalizePath(postInstance.featuredImage);\n        this.featuredImageData = postInstance.featuredImageData;\n        this.fileName = postInstance.featuredImageFilename;\n        this.postID = postInstance.id;\n        this.db = postInstance.db;\n        this.siteDir = postInstance.siteDir;\n    }\n\n    /*\n     * Save image\n     */\n    save() {\n        // Detect old image if existed\n        let result = this.db.prepare('SELECT featured_image_id FROM posts WHERE id = @id').get({\n            id: this.postID\n        });\n\n        if (!result) {\n            return;\n        }\n\n        let featuredImageId = parseInt(result.featured_image_id, 10);\n\n        // Check if user removed image or image was empty\n        if(this.featuredImage === '' && featuredImageId > 0) {\n            this.delete();\n            return;\n        }\n\n        // Store a new image\n        if(this.featuredImage) {\n            // If previous image existed\n            if(featuredImageId > 0) {\n                // Remove them\n                this.delete();\n            }\n            // And then we can store a new one\n            this.store();\n        }\n\n        this.storeImageAdditionalData();\n    }\n\n    /*\n     * Delete image\n     */\n    delete() {\n        if(this.postID > 0) {\n            this.db.prepare(`UPDATE posts SET featured_image_id = 0 WHERE id = @id`).run({ id: this.postID });\n            this.db.exec(`DELETE FROM posts_images WHERE post_id = ${parseInt(this.postID, 10)}`);\n        }\n    }\n\n    /*\n     * Store image on the app data\n     */\n    store() {\n        let self = this;\n        // Check if the image not exist\n        let directoryPath = this.getMediaPath();\n        let fileNameData = path.parse(this.fileName);\n        let finalFileName = slug(fileNameData.name, false, true) + fileNameData.ext;\n        let finalFilePath = path.join(directoryPath, finalFileName);\n\n        // Save image data in DB\n        let simplifiedFilePath = normalizePath(finalFilePath).replace(this.getMediaPath(), '');\n        simplifiedFilePath = simplifiedFilePath.replace('/', '').replace('\\\\', '');\n\n        let imagesSqlQuery = this.db.prepare(`INSERT INTO posts_images VALUES(null, @id, @path, '', '', @data)`);\n        imagesSqlQuery.run({\n            id: this.postID, \n            path: simplifiedFilePath, \n            data: JSON.stringify(this.featuredImageData)\n        });\n        let featuredImageId = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n\n        // Update post entry in DB\n        let postsSqlQuery = this.db.prepare(`UPDATE posts SET featured_image_id = @imageID WHERE id = @id`);\n        postsSqlQuery.run({\n            imageID: featuredImageId,\n            id: this.postID\n        });\n    }\n\n    /*\n     * Store image additional data\n     */\n    storeImageAdditionalData() {\n        // Update featured image entry in DB\n        let imageSqlQuery = this.db.prepare(`UPDATE posts_images SET additional_data = @data WHERE post_id = @id`);\n\n        imageSqlQuery.run({\n            data: JSON.stringify(this.featuredImageData),\n            id: this.postID\n        });\n    }\n\n    /*\n     * Retrieve media path\n     */\n    getMediaPath() {\n        let mediaPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.postID).toString());\n        mediaPath = normalizePath(mediaPath);\n\n        return mediaPath;\n    }\n\n    /*\n     * Delete images connected with a specific post ID\n     */\n    static deleteImagesDirectory(siteDir, itemType, itemID) {\n        let dirPath = path.join(siteDir, 'input', 'media', itemType, (itemID).toString());\n        let responsiveDirPath = path.join(siteDir, 'input', 'media', itemType, (itemID).toString(), 'responsive');\n        let galleryDirPath = path.join(siteDir, 'input', 'media', itemType, (itemID).toString(), 'gallery');\n\n        for (let directoryPath of [galleryDirPath, responsiveDirPath, dirPath]) {\n            if (Utils.dirExists(directoryPath)) {\n                fs.readdirSync(directoryPath).forEach(function (file) {\n                    let curPath = path.join(directoryPath, file);\n\n                    if (!fs.lstatSync(curPath).isDirectory()) {\n                        fs.unlinkSync(curPath);\n                    }\n                });\n\n                fs.rmSync(directoryPath, { recursive: true });\n            }\n        }\n    }\n\n    /*\n     * Copy images connected with a specific post ID to other post ID\n     */\n    static copyImagesDirectory(siteDir, postID, newPostID) {\n        let dirPath = path.join(siteDir, 'input', 'media', 'posts', (postID).toString());\n        let newDirPath = path.join(siteDir, 'input', 'media', 'posts', (newPostID).toString());\n\n        // Copy files from the old directory to the new directory\n        if(Utils.dirExists(dirPath)) {\n            fs.copySync(dirPath, newDirPath);\n        }\n    }\n}\n\nmodule.exports = ImageHelper;\n"
  },
  {
    "path": "app/back-end/helpers/slug.js",
    "content": "const transliterate = require('transliteration').transliterate;\nconst slug = require('slug');\n\n/*\n * Custom mode of rfc3986 without unicode symbols\n */\nslug.defaults.modes['rfc3986-non-unicode'] = {\n    replacement: '-',                   // replace spaces with replacement\n    symbols: false,                     // replace unicode symbols or not\n    remove: /[\\.]/g,                    // (optional) regex to remove characters\n    lower: true,                        // result in lower case\n    charmap: slug.charmap,              // replace special characters\n    multicharmap: slug.multicharmap,    // replace multi-characters\n    trim: true,                         // remove leading and trailing replacement chars\n};\n\nslug.defaults.modes['rfc3986-non-unicode-with-dots'] = {\n    replacement: '-',                   // replace spaces with replacement\n    symbols: false,                     // replace unicode symbols or not\n    lower: true,                        // result in lower case\n    charmap: slug.charmap,              // replace special characters\n    multicharmap: slug.multicharmap,    // replace multi-characters\n    trim: true,                         // remove leading and trailing replacement chars\n};\n\nslug.defaults.modes['rfc3986-non-unicode-with-dots-no-lower'] = {\n    replacement: '-',                   // replace spaces with replacement\n    symbols: false,                     // replace unicode symbols or not\n    lower: false,                       // result in lower case\n    charmap: slug.charmap,              // replace special characters\n    multicharmap: slug.multicharmap,    // replace multi-characters\n    trim: true,                         // remove leading and trailing replacement chars\n};\n\nslug.defaults.mode = 'rfc3986-non-unicode';\n\n/**\n * Define custom slug charmap\n */\nslug.defaults.charmap['ä'] = 'ae';\nslug.defaults.charmap['Ä'] = 'AE';\nslug.defaults.charmap['ö'] = 'oe';\nslug.defaults.charmap['Ö'] = 'OE';\nslug.defaults.charmap['ü'] = 'ue';\nslug.defaults.charmap['Ü'] = 'UE';\nslug.defaults.charmap['ß'] = 'ss';\nslug.defaults.charmap['ẞ'] = 'SS';\n\nfunction createSlug(textToSlugify, filenameMode = false, saveLowerChars = false) {\n    textToSlugify = transliterate(textToSlugify, { replace: [\n        ['ä', 'ae'], \n        ['Ä', 'AE'], \n        ['ö', 'oe'], \n        ['Ö', 'OE'], \n        ['ü', 'ue'], \n        ['Ü', 'UE'], \n        ['ß', 'ss'], \n        ['ẞ', 'SS'],\n        ['«', ''],\n        ['»', ''],\n        ['$', '']\n    ] });\n\n    if(!filenameMode) {\n        if(saveLowerChars) {\n            slug.defaults.mode = 'rfc3986-non-unicode-with-dots-no-lower';\n        }\n\n        textToSlugify = slug(textToSlugify);\n        slug.defaults.mode = 'rfc3986-non-unicode';\n    } else {\n        slug.defaults.mode = 'rfc3986-non-unicode-with-dots';\n        textToSlugify = slug(textToSlugify);\n        slug.defaults.mode = 'rfc3986-non-unicode';\n    }\n\n    return textToSlugify;\n}\n\nmodule.exports = createSlug;\n"
  },
  {
    "path": "app/back-end/helpers/specs/avatar.spec.js",
    "content": "const assert = require('assert');\nconst path = require('path');\nconst avatarHelper = require('../avatar.js');\n\ndescribe('Avatar Helper', function() {\n    it('should detect if avatar uses local file', function() {\n        assert.strictEqual(false, avatarHelper.isLocalAvatar(''));\n        assert.strictEqual(false, avatarHelper.isLocalAvatar('http://gravatar.com'));\n        assert.strictEqual(false, avatarHelper.isLocalAvatar('https://gravatar.com'));\n        assert.strictEqual(false, avatarHelper.isLocalAvatar('http://domain.com/media/website/img.jpg'));\n    });\n\n    it('should detect if avatar uses Gravatar', function() {\n        assert.strictEqual(true, avatarHelper.isGravatar(''));\n        assert.strictEqual(true, avatarHelper.isGravatar('http://gravatar.com'));\n        assert.strictEqual(true, avatarHelper.isGravatar('https://gravatar.com'));\n        assert.strictEqual(true, avatarHelper.isGravatar('http://domain.com/media/website/img.jpg'));\n    });\n\n    it('should return avatar object: alt, dimensions, url', function() {\n        const avatarTestPath = path.join(__dirname, 'mock-data', 'avatar.png');\n        const authorObject = {\n            name: \"Test Author\",\n            avatar: \"http://domain.com/media/website/avatar.png\"\n        };\n\n        assert.strictEqual(false, avatarHelper.getAvatarData(authorObject, ''));\n\n        assert.deepEqual({\n            alt: \"Test Author\",\n            url: \"http://domain.com/media/website/avatar.png\",\n            width: 240,\n            height: 127\n        }, avatarHelper.getAvatarData(authorObject, avatarTestPath));\n\n        assert.deepEqual({\n            alt: \"Test Author\",\n            url: \"http://domain.com/media/website/avatar.png\",\n            width: 240,\n            height: 240\n        }, avatarHelper.getAvatarData(authorObject));\n    });\n});\n"
  },
  {
    "path": "app/back-end/helpers/specs/slug.spec.js",
    "content": "const assert = require('assert');\nconst slug = require('../slug.js');\n\ndescribe('Slug creation', function() {\n    it('should empty string when there is no chars', function() {\n        assert.strictEqual('', slug('       '));\n        assert.strictEqual('', slug(\"  \\t \\n  \\r \"));\n    });\n\n    it('should remove white spaces', function() {\n        assert.strictEqual('lorem-ipsum', slug('lorem ipsum'));\n        assert.strictEqual('lorem-ipsum', slug('lorem\\nipsum'));\n        assert.strictEqual('lorem-ipsum', slug('lorem\\tipsum'));\n        assert.strictEqual('lorem-ipsum', slug('lorem\\ripsum'));\n    });\n\n    it('should change capital letters to lower case', function() {\n        assert.strictEqual('lorem-ipsum', slug('Lorem Ipsum'));\n    });\n\n    it('should remove dots from the slug', function() {\n        assert.strictEqual('loremipsum', slug('lorem.ipsum'));\n        assert.strictEqual('loremipsum', slug('lorem.Ipsum'));\n        assert.strictEqual('loremipsum', slug('Lorem.Ipsum'));\n    });\n\n    it('should remove special chars from the slug', function() {\n        assert.strictEqual('loremipsum', slug('lorem?!ipsum'));\n        assert.strictEqual('lorem-ipsum', slug('lorem? Ipsum!'));\n        assert.strictEqual('loremipsum', slug('Lorem;Ipsum:+'));\n    });\n\n    it('should support japanese chars', function() {\n        assert.strictEqual('konnitihashi-jie', slug('こんにちは世界'));\n        assert.strictEqual('yaa-jin-ri-hayuan-qi-desuka', slug('やあ！ 今日は元気ですか？'));\n        assert.strictEqual('yaa-si-noliang-i-sorehazututochang-i-jin-ri-hayuan-qi-desuka', slug('やあ！ 私の良い。 それはずっと長い。 今日は元気ですか？'));\n    });\n\n    it('should support korean chars', function() {\n        assert.strictEqual('annyeonghaseyo-segye', slug('안녕하세요 세계'));\n        assert.strictEqual('annyeong-cingu-oneul-eoddeoni', slug('안녕 친구! 오늘 어떠니?'));\n        assert.strictEqual('annyeong-cingu-nae-iig-neomu-gilda-oneul-eoddeoni', slug('안녕 친구! 내 이익. 너무 길다. 오늘 어떠니?'));\n    });\n\n    it('should support chinese chars', function() {\n        assert.strictEqual('ni-haoshi-jie', slug('你好，世界'));\n        assert.strictEqual('ni-zen-mo-jiao-pei', slug('你怎麼交配'));\n        assert.strictEqual('ni-zen-mo-jiao-pei-wo-hen-hao', slug('你怎麼交配 我很好！'));\n    });\n\n    it('should not remove dots in the filename mode', function() {\n        assert.strictEqual('indexhtml', slug('index.html', false));\n        assert.strictEqual('index.html', slug('index.html', true));\n    });\n\n    it('should remove some special characters', function() {\n        assert.strictEqual('title-with-and-arrows', slug('Title with « and » arrows'));\n        assert.strictEqual('title-with-typoraphical-quotes-and-normal-quotes', slug('Title with „typoraphical quotes“ and \"normal quotes\"'));\n        assert.strictEqual('title-with-brackets-in-different-forms', slug('Title (with) [brackets] {in} ⟨different forms⟩'));\n        assert.strictEqual('title-with-different-types-of-dashes', slug('Title with different - types – of — dashes'));\n        assert.strictEqual('title-with-many-apostrophes-many-many', slug('Title with many \\' apostrophes \\‘ many \\’ many'));\n        assert.strictEqual('title-with-dots-and-commas', slug('Title with dots . and commas,'));\n        assert.strictEqual('and-another-characters', slug('And another characters ; : ? !'));\n        assert.strictEqual('also-ellipsis', slug('Also ellipsis…'));\n        assert.strictEqual('and-slashes', slug('And slashes \\/ \\\\'));\n        assert.strictEqual('and-other-chars', slug('And other chars * # $ @ ^ % ♥ ☆'));\n    });\n});\n"
  },
  {
    "path": "app/back-end/helpers/updates.helper.js",
    "content": "const fs = require('fs');\nconst FileHelper = require('./file.js');\nconst https = require('https');\n\nclass UpdatesHelper {\n    constructor (config) {\n        this.event = config.event;\n        this.filePath = config.filePath;\n        this.url = config.url;\n        this.forceDownload = config.forceDownload;\n    }\n\n    retrieve () {\n        if (this.forceDownload || !fs.existsSync(this.filePath)) {\n            this.download();\n        } else {\n            this.readExistingData();\n        }\n    }\n\n    download () {\n        https.get(this.url, res => {\n            let body = '';\n\n            res.on('data', chunk => { \n                body += chunk; \n            });\n\n            res.on('end', () => {\n                fs.writeFileSync(this.filePath, body, 'utf8');\n                this.handleResponse(body, true);\n            });\n        }).on('error', (err) => {\n            this.sendError(err);\n        });\n    }\n\n    sendError (err) {\n        this.event.sender.send('app-notifications-retrieved', { \n            status: false,\n            error: (err && err.message) ? err.message : 'An unknown error occurred while retrieving notifications.'\n        });\n    }\n\n    readExistingData () {\n        if (fs.existsSync(this.filePath)) {\n            let body = FileHelper.readFileSync(this.filePath, 'utf8');\n            this.handleResponse(body, false);\n        }\n    }\n\n    handleResponse (body, downloaded) {\n        let response = false;\n\n        try {\n            response = JSON.parse(body);\n        } catch(e) {\n            response = false;\n        }\n\n        if (response) {\n            this.event.sender.send('app-notifications-retrieved', {\n                status: true,\n                downloaded: downloaded,\n                notifications: response \n            });\n        } else {\n            this.sendError(response);\n        }\n    }\n}\n\nmodule.exports = UpdatesHelper;\n"
  },
  {
    "path": "app/back-end/helpers/utils.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./file.js');\nconst normalizePath = require('normalize-path');\n\n/*\n * Other helper functions\n */\nclass UtilsHelper {\n    /*\n     *\n     *  Object helper functions\n     *\n     */\n\n    /*\n     * Deep merge for objects as Object.assign not merge objects properly\n     */\n    static mergeObjects(target, source) {\n        if (typeof target !== 'object') {\n            target = {};\n        }\n\n        for (let property in source) {\n            if (source.hasOwnProperty(property)) {\n                let sourceProperty = source[property];\n\n                if (typeof sourceProperty === 'object' && !Array.isArray(sourceProperty) && !(sourceProperty instanceof Date)) {\n                    target[property] = UtilsHelper.mergeObjects(target[property], sourceProperty);\n                    continue;\n                } else if(sourceProperty instanceof Date) {\n                    target[property] = new Date(sourceProperty.getTime());\n                    continue;\n                }\n\n                target[property] = sourceProperty;\n            }\n        }\n\n        for (let a = 2, l = arguments.length; a < l; a++) {\n            UtilsHelper.mergeObjects(target, arguments[a]);\n        }\n\n        return target;\n    }\n\n    /*\n     *\n     *  Filesystem helper functions\n     *\n     */\n\n    /*\n     * Check if the dir exists\n     */\n    static dirExists(dirPath) {\n        let dirStat = false;\n\n        try {\n            dirStat = fs.statSync(dirPath);\n        } catch(e) {}\n\n        if(dirStat && dirStat.isDirectory()) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /*\n     * Check if file exists\n     */\n    static fileExists(filePath) {\n        let fileStat = false;\n\n        try {\n            fileStat = fs.statSync(filePath);\n        } catch(e) {\n            return false;\n        }\n\n        if (fileStat && !fileStat.isDirectory()) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /*\n     *\n     *  Responsive images helper functions\n     *\n     */\n\n    /*\n     * Return true if responsive images config exists\n     */\n    static responsiveImagesConfigExists(themeConfig, type = false) {\n        let files = themeConfig.files;\n\n        if(type === false) {\n            return !!files &&\n                   !!files.responsiveImages &&\n                   !!files.responsiveImages.contentImages &&\n                   !!files.responsiveImages.contentImages.dimensions;\n        }\n\n        // When we want to check if configuration for a specific images exists\n        return !!files &&\n               !!files.responsiveImages &&\n               !!files.responsiveImages[type] &&\n               !!files.responsiveImages[type].dimensions;\n    }\n\n    /*\n     * Return responsive image dimensions for given config\n     */\n    static responsiveImagesDimensions(themeConfig, type, group = false) {\n        if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            return false;\n        }\n\n        if(UtilsHelper.responsiveImagesConfigExists(themeConfig, type)) {\n            let dimensions = false;\n\n            if(themeConfig.files.responsiveImages[type]) {\n                dimensions = themeConfig.files.responsiveImages[type].dimensions;\n            } else {\n                return false;\n            }\n\n            if(!group) {\n                return UtilsHelper.responsiveImagesDimensionNames(dimensions);\n            }\n\n            return UtilsHelper.responsiveImagesDimensionNames(dimensions, group);\n        }\n\n        return false;\n    }\n\n    /*\n     * Return responsive image dimensions data\n     */\n    static responsiveImagesData(themeConfig, type, group = false) {\n        if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            return false;\n        }\n\n        if(UtilsHelper.responsiveImagesConfigExists(themeConfig, type)) {\n            let dimensions = false;\n\n            if(themeConfig.files.responsiveImages[type]) {\n                dimensions = themeConfig.files.responsiveImages[type].dimensions;\n            } else {\n                console.log('TYPE: ' + type + ' NOT EXISTS!');\n                return false;\n            }\n\n            let filteredDimensions = false;\n            let dimensionNames = Object.keys(dimensions);\n\n            if(!group) {\n                return dimensions;\n            }\n\n            for(let name of dimensionNames) {\n                if(dimensions[name].group.split(',').indexOf(group) > -1) {\n                    if(filteredDimensions === false) {\n                        filteredDimensions = {};\n                    }\n\n                    filteredDimensions[name] = Object.assign({}, dimensions[name]);\n                }\n            }\n\n            return filteredDimensions;\n        }\n\n        return false;\n    }\n\n    /*\n     * Return responsive images groups\n     */\n    static responsiveImagesGroups(themeConfig, type) {\n        if (!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            return false;\n        }\n\n        if (UtilsHelper.responsiveImagesConfigExists(themeConfig, type)) {\n            let groups = false;\n            let dimensions = false;\n\n            if(themeConfig.files.responsiveImages[type]) {\n                dimensions = themeConfig.files.responsiveImages[type].dimensions;\n            } else {\n                return false;\n            }\n\n            let keys = Object.keys(dimensions);\n\n            for(let key of keys) {\n                if(dimensions[key].group) {\n                    if(groups === false) {\n                        groups = [];\n                    }\n\n                    let foundedGroups = dimensions[key].group.split(',');\n\n                    for(let foundedGroup of foundedGroups) {\n                        if (groups.indexOf(foundedGroup)) {\n                            groups.push(foundedGroup);\n                        }\n                    }\n                }\n            }\n\n            return groups;\n        }\n\n        return false;\n    }\n\n    /*\n     * Return responsive image dimensions for given config\n     */\n    static responsiveImagesDimensionNames(dimensions, group = false) {\n        // Get object keys for group type check\n        let keys = Object.keys(dimensions);\n        let dimensionNames = false;\n\n        // When we have groups and the group param is set - filter results to a specific group\n        if(group !== false) {\n            for(let key of keys) {\n                if(dimensions[key].group.split(',').indexOf(group) > -1) {\n                    if(dimensionNames === false) {\n                        dimensionNames = [];\n                    }\n\n                    dimensionNames.push(key);\n                }\n            }\n        } else {\n            // When there is no groups\n            dimensionNames = Object.keys(dimensions);\n        }\n\n        return dimensionNames;\n    }\n\n    /*\n     * Return file when there is no override or file path if the override exists\n     */\n    static fileIsOverrided(inputDir, themeName, filePath) {\n        let basePath;\n        let overridesDir;\n        let overridedFilePath;\n\n        if(!filePath) {\n            basePath = normalizePath(path.join(inputDir));\n            overridesDir = normalizePath(inputDir.replace(/[\\\\\\/]{1,1}$/, '') + '-override');\n            overridedFilePath = normalizePath(themeName).replace(basePath, overridesDir);\n        } else {\n            basePath = normalizePath(path.join(inputDir, 'themes', themeName));\n            overridesDir = normalizePath(path.join(inputDir, 'themes', themeName + '-override'));\n            overridedFilePath = normalizePath(filePath).replace(basePath, overridesDir);\n        }\n\n        if(!UtilsHelper.dirExists(overridesDir)) {\n            return false;\n        }\n\n        if(!UtilsHelper.fileExists(overridedFilePath)) {\n            return false;\n        }\n\n        return overridedFilePath;\n    }\n\n    /*\n     * Loads theme config JSON\n     */\n    static loadThemeConfig(inputDir, themeName) {\n        let themeConfig;\n        let themeConfigPath;\n        let overridedThemeConfigPath;\n\n        if(!themeName) {\n            themeConfigPath = path.join(inputDir, 'config.json');\n            overridedThemeConfigPath = UtilsHelper.fileIsOverrided(inputDir, themeConfigPath);\n        } else {\n            themeName = themeName.toLowerCase();\n            themeConfigPath = path.join(inputDir, 'themes', themeName, 'config.json');\n            overridedThemeConfigPath = UtilsHelper.fileIsOverrided(inputDir, themeName, themeConfigPath);\n        }\n\n        if(overridedThemeConfigPath) {\n            themeConfigPath = overridedThemeConfigPath;\n        }\n\n        try {\n            themeConfig = JSON.parse(FileHelper.readFileSync(themeConfigPath));\n        } catch(e) {\n            console.log('The theme config.json file is corrupted');\n            return {};\n        }\n\n        return themeConfig;\n    }\n\n    /**\n     * Require file without cache\n     */\n    static requireWithNoCache(module, params = false) {\n        delete require.cache[require.resolve(module)];\n\n        if (params) {\n            return require(module)(params);\n        }\n\n        return require(module);\n    }\n\n    /**\n     * Compare arrays regardles of order\n     */\n    static arraysHaveTheSameContent (arrayA, arrayB) {\n        if (arrayA.length !== arrayB.length) {\n            return false;\n        }\n\n        let uniqueValues = new Set([...arrayA, ...arrayB]);\n\n        for (let value of uniqueValues) {\n            let arrayACount = arrayA.filter(item => item === value).length;\n            let arrayBCount = arrayB.filter(item => item === value).length;\n          \n            if (arrayACount !== arrayBCount) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n}\n\nmodule.exports = UtilsHelper;\n"
  },
  {
    "path": "app/back-end/helpers/validators/language-config.js",
    "content": "const FileHelper = require('../file.js');\n\n/**\n * Checks if language config file meets the requirements\n *\n * @param configPath - path to the file\n * @returns {boolean|string}\n */\nfunction languageConfigValidator(configPath) {\n    let configContent = FileHelper.readFileSync(configPath);\n    let configParsed = false;\n\n    try {\n        configParsed = JSON.parse(configContent);\n    } catch(e) {\n        return 'Invalid JSON structure';\n    }\n\n    if(!configParsed.name) {\n        return 'Missing name field in config.json';\n    }\n\n    if(!configParsed.version) {\n        return 'Missing version field in config.json';\n    }\n\n    if(!configParsed.author) {\n        return 'Missing author field in config.json';\n    }\n\n    if(!configParsed.publiiSupport) {\n        return 'Missing publiiSupport field in config.json';\n    }\n\n    return true;\n}\n\nmodule.exports = languageConfigValidator;\n"
  },
  {
    "path": "app/back-end/helpers/validators/plugin-config.js",
    "content": "const FileHelper = require('../file.js');\n\n/**\n * Checks if plugin config file meets the requirements\n *\n * @param configPath - path to the file\n * @returns {boolean|string}\n */\nfunction pluginConfigValidator(configPath) {\n    let configContent = FileHelper.readFileSync(configPath);\n    let configParsed = false;\n\n    try {\n        configParsed = JSON.parse(configContent);\n    } catch(e) {\n        return 'Invalid JSON structure';\n    }\n\n    if(!configParsed.name) {\n        return 'Missing name field in plugin.json';\n    }\n\n    if(!configParsed.version) {\n        return 'Missing version field in plugin.json';\n    }\n\n    if(!configParsed.author) {\n        return 'Missing author field in plugin.json';\n    }\n\n    if(!configParsed.scope) {\n        return 'Missing scope field in plugin.json';\n    }\n\n    if(!configParsed.minimumPubliiVersion) {\n        return 'Missing minimumPubliiVersion field in plugin.json';\n    }\n\n    return true;\n}\n\nmodule.exports = pluginConfigValidator;\n"
  },
  {
    "path": "app/back-end/image.js",
    "content": "/*\n * Image instance\n */\n\nconst FileHelper = require('./helpers/file.js');\nconst fs = require('fs-extra');\nconst os = require('os');\nconst path = require('path');\nconst Model = require('./model.js');\nconst sizeOf = require('image-size');\nconst normalizePath = require('normalize-path');\nconst Themes = require('./themes.js');\nconst Utils = require('./helpers/utils.js');\nconst slug = require('./helpers/slug');\nconst { Jimp } = require('jimp');\n// Default config\nconst defaultAstCurrentSiteConfig = require('./../config/AST.currentSite.config');\nlet sharp = require('sharp');\n// Reduce concurrency on Linux systems to avoid crashes\nif (os.platform() === 'linux') {\n    sharp.concurrency(1);\n}\n\nclass Image extends Model {\n    constructor(appInstance, imageData) {\n        super(appInstance, imageData);\n        // Post ID\n        this.id = parseInt(imageData.id, 10);\n\n        if (imageData.id === 'website') {\n            this.id = 'website';\n        } else if (imageData.id === 'defaults') {\n            this.id = 'defaults';\n        }\n\n        // App instance\n        this.appInstance = appInstance;\n        // Image Path\n        this.path = imageData.path;\n        // Image Type\n        this.imageType = 'contentImages';\n        // Plugin dir\n        this.pluginDir = imageData.pluginDir;\n\n        if (imageData.imageType) {\n            this.imageType = imageData.imageType;\n        }\n    }\n\n    /**\n     * Generate unique file name\n     */\n    generateFileName (fileName, suffix, dirPath, galleryDirPath) {\n        let newPath = '';\n        let fileSuffix = '';\n        let finalFileName = path.parse(fileName);\n\n        if (suffix > 1) {\n            fileSuffix = '-' + suffix;\n        }\n\n        finalFileName = slug(finalFileName.name, false, true) + fileSuffix + finalFileName.ext;\n        newPath = path.join(dirPath, finalFileName);\n\n        if (this.imageType === 'galleryImages') {\n            newPath = path.join(galleryDirPath, finalFileName);\n        }\n\n        if (fs.existsSync(newPath)) {\n            return this.generateFileName(fileName, suffix + 1, dirPath, galleryDirPath);\n        }\n\n        return newPath;\n    }\n\n    /*\n     * Save Image\n     */\n    save (generateResponsiveImages = true) {\n        let self = this;\n        let newPath = '';\n\n        // If image is uploaded to a new post\n        if (this.id === 0) {\n            // Store it in the temp directory\n            this.id = 'temp';\n        }\n\n        // For images added to existing posts\n        if (!this.path) {\n            return;\n        }\n\n        let fileName = this.path.split('/');\n\n        if (fileName.length === 1) {\n            fileName = this.path.split('\\\\');\n        }\n\n        fileName = fileName.pop();\n        // Store the image in the proper directory\n        let dirPath = '';\n        let galleryDirPath = '';\n        let responsiveDirPath = '';\n\n        if (this.id === 'defaults' && this.imageType === 'contentImages') {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'posts', 'defaults'); \n            responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'posts', 'defaults', 'responsive');\n        } else if (this.id === 'defaults' && this.imageType === 'tagImages') {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'tags', 'defaults'); \n            responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'tags', 'defaults', 'responsive');\n        } else if (this.id === 'defaults' && this.imageType === 'authorImages') {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'authors', 'defaults'); \n            responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'authors', 'defaults', 'responsive');\n        } else if (this.imageType === 'pluginImages') {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'plugins', this.pluginDir); \n        } else if (this.id === 'website' || this.imageType === 'optionImages') {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'website');\n            responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'website', 'responsive');\n        } else if (this.imageType === 'tagImages' && this.id) {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'tags', (this.id).toString());\n            responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'tags', (this.id).toString(), 'responsive');\n        } else if (this.imageType === 'authorImages' && this.id) {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'authors', (this.id).toString());\n            responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'authors', (this.id).toString(), 'responsive');\n        } else {\n            dirPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString());\n            responsiveDirPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString(), 'responsive');\n\n            if (this.imageType === 'galleryImages') {\n                galleryDirPath = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString(), 'gallery');\n            }\n        }\n\n        // If dir not exists - create it\n        if (!Utils.dirExists(dirPath)) {\n            fs.mkdirSync(dirPath, { recursive: true });\n\n            if (responsiveDirPath !== '') {\n                fs.mkdirSync(responsiveDirPath, { recursive: true });\n            }\n        }\n\n        // If gallery directory not exist - create it\n        if (galleryDirPath !== '' && !Utils.dirExists(galleryDirPath)) {\n            fs.mkdirSync(galleryDirPath, { recursive: true });\n        }\n\n        // If responsive directory not exist - create it\n        if (responsiveDirPath !== '' && !Utils.dirExists(responsiveDirPath)) {\n            fs.mkdirSync(responsiveDirPath, { recursive: true });\n        }\n\n        newPath = this.generateFileName(fileName, 1, dirPath, galleryDirPath);\n\n        // Store main image\n        try {\n            fs.readFile(this.path, function(err, data) {\n                if (err) throw err;\n\n                fs.writeFile(newPath, data, function(err) {\n                    if (err) throw err;\n\n                    let pathData = path.parse(newPath);\n\n                    // Save responsive images\n                    if (generateResponsiveImages && self.allowedImageExtension(pathData.ext)) {\n                        self.createResponsiveImages(newPath, self.imageType);\n                    }\n\n                    process.send({\n                        type: \"image-copied\"\n                    });\n                });\n            });\n        } catch (err) {\n            return {\n                size: [0, 0],\n                url: 'ERROR'\n            }\n        }\n\n        // Get image dimensions \n        let dimensions = [false, false];\n\n        if (path.parse(this.path).ext === '.svg') {\n            dimensions = this.getSvgImageDimensions(this.path);\n        } else {\n            try {\n                dimensions = sizeOf(this.path);\n            } catch(e) {\n                console.log('back-end/image.js - wrong image path - missing dimensions');\n                dimensions = [false, false];\n            }\n        }\n\n        let filename = path.parse(newPath).base;\n\n        // Return the image dimensions and new location\n        return {\n            size: [dimensions.width, dimensions.height],\n            url: 'file:///' + normalizePath(newPath),\n            filename: filename,\n            newPath: newPath\n        };\n    }\n\n    /*\n     * Save responsive images\n     */\n    createResponsiveImages(originalPath, imageType = 'contentImages') {\n        let defaultSiteConfig = JSON.parse(JSON.stringify(defaultAstCurrentSiteConfig));\n        let themesHelper = new Themes(this.application, { site: this.site });\n        let currentTheme = themesHelper.currentTheme();\n        let siteConfigPath = path.join(this.siteDir, 'input', 'config', 'site.config.json');\n        let siteConfig = JSON.parse(FileHelper.readFileSync(siteConfigPath));\n        siteConfig = Utils.mergeObjects(defaultSiteConfig, siteConfig);\n        let imagesQuality = 60;\n        let alphaQuality = 100;\n        let forceWebp = false;\n        let webpLossless = false;\n        let imageExtension = path.parse(originalPath).ext;\n        let imageDimensions = {\n            width: false, \n            height: false\n        };\n\n        if (imageType === 'pluginImages') {\n            return [];\n        }\n\n        if (!siteConfig.advanced.responsiveImages && imageType !== 'galleryImages') {\n            return ['NO-RESPONSIVE-IMAGES'];\n        }\n\n        if (!this.allowedImageExtension(imageExtension)) {\n            return [];\n        }\n\n        try {\n            imageDimensions = sizeOf(this.path);\n        } catch(e) {\n            imageDimensions = {\n                width: false, \n                height: false\n            };\n        }\n\n        if (\n            siteConfig?.advanced?.imagesQuality &&\n            !isNaN(parseInt(siteConfig.advanced.imagesQuality, 10))\n        ) {\n            imagesQuality = siteConfig.advanced.imagesQuality;\n            imagesQuality = parseInt(imagesQuality);\n\n            if (imagesQuality < 1 || imagesQuality > 100) {\n                imagesQuality = 60;\n            }\n        }\n\n        if (\n            siteConfig?.advanced?.alphaQuality &&\n            !isNaN(parseInt(siteConfig.advanced.alphaQuality, 10))\n        ) {\n            alphaQuality = siteConfig.advanced.alphaQuality;\n            alphaQuality = parseInt(alphaQuality);\n\n            if (alphaQuality < 1 || alphaQuality > 100) {\n                alphaQuality = 100;\n            }\n        }\n\n        if (siteConfig?.advanced?.webpLossless) {\n            webpLossless = !!siteConfig.advanced.webpLossless;\n        }\n\n        if (siteConfig?.advanced?.forceWebp && !this.shouldUseJimp()) {\n            forceWebp = !!siteConfig.advanced.forceWebp;\n        }\n\n        // If there is no selected theme\n        if (currentTheme === 'not selected' && imageType !== 'galleryImages') {\n            return false;\n        }\n\n        // Load theme config\n        let themeDirPath = path.join(this.siteDir, 'input', 'themes', currentTheme);\n        let themeConfigPath = path.join(this.siteDir, 'input', 'config', 'theme.config.json');\n        let themeConfig = Themes.loadThemeConfig(themeConfigPath, themeDirPath);\n        let dimensions = false;\n        let dimensionsConfig = false;\n\n        if (['featuredImages', 'optionImages', 'tagImages', 'authorImages'].indexOf(imageType) > -1) {\n            if (Utils.responsiveImagesConfigExists(themeConfig, imageType)) {\n                dimensions = Utils.responsiveImagesDimensions(themeConfig, imageType);\n                dimensionsConfig = Utils.responsiveImagesData(themeConfig, imageType);\n            } else if (Utils.responsiveImagesConfigExists(themeConfig, 'featuredImages')) {\n                dimensions = Utils.responsiveImagesDimensions(themeConfig, 'featuredImages');\n                dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'featuredImages');\n            } else if (Utils.responsiveImagesConfigExists(themeConfig, 'contentImages')) {\n                dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages');\n                dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'contentImages');\n            }\n        } else if (imageType === 'contentImages' && Utils.responsiveImagesConfigExists(themeConfig, 'contentImages')) {\n            dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages');\n            dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'contentImages');\n        } else if (imageType === 'galleryImages' && Utils.responsiveImagesConfigExists(themeConfig, 'galleryImages')) {\n            dimensions = Utils.responsiveImagesDimensions(themeConfig, 'galleryImages');\n            dimensionsConfig = Utils.responsiveImagesData(themeConfig, 'galleryImages');\n\n            if (!dimensionsConfig) {\n                dimensions = ['thumbnail'];\n\n                dimensionsConfig = [];\n                dimensionsConfig['thumbnail'] = {\n                    crop: true,\n                    height: 240,\n                    width: 240\n                };\n            }\n        }\n\n        if (!dimensions) {\n            return false;\n        }\n\n        let targetImagesDir = path.parse(originalPath).dir;\n\n        if (imageType !== 'galleryImages') {\n            targetImagesDir = path.join(targetImagesDir, 'responsive');\n        }\n\n        let promises = [];\n\n        // create responsive images for each size\n        for (let name of dimensions) {\n            let finalHeight = dimensionsConfig[name].height;\n            let finalWidth = dimensionsConfig[name].width;\n            let cropImage = dimensionsConfig[name].crop;\n            let filename = path.parse(originalPath).name;\n            let extension = path.parse(originalPath).ext;\n            let destinationPath = path.join(targetImagesDir, filename + '-' + name + extension);\n            let result;\n            let shouldBeChangedToWebp = false;\n\n            if (!this.shouldUseJimp() && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) {\n                shouldBeChangedToWebp = true;\n            }\n\n            if (forceWebp && shouldBeChangedToWebp) {\n                destinationPath = path.join(targetImagesDir, filename + '-' + name + '.webp');\n            }\n\n            if (!this.allowedImageExtension(extension)) {\n                continue;\n            }\n\n            if (imageDimensions.width !== false && finalWidth !== 'auto' && finalWidth > imageDimensions.width) {\n                finalWidth = imageDimensions.width;\n            }\n\n            if (imageDimensions.height !== false && finalHeight !== 'auto' && finalHeight > imageDimensions.height) {\n                finalHeight = imageDimensions.height;\n            }\n\n            if (finalHeight === 'auto') {\n                finalHeight = null;\n            }\n            \n            if (finalWidth === 'auto') {\n                finalWidth = null;\n            }\n\n            if (cropImage) {\n                if (this.shouldUseJimp()) {\n                    result = new Promise (async (resolve, reject) => {\n                        try {\n                            let image = await Jimp.read(originalPath);\n                            console.log('JIMP COVER', finalWidth, ' x ', finalHeight);\n\n                            if (finalWidth === null || finalHeight === null) {\n                                let resizeOptions = {};\n                                if (finalWidth !== null) {\n                                    resizeOptions.w = finalWidth;\n                                }\n\n                                if (finalHeight !== null) {\n                                    resizeOptions.h = finalHeight;\n                                }\n\n                                image.resize(resizeOptions);\n                                await image.write(destinationPath, { quality: imagesQuality });\n                                resolve(destinationPath);\n                            } else {\n                                image.cover({ w: finalWidth, h: finalHeight });\n                                await image.write(destinationPath, { quality: imagesQuality });\n                                resolve(destinationPath);\n                            }\n                        } catch (err) {\n                            console.log(err);\n                            reject(err);\n                        }\n                    });\n                } else {\n                    result = new Promise ((resolve, reject) => {\n                        if (extension.toLowerCase() === '.png' && !forceWebp) {\n                            sharp(originalPath)\n                                .withMetadata()\n                                .resize(finalWidth, finalHeight, { withoutEnlargement: true, fastShrinkOnLoad: false })\n                                .toBuffer()\n                                .then(function (outputBuffer) {\n                                    let wstream = fs.createWriteStream(destinationPath);\n                                    wstream.write(outputBuffer);\n                                    wstream.end();\n\n                                    resolve(destinationPath);\n                                }).catch(err => reject(err))\n                        } else if (extension.toLowerCase() === '.webp' || (forceWebp && shouldBeChangedToWebp)) {\n                            let webpConfig = {\n                                quality: imagesQuality,\n                                alphaQuality: alphaQuality,\n                            };\n\n                            if (webpLossless) {\n                                webpConfig = {\n                                    lossless: true\n                                };\n                            }\n\n                            sharp(originalPath)\n                                .autoOrient()\n                                .withMetadata()\n                                .resize(finalWidth, finalHeight, { withoutEnlargement: true, fastShrinkOnLoad: false })\n                                .webp(webpConfig)\n                                .toBuffer()\n                                .then(function (outputBuffer) {\n                                    let wstream = fs.createWriteStream(destinationPath);\n                                    wstream.write(outputBuffer);\n                                    wstream.end();\n\n                                    resolve(destinationPath);\n                                }).catch(err => reject(err))\n                        } else {\n                            sharp(originalPath)\n                                .withMetadata()\n                                .resize(finalWidth, finalHeight, { withoutEnlargement: true, fastShrinkOnLoad: false })\n                                .jpeg({\n                                    quality: imagesQuality\n                                })\n                                .toBuffer()\n                                .then(function (outputBuffer) {\n                                    let wstream = fs.createWriteStream(destinationPath);\n                                    wstream.write(outputBuffer);\n                                    wstream.end();\n\n                                    resolve(destinationPath);\n                                }).catch(err => reject(err))\n                        }\n                    }).catch(err => console.log(err));\n                }\n            } else {\n                if (this.shouldUseJimp()) {\n                    result = new Promise (async (resolve, reject) => {\n                        try {\n                            const image = await Jimp.read(originalPath);\n                            console.log('JIMP RESIZE/SCALE TO FIT', finalWidth, ' x ', finalHeight);\n\n                            if (finalWidth && finalHeight) {\n                                image.scaleToFit({ w: finalWidth, h: finalHeight });\n                            } else if (finalWidth) {\n                                image.resize({ w: finalWidth });\n                            } else if (finalHeight) {\n                                image.resize({ h: finalHeight });\n                            }\n\n                            await image.write(destinationPath, { quality: imagesQuality });\n                            \n                            resolve(destinationPath);\n                        } catch (err) {\n                            console.error(err);\n                            reject(err);\n                        }\n                    });\n                } else {\n                    result = new Promise ((resolve, reject) => {\n                        if (extension.toLowerCase() === '.png' && !forceWebp) {\n                            sharp(originalPath)\n                                .withMetadata()\n                                .resize(finalWidth, finalHeight, { fit: 'inside', withoutEnlargement: true, fastShrinkOnLoad: false })\n                                .toBuffer()\n                                .then(function (outputBuffer) {\n                                    let wstream = fs.createWriteStream(destinationPath);\n                                    wstream.write(outputBuffer);\n                                    wstream.end();\n                                    resolve(destinationPath);\n                                }).catch(err => reject(err));\n                        } else if (extension.toLowerCase() === '.webp' || (forceWebp && shouldBeChangedToWebp)) {\n                            let webpConfig = {\n                                quality: imagesQuality,\n                                alphaQuality: alphaQuality,\n                            };\n\n                            if (webpLossless) {\n                                webpConfig = {\n                                    lossless: true\n                                };\n                            }\n\n                            sharp(originalPath)\n                                .autoOrient()\n                                .withMetadata()\n                                .resize(finalWidth, finalHeight, { fit: 'inside', withoutEnlargement: true, fastShrinkOnLoad: false })\n                                .webp(webpConfig)\n                                .toBuffer()\n                                .then(function (outputBuffer) {\n                                    let wstream = fs.createWriteStream(destinationPath);\n                                    wstream.write(outputBuffer);\n                                    wstream.end();\n                                    resolve(destinationPath);\n                                }).catch(err => reject(err));\n                        } else {\n                            sharp(originalPath)\n                                .withMetadata()\n                                .resize(finalWidth, finalHeight, { fit: 'inside', withoutEnlargement: true, fastShrinkOnLoad: false })\n                                .jpeg({\n                                    quality: imagesQuality\n                                })\n                                .toBuffer()\n                                .then(function (outputBuffer) {\n                                    let wstream = fs.createWriteStream(destinationPath);\n                                    wstream.write(outputBuffer);\n                                    wstream.end();\n                                    resolve(destinationPath);\n                                }).catch(err => reject(err));\n                        }\n                    }).catch(err => console.log(err));\n                }\n            }\n\n            promises.push(result);\n        }\n\n        return promises;\n    }\n\n    /*\n     * Get SVG image dimensions\n     */\n    getSvgImageDimensions(imagePath) {\n        let result = {\n            height: false,\n            width: false\n        };\n\n        // Get content of the SVG image\n        let svgFileContent = FileHelper.readFileSync(imagePath, 'utf8');\n        // Look for the non-percentage values in the <svg> tag\n        let svgWidth = svgFileContent.match(/\\<svg.*width=['\"]{1}(.*?)['\"]{1}.*\\>/mi);\n        let svgHeight = svgFileContent.match(/\\<svg.*height=['\"]{1}(.*?)['\"]{1}.*\\>/mi);\n        let svgViewBox = svgFileContent.match(/\\<svg.*viewBox=['\"]{1}(.*?)['\"]{1}.*\\>/mi);\n\n        if (svgWidth && svgHeight && svgWidth[1].indexOf('%') === 1 && svgHeight[1].indexOf('%') === 1) {\n            result.height = parseInt(svgHeight, 10);\n            result.width = parseInt(svgWidth, 10);\n        } else if (svgViewBox && svgViewBox[1]) {\n            svgViewBox = svgViewBox[1].split(' ');\n\n            if (svgViewBox.length === 4) {\n                result.height = svgViewBox[3];\n                result.width = svgViewBox[2];\n            }\n        }\n\n        return result;\n    }\n\n    /*\n     * Check if the image has supported image extension\n     */\n    allowedImageExtension(extension) {\n        let allowedExtensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG', '.webp', '.WEBP'];\n\n        if (this.shouldUseJimp()) {\n            allowedExtensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG'];\n        }\n\n        return allowedExtensions.indexOf(extension) > -1;\n    }\n\n    /*\n     * Detect if Jimp should be used\n     */\n    shouldUseJimp() {\n        return this.appInstance.appConfig.resizeEngine && this.appInstance.appConfig.resizeEngine === 'jimp';\n    }\n}\n\nmodule.exports = Image;\n"
  },
  {
    "path": "app/back-end/languages.js",
    "content": "/*\n * Languages instance\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./helpers/file.js');\nconst UtilsHelper = require('./helpers/utils.js');\nconst languageConfigValidator = require('./helpers/validators/language-config.js');\nconst normalizePath = require('normalize-path');\n\nclass Languages {\n    constructor(appInstance) {\n        this.basePath = appInstance.appDir;\n        this.languagesPath = path.join(this.basePath, 'languages');\n        this.appInstance = appInstance;\n    }\n\n    /*\n     * Load languages from a specific path\n     */\n    loadLanguages () {\n        let pathToLanguages = this.languagesPath;\n        let pathToDefaultLanguages = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked');\n        let output = [];\n\n        // Load default languages\n        let defaultLanguages = fs.readdirSync(pathToDefaultLanguages);\n\n        for(let i = 0; i < defaultLanguages.length; i++) {\n            if (defaultLanguages[i][0] === '.' || !UtilsHelper.dirExists(path.join(pathToDefaultLanguages, defaultLanguages[i]))) {\n                continue;\n            }\n\n            let configPath = path.join(pathToDefaultLanguages, defaultLanguages[i], 'config.json');\n\n            // Load only proper languages\n            if (!fs.existsSync(configPath)) {\n                continue;\n            }\n\n            // Load only properly configured languages\n            if(languageConfigValidator(configPath) !== true) {\n                continue;\n            }\n\n            let languageData = FileHelper.readFileSync(configPath, 'utf8');\n            languageData = JSON.parse(languageData);\n\n            output.push({\n                type: 'default',\n                directory: defaultLanguages[i],\n                name: languageData.name,\n                version: languageData.version,\n                author: languageData.author,\n                publiiSupport: languageData.publiiSupport,\n                momentLocale: languageData.momentLocale,\n                wysiwygTranslation: languageData.wysiwygTranslation\n            });\n        }\n\n        // Load additional languages\n        let filesAndDirs = fs.readdirSync(pathToLanguages);\n\n        for(let i = 0; i < filesAndDirs.length; i++) {\n            if (filesAndDirs[i][0] === '.' || !UtilsHelper.dirExists(path.join(pathToLanguages, filesAndDirs[i]))) {\n                continue;\n            }\n\n            let configPath = path.join(pathToLanguages, filesAndDirs[i], 'config.json');\n\n            // Load only proper languages\n            if (!fs.existsSync(configPath)) {\n                continue;\n            }\n\n            // Load only properly configured languages\n            if(languageConfigValidator(configPath) !== true) {\n                continue;\n            }\n\n            let languageData = FileHelper.readFileSync(configPath, 'utf8');\n            languageData = JSON.parse(languageData);\n\n            output.push({\n                type: 'installed',\n                directory: filesAndDirs[i],\n                name: languageData.name,\n                version: languageData.version,\n                author: languageData.author,\n                publiiSupport: languageData.publiiSupport,\n                momentLocale: languageData.momentLocale,\n                wysiwygTranslation: languageData.wysiwygTranslation\n            });\n        }\n\n        return output;\n    }\n\n    /*\n     * Remove specific language from the app directory\n     */\n    removeLanguage(directory) {\n        fs.removeSync(path.join(this.languagesPath, directory));\n    }\n\n    /*\n     * Fixes path for the media file\n     */\n    normalizeLanguageImagePath(imagePath) {\n        // Save the image if necessary\n        imagePath = normalizePath(imagePath);\n        imagePath = imagePath.replace('file:/', '');\n\n        return imagePath;\n    }\n\n    /**\n     * Load translations\n     */\n    loadTranslations (languageName = 'en-gb', type = 'default') {\n        let translationsPath = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked');\n\n        if (type !== 'default') {\n            translationsPath = this.languagesPath;\n        }\n\n        translationsPath = path.join(translationsPath, languageName, 'translations.json');\n\n        if (!UtilsHelper.fileExists(translationsPath)) {\n            return false;\n        }\n\n        return UtilsHelper.requireWithNoCache(translationsPath);\n    }\n\n    /**\n     * Load translations\n     */\n     loadWysiwygTranslation (languageName = 'en-gb', type = 'default') {\n        let translationsPath = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked');\n\n        if (type !== 'default') {\n            translationsPath = this.languagesPath;\n        }\n\n        translationsPath = path.join(translationsPath, languageName, 'wysiwyg.json');\n\n        if (!UtilsHelper.fileExists(translationsPath)) {\n            return false;\n        }\n\n        return UtilsHelper.requireWithNoCache(translationsPath);\n    }\n\n    /**\n     * Load language config\n     */\n    loadLanguageConfig (languageName = 'en-gb', type = 'default') {\n        let configPath = path.join(__dirname, '..', 'default-files', 'default-languages').replace('app.asar', 'app.asar.unpacked');\n\n        if (type !== 'default') {\n            configPath = this.languagesPath;\n        }\n\n        configPath = path.join(configPath, languageName, 'config.json');\n\n        if (!UtilsHelper.fileExists(configPath)) {\n            return false;   \n        }\n\n        let languageConfig = FileHelper.readFileSync(configPath, 'utf8');\n\n        try {\n            languageConfig = JSON.parse(languageConfig);\n        } catch (e) {\n            return false;\n        }\n\n        return languageConfig;\n    }\n}\n\nmodule.exports = Languages;\n"
  },
  {
    "path": "app/back-end/migrators/site-config.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst slug = require('./../helpers/slug');\nconst Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3');\nconst DBUtils = require('../helpers/db.utils.js');\n\nclass SiteConfigMigrator {\n    static moveOldAuthorData(appInstance, siteConfig) {\n        // Check if old author data exists\n        if(!siteConfig.author && siteConfig.author !== null) {\n            // Return unmodified site config\n            return siteConfig;\n        }\n\n        console.log('MIRATION IN: ' + siteConfig.name);\n\n        // If yes - save them in database as author with ID = 1\n        let siteDir = path.join(appInstance.sitesDir, siteConfig.name);\n        let dbPath = path.join(siteDir, 'input', 'db.sqlite');\n        let db = new DBUtils(new Database(dbPath));\n        let newAuthorName = siteConfig.author.name;\n        let newAuthorUsername = slug(newAuthorName);\n        let newAuthorConfig = {\n            avatar: siteConfig.author.avatar,\n            email: siteConfig.author.email,\n            description: siteConfig.author.description,\n            useGravatar: siteConfig.author.useGravatar\n        };\n            newAuthorConfig = JSON.stringify(newAuthorConfig);\n        let configFilePath = path.join(siteDir, 'input', 'config', 'site.config.json');\n        let sqlQuery = db.prepare(`\n            UPDATE \n                authors \n            SET \n                name = @name, \n                username = @username, \n                config = @config \n            WHERE \n                id = 1;\n        `);\n\n        sqlQuery.run({\n            name: newAuthorName,\n            username: newAuthorUsername,\n            config: newAuthorConfig\n        });\n\n        // Remove from the config author data\n        delete siteConfig.author;\n        fs.writeFileSync(configFilePath, JSON.stringify(siteConfig, null, 4), {'flags': 'w'});\n\n        // close DB connection\n        db.close();\n\n        // Return modified (or not) site config\n        return siteConfig;\n    }\n}\n\nmodule.exports = SiteConfigMigrator;\n"
  },
  {
    "path": "app/back-end/model.js",
    "content": "/*\n * Model base class\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nclass Model {\n    /**\n     * Model constructor\n     *\n     * @param appInstance\n     * @param data\n     */\n    constructor(appInstance, data) {\n        this.application = appInstance;\n        this.db = this.application.db;\n        this.site = data.site;\n        this.appDir = this.application.appDir;\n        this.siteDir = path.join(this.application.sitesDir, this.site);\n        this.dbPath = path.join(this.siteDir, 'input', 'db.sqlite');\n    }\n\n    /**\n     * Escapes given string\n     *\n     * @param stringToEscape\n     * @returns {*}\n     */\n    escape(stringToEscape) {\n        if(stringToEscape == '') {\n            return stringToEscape;\n        }\n\n        return stringToEscape\n            .replace(/\\\\/g, \"\\\\\\\\\")\n            .replace(/\\'/g, \"\\\\\\'\")\n            .replace(/\\\"/g, \"\\\\\\\"\")\n            .replace(/\\n/g, \"\\\\\\n\")\n            .replace(/\\r/g, \"\\\\\\r\")\n            .replace(/\\x00/g, \"\\\\\\x00\")\n            .replace(/\\x1a/g, \"\\\\\\x1a\");\n    }\n\n    /**\n     * Modify field\n     * \n     * @param {string} table - table name\n     * @param {number} itemID - item ID\n     * @param {Array} fieldsToUpdate - array of fields to update\n     * \n     *   [{\n     *       field: 'slug',\n     *       value: 'new-slug-value',\n     *       type: 'field'\n     *   },\n     *   {\n     *       field: '_core',\n     *       subfield: 'metaDesc'\n     *       value: 'New Title',\n     *       type: 'json'\n     *   }]\n     */\n    updateField (table, itemID, fieldsToUpdate) {\n        const ALLOWED_TABLES = [\n            'authors',\n            'posts',\n            'posts_additional_data',\n            'tags'\n        ];\n\n        const ALLOWED_COLUMNS_BY_TABLE = {\n            'authors': ['name', 'username', 'config', 'additional_data'],\n            'posts': ['slug', 'title', 'text'],\n            'posts_additional_data': ['key', 'value'],\n            'tags': ['name', 'slug', 'description', 'additional_data']\n        };\n\n        if (!ALLOWED_TABLES.includes(table)) {\n            return {\n                status: 'error',\n                error: 'Invalid table name: ' + table\n            };\n        }\n\n        let invalidField = false;\n\n        fieldsToUpdate.forEach(fieldObj => {\n            if (invalidField) {\n                return;\n            }\n\n            let dbColumnName;\n            let tableName = table;\n            let idColumn = 'id';\n\n            if (table === 'posts' && fieldObj.field === '_core') {\n                tableName = 'posts_additional_data';\n            }\n\n            if (tableName === 'posts_additional_data') {\n                idColumn = 'post_id';\n            }\n\n            let allowedFieldsForThisTable = ALLOWED_COLUMNS_BY_TABLE[tableName] || [];\n            \n            if (tableName === 'posts_additional_data') {\n                dbColumnName = 'value';\n            } else {\n                dbColumnName = fieldObj.field;\n            }\n\n            if (!allowedFieldsForThisTable.includes(dbColumnName)) {\n                invalidField = dbColumnName;\n                return;\n            }\n\n            if (fieldObj.type === 'field') {\n                let sql = this.db.prepare(`UPDATE ${tableName} SET ${dbColumnName} = @column WHERE ${idColumn} = @id`);\n                sql.run({\n                    column: fieldObj.value, \n                    id: itemID\n                });\n            } else if (fieldObj.type === 'json') {\n                let sqlSelect;\n                let sqlUpdate;\n                let jsonData;\n\n                if (table === 'posts_additional_data') {\n                    sqlSelect = this.db.prepare(`SELECT value FROM ${tableName} WHERE ${idColumn} = @id AND key = @field`);\n                    let row = sqlSelect.get({\n                        id: itemID, \n                        field: fieldObj.field\n                    });\n                    jsonData = row ? JSON.parse(row.value) : {};\n                } else {\n                    sqlSelect = this.db.prepare(`SELECT ${dbColumnName} FROM ${tableName} WHERE ${idColumn} = @id`);\n                    let row = sqlSelect.get({\n                        id: itemID\n                    });\n                    jsonData = row ? JSON.parse(row[dbColumnName]) : {};\n                }\n\n                jsonData[fieldObj.subfield] = fieldObj.value;\n                let newJsonString = JSON.stringify(jsonData);\n\n                if (table === 'posts_additional_data') {\n                    sqlUpdate = this.db.prepare(`UPDATE ${tableName} SET value = @json WHERE ${idColumn} = @id AND key = @field`);\n                    sqlUpdate.run({\n                        json: newJsonString, \n                        id: itemID, \n                        field: fieldObj.field\n                    });\n                } else {\n                    sqlUpdate = this.db.prepare(`UPDATE ${tableName} SET ${dbColumnName} = @json WHERE ${idColumn} = @id AND key = @field`);\n                    sqlUpdate.run({\n                        json: newJsonString, \n                        id: itemID,\n                        field: fieldObj.field\n                    });\n                }\n            }\n        });\n\n        if (invalidField) {\n            return {\n                status: 'error',\n                error: 'Invalid table column name: ' + invalidField\n            }; \n        }\n\n        return {\n            status: 'success'\n        };\n    }\n}\n\nmodule.exports = Model;\n"
  },
  {
    "path": "app/back-end/modules/backup/backup.js",
    "content": "/*\n * Class used to create backups\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst Utils = require('./../../helpers/utils.js');\nconst moment = require('moment');\nconst archiver = require('archiver');\nconst tar = require('tar-fs');\nconst { shell } = require('electron');\n\nclass Backup {\n    /**\n     * Loads list of backups\n     *\n     * @param siteName\n     * @param backupsDir\n     * @returns {*}\n     */\n    static loadList(siteName, backupsDir) {\n        let backupsPath = path.join(backupsDir, siteName);\n\n        if(!Utils.dirExists(backupsPath)) {\n            if(Utils.dirExists(backupsDir)) {\n                fs.mkdirSync(backupsPath, { recursive: true });\n            } else {\n                return false;\n            }\n        }\n\n        let files = [];\n        let allFiles = fs.readdirSync(backupsPath);\n        let index = 0;\n\n        for(let file of allFiles) {\n            if(path.parse(file).ext !== '.tar') {\n                continue;\n            }\n\n            let stats = fs.statSync(path.join(backupsPath, file));\n            let size = Backup.convertToMegabytes(stats.size);\n            let createdAt = stats.birthtime || stats.mtime;\n\n            files.push({\n                id: index,\n                name: file,\n                size: size,\n                url: path.join(backupsPath, file),\n                createdAt: Date.parse(createdAt)\n            });\n\n            index++;\n        }\n\n        files.sort((a,b) => {\n            return b.createdAt - a.createdAt;\n        });\n\n        files = files.map(item => {\n            item.createdAt = moment(new Date(item.createdAt)).format('MM-DD-YYYY HH:mm');\n\n            return item;\n        });\n\n        return files;\n    }\n\n    /**\n     * Creates backup\n     *\n     * @param siteName\n     * @param backupFilename\n     * @param backupsDir\n     * @param sourceDir\n     */\n    static async create(siteName, backupFilename, backupsDir, sourceDir) {\n        let sourcePath = path.join(sourceDir);\n        let backupsPath = path.join(backupsDir, siteName);\n\n        if(!Utils.dirExists(backupsPath)) {\n            if(Utils.dirExists(backupsDir)) {\n                fs.mkdirSync(backupsPath, { recursive: true });\n            } else {\n                return {\n                    type: 'app-backup-create-error',\n                    status: false,\n                    error: 'core.backup.locationDoesNotExists'\n                };\n            }\n        }\n\n        if (Utils.dirExists(backupsPath)) {\n            backupFilename = backupFilename.replace(/[^a-z0-9\\-\\_]/gmi, '');\n            let backupFile = path.join(backupsPath, backupFilename + '.tar');\n            let createOperation = new Promise(function (resolve, reject) {\n                let output = fs.createWriteStream(backupFile);\n                let archive = archiver('tar');\n                \n                output.on('error', function (err) {\n                    resolve({\n                        type: 'app-backup-create-error',\n                        status: false,\n                        error: err\n                    });\n                });\n\n                output.on('close', function () {\n                    resolve({\n                        type: 'app-backup-create-success',\n                        backups: Backup.loadList(siteName, backupsDir)\n                    });\n                });\n\n                archive.on('error', function (err) {\n                    resolve({\n                        type: 'app-backup-create-error',\n                        status: false,\n                        error: err\n                    });\n                });\n\n                archive.pipe(output);\n                archive.append((+new Date).toString(), { name: 'backup-date.log' });\n                archive.directory(sourcePath + '/input/', 'input');\n                archive.finalize();\n            });\n\n            let results = await createOperation;\n            return results;\n        }\n\n        return {\n            type: 'app-backup-create-error',\n            status: false,\n            error: 'core.backup.locationDoesNotExists'\n        };\n    }\n\n    /**\n     * Removes backup\n     *\n     * @param siteName\n     * @param backupsNames\n     * @param backupsDir\n     * @returns {{status: boolean, backups: *}}\n     */\n    static async remove(siteName, backupsNames, backupsDir) {\n        for(let backupName of backupsNames) {\n            let backupFilePath = path.join(backupsDir, siteName, backupName);\n\n            if (!Utils.fileExists(backupFilePath)) {\n                return {\n                    status: false,\n                    backups: Backup.loadList(siteName, backupsDir)\n                };\n            }\n\n            try {\n                await shell.trashItem(backupFilePath);\n            } catch (e) {\n                console.log('ERR:', e);\n                return Promise.resolve({\n                    status: false,\n                    backups: Backup.loadList(siteName, backupsDir)\n                });\n            }\n        }\n\n        return Promise.resolve({\n            status: true,\n            backups: Backup.loadList(siteName, backupsDir)\n        });\n    }\n\n    /**\n     * Renames backup\n     *\n     * @param siteName\n     * @param oldBackupName\n     * @param newBackupName\n     * @param backupsDir\n     * @returns {{status: boolean, backups: *}}\n     */\n    static rename(siteName, oldBackupName, newBackupName, backupsDir) {\n        let oldBackupFilePath = path.join(backupsDir, siteName, oldBackupName + '.tar');\n        let newBackupFilePath = path.join(backupsDir, siteName, newBackupName + '.tar');\n\n        if (!Utils.fileExists(oldBackupFilePath) || Utils.fileExists(newBackupFilePath)) {\n            return {\n                status: false,\n                backups: Backup.loadList(siteName, backupsDir)\n            };\n        }\n\n        try {\n            fs.renameSync(oldBackupFilePath, newBackupFilePath);\n        } catch (e) {\n            return {\n                status: false,\n                backups: Backup.loadList(siteName, backupsDir)\n            };\n        }\n\n        return {\n            status: true,\n            backups: Backup.loadList(siteName, backupsDir)\n        };\n    }\n\n    /**\n     * Restores backup\n     *\n     * @param siteName\n     * @param backupName\n     * @param backupsDir\n     * @param destinationDir\n     * @param tempDir\n     */\n    static async restore(siteName, backupName, backupsDir, destinationDir, tempDir, appInstance) {\n        let backupFilePath = path.join(backupsDir, siteName, backupName);\n        let destinationPath = path.join(destinationDir, siteName);\n\n        if (!Utils.fileExists(backupFilePath)) {\n            return {\n                type: 'app-backup-restore-error',\n                status: false,\n                error: 'core.backup.fileDoesNotExists'\n            };\n        }\n\n        if (!Utils.dirExists(destinationDir)) {\n            return {\n                type: 'app-backup-restore-error',\n                status: false,\n                error: 'core.backup.destinationDirectoryDoesNotExists'\n            };\n        }\n\n        if(!Utils.dirExists(tempDir)) {\n            fs.mkdirSync(tempDir, { recursive: true });\n\n            if(!Utils.dirExists(tempDir)) {\n                return {\n                    type: 'app-backup-restore-error',\n                    status: false,\n                    error: 'core.backup.temporaryDirectoryDoesNotExists'\n                };\n            }\n        }\n\n        // Empty the temp directory before extracting the backups content\n        fs.emptyDirSync(tempDir);\n\n        let restoreOperation = new Promise(function (resolve, reject) {\n            fs.createReadStream(backupFilePath)\n                .on('error', function(err) {\n                    resolve({\n                        type: 'app-backup-restore-error',\n                        status: false,\n                        error: 'core.backup.errorDuringReadingBackupFile'\n                    });\n                })\n                .pipe(tar.extract(tempDir, {\n                    finish: () => {\n                    // Verify the backup\n                    let backupTest = Backup.verify(tempDir, siteName);\n        \n                    if(!backupTest) {    \n                        resolve({\n                            type: 'app-backup-restore-error',\n                            status: false,\n                            error: 'core.backup.errorDuringReadingBackupFile'\n                        });\n\n                        return;\n                    }\n        \n                    // Close DB connection and remove site dir contents\n                    if (appInstance.db) {\n                        try {\n                            appInstance.db.close();\n                        } catch (e) {\n                            console.log('[BACKUP RESTORE] DB already closed');\n                        }\n                    }\n        \n                    fs.emptyDirSync(destinationPath);\n        \n                    // Move files from the temp dir to the site dir\n                    let backupContents = fs.readdirSync(tempDir);\n        \n                    for(let content of backupContents) {\n                        fs.moveSync(\n                            path.join(tempDir, content),\n                            path.join(destinationPath, content)\n                        );\n                    }\n        \n                    resolve({\n                        type: 'app-backup-restore-success',\n                        status: true\n                    });\n                }}));\n        });\n\n        let results = await restoreOperation;\n        return results;\n    }\n\n    /**\n     * Verifies backup\n     *\n     * @param backupDir\n     * @param siteName\n     * @returns {boolean}\n     */\n    static verify(backupDir, siteName) {\n        let foundedErrors = false;\n        let configFilePath = path.join(backupDir, 'input', 'config', 'site.config.json');\n        let dirsToCheck = [\n            path.join(backupDir, 'input'),\n            path.join(backupDir, 'input', 'config'),\n            path.join(backupDir, 'input', 'media'),\n            path.join(backupDir, 'input', 'themes'),\n        ];\n        let filesToCheck = [\n            path.join(backupDir, 'input', 'db.sqlite'),\n            configFilePath\n        ];\n\n        for(let i = 0; i < dirsToCheck.length; i++) {\n            if (!Utils.dirExists(dirsToCheck[i])) {\n                foundedErrors = true;\n            }\n        }\n\n        for(let i = 0; i < filesToCheck.length; i++) {\n            if (!Utils.fileExists(filesToCheck[i])) {\n                foundedErrors = true;\n            }\n        }\n\n        // If errors were founded\n        if(foundedErrors) {\n            parentPort.postMessage({\n                type: 'app-backup-restore-error',\n                status: false,\n                error: 'core.backup.fileIsCorrupted'\n            });\n\n            return false;\n        }\n\n        Backup.checkSiteName(siteName, configFilePath);\n\n        return true;\n    }\n\n    /**\n     *\n     * Check if the site name in the config file is the same as the current site name\n     *\n     * if not - change the config before the backup restore\n     *\n     * @param siteName - name of the website to check\n     * @param configFilePath - path to the temporary config file\n     *\n     */\n    static checkSiteName(siteName, configFilePath) {\n        let configContent = FileHelper.readFileSync(configFilePath);\n\n        try {\n            configContent = JSON.parse(configContent);\n\n            if(configContent.name !== siteName) {\n                configContent.name = siteName;\n                fs.writeFileSync(configFilePath, JSON.stringify(configContent, null, 4));\n            }\n        } catch(e) {\n            console.log('modules/backup.js: Wrong site.config.json file');\n        }\n    }\n\n    /**\n     * Converts bytes to megabytes\n     *\n     * @param fileSizeInBytes\n     * @returns {string}\n     */\n    static convertToMegabytes(fileSizeInBytes) {\n        return Number(fileSizeInBytes / (1024 * 1024)).toFixed(2) + ' MB';\n    }\n}\n\nmodule.exports = Backup;\n"
  },
  {
    "path": "app/back-end/modules/backup/create-from-backup.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst tar = require('tar-fs');\nconst Utils = require('./../../helpers/utils.js');\n\nclass CreateFromBackup {\n    constructor (appInstance, backupPath) {\n        this.backupPath = backupPath;\n        this.appInstance = appInstance;\n        this.baseDir = path.join(this.appInstance.appDir, 'temp');\n        this.tempDir = path.join(this.baseDir, 'backup-to-restore');\n    }\n\n    async prepareBackupToRestore () {\n        if (!this.checkExtension()) {\n            return {\n                status: 'error',\n                type: 'unsupported-format'\n            };\n        }\n\n        return await this.unpackBackup();\n    }\n\n    checkExtension () {\n        if (this.backupPath.substr(-4) === '.tar') {\n            return true;\n        }\n\n        return false;\n    }\n\n    async unpackBackup () {\n        this.removeBackupFilesIfNecessary();\n\n        let extractOperation = new Promise((resolve, reject) => {\n            fs.createReadStream(this.backupPath).on('error', (err) => {\n                this.removeBackupFilesIfNecessary();\n                resolve({\n                    status: 'error',\n                    type: 'unpack-error'\n                });\n            }).pipe(tar.extract(this.tempDir, {\n                finish: () => {\n                let backupTestResult = this.verifyBackup(this.tempDir);\n    \n                if (!backupTestResult) {\n                    this.removeBackupFilesIfNecessary();\n                      \n                    resolve({\n                        status: 'error',\n                        type: 'invalid-backup-content'\n                    });\n\n                    return;\n                }\n\n                let siteNameData = this.getSiteName();\n\n                if (!siteNameData) {\n                    this.removeBackupFilesIfNecessary();\n\n                    resolve({\n                        status: 'error',\n                        type: 'invalid-site-data'\n                    });\n\n                    return;\n                }\n    \n                resolve({\n                    status: 'success',\n                    type: 'unpack-success',\n                    data: {\n                        displayName: siteNameData.displayName,\n                        catalogName: siteNameData.catalogName\n                    }\n                });\n            }}));\n        });\n\n        let results = await extractOperation;\n        return results;\n    }\n\n    verifyBackup(backupDir) {\n        let foundedErrors = false;\n        let configFilePath = path.join(backupDir, 'input', 'config', 'site.config.json');\n        let dirsToCheck = [\n            path.join(backupDir, 'input'),\n            path.join(backupDir, 'input', 'config'),\n            path.join(backupDir, 'input', 'media'),\n            path.join(backupDir, 'input', 'themes'),\n        ];\n        let filesToCheck = [\n            path.join(backupDir, 'input', 'db.sqlite'),\n            configFilePath\n        ];\n\n        for(let i = 0; i < dirsToCheck.length; i++) {\n            if (!Utils.dirExists(dirsToCheck[i])) {\n                foundedErrors = true;\n            }\n        }\n\n        for(let i = 0; i < filesToCheck.length; i++) {\n            if (!Utils.fileExists(filesToCheck[i])) {\n                foundedErrors = true;\n            }\n        }\n\n        // If errors were founded\n        if(foundedErrors) {\n            return false;\n        }\n\n        return true;\n    }\n\n    getSiteName () {\n        let configFilePath = path.join(this.tempDir, 'input', 'config', 'site.config.json');\n        let configContent = FileHelper.readFileSync(configFilePath, 'utf8');\n        let siteNameData = false;\n\n        try {\n            let parsedConfig = JSON.parse(configContent);\n            siteNameData = {\n                displayName: parsedConfig.displayName,\n                catalogName: parsedConfig.name\n            };\n        } catch (e) {\n            siteNameData = false;\n        }\n\n        return siteNameData;\n    }\n\n    removeBackupFilesIfNecessary () {\n        if (fs.existsSync(this.tempDir)) {\n            fs.emptyDirSync(this.tempDir);\n        }\n    }\n}\n\nmodule.exports = CreateFromBackup;\n"
  },
  {
    "path": "app/back-end/modules/custom-changes/ftp/LICENSE",
    "content": "Copyright Brian White. All rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to\ndeal in the Software without restriction, including without limitation the\nrights to use, copy, modify, merge, publish, distribute, sublicense, and/or\nsell copies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\nIN THE SOFTWARE."
  },
  {
    "path": "app/back-end/modules/custom-changes/ftp/README.md",
    "content": "Description\n===========\n\nnode-ftp is an FTP client module for [node.js](http://nodejs.org/) that provides an asynchronous interface for communicating with an FTP server.\n\n\nRequirements\n============\n\n* [node.js](http://nodejs.org/) -- v0.8.0 or newer\n\n\nInstall\n=======\n\n    npm install ftp\n\n\nExamples\n========\n\n* Get a directory listing of the current (remote) working directory:\n\n```javascript\n  var Client = require('ftp');\n\n  var c = new Client();\n  c.on('ready', function() {\n    c.list(function(err, list) {\n      if (err) throw err;\n      console.dir(list);\n      c.end();\n    });\n  });\n  // connect to localhost:21 as anonymous\n  c.connect();\n```\n\n* Download remote file 'foo.txt' and save it to the local file system:\n\n```javascript\n  var Client = require('ftp');\n  var fs = require('fs');\n\n  var c = new Client();\n  c.on('ready', function() {\n    c.get('foo.txt', function(err, stream) {\n      if (err) throw err;\n      stream.once('close', function() { c.end(); });\n      stream.pipe(fs.createWriteStream('foo.local-copy.txt'));\n    });\n  });\n  // connect to localhost:21 as anonymous\n  c.connect();\n```\n\n* Upload local file 'foo.txt' to the server:\n\n```javascript\n  var Client = require('ftp');\n  var fs = require('fs');\n\n  var c = new Client();\n  c.on('ready', function() {\n    c.put('foo.txt', 'foo.remote-copy.txt', function(err) {\n      if (err) throw err;\n      c.end();\n    });\n  });\n  // connect to localhost:21 as anonymous\n  c.connect();\n```\n\n\nAPI\n===\n\nEvents\n------\n\n* **greeting**(< _string_ >msg) - Emitted after connection. `msg` is the text the server sent upon connection.\n\n* **ready**() - Emitted when connection and authentication were sucessful.\n\n* **close**(< _boolean_ >hadErr) - Emitted when the connection has fully closed.\n\n* **end**() - Emitted when the connection has ended.\n\n* **error**(< _Error_ >err) - Emitted when an error occurs. In case of protocol-level errors, `err` contains a 'code' property that references the related 3-digit FTP response code.\n\n\nMethods\n-------\n\n**\\* Note: As with the 'error' event, any error objects passed to callbacks will have a 'code' property for protocol-level errors.**\n\n* **(constructor)**() - Creates and returns a new FTP client instance.\n\n* **connect**(< _object_ >config) - _(void)_ - Connects to an FTP server. Valid config properties:\n\n    * host - _string_ - The hostname or IP address of the FTP server. **Default:** 'localhost'\n\n    * port - _integer_ - The port of the FTP server. **Default:** 21\n\n    * secure - _mixed_ - Set to true for both control and data connection encryption, 'control' for control connection encryption only, or 'implicit' for implicitly encrypted control connection (this mode is deprecated in modern times, but usually uses port 990) **Default:** false\n\n    * secureOptions - _object_ - Additional options to be passed to `tls.connect()`. **Default:** (none)\n\n    * user - _string_ - Username for authentication. **Default:** 'anonymous'\n\n    * password - _string_ - Password for authentication. **Default:** 'anonymous@'\n\n    * connTimeout - _integer_ - How long (in milliseconds) to wait for the control connection to be established. **Default:** 10000\n\n    * pasvTimeout - _integer_ - How long (in milliseconds) to wait for a PASV data connection to be established. **Default:** 10000\n\n    * keepalive - _integer_ - How often (in milliseconds) to send a 'dummy' (NOOP) command to keep the connection alive. **Default:** 10000\n\n* **end**() - _(void)_ - Closes the connection to the server after any/all enqueued commands have been executed.\n\n* **destroy**() - _(void)_ - Closes the connection to the server immediately.\n\n### Required \"standard\" commands (RFC 959)\n\n* **list**([< _string_ >path, ][< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Retrieves the directory listing of `path`. `path` defaults to the current working directory. `useCompression` defaults to false. `callback` has 2 parameters: < _Error_ >err, < _array_ >list. `list` is an array of objects with these properties:\n\n      * type - _string_ - A single character denoting the entry type: 'd' for directory, '-' for file (or 'l' for symlink on **\\*NIX only**).\n\n      * name - _string_ - The name of the entry.\n\n      * size - _string_ - The size of the entry in bytes.\n\n      * date - _Date_ - The last modified date of the entry.\n\n      * rights - _object_ - The various permissions for this entry **(*NIX only)**.\n\n          * user - _string_ - An empty string or any combination of 'r', 'w', 'x'.\n\n          * group - _string_ - An empty string or any combination of 'r', 'w', 'x'.\n\n          * other - _string_ - An empty string or any combination of 'r', 'w', 'x'.\n     \n      * owner - _string_ - The user name or ID that this entry belongs to **(*NIX only)**.\n\n      * group - _string_ - The group name or ID that this entry belongs to **(*NIX only)**.\n\n      * target - _string_ - For symlink entries, this is the symlink's target **(*NIX only)**.\n\n      * sticky - _boolean_ - True if the sticky bit is set for this entry **(*NIX only)**.\n\n* **get**(< _string_ >path, [< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Retrieves a file at `path` from the server. `useCompression` defaults to false. `callback` has 2 parameters: < _Error_ >err, < _ReadableStream_ >fileStream.\n\n* **put**(< _mixed_ >input, < _string_ >destPath, [< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Sends data to the server to be stored as `destPath`. `input` can be a ReadableStream, a Buffer, or a path to a local file. `useCompression` defaults to false. `callback` has 1 parameter: < _Error_ >err.\n\n* **append**(< _mixed_ >input, < _string_ >destPath, [< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Same as **put()**, except if `destPath` already exists, it will be appended to instead of overwritten.\n\n* **rename**(< _string_ >oldPath, < _string_ >newPath, < _function_ >callback) - _(void)_ - Renames `oldPath` to `newPath` on the server. `callback` has 1 parameter: < _Error_ >err.\n\n* **logout**(< _function_ >callback) - _(void)_ - Logout the user from the server. `callback` has 1 parameter: < _Error_ >err.\n\n* **delete**(< _string_ >path, < _function_ >callback) - _(void)_ - Deletes a file, `path`, on the server. `callback` has 1 parameter: < _Error_ >err.\n\n* **cwd**(< _string_ >path, < _function_ >callback) - _(void)_ - Changes the current working directory to `path`. `callback` has 2 parameters: < _Error_ >err, < _string_ >currentDir. Note: `currentDir` is only given if the server replies with the path in the response text.\n\n* **abort**(< _function_ >callback) - _(void)_ - Aborts the current data transfer (e.g. from get(), put(), or list()). `callback` has 1 parameter: < _Error_ >err.\n\n* **site**(< _string_ >command, < _function_ >callback) - _(void)_ - Sends `command` (e.g. 'CHMOD 755 foo', 'QUOTA') using SITE. `callback` has 3 parameters: < _Error_ >err, < _string >responseText, < _integer_ >responseCode.\n\n* **status**(< _function_ >callback) - _(void)_ - Retrieves human-readable information about the server's status. `callback` has 2 parameters: < _Error_ >err, < _string_ >status.\n\n* **ascii**(< _function_ >callback) - _(void)_ - Sets the transfer data type to ASCII. `callback` has 1 parameter: < _Error_ >err.\n\n* **binary**(< _function_ >callback) - _(void)_ - Sets the transfer data type to binary (default at time of connection). `callback` has 1 parameter: < _Error_ >err.\n\n### Optional \"standard\" commands (RFC 959)\n\n* **mkdir**(< _string_ >path, [< _boolean_ >recursive, ]< _function_ >callback) - _(void)_ - Creates a new directory, `path`, on the server. `recursive` is for enabling a 'mkdir -p' algorithm and defaults to false. `callback` has 1 parameter: < _Error_ >err.\n\n* **rmdir**(< _string_ >path, [< _boolean_ >recursive, ]< _function_ >callback) - _(void)_ - Removes a directory, `path`, on the server. If `recursive`, this call will delete the contents of the directory if it is not empty. `callback` has 1 parameter: < _Error_ >err.\n\n* **cdup**(< _function_ >callback) - _(void)_ - Changes the working directory to the parent of the current directory. `callback` has 1 parameter: < _Error_ >err.\n\n* **pwd**(< _function_ >callback) - _(void)_ - Retrieves the current working directory. `callback` has 2 parameters: < _Error_ >err, < _string_ >cwd.\n\n* **system**(< _function_ >callback) - _(void)_ - Retrieves the server's operating system. `callback` has 2 parameters: < _Error_ >err, < _string_ >OS.\n\n* **listSafe**([< _string_ >path, ][< _boolean_ >useCompression, ]< _function_ >callback) - _(void)_ - Similar to list(), except the directory is temporarily changed to `path` to retrieve the directory listing. This is useful for servers that do not handle characters like spaces and quotes in directory names well for the LIST command. This function is \"optional\" because it relies on pwd() being available.\n\n### Extended commands (RFC 3659)\n\n* **size**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves the size of `path`. `callback` has 2 parameters: < _Error_ >err, < _integer_ >numBytes.\n\n* **lastMod**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves the last modified date and time for `path`. `callback` has 2 parameters: < _Error_ >err, < _Date_ >lastModified.\n\n* **restart**(< _integer_ >byteOffset, < _function_ >callback) - _(void)_ - Sets the file byte offset for the next file transfer action (get/put) to `byteOffset`. `callback` has 1 parameter: < _Error_ >err.\n"
  },
  {
    "path": "app/back-end/modules/custom-changes/ftp/TODO",
    "content": " - Add support for some SITE commands such as CHMOD, CHGRP, and QUOTA\n - Active (non-passive) data connections\n - IPv6 support\n"
  },
  {
    "path": "app/back-end/modules/custom-changes/ftp/lib/connection.js",
    "content": "var fs = require('fs'),\n    tls = require('tls'),\n    zlib = require('zlib'),\n    Socket = require('net').Socket,\n    EventEmitter = require('events').EventEmitter,\n    inherits = require('util').inherits,\n    inspect = require('util').inspect;\n\nvar Parser = require('./parser');\nvar XRegExp = require('xregexp').XRegExp;\n\nvar REX_TIMEVAL = XRegExp.cache('^(?<year>\\\\d{4})(?<month>\\\\d{2})(?<date>\\\\d{2})(?<hour>\\\\d{2})(?<minute>\\\\d{2})(?<second>\\\\d+)(?:.\\\\d+)?$'),\n    RE_PASV = /([\\d]+),([\\d]+),([\\d]+),([\\d]+),([-\\d]+),([-\\d]+)/,\n    RE_EOL = /\\r?\\n/g,\n    RE_WD = /\"(.+)\"(?: |$)/,\n    RE_SYST = /^([^ ]+)(?: |$)/;\n\nvar /*TYPE = {\n      SYNTAX: 0,\n      INFO: 1,\n      SOCKETS: 2,\n      AUTH: 3,\n      UNSPEC: 4,\n      FILESYS: 5\n    },*/\n    RETVAL = {\n      PRELIM: 1,\n      OK: 2,\n      WAITING: 3,\n      ERR_TEMP: 4,\n      ERR_PERM: 5\n    },\n    /*ERRORS = {\n      421: 'Service not available, closing control connection',\n      425: 'Can\\'t open data connection',\n      426: 'Connection closed; transfer aborted',\n      450: 'Requested file action not taken / File unavailable (e.g., file busy)',\n      451: 'Requested action aborted: local error in processing',\n      452: 'Requested action not taken / Insufficient storage space in system',\n      500: 'Syntax error / Command unrecognized',\n      501: 'Syntax error in parameters or arguments',\n      502: 'Command not implemented',\n      503: 'Bad sequence of commands',\n      504: 'Command not implemented for that parameter',\n      530: 'Not logged in',\n      532: 'Need account for storing files',\n      550: 'Requested action not taken / File unavailable (e.g., file not found, no access)',\n      551: 'Requested action aborted: page type unknown',\n      552: 'Requested file action aborted / Exceeded storage allocation (for current directory or dataset)',\n      553: 'Requested action not taken / File name not allowed'\n    },*/\n    bytesNOOP = Buffer.from('NOOP\\r\\n');\n\nvar FTP = module.exports = function() {\n  if (!(this instanceof FTP))\n    return new FTP();\n\n  this._socket = undefined;\n  this._pasvSock = undefined;\n  this._feat = undefined;\n  this._curReq = undefined;\n  this._queue = [];\n  this._secstate = undefined;\n  this._debug = undefined;\n  this._keepalive = undefined;\n  this._ending = false;\n  this._parser = undefined;\n  this.options = {\n    host: undefined,\n    port: undefined,\n    user: undefined,\n    password: undefined,\n    secure: false,\n    secureOptions: undefined,\n    connTimeout: undefined,\n    pasvTimeout: undefined,\n    aliveTimeout: undefined\n  };\n  this.connected = false;\n};\ninherits(FTP, EventEmitter);\n\nFTP.prototype.connect = function(options) {\n  var self = this;\n  if (typeof options !== 'object')\n    options = {};\n  this.connected = false;\n  this.options.host = options.host || 'localhost';\n  this.options.port = options.port || 21;\n  this.options.user = options.user || 'anonymous';\n  this.options.password = options.password || 'anonymous@';\n  this.options.secure = options.secure || false;\n  this.options.secureOptions = options.secureOptions;\n  this.options.connTimeout = options.connTimeout || 10000;\n  this.options.pasvTimeout = options.pasvTimeout || 10000;\n  this.options.aliveTimeout = options.keepalive || 10000;\n\n  if (typeof options.debug === 'function')\n    this._debug = options.debug;\n\n  var secureOptions,\n      debug = this._debug,\n      socket = new Socket();\n\n  socket.setTimeout(0);\n  socket.setKeepAlive(false);\n\n  this._parser = new Parser({ debug: debug });\n  this._parser.on('response', function(code, text) {\n    var retval = code / 100 >> 0;\n    if (retval === RETVAL.ERR_TEMP || retval === RETVAL.ERR_PERM) {\n      if (self._curReq)\n        self._curReq.cb(makeError(code, text), undefined, code);\n      else\n        self.emit('error', makeError(code, text));\n    } else if (self._curReq)\n      self._curReq.cb(undefined, text, code);\n\n    // a hack to signal we're waiting for a PASV data connection to complete\n    // first before executing any more queued requests ...\n    //\n    // also: don't forget our current request if we're expecting another\n    // terminating response ....\n    if (self._curReq && retval !== RETVAL.PRELIM) {\n      self._curReq = undefined;\n      self._send();\n    }\n\n    noopreq.cb();\n  });\n\n  if (this.options.secure) {\n    secureOptions = {};\n    secureOptions.host = this.options.host;\n    for (var k in this.options.secureOptions)\n      secureOptions[k] = this.options.secureOptions[k];\n    secureOptions.socket = socket;\n    this.options.secureOptions = secureOptions;\n  }\n\n  if (this.options.secure === 'implicit')\n    this._socket = tls.connect(secureOptions, onconnect);\n  else {\n    socket.once('connect', onconnect);\n    this._socket = socket;\n  }\n\n  var noopreq = {\n        cmd: 'NOOP',\n        cb: function() {\n          clearTimeout(self._keepalive);\n          self._keepalive = setTimeout(donoop, self.options.aliveTimeout);\n        }\n      };\n\n  function donoop() {\n    if (!self._socket || !self._socket.writable)\n      clearTimeout(self._keepalive);\n    else if (!self._curReq && self._queue.length === 0) {\n      self._curReq = noopreq;\n      debug&&debug('[connection] > NOOP');\n      self._socket.write(bytesNOOP);\n    } else\n      noopreq.cb();\n  }\n\n  function onconnect() {\n    clearTimeout(timer);\n    clearTimeout(self._keepalive);\n    self.connected = true;\n    self._socket = socket; // re-assign for implicit secure connections\n\n    var cmd;\n\n    if (self._secstate) {\n      if (self._secstate === 'upgraded-tls' && self.options.secure === true) {\n        cmd = 'PBSZ';\n        self._send('PBSZ 0', reentry, true);\n      } else {\n        cmd = 'USER';\n        self._send('USER ' + self.options.user, reentry, true);\n      }\n    } else {\n      self._curReq = {\n        cmd: '',\n        cb: reentry\n      };\n    }\n\n    function reentry(err, text, code) {\n      if (err && (!cmd || cmd === 'USER' || cmd === 'PASS' || cmd === 'TYPE')) {\n        self.emit('error', err);\n        return self._socket && self._socket.end();\n      }\n      if ((cmd === 'AUTH TLS' && code !== 234 && self.options.secure !== true)\n          || (cmd === 'AUTH SSL' && code !== 334)\n          || (cmd === 'PBSZ' && code !== 200)\n          || (cmd === 'PROT' && code !== 200)) {\n        self.emit('error', makeError(code, 'Unable to secure connection(s)'));\n        return self._socket && self._socket.end();\n      }\n\n      if (!cmd) {\n        // sometimes the initial greeting can contain useful information\n        // about authorized use, other limits, etc.\n        self.emit('greeting', text);\n\n        if (self.options.secure && self.options.secure !== 'implicit') {\n          cmd = 'AUTH TLS';\n          self._send(cmd, reentry, true);\n        } else {\n          cmd = 'USER';\n          self._send('USER ' + self.options.user, reentry, true);\n        }\n      } else if (cmd === 'USER') {\n        if (code !== 230) {\n          // password required\n          if (!self.options.password) {\n            self.emit('error', makeError(code, 'Password required'));\n            return self._socket && self._socket.end();\n          }\n          cmd = 'PASS';\n          self._send('PASS ' + self.options.password, reentry, true);\n        } else {\n          // no password required\n          cmd = 'PASS';\n          reentry(undefined, text, code);\n        }\n      } else if (cmd === 'PASS') {\n        cmd = 'FEAT';\n        self._send(cmd, reentry, true);\n      } else if (cmd === 'FEAT') {\n        if (!err)\n          self._feat = Parser.parseFeat(text);\n        cmd = 'TYPE';\n        self._send('TYPE I', reentry, true);\n      } else if (cmd === 'TYPE')\n        self.emit('ready');\n      else if (cmd === 'PBSZ') {\n        cmd = 'PROT';\n        self._send('PROT P', reentry, true);\n      } else if (cmd === 'PROT') {\n        cmd = 'USER';\n        self._send('USER ' + self.options.user, reentry, true);\n      } else if (cmd.substr(0, 4) === 'AUTH') {\n        if (cmd === 'AUTH TLS' && code !== 234) {\n          cmd = 'AUTH SSL';\n          return self._send(cmd, reentry, true);\n        } else if (cmd === 'AUTH TLS')\n          self._secstate = 'upgraded-tls';\n        else if (cmd === 'AUTH SSL')\n          self._secstate = 'upgraded-ssl';\n        socket.removeAllListeners('data');\n        socket.removeAllListeners('error');\n        socket._decoder = null;\n        self._curReq = null; // prevent queue from being processed during\n                             // TLS/SSL negotiation\n        secureOptions.socket = self._socket;\n        secureOptions.session = undefined;\n        socket = tls.connect(secureOptions, onconnect);\n        socket.setEncoding('binary');\n        socket.on('data', ondata);\n        socket.once('end', onend);\n        socket.on('error', onerror);\n      }\n    }\n  }\n\n  socket.on('data', ondata);\n  function ondata(chunk) {\n    debug&&debug('[connection] < ' + inspect(chunk.toString('binary')));\n    if (self._parser)\n      self._parser.write(chunk);\n  }\n\n  socket.on('error', onerror);\n  function onerror(err) {\n    clearTimeout(timer);\n    clearTimeout(self._keepalive);\n    self.emit('error', err);\n  }\n\n  socket.once('end', onend);\n  function onend() {\n    ondone();\n    self.emit('end');\n  }\n\n  socket.once('close', function(had_err) {\n    ondone();\n    self.emit('close', had_err);\n  });\n\n  var hasReset = false;\n  function ondone() {\n    if (!hasReset) {\n      hasReset = true;\n      clearTimeout(timer);\n      self._reset();\n    }\n  }\n\n  var timer = setTimeout(function() {\n    self.emit('error', new Error('Timeout while connecting to server'));\n    self._socket && self._socket.destroy();\n    self._reset();\n  }, this.options.connTimeout);\n\n  this._socket.connect(this.options.port, this.options.host);\n};\n\nFTP.prototype.end = function() {\n  if (this._queue.length)\n    this._ending = true;\n  else\n    this._reset();\n};\n\nFTP.prototype.destroy = function() {\n  this._reset();\n};\n\n// \"Standard\" (RFC 959) commands\nFTP.prototype.ascii = function(cb) {\n  return this._send('TYPE A', cb);\n};\n\nFTP.prototype.binary = function(cb) {\n  return this._send('TYPE I', cb);\n};\n\nFTP.prototype.abort = function(immediate, cb) {\n  if (typeof immediate === 'function') {\n    cb = immediate;\n    immediate = true;\n  }\n  if (immediate)\n    this._send('ABOR', cb, true);\n  else\n    this._send('ABOR', cb);\n};\n\nFTP.prototype.cwd = function(path, cb, promote) {\n  this._send('CWD ' + path, function(err, text, code) {\n    if (err)\n      return cb(err);\n    var m = RE_WD.exec(text);\n    cb(undefined, m ? m[1] : undefined);\n  }, promote);\n};\n\nFTP.prototype.delete = function(path, cb) {\n  this._send('DELE ' + path, cb);\n};\n\nFTP.prototype.site = function(cmd, cb) {\n  this._send('SITE ' + cmd, cb);\n};\n\nFTP.prototype.status = function(cb) {\n  this._send('STAT', cb);\n};\n\nFTP.prototype.rename = function(from, to, cb) {\n  var self = this;\n  this._send('RNFR ' + from, function(err) {\n    if (err)\n      return cb(err);\n\n    self._send('RNTO ' + to, cb, true);\n  });\n};\n\nFTP.prototype.logout = function(cb) {\n  this._send('QUIT', cb);\n};\n\nFTP.prototype.listSafe = function(path, zcomp, cb) {\n  if (typeof path === 'string') {\n    var self = this;\n    // store current path\n    this.pwd(function(err, origpath) {\n      if (err) return cb(err);\n      // change to destination path\n      self.cwd(path, function(err) {\n        if (err) return cb(err);\n        // get dir listing\n        self.list(zcomp || false, function(err, list) {\n          // change back to original path\n          if (err) return self.cwd(origpath, cb);\n          self.cwd(origpath, function(err) {\n            if (err) return cb(err);\n            cb(err, list);\n          });\n        });\n      });\n    });\n  } else\n    this.list(path, zcomp, cb);\n};\n\nFTP.prototype.list = function(path, zcomp, cb) {\n  var self = this, cmd;\n\n  if (typeof path === 'function') {\n    // list(function() {})\n    cb = path;\n    path = undefined;\n    cmd = 'LIST';\n    zcomp = false;\n  } else if (typeof path === 'boolean') {\n    // list(true, function() {})\n    cb = zcomp;\n    zcomp = path;\n    path = undefined;\n    cmd = 'LIST';\n  } else if (typeof zcomp === 'function') {\n    // list('/foo', function() {})\n    cb = zcomp;\n    cmd = 'LIST ' + path;\n    zcomp = false;\n  } else\n    cmd = 'LIST ' + path;\n\n  this._pasv(function(err, sock) {\n    if (err)\n      return cb(err);\n\n    if (self._queue[0] && self._queue[0].cmd === 'ABOR') {\n      sock.destroy();\n      return cb();\n    }\n\n    var sockerr, done = false, replies = 0, entries, buffer = '', source = sock;\n\n    if (zcomp) {\n      source = zlib.createInflate();\n      sock.pipe(source);\n    }\n\n    source.on('data', function(chunk) { buffer += chunk.toString('binary'); });\n    source.once('error', function(err) {\n      if (!sock.aborting)\n        sockerr = err;\n    });\n    source.once('end', ondone);\n    source.once('close', ondone);\n\n    function ondone() {\n      done = true;\n      final();\n    }\n    function final() {\n      if (done && replies === 2) {\n        replies = 3;\n        if (sockerr)\n          return cb(new Error('Unexpected data connection error: ' + sockerr));\n        if (sock.aborting)\n          return cb();\n\n        // process received data\n        entries = buffer.split(RE_EOL);\n        entries.pop(); // ending EOL\n        var parsed = [];\n        for (var i = 0, len = entries.length; i < len; ++i) {\n          var parsedVal = Parser.parseListEntry(entries[i]);\n          if (parsedVal !== null)\n            parsed.push(parsedVal);\n        }\n\n        if (zcomp) {\n          self._send('MODE S', function() {\n            cb(undefined, parsed);\n          }, true);\n        } else\n          cb(undefined, parsed);\n      }\n    }\n\n    if (zcomp) {\n      self._send('MODE Z', function(err, text, code) {\n        if (err) {\n          sock.destroy();\n          return cb(makeError(code, 'Compression not supported'));\n        }\n        sendList();\n      }, true);\n    } else\n      sendList();\n\n    function sendList() {\n      // this callback will be executed multiple times, the first is when server\n      // replies with 150 and then a final reply to indicate whether the\n      // transfer was actually a success or not\n      self._send(cmd, function(err, text, code) {\n        if (err) {\n          sock.destroy();\n          if (zcomp) {\n            self._send('MODE S', function() {\n              cb(err);\n            }, true);\n          } else\n            cb(err);\n          return;\n        }\n\n        // some servers may not open a data connection for empty directories\n        if (++replies === 1 && code === 226) {\n          replies = 2;\n          sock.destroy();\n          final();\n        } else if (replies === 2)\n          final();\n      }, true);\n    }\n  });\n};\n\nFTP.prototype.get = function(path, zcomp, cb) {\n  var self = this;\n  if (typeof zcomp === 'function') {\n    cb = zcomp;\n    zcomp = false;\n  }\n\n  this._pasv(function(err, sock) {\n    if (err)\n      return cb(err);\n\n    if (self._queue[0] && self._queue[0].cmd === 'ABOR') {\n      sock.destroy();\n      return cb();\n    }\n\n    // modify behavior of socket events so that we can emit 'error' once for\n    // either a TCP-level error OR an FTP-level error response that we get when\n    // the socket is closed (e.g. the server ran out of space).\n    var sockerr, started = false, lastreply = false, done = false,\n        source = sock;\n\n    if (zcomp) {\n      source = zlib.createInflate();\n      sock.pipe(source);\n      sock._emit = sock.emit;\n      sock.emit = function(ev, arg1) {\n        if (ev === 'error') {\n          if (!sockerr)\n            sockerr = arg1;\n          return;\n        }\n        sock._emit.apply(sock, Array.prototype.slice.call(arguments));\n      };\n    }\n\n    source._emit = source.emit;\n    source.emit = function(ev, arg1) {\n      if (ev === 'error') {\n        if (!sockerr)\n          sockerr = arg1;\n        return;\n      } else if (ev === 'end' || ev === 'close') {\n        if (!done) {\n          done = true;\n          ondone();\n        }\n        return;\n      }\n      source._emit.apply(source, Array.prototype.slice.call(arguments));\n    };\n\n    function ondone() {\n      if (done && lastreply) {\n        self._send('MODE S', function() {\n          source._emit('end');\n          source._emit('close');\n        }, true);\n      }\n    }\n\n    sock.pause();\n\n    if (zcomp) {\n      self._send('MODE Z', function(err, text, code) {\n        if (err) {\n          sock.destroy();\n          return cb(makeError(code, 'Compression not supported'));\n        }\n        sendRetr();\n      }, true);\n    } else\n      sendRetr();\n\n    function sendRetr() {\n      // this callback will be executed multiple times, the first is when server\n      // replies with 150, then a final reply after the data connection closes\n      // to indicate whether the transfer was actually a success or not\n      self._send('RETR ' + path, function(err, text, code) {\n        if (sockerr || err) {\n          sock.destroy();\n          if (!started) {\n            if (zcomp) {\n              self._send('MODE S', function() {\n                cb(sockerr || err);\n              }, true);\n            } else\n              cb(sockerr || err);\n          } else {\n            source._emit('error', sockerr || err);\n            source._emit('close', true);\n          }\n          return;\n        }\n        // server returns 125 when data connection is already open; we treat it\n        // just like a 150\n        if (code === 150 || code === 125) {\n          started = true;\n          cb(undefined, source);\n          sock.resume();\n        } else {\n          lastreply = true;\n          ondone();\n        }\n      }, true);\n    }\n  });\n};\n\nFTP.prototype.put = function(input, path, zcomp, cb) {\n  this._store('STOR ' + path, input, zcomp, cb);\n};\n\nFTP.prototype.append = function(input, path, zcomp, cb) {\n  this._store('APPE ' + path, input, zcomp, cb);\n};\n\nFTP.prototype.pwd = function(cb) { // PWD is optional\n  var self = this;\n  this._send('PWD', function(err, text, code) {\n    if (code === 502) {\n      return self.cwd('.', function(cwderr, cwd) {\n        if (cwderr)\n          return cb(cwderr);\n        if (cwd === undefined)\n          cb(err);\n        else\n          cb(undefined, cwd);\n      }, true);\n    } else if (err)\n      return cb(err);\n    cb(undefined, RE_WD.exec(text)[1]);\n  });\n};\n\nFTP.prototype.cdup = function(cb) { // CDUP is optional\n  var self = this;\n  this._send('CDUP', function(err, text, code) {\n    if (code === 502)\n      self.cwd('..', cb, true);\n    else\n      cb(err);\n  });\n};\n\nFTP.prototype.mkdir = function(path, recursive, cb) { // MKD is optional\n  if (typeof recursive === 'function') {\n    cb = recursive;\n    recursive = false;\n  }\n  if (!recursive)\n    this._send('MKD ' + path, cb);\n  else {\n    var self = this, owd, abs, dirs, dirslen, i = -1, searching = true;\n\n    abs = (path[0] === '/');\n\n    var nextDir = function() {\n      if (++i === dirslen) {\n        // return to original working directory\n        return self._send('CWD ' + owd, cb, true);\n      }\n      if (searching) {\n        self._send('CWD ' + dirs[i], function(err, text, code) {\n          if (code === 550) {\n            searching = false;\n            --i;\n          } else if (err) {\n            // return to original working directory\n            return self._send('CWD ' + owd, function() {\n              cb(err);\n            }, true);\n          }\n          nextDir();\n        }, true);\n      } else {\n        self._send('MKD ' + dirs[i], function(err, text, code) {\n          if (err) {\n            // return to original working directory\n            return self._send('CWD ' + owd, function() {\n              cb(err);\n            }, true);\n          }\n          self._send('CWD ' + dirs[i], nextDir, true);\n        }, true);\n      }\n    };\n    this.pwd(function(err, cwd) {\n      if (err)\n        return cb(err);\n      owd = cwd;\n      if (abs)\n        path = path.substr(1);\n      if (path[path.length - 1] === '/')\n        path = path.substring(0, path.length - 1);\n      dirs = path.split('/');\n      dirslen = dirs.length;\n      if (abs)\n        self._send('CWD /', function(err) {\n          if (err)\n            return cb(err);\n          nextDir();\n        }, true);\n      else\n        nextDir();\n    });\n  }\n};\n\nFTP.prototype.rmdir = function(path, recursive, cb) { // RMD is optional\n  if (typeof recursive === 'function') {\n    cb = recursive;\n    recursive = false;\n  }\n  if (!recursive) {\n    return this._send('RMD ' + path, cb);\n  }\n  \n  var self = this;\n  this.list(path, function(err, list) {\n    if (err) return cb(err);\n    var idx = 0;\n    \n    // this function will be called once per listing entry\n    var deleteNextEntry;\n    deleteNextEntry = function(err) {\n      if (err) return cb(err);\n      if (idx >= list.length) {\n        if (list[0] && list[0].name === path) {\n          return cb(null);\n        } else {\n          return self.rmdir(path, cb);\n        }\n      }\n      \n      var entry = list[idx++];\n      \n      // get the path to the file\n      var subpath = null;\n      if (entry.name[0] === '/') {\n        // this will be the case when you call deleteRecursively() and pass\n        // the path to a plain file\n        subpath = entry.name;\n      } else {\n        if (path[path.length - 1] == '/') {\n          subpath = path + entry.name;\n        } else {\n          subpath = path + '/' + entry.name\n        }\n      }\n      \n      // delete the entry (recursively) according to its type\n      if (entry.type === 'd') {\n        if (entry.name === \".\" || entry.name === \"..\") {\n          return deleteNextEntry();\n        }\n        self.rmdir(subpath, true, deleteNextEntry);\n      } else {\n        self.delete(subpath, deleteNextEntry);\n      }\n    }\n    deleteNextEntry();\n  });\n};\n\nFTP.prototype.system = function(cb) { // SYST is optional\n  this._send('SYST', function(err, text) {\n    if (err)\n      return cb(err);\n    cb(undefined, RE_SYST.exec(text)[1]);\n  });\n};\n\n// \"Extended\" (RFC 3659) commands\nFTP.prototype.size = function(path, cb) {\n  var self = this;\n  this._send('SIZE ' + path, function(err, text, code) {\n    if (code === 502) {\n      // Note: this may cause a problem as list() is _appended_ to the queue\n      return self.list(path, function(err, list) {\n        if (err)\n          return cb(err);\n        if (list.length === 1)\n          cb(undefined, list[0].size);\n        else {\n          // path could have been a directory and we got a listing of its\n          // contents, but here we echo the behavior of the real SIZE and\n          // return 'File not found' for directories\n          cb(new Error('File not found'));\n        }\n      }, true);\n    } else if (err)\n      return cb(err);\n    cb(undefined, parseInt(text, 10));\n  });\n};\n\nFTP.prototype.lastMod = function(path, cb) {\n  var self = this;\n  this._send('MDTM ' + path, function(err, text, code) {\n    if (code === 502) {\n      return self.list(path, function(err, list) {\n        if (err)\n          return cb(err);\n        if (list.length === 1)\n          cb(undefined, list[0].date);\n        else\n          cb(new Error('File not found'));\n      }, true);\n    } else if (err)\n      return cb(err);\n    var val = XRegExp.exec(text, REX_TIMEVAL), ret;\n    if (!val)\n      return cb(new Error('Invalid date/time format from server'));\n    ret = new Date(val.year + '-' + val.month + '-' + val.date + 'T' + val.hour\n                   + ':' + val.minute + ':' + val.second);\n    cb(undefined, ret);\n  });\n};\n\nFTP.prototype.restart = function(offset, cb) {\n  this._send('REST ' + offset, cb);\n};\n\n\n\n// Private/Internal methods\nFTP.prototype._pasv = function(cb) {\n  var self = this, first = true, ip, port;\n  this._send('PASV', function reentry(err, text) {\n    if (err)\n      return cb(err);\n\n    self._curReq = undefined;\n\n    if (first) {\n      var m = RE_PASV.exec(text);\n      if (!m)\n        return cb(new Error('Unable to parse PASV server response'));\n      ip = m[1];\n      ip += '.';\n      ip += m[2];\n      ip += '.';\n      ip += m[3];\n      ip += '.';\n      ip += m[4];\n      port = (parseInt(m[5], 10) * 256) + parseInt(m[6], 10);\n\n      first = false;\n    }\n    self._pasvConnect(ip, port, function(err, sock) {\n      if (err) {\n        // try the IP of the control connection if the server was somehow\n        // misconfigured and gave for example a LAN IP instead of WAN IP over\n        // the Internet\n        if (self._socket && ip !== self._socket.remoteAddress) {\n          ip = self._socket.remoteAddress;\n          return reentry();\n        }\n\n        // automatically abort PASV mode\n        self._send('ABOR', function() {\n          cb(err);\n          self._send();\n        }, true);\n\n        return;\n      }\n      cb(undefined, sock);\n      self._send();\n    });\n  });\n};\n\nFTP.prototype._pasvConnect = function(ip, port, cb) {\n  var self = this,\n      socket = new Socket(),\n      sockerr,\n      timedOut = false,\n      timer = setTimeout(function() {\n        timedOut = true;\n        socket.destroy();\n        cb(new Error('Timed out while making data connection'));\n      }, this.options.pasvTimeout);\n\n  socket.setTimeout(0);\n\n  socket.once('connect', function() {\n    self._debug&&self._debug('[connection] PASV socket connected');\n    if (self.options.secure === true) {\n      self.options.secureOptions.socket = socket;\n      self.options.secureOptions.session = self._socket.getSession();\n      //socket.removeAllListeners('error');\n      socket = tls.connect(self.options.secureOptions);\n      //socket.once('error', onerror);\n      socket.setTimeout(0);\n    }\n    clearTimeout(timer);\n    self._pasvSocket = socket;\n    cb(undefined, socket);\n  });\n  socket.once('error', onerror);\n  function onerror(err) {\n    sockerr = err;\n  }\n  socket.once('end', function() {\n    clearTimeout(timer);\n  });\n  socket.once('close', function(had_err) {\n    clearTimeout(timer);\n    if (!self._pasvSocket && !timedOut) {\n      var errmsg = 'Unable to make data connection';\n      if (sockerr) {\n        errmsg += '( ' + sockerr + ')';\n        sockerr = undefined;\n      }\n      cb(new Error(errmsg));\n    }\n    self._pasvSocket = undefined;\n  });\n\n  socket.connect(port, ip);\n};\n\nFTP.prototype._store = function(cmd, input, zcomp, cb) {\n  var isBuffer = Buffer.isBuffer(input);\n\n  if (!isBuffer && input.pause !== undefined)\n    input.pause();\n\n  if (typeof zcomp === 'function') {\n    cb = zcomp;\n    zcomp = false;\n  }\n\n  var self = this;\n  this._pasv(function(err, sock) {\n    if (err)\n      return cb(err);\n\n    if (self._queue[0] && self._queue[0].cmd === 'ABOR') {\n      sock.destroy();\n      return cb();\n    }\n\n    var sockerr, dest = sock;\n    sock.once('error', function(err) {\n      sockerr = err;\n    });\n\n    if (zcomp) {\n      self._send('MODE Z', function(err, text, code) {\n        if (err) {\n          sock.destroy();\n          return cb(makeError(code, 'Compression not supported'));\n        }\n        // draft-preston-ftpext-deflate-04 says min of 8 should be supported\n        dest = zlib.createDeflate({ level: 8 });\n        dest.pipe(sock);\n        sendStore();\n      }, true);\n    } else\n      sendStore();\n\n    function sendStore() {\n      // this callback will be executed multiple times, the first is when server\n      // replies with 150, then a final reply after the data connection closes\n      // to indicate whether the transfer was actually a success or not\n      self._send(cmd, function(err, text, code) {\n        if (sockerr || err) {\n          if (zcomp) {\n            self._send('MODE S', function() {\n              cb(sockerr || err);\n            }, true);\n          } else\n            cb(sockerr || err);\n          return;\n        }\n\n        if (code === 150 || code === 125) {\n          if (isBuffer)\n            dest.end(input);\n          else if (typeof input === 'string') {\n            // check if input is a file path or just string data to store\n            fs.stat(input, function(err, stats) {\n              if (err)\n                dest.end(input);\n              else\n                fs.createReadStream(input).pipe(dest);\n            });\n          } else {\n            input.pipe(dest);\n            input.resume();\n          }\n        } else {\n          if (zcomp)\n            self._send('MODE S', cb, true);\n          else\n            cb();\n        }\n      }, true);\n    }\n  });\n};\n\nFTP.prototype._send = function(cmd, cb, promote) {\n  clearTimeout(this._keepalive);\n  if (cmd !== undefined) {\n    if (promote)\n      this._queue.unshift({ cmd: cmd, cb: cb });\n    else\n      this._queue.push({ cmd: cmd, cb: cb });\n  }\n  var queueLen = this._queue.length;\n  if (!this._curReq && queueLen && this._socket && this._socket.readable) {\n    this._curReq = this._queue.shift();\n    if (this._curReq.cmd === 'ABOR' && this._pasvSocket)\n      this._pasvSocket.aborting = true;\n    this._debug&&this._debug('[connection] > ' + inspect(this._curReq.cmd));\n    this._socket.write(this._curReq.cmd + '\\r\\n');\n  } else if (!this._curReq && !queueLen && this._ending)\n    this._reset();\n};\n\nFTP.prototype._reset = function() {\n  if (this._pasvSock && this._pasvSock.writable)\n    this._pasvSock.end();\n  if (this._socket && this._socket.writable)\n    this._socket.end();\n  this._socket = undefined;\n  this._pasvSock = undefined;\n  this._feat = undefined;\n  this._curReq = undefined;\n  this._secstate = undefined;\n  clearTimeout(this._keepalive);\n  this._keepalive = undefined;\n  this._queue = [];\n  this._ending = false;\n  this._parser = undefined;\n  this.options.host = this.options.port = this.options.user\n                    = this.options.password = this.options.secure\n                    = this.options.connTimeout = this.options.pasvTimeout\n                    = this.options.keepalive = this._debug = undefined;\n  this.connected = false;\n};\n\n// Utility functions\nfunction makeError(code, text) {\n  var err = new Error(text);\n  err.code = code;\n  return err;\n}\n"
  },
  {
    "path": "app/back-end/modules/custom-changes/ftp/lib/parser.js",
    "content": "var WritableStream = require('stream').Writable\n                     || require('readable-stream').Writable,\n    inherits = require('util').inherits,\n    inspect = require('util').inspect;\n\nvar XRegExp = require('xregexp').XRegExp;\n\nvar REX_LISTUNIX = XRegExp.cache('^(?<type>[\\\\-ld])(?<permission>([\\\\-r][\\\\-w][\\\\-xstT]){3})(?<acl>(\\\\+))?\\\\s+(?<inodes>\\\\d+)\\\\s+(?<owner>\\\\S+)\\\\s+(?<group>\\\\S+)\\\\s+(?<size>\\\\d+)\\\\s+(?<timestamp>((?<month1>\\\\w{3})\\\\s+(?<date1>\\\\d{1,2})\\\\s+(?<hour>\\\\d{1,2}):(?<minute>\\\\d{2}))|((?<month2>\\\\w{3})\\\\s+(?<date2>\\\\d{1,2})\\\\s+(?<year>\\\\d{4})))\\\\s+(?<name>.+)$'),\n    REX_LISTMSDOS = XRegExp.cache('^(?<month>\\\\d{2})(?:\\\\-|\\\\/)(?<date>\\\\d{2})(?:\\\\-|\\\\/)(?<year>\\\\d{2,4})\\\\s+(?<hour>\\\\d{2}):(?<minute>\\\\d{2})\\\\s{0,1}(?<ampm>[AaMmPp]{1,2})\\\\s+(?:(?<size>\\\\d+)|(?<isdir>\\\\<DIR\\\\>))\\\\s+(?<name>.+)$'),\n    RE_ENTRY_TOTAL = /^total/,\n    RE_RES_END = /(?:^|\\r?\\n)(\\d{3}) [^\\r\\n]*\\r?\\n/,\n    RE_EOL = /\\r?\\n/g,\n    RE_DASH = /\\-/g;\n\nvar MONTHS = {\n      jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,\n      jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12\n    };\n\nfunction Parser(options) {\n  if (!(this instanceof Parser))\n    return new Parser(options);\n  WritableStream.call(this);\n\n  this._buffer = '';\n  this._debug = options.debug;\n}\ninherits(Parser, WritableStream);\n\nParser.prototype._write = function(chunk, encoding, cb) {\n  var m, code, reRmLeadCode, rest = '', debug = this._debug;\n\n  this._buffer += chunk.toString('binary');\n\n  while (m = RE_RES_END.exec(this._buffer)) {\n    // support multiple terminating responses in the buffer\n    rest = this._buffer.substring(m.index + m[0].length);\n    if (rest.length)\n      this._buffer = this._buffer.substring(0, m.index + m[0].length);\n\n    debug&&debug('[parser] < ' + inspect(this._buffer));\n\n    // we have a terminating response line\n    code = parseInt(m[1], 10);\n\n    // RFC 959 does not require each line in a multi-line response to begin\n    // with '<code>-', but many servers will do this.\n    //\n    // remove this leading '<code>-' (or '<code> ' from last line) from each\n    // line in the response ...\n    reRmLeadCode = '(^|\\\\r?\\\\n)';\n    reRmLeadCode += m[1];\n    reRmLeadCode += '(?: |\\\\-)';\n    reRmLeadCode = new RegExp(reRmLeadCode, 'g');\n    var text = this._buffer.replace(reRmLeadCode, '$1').trim();\n    this._buffer = rest;\n\n    debug&&debug('[parser] Response: code=' + code + ', buffer=' + inspect(text));\n    this.emit('response', code, text);\n  }\n\n  cb();\n};\n\nParser.parseFeat = function(text) {\n  var lines = text.split(RE_EOL);\n  lines.shift(); // initial response line\n  lines.pop(); // final response line\n\n  for (var i = 0, len = lines.length; i < len; ++i)\n    lines[i] = lines[i].trim();\n\n  // just return the raw lines for now\n  return lines;\n};\n\nParser.parseListEntry = function(line) {\n  var ret,\n      info,\n      month, day, year,\n      hour, mins;\n\n  if (ret = XRegExp.exec(line, REX_LISTUNIX)) {\n    info = {\n      type: ret.type,\n      name: undefined,\n      target: undefined,\n      sticky: false,\n      rights: {\n        user: ret.permission.substr(0, 3).replace(RE_DASH, ''),\n        group: ret.permission.substr(3, 3).replace(RE_DASH, ''),\n        other: ret.permission.substr(6, 3).replace(RE_DASH, '')\n      },\n      acl: (ret.acl === '+'),\n      owner: ret.owner,\n      group: ret.group,\n      size: parseInt(ret.size, 10),\n      date: undefined\n    };\n\n    // check for sticky bit\n    var lastbit = info.rights.other.slice(-1);\n    if (lastbit === 't') {\n      info.rights.other = info.rights.other.slice(0, -1) + 'x';\n      info.sticky = true;\n    } else if (lastbit === 'T') {\n      info.rights.other = info.rights.other.slice(0, -1);\n      info.sticky = true;\n    }\n\n    if (ret.month1 !== undefined) {\n      month = parseInt(MONTHS[ret.month1.toLowerCase()], 10);\n      day = parseInt(ret.date1, 10);\n      year = (new Date()).getFullYear();\n      hour = parseInt(ret.hour, 10);\n      mins = parseInt(ret.minute, 10);\n      if (month < 10)\n        month = '0' + month;\n      if (day < 10)\n        day = '0' + day;\n      if (hour < 10)\n        hour = '0' + hour;\n      if (mins < 10)\n        mins = '0' + mins;\n      info.date = new Date(year + '-'\n                           + month + '-'\n                           + day + 'T'\n                           + hour + ':'\n                           + mins);\n      // If the date is in the past but no more than 6 months old, year\n      // isn't displayed and doesn't have to be the current year.\n      // \n      // If the date is in the future (less than an hour from now), year\n      // isn't displayed and doesn't have to be the current year.\n      // That second case is much more rare than the first and less annoying.\n      // It's impossible to fix without knowing about the server's timezone,\n      // so we just don't do anything about it.\n      // \n      // If we're here with a time that is more than 28 hours into the\n      // future (1 hour + maximum timezone offset which is 27 hours),\n      // there is a problem -- we should be in the second conditional block\n      if (info.date.getTime() - Date.now() > 100800000) {\n        info.date = new Date((year - 1) + '-'\n                             + month + '-'\n                             + day + 'T'\n                             + hour + ':'\n                             + mins);\n      }\n\n      // If we're here with a time that is more than 6 months old, there's\n      // a problem as well.\n      // Maybe local & remote servers aren't on the same timezone (with remote\n      // ahead of local)\n      // For instance, remote is in 2014 while local is still in 2013. In\n      // this case, a date like 01/01/13 02:23 could be detected instead of\n      // 01/01/14 02:23 \n      // Our trigger point will be 3600*24*31*6 (since we already use 31\n      // as an upper bound, no need to add the 27 hours timezone offset)\n      if (Date.now() - info.date.getTime() > 16070400000) {\n        info.date = new Date((year + 1) + '-'\n                             + month + '-'\n                             + day + 'T'\n                             + hour + ':'\n                             + mins);\n      }\n    } else if (ret.month2 !== undefined) {\n      month = parseInt(MONTHS[ret.month2.toLowerCase()], 10);\n      day = parseInt(ret.date2, 10);\n      year = parseInt(ret.year, 10);\n      if (month < 10)\n        month = '0' + month;\n      if (day < 10)\n        day = '0' + day;\n      info.date = new Date(year + '-' + month + '-' + day);\n    }\n    if (ret.type === 'l') {\n      var pos = ret.name.indexOf(' -> ');\n      info.name = ret.name.substring(0, pos);\n      info.target = ret.name.substring(pos+4);\n    } else\n      info.name = ret.name;\n    ret = info;\n  } else if (ret = XRegExp.exec(line, REX_LISTMSDOS)) {\n    info = {\n      name: ret.name,\n      type: (ret.isdir ? 'd' : '-'),\n      size: (ret.isdir ? 0 : parseInt(ret.size, 10)),\n      date: undefined,\n    };\n    month = parseInt(ret.month, 10),\n    day = parseInt(ret.date, 10),\n    year = parseInt(ret.year, 10),\n    hour = parseInt(ret.hour, 10),\n    mins = parseInt(ret.minute, 10);\n\n    if (year < 70)\n      year += 2000;\n    else\n      year += 1900;\n\n    if (ret.ampm[0].toLowerCase() === 'p' && hour < 12)\n      hour += 12;\n    else if (ret.ampm[0].toLowerCase() === 'a' && hour === 12)\n      hour = 0;\n\n    info.date = new Date(year, month - 1, day, hour, mins);\n\n    ret = info;\n  } else if (!RE_ENTRY_TOTAL.test(line))\n    ret = line; // could not parse, so at least give the end user a chance to\n                // look at the raw listing themselves\n\n  return ret;\n};\n\nmodule.exports = Parser;\n"
  },
  {
    "path": "app/back-end/modules/custom-changes/ftp/package.json",
    "content": "{\n  \"_args\": [\n    [\n      {\n        \"raw\": \"ftp@^0.3.10\",\n        \"scope\": null,\n        \"escapedName\": \"ftp\",\n        \"name\": \"ftp\",\n        \"rawSpec\": \"^0.3.10\",\n        \"spec\": \">=0.3.10 <0.4.0\",\n        \"type\": \"range\"\n      },\n      \"/Users/dziudek/Desktop/Publii/app\"\n    ]\n  ],\n  \"_from\": \"ftp@>=0.3.10 <0.4.0\",\n  \"_id\": \"ftp@0.3.10\",\n  \"_inCache\": true,\n  \"_installable\": true,\n  \"_location\": \"/ftp\",\n  \"_npmUser\": {\n    \"name\": \"mscdex\",\n    \"email\": \"mscdex@mscdex.net\"\n  },\n  \"_npmVersion\": \"1.4.28\",\n  \"_phantomChildren\": {},\n  \"_requested\": {\n    \"raw\": \"ftp@^0.3.10\",\n    \"scope\": null,\n    \"escapedName\": \"ftp\",\n    \"name\": \"ftp\",\n    \"rawSpec\": \"^0.3.10\",\n    \"spec\": \">=0.3.10 <0.4.0\",\n    \"type\": \"range\"\n  },\n  \"_requiredBy\": [\n    \"/\"\n  ],\n  \"_resolved\": \"https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz\",\n  \"_shasum\": \"9197d861ad8142f3e63d5a83bfe4c59f7330885d\",\n  \"_shrinkwrap\": null,\n  \"_spec\": \"ftp@^0.3.10\",\n  \"_where\": \"/Users/dziudek/Desktop/Publii/app\",\n  \"author\": {\n    \"name\": \"Brian White\",\n    \"email\": \"mscdex@mscdex.net\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/mscdex/node-ftp/issues\"\n  },\n  \"dependencies\": {\n    \"readable-stream\": \"1.1.x\",\n    \"xregexp\": \"2.0.0\"\n  },\n  \"description\": \"An FTP client module for node.js\",\n  \"devDependencies\": {},\n  \"directories\": {},\n  \"dist\": {\n    \"shasum\": \"9197d861ad8142f3e63d5a83bfe4c59f7330885d\",\n    \"tarball\": \"https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz\"\n  },\n  \"engines\": {\n    \"node\": \">=0.8.0\"\n  },\n  \"homepage\": \"https://github.com/mscdex/node-ftp\",\n  \"keywords\": [\n    \"ftp\",\n    \"client\",\n    \"transfer\"\n  ],\n  \"licenses\": [\n    {\n      \"type\": \"MIT\",\n      \"url\": \"http://github.com/mscdex/node-ftp/raw/master/LICENSE\"\n    }\n  ],\n  \"main\": \"./lib/connection\",\n  \"maintainers\": [\n    {\n      \"name\": \"mscdex\",\n      \"email\": \"mscdex@mscdex.net\"\n    }\n  ],\n  \"name\": \"ftp\",\n  \"optionalDependencies\": {},\n  \"readme\": \"ERROR: No README data found!\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+ssh://git@github.com/mscdex/node-ftp.git\"\n  },\n  \"scripts\": {\n    \"test\": \"node test/test.js\"\n  },\n  \"version\": \"0.3.10\"\n}\n"
  },
  {
    "path": "app/back-end/modules/deploy/deployment.js",
    "content": "// Necessary packages\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst crypto = require('crypto');\nconst normalizePath = require('normalize-path');\nconst isBinaryFileSync = require('isbinaryfile').isBinaryFileSync;\nconst slug = require('./../../helpers/slug');\nconst FTP = require('./ftp.js');\nconst FTPAlt = require('./ftp-alt.js');\nconst SFTP = require('./sftp.js');\nconst S3 = require('./s3.js');\nconst Git = require('./git.js');\nconst GithubPages = require('./github-pages.js');\nconst GitlabPages = require('./gitlab-pages.js');\nconst Netlify = require('./netlify.js');\nconst GoogleCloud = require('./google-cloud.js');\nconst ManualDeployment = require('./manual.js');\n\n/**\n *\n * Class used to upload files to:\n *\n * (S)FTP(S),\n * S3 server,\n * Git\n * Github Pages,\n * Gitlab Pages,\n * Netlify,\n * Google Cloud,\n * Manually\n *\n */\nclass Deployment {\n    /**\n     * Constructor\n     *\n     * @param appDir\n     * @param sitesDir\n     * @param siteConfig\n     */\n    constructor (appDir, sitesDir, siteConfig, useAltFtp) {\n        this.appDir = appDir;\n        this.siteConfig = siteConfig;\n        this.siteName = this.siteConfig.name;\n        this.sitesDir = sitesDir;\n        this.useAltFtp = useAltFtp;\n        this.progressOfDeleting = 0;\n        this.progressOfUploading = 0;\n        this.client = false;\n        this.filesToRemove = [];\n        this.filesToUpload = [];\n        this.operationsCounter = 0;\n        this.syncRevision = crypto.randomUUID();\n    }\n\n    /**\n     * Tests connection\n     *\n     * @param app\n     * @param deploymentConfig\n     * @param siteName\n     */\n    async testConnection (app, deploymentConfig, siteName, uuid) {\n        let connection = false;\n\n        switch(deploymentConfig.protocol) {\n            case 'sftp':\n            case 'sftp+key':        connection = new SFTP();                        break;\n            case 's3':              connection = new S3();                          break;\n            case 'netlify':         connection = new Netlify();                     break;\n            case 'google-cloud':    connection = new GoogleCloud();                 break;\n            case 'git':             connection = new Git();                         break;\n            case 'github-pages':    connection = new GithubPages(deploymentConfig); break;\n            case 'gitlab-pages':    connection = new GitlabPages();                 break;\n            default:   \n                if (this.useAltFtp) {  \n                    connection = new FTPAlt();    \n                } else {\n                    connection = new FTP();\n                }               \n                break;\n        }\n\n        if (connection) {\n            await connection.testConnection(app, deploymentConfig, siteName, uuid);\n        }\n    }\n\n    /**\n     * Inits connection\n     */\n    async initSession () {\n        switch(this.siteConfig.deployment.protocol) {\n            case 'sftp':\n            case 'sftp+key':        this.client = new SFTP(this);               break;\n            case 's3':              this.client = new S3(this);                 break;\n            case 'git':             this.client = new Git(this);                break;\n            case 'github-pages':    this.client = new GithubPages(this);        break;\n            case 'gitlab-pages':    this.client = new GitlabPages(this);        break;\n            case 'netlify':         this.client = new Netlify(this);            break;\n            case 'google-cloud':    this.client = new GoogleCloud(this);        break;\n            case 'manual':          this.client = new ManualDeployment(this);   break;\n            default:                \n                if (this.useAltFtp) {     \n                    this.client = new FTPAlt(this); \n                } else {\n                    this.client = new FTP(this);\n                }                \n                break;\n        }\n\n        await this.client.initConnection();\n    }\n\n    /**\n     * Set input directory on local machine\n     */\n    setInput () {\n        // Set the output dir as a source of the files to upload\n        let basePath = path.join(this.sitesDir, this.siteName);\n        this.inputDir = path.join(basePath, 'output');\n        this.configDir = path.join(this.sitesDir, this.siteName, 'input', 'config');\n    }\n\n    /**\n     * Sets output directory on the server\n     */\n    setOutput (useEmpty = false) {\n        if (useEmpty) {\n            this.outputDir = '';\n        } else {\n            this.outputDir = this.siteConfig.deployment.path;\n        }\n    }\n\n    /**\n     * Prepares list of local files\n     */\n    prepareLocalFilesList () {\n        let tempFileList = this.readDirRecursiveSync(this.inputDir);\n        let fileList = [];\n\n        for (let filePath of tempFileList) {\n            if (filePath === '.git') {\n                continue;\n            }\n\n            if (filePath === '.htaccess' || filePath === '.htpasswd' || filePath === '_redirects') {\n                let excludedProtocols = ['s3', 'github-pages', 'google-cloud', 'netlify'];\n\n                if (excludedProtocols.indexOf(this.siteConfig.deployment.protocol) === -1) {\n                    fileList.push({\n                        path: filePath,\n                        type: 'file',\n                        md5: crypto.createHash('md5').update(FileHelper.readFileSync(path.join(this.inputDir, filePath))).digest('hex')\n                    });\n                }\n\n                continue;\n            }\n\n            // Put directory\n            if (fs.lstatSync(path.join(this.inputDir, filePath)).isDirectory()) {\n                if (filePath.indexOf('/') === 0) {\n                    filePath = filePath.substr(1);\n                }\n\n                if (\n                    this.siteConfig.deployment.protocol !== 'google-cloud' &&\n                    this.siteConfig.deployment.protocol !== 'gitlab-pages'\n                ) {\n                    fileList.push({\n                        path: filePath,\n                        type: 'directory',\n                        md5: false\n                    });\n                }\n\n                continue;\n            }\n\n            // Put file\n            let fileMD5 = false;\n\n            if (isBinaryFileSync(path.join(this.inputDir, filePath))) {\n                let stats = fs.statSync(path.join(this.inputDir, filePath));\n                // below operations are required for backward-compatibility with previously used md5 module\n                // it differently handled integer values\n                let fileSizePrepared = Buffer.from((stats.size).toString().split(''));\n                fileMD5 = crypto.createHash('md5').update(fileSizePrepared).digest('hex');\n            } else {\n                fileMD5 = crypto.createHash('md5').update(FileHelper.readFileSync(path.join(this.inputDir, filePath))).digest('hex');\n            }\n\n            fileList.push({\n                path: filePath,\n                type: 'file',\n                md5: fileMD5\n            });\n        }\n\n        // Save the files list\n        fs.writeFileSync(\n            path.join(this.inputDir, 'files.publii.json'),\n            JSON.stringify(fileList, null, 4),\n            {'flags': 'w'}\n        );\n    }\n\n    /**\n     * Check if local list is equal to the server expected copy\n     * \n     * @param fileContent \n     */\n    checkLocalListWithRemoteList (fileContent) {\n        try {\n            if (typeof fileContent === 'Buffer') {\n                fileContent = fileContent.toString();\n            }\n\n            let content = JSON.parse(fileContent);\n\n            if (content.revision) {\n                let syncRevisionPath = path.join(this.configDir, 'sync-revision.json'); \n                let revisionID = false;\n\n                if (fs.existsSync(syncRevisionPath)) {\n                    let syncRevisionContent = FileHelper.readFileSync(syncRevisionPath);\n                    syncRevisionContent = JSON.parse(syncRevisionContent);\n                    revisionID = syncRevisionContent.revision;\n                }\n                \n                if (revisionID) {\n                    let isExpectedCopy = revisionID === content.revision;\n                    this.compareFilesList(isExpectedCopy);\n                } else {\n                    let filesToCheck = FileHelper.readFileSync(path.join(this.configDir, 'files-remote.json'));\n                    let checkSum = crypto.createHash('md5').update(filesToCheck).digest('hex');\n                    let isExpectedCopy = checkSum === content.revision;\n                    this.compareFilesList(isExpectedCopy);\n                }\n            } else {\n                fs.writeFileSync(path.join(this.configDir, 'files-remote.json'), fileContent);\n                this.compareFilesList(true);\n            }\n        } catch (e) {\n            this.compareFilesList(false);\n        }\n    }\n\n    /**\n     * Compares remote and local files lists\n     *\n     * @param remoteFileListExists\n     */\n    compareFilesList (remoteFileListExists = false) {\n        let remoteFiles = false;\n\n        if (remoteFileListExists) {\n            remoteFiles = FileHelper.readFileSync(path.join(this.configDir, 'files-remote.json'), 'utf8');\n\n            if (remoteFiles) {\n                try {\n                    remoteFiles = JSON.parse(remoteFiles);\n\n                    if (this.siteConfig.deployment.protocol === 'gitlab-pages') {\n                        remoteFiles = remoteFiles.map(file => {\n                            return file;\n                        });\n                    }\n                } catch (e) {\n                    remoteFiles = false;\n                    console.log('Malformed files-remote.json file: ' + e);\n                }\n            }\n        }\n\n        // wait for user interaction if there are no remote files list and syncDate exists under site configuration\n        if (!remoteFiles && this.siteConfig.syncDate) {\n            process.send({\n                type: 'web-contents',\n                message: 'no-remote-files',\n                value: false\n            });\n            return;\n        }\n        \n        this.continueSync(remoteFiles);\n    }\n\n    /**\n     * Wait for user answer or just continue sync if remote files list exists\n     */\n    continueSync (remoteFiles) {\n        let localFiles = FileHelper.readFileSync(path.join(this.inputDir, 'files.publii.json'), 'utf8');\n        \n        if (localFiles) {\n            localFiles = JSON.parse(localFiles);\n        }\n\n        // Detect files to remove\n        let filesToRemove = [];\n\n        if (remoteFiles) {\n            for (let remoteFile of remoteFiles) {\n                let fileFounded = false;\n\n                for (let localFile of localFiles) {\n                    if (localFile.path === remoteFile.path) {\n                        fileFounded = true;\n                        break;\n                    }\n                }\n\n                if (!fileFounded) {\n                    if (\n                        (this.siteConfig.deployment.protocol === 'google-cloud' || this.siteConfig.deployment.protocol === 'gitlab-pages') &&\n                        remoteFile.type === 'directory'\n                    ) {\n                        continue;\n                    }\n\n                    filesToRemove.push({\n                        path: remoteFile.path,\n                        type: remoteFile.type\n                    });\n                }\n            }\n        }\n\n        // Detect files to upload\n        let filesToUpload = [];\n\n        for (let localFile of localFiles) {\n            let fileShouldBeUploaded = true;\n\n            if (remoteFiles) {\n                for (let remoteFile of remoteFiles) {\n                    if(\n                        localFile.path === remoteFile.path &&\n                        localFile.md5 === remoteFile.md5\n                    ) {\n                        fileShouldBeUploaded = false;\n                        break;\n                    }\n                }\n            }\n\n            if (fileShouldBeUploaded) {\n                if (\n                    (\n                        this.siteConfig.deployment.protocol === 'google-cloud' || \n                        this.siteConfig.deployment.protocol === 'gitlab-pages'\n                    ) &&\n                    localFile.type === 'directory'\n                ) {\n                    continue;\n                }\n\n                filesToUpload.push({\n                    path: localFile.path,\n                    type: localFile.type\n                });\n            }\n        }\n\n        this.filesToRemove = filesToRemove;\n        this.filesToUpload = filesToUpload;\n\n        if (this.siteConfig.deployment.protocol === 's3') {\n            this.operationsCounter = this.filesToRemove.filter(file => file.type === 'file').length +\n                                     this.filesToUpload.filter(file => file.type === 'file').length + 1;\n        } else {\n            this.operationsCounter = this.filesToRemove.length + this.filesToUpload.length + 1;\n        }\n\n        this.currentOperationNumber = 0;\n        console.log('Founded ' + this.operationsCounter + ' operations to do');\n        this.progressPerFile = 90.0 / this.operationsCounter;\n        this.sortFiles();\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8,\n                operations: false\n            }\n        });\n\n        this.removeFile();\n    }\n\n    /**\n     * Move files or directories to the beginning\n     */\n    sortFiles () {\n        this.filesToRemove = this.filesToRemove.sort(function(fileA, fileB) {\n            if(fileA.type === 'directory') {\n                return -1;\n            }\n\n            if(fileB.type === 'directory') {\n                return 1;\n            }\n\n            return 0;\n        });\n\n        this.filesToUpload = this.filesToUpload.sort((fileA, fileB) => {\n            if (fileA.type === 'directory') {\n                return 1;\n            }\n\n            if (fileB.type === 'directory') {\n                return -1;\n            }\n\n            // Images will be uploaded at the end\n            if (isBinaryFileSync(path.join(this.inputDir, fileA.path))) {\n                return -1;\n            }\n\n            if (isBinaryFileSync(path.join(this.inputDir, fileB.path))) {\n                return 1;\n            }\n\n            return 0;\n        });\n\n        // Reorder directories to put higher order directories at the beginning\n        this.filesToUpload = this.filesToUpload.sort(function(fileA, fileB) {\n            if (fileA.type === 'directory' && fileB.type === 'directory') {\n                if (fileA.path.length <= fileB.path.length) {\n                    return 1;\n                } else {\n                    return -1;\n                }\n            }\n\n            if (fileA.type === 'directory') {\n                return 1;\n            }\n\n            if (fileB.type === 'directory') {\n                return -1;\n            }\n\n            return 0;\n        });\n\n        // Reorder directories only\n        this.saveConnectionFilesLog(JSON.stringify(this.filesToUpload), 'to-upload');\n        this.saveConnectionFilesLog(JSON.stringify(this.filesToRemove), 'to-delete');\n    }\n\n    /**\n     * Removes file\n     */\n    removeFile () {\n        if (this.siteConfig.deployment.protocol === 's3') {\n            this.client.removeFile();\n            return;\n        }\n\n        if (this.siteConfig.deployment.protocol === 'gitlab-pages') {\n            this.client.startSync();\n            return;\n        }\n\n        let self = this;\n\n        if (this.filesToRemove.length > 0) {\n            let fileToRemove = this.filesToRemove.pop();\n\n            if (fileToRemove.type === 'file') {\n                this.client.removeFile(normalizePath(path.join(this.outputDir, fileToRemove.path)));\n            } else {\n                this.client.removeDirectory(normalizePath(path.join(this.outputDir, fileToRemove.path)));\n            }\n        } else {\n            self.progressOfUploading = self.progressOfDeleting;\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 8 + Math.floor(self.progressOfUploading),\n                    operations: [self.currentOperationNumber ,self.operationsCounter]\n                }\n            });\n\n            this.uploadFile();\n        }\n    }\n\n    /**\n     * Uploads file\n     */\n    uploadFile () {\n        let self = this;\n\n        if (this.filesToUpload.length > 0) {\n            let fileToUpload = this.filesToUpload.pop();\n\n            if (fileToUpload.type === 'file') {\n                this.client.uploadFile(\n                    normalizePath(path.join(this.inputDir, fileToUpload.path)),\n                    normalizePath(path.join(this.outputDir, fileToUpload.path))\n                );\n            } else {\n                this.client.uploadDirectory(\n                    normalizePath(path.join(this.inputDir, fileToUpload.path)),\n                    normalizePath(path.join(this.outputDir, fileToUpload.path))\n                );\n            }\n        } else {\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 98,\n                    operations: [\n                        self.currentOperationNumber,\n                        self.operationsCounter\n                    ]\n                }\n            });\n\n            this.client.uploadNewFileList();\n        }\n    }\n\n    /**\n     * Function used to get recursive list of the files and directories\n     * in the specific dir\n     *\n     * @param dir\n     * @param filelist\n     *\n     * @returns {Array}\n     */\n    readDirRecursiveSync (dir, filelist) {\n        let self = this;\n        let files = fs.readdirSync(dir);\n        filelist = filelist || [];\n\n        files.forEach(function(file) {\n            if (file === '.git') {\n                return;\n            }\n\n            if (fs.statSync(path.join(dir, file)).isDirectory()) {\n                filelist.push(normalizePath(path.join(dir.replace(self.inputDir, ''), file)));\n                filelist = self.readDirRecursiveSync(path.join(dir, file), filelist);\n            } else {\n                if(file.indexOf('.') !== 0 || file === '.htaccess' || file === '.htpasswd' || file === '_redirects') {\n                    filelist.push(normalizePath(path.join(dir.replace(self.inputDir, ''), file)));\n                }\n            }\n        });\n\n        return filelist;\n    };\n\n    /**\n     * Save connection files log\n     *\n     * @param files\n     * @param suffix\n     */\n    saveConnectionFilesLog (files, suffix = '') {\n        if (suffix !== '') {\n            suffix = '-' + suffix;\n        }\n\n        let logPath = path.join(this.appDir, 'connection-files-log' + suffix + '.txt');\n        fs.writeFileSync(logPath, files);\n    }\n}\n\nmodule.exports = Deployment;\n"
  },
  {
    "path": "app/back-end/modules/deploy/ftp-alt.js",
    "content": "/*\n * Class used to upload files to the FTP(S) server\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst ftp = require('basic-ftp');\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst normalizePath = require('normalize-path');\nconst stripTags = require('striptags');\n\nclass FTPAlt {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.connection = false;\n        this.softUploadErrors = {};\n        this.hardUploadErrors = [];\n    }\n\n    async initConnection() {\n        let waitForTimeout = true;\n        let ftpPassword = this.deployment.siteConfig.deployment.password;\n        let account = slug(this.deployment.siteConfig.name);\n        let secureConnection = false;\n\n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        this.connection = new ftp.Client(15000);\n        this.connection.ftp.verbose = true;\n        this.connection.ftp.log = this.connectionDebugger;\n\n        if (ftpPassword === 'publii ' + account) {\n            ftpPassword = await passwordSafeStorage.getPassword('publii', account);\n        }\n\n        if (this.deployment.siteConfig.deployment.protocol !== 'ftp') {\n            secureConnection = true;\n        }\n\n        let connectionParams = {\n            host: this.deployment.siteConfig.deployment.server,\n            port: this.deployment.siteConfig.deployment.port,\n            user: this.deployment.siteConfig.deployment.username,\n            password: ftpPassword,\n            secure: secureConnection,\n            secureOptions: {\n                host: this.deployment.siteConfig.deployment.server,\n                port: this.deployment.siteConfig.deployment.port,\n                user: this.deployment.siteConfig.deployment.username,\n                password: ftpPassword,\n                rejectUnauthorized: this.deployment.siteConfig.deployment.rejectUnauthorized\n            }\n        };\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 6,\n                operations: false\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        await this.connection.access(connectionParams);\n        waitForTimeout = false;\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-success'\n        });\n\n        this.deployment.setInput();\n        this.deployment.setOutput();\n        this.deployment.prepareLocalFilesList();\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 7,\n                operations: false\n            }\n        });\n\n        this.downloadFilesList();\n\n        setTimeout(function() {\n            if (waitForTimeout === true) {\n                this.connection.close();\n                console.log(`[${ new Date().toUTCString() }] Request timeout...`);\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error'\n                });\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }\n        }, 20000);\n    }\n\n    async downloadFilesList() {\n        try {\n            await this.connection.downloadTo(\n                normalizePath(path.join(this.deployment.configDir, 'remote-files.json')), \n                normalizePath(path.join(this.deployment.outputDir, 'files.publii.json'))\n            );\n            let fileToCompare = FileHelper.readFileSync(normalizePath(path.join(this.deployment.configDir, 'remote-files.json')));\n            this.deployment.checkLocalListWithRemoteList(fileToCompare);\n            console.log(`[${ new Date().toUTCString() }] <- files.publii.json`);\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 8,\n                    operations: false\n                }\n            });\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] (!) ERROR WHILE DOWNLOADING files-remote.json`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n            this.deployment.compareFilesList(false);\n        }\n    }\n\n    async uploadNewFileList() {\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 99,\n                operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter]\n            }\n        });\n\n        try {\n            await this.connection.uploadFrom(\n                normalizePath(path.join(this.deployment.inputDir, 'files.publii.json')),\n                normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')),\n            );\n\n            console.log(`[${ new Date().toUTCString() }] -> files.publii.json`);\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n        }\n\n        this.connection.close();\n        console.log(`[${ new Date().toUTCString() }] FTP CONNECTION CLOSED`);\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 100,\n                operations: false\n            }\n        });\n\n        process.send({\n            type: 'sender',\n            message: 'app-deploy-uploaded',\n            value: {\n                status: true,\n                issues: this.hardUploadErrors.length > 0\n            }\n        });\n\n        setTimeout(function () {\n            process.kill(process.pid, 'SIGTERM');\n        }, 1000);\n    }\n\n    async uploadFile(input, output) {\n        try {\n            await this.connection.uploadFrom(input, output)\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${output}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n            setTimeout(() => {\n                if (!this.softUploadErrors[input]) {\n                    this.softUploadErrors[input] = 1;\n                } else {\n                    this.softUploadErrors[input]++;\n                }\n\n                if (this.softUploadErrors[input] <= 5) {\n                    this.uploadFile(input, output);\n                } else {\n                    this.hardUploadErrors.push(input);\n                    this.deployment.currentOperationNumber++;\n                    console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`);\n                    this.deployment.progressOfUploading += this.deployment.progressPerFile;\n                    this.updateProgress('progressOfUploading');\n                    this.deployment.uploadFile();\n                }\n            }, 500);\n\n            return;\n        }\n\n        this.deployment.currentOperationNumber++;\n        console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`);\n        this.deployment.progressOfUploading += this.deployment.progressPerFile;\n        this.updateProgress('progressOfUploading');\n        this.deployment.uploadFile();\n    }\n\n    async uploadDirectory(input, output) {\n        try {\n            await this.connection.ensureDir(output);\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n            setTimeout(async () => {\n                if(!this.softUploadErrors[input]) {\n                    this.softUploadErrors[input] = 1;\n                } else {\n                    this.softUploadErrors[input]++;\n                }\n\n                if (this.softUploadErrors[input] <= 5) {\n                    await this.uploadDirectory(input, output);\n                } else {\n                    this.hardUploadErrors.push(input);\n                    this.deployment.currentOperationNumber++;\n                    console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`);\n                    this.deployment.progressOfUploading += this.deployment.progressPerFile;\n                    this.updateProgress('progressOfUploading');\n                    this.deployment.uploadFile();\n                }\n            }, 500);\n\n            return;\n        }\n\n        try {\n            let rootPath = this.deployment.outputDir;\n\n            if (!rootPath) {\n                rootPath = '/';\n            }\n\n            await this.connection.cd(rootPath);\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] CD error ${err.message}`);\n        }\n\n        this.deployment.currentOperationNumber++;\n        console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`);\n        this.deployment.progressOfUploading += this.deployment.progressPerFile;\n        this.updateProgress('progressOfUploading');\n        this.deployment.uploadFile();\n    }\n\n    async removeFile(input) {\n        try {\n            await this.connection.remove(input);\n            console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);            \n        }\n\n        this.deployment.currentOperationNumber++;\n        this.deployment.progressOfDeleting += this.deployment.progressPerFile;\n        this.updateProgress('progressOfDeleting');\n        this.deployment.removeFile();\n    }\n\n    async removeDirectory(input) {\n        try {\n            await this.connection.removeDir(input);\n            console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ERROR REMOVE DIR: ${input}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n        }\n\n        this.deployment.currentOperationNumber++;\n        this.deployment.progressOfDeleting += this.deployment.progressPerFile;\n        this.updateProgress('progressOfDeleting');\n        this.deployment.removeFile();\n    }\n\n    updateProgress (progressType) {\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8 + Math.floor(this.deployment[progressType]),\n                operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter]\n            }\n        });\n    }\n\n    async testConnection(app, deploymentConfig, siteName, uuid) {\n        let client = new ftp.Client(15000);\n        client.ftp.verbose = true;\n        client.ftp.log = this.connectionDebugger;\n\n        let waitForTimeout = true;\n        let ftpPassword = deploymentConfig.password;\n        let account = slug(siteName);\n        let secureConnection = false;\n\n        if (uuid) {\n            account = uuid;\n        }\n\n        if(ftpPassword === 'publii ' + account) {\n            ftpPassword = await passwordSafeStorage.getPassword('publii', account);\n        }\n\n        if(deploymentConfig.protocol !== 'ftp') {\n            secureConnection = true;\n        }\n\n        let connectionParams = {\n            host: deploymentConfig.server,\n            port: deploymentConfig.port,\n            user: deploymentConfig.username,\n            password: ftpPassword,\n            secure: secureConnection,\n            secureOptions: {\n                host: deploymentConfig.server,\n                port: deploymentConfig.port,\n                user: deploymentConfig.username,\n                password: ftpPassword,\n                rejectUnauthorized: deploymentConfig.rejectUnauthorized\n            }\n        };\n\n        let testFilePath = normalizePath(path.join(app.sitesDir, siteName, 'input', 'publii.test'));\n        fs.writeFileSync(testFilePath, 'It is a test file. You can remove it.');\n\n        try {\n            await client.access(connectionParams);\n            waitForTimeout = false;\n        } catch (err) {\n            client.close();\n            app.mainWindow.webContents.send('app-deploy-test-error', { \n                message: stripTags((err.message).toString())\n            });\n        }\n\n        try {\n            await client.uploadFrom(normalizePath(testFilePath), normalizePath(path.join(deploymentConfig.path, 'publii.test')));\n        } catch (err) {\n            app.mainWindow.webContents.send('app-deploy-test-write-error');\n\n            if (fs.existsSync(testFilePath)) {\n                fs.unlinkSync(testFilePath);\n            }\n\n            client.close();\n            return; \n        }\n\n        try {\n            await client.remove(normalizePath(path.join(deploymentConfig.path, 'publii.test')));\n        } catch (err) {\n            app.mainWindow.webContents.send('app-deploy-test-write-error');\n            \n            if (fs.existsSync(testFilePath)) {\n                fs.unlinkSync(testFilePath);\n            }\n\n            client.close();\n            return;\n        }\n\n        app.mainWindow.webContents.send('app-deploy-test-success');\n            \n        if (fs.existsSync(testFilePath)) {\n            fs.unlinkSync(testFilePath);\n        }\n\n        client.close();\n\n        setTimeout(function() {\n            if (waitForTimeout === true) {\n                client.close();\n                app.mainWindow.webContents.send('app-deploy-test-error');\n            }\n        }, 15000);\n    }\n\n    connectionDebugger(message) {\n        if(message.indexOf(\"PASS \") > -1) {\n            message = '> PASS ******************************';\n        }\n\n        message = `[${ new Date().toUTCString() }] ${message}`;\n        console.log(message);\n    }\n}\n\nmodule.exports = FTPAlt;\n"
  },
  {
    "path": "app/back-end/modules/deploy/ftp.js",
    "content": "/*\n * Class used to upload files to the FTP(S) server\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst ftpClient = require('./../custom-changes/ftp');\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst normalizePath = require('normalize-path');\nconst stripTags = require('striptags');\n\nclass FTP {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.connection = false;\n        this.econnresetCounter = 0;\n        this.softUploadErrors = {};\n        this.hardUploadErrors = [];\n    }\n\n    async initConnection() {\n        let self = this;\n        let waitForTimeout = true;\n        let ftpPassword = this.deployment.siteConfig.deployment.password;\n        let account = slug(this.deployment.siteConfig.name);\n        let secureConnection = false;\n\n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        this.connection = new ftpClient();\n\n        if(ftpPassword === 'publii ' + account) {\n            ftpPassword = await passwordSafeStorage.getPassword('publii', account);\n        }\n\n        if(this.deployment.siteConfig.deployment.protocol !== 'ftp') {\n            secureConnection = true;\n        }\n\n        let connectionParams = {\n            host: this.deployment.siteConfig.deployment.server,\n            port: this.deployment.siteConfig.deployment.port,\n            user: this.deployment.siteConfig.deployment.username,\n            password: ftpPassword,\n            secure: secureConnection,\n            secureOptions: {\n                host: this.deployment.siteConfig.deployment.server,\n                port: this.deployment.siteConfig.deployment.port,\n                user: this.deployment.siteConfig.deployment.username,\n                password: ftpPassword,\n                rejectUnauthorized: this.deployment.siteConfig.deployment.rejectUnauthorized\n            },\n            connTimeout: 15000,\n            pasvTimeout: 15000,\n            debug: self.connectionDebugger.bind(self)\n        };\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 6,\n                operations: false\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        this.connection.connect(connectionParams);\n\n        this.connection.on('ready', function() {\n            waitForTimeout = false;\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-success'\n            });\n\n            self.deployment.setInput();\n            self.deployment.setOutput();\n            self.deployment.prepareLocalFilesList();\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 7,\n                    operations: false\n                }\n            });\n\n            self.downloadFilesList();\n        });\n\n        this.connection.on('error', function(err) {\n            console.log(`[${ new Date().toUTCString() }] FTP ERROR: ${err}`);\n\n            if(typeof err === \"string\" && err.indexOf('ECONNRESET') > -1) {\n                self.econnresetCounter++;\n\n                if(self.econnresetCounter <= 5) {\n                    return;\n                }\n            }\n\n            if(waitForTimeout) {\n                waitForTimeout = false;\n                self.connection.destroy();\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error',\n                    value: {\n                        additionalMessage: stripTags((err.message).toString())\n                    }\n                });\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }\n        });\n\n        this.connection.on('close', function(err) {\n            console.log(`[${ new Date().toUTCString() }] FTP CONNECTION CLOSED: ${err}`);\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n\n        setTimeout(function() {\n            if(waitForTimeout === true) {\n                self.connection.destroy();\n                console.log(`[${ new Date().toUTCString() }] Request timeout...`);\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error'\n                });\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }\n        }, 20000);\n    }\n\n    downloadFilesList() {\n        let self = this;\n\n        this.connection.get(\n            normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')),\n            function(err, fileStream) {\n                console.log(`[${ new Date().toUTCString() }] <- files.publii.json`);\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8,\n                        operations: false\n                    }\n                });\n\n                if (!err) {\n                    let fileStreamChunks = [];\n\n                    fileStream.on('data', (chunk) => {\n                        fileStreamChunks.push(chunk.toString());\n                    });\n\n                    fileStream.on('end', () => {\n                        let completeFile = fileStreamChunks.join('');\n                        self.deployment.checkLocalListWithRemoteList(completeFile);\n                    });\n                } else {\n                    console.log(`[${ new Date().toUTCString() }] (!) ERROR WHILE DOWNLOADING files-remote.json`);\n                    console.log(`[${ new Date().toUTCString() }] ${err}`);\n                    self.deployment.compareFilesList(false);\n                }\n            }\n        );\n    }\n\n    uploadNewFileList() {\n        let self = this;\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 99,\n                operations: [self.deployment.currentOperationNumber ,self.deployment.operationsCounter]\n            }\n        });\n\n        this.connection.put(\n            normalizePath(path.join(this.deployment.inputDir, 'files.publii.json')),\n            normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')),\n            function(err) {\n                console.log(`[${ new Date().toUTCString() }] -> files.publii.json`);\n\n                if (err) {\n                    console.log(`[${ new Date().toUTCString() }] ${err}`);\n                }\n\n                self.connection.logout(function(err) {\n                    if (err) {\n                        console.log(`[${ new Date().toUTCString() }] ${err}`);\n                    }\n\n                    self.connection.end();\n                    self.connection.destroy();\n                });\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 100,\n                        operations: false\n                    }\n                });\n\n                process.send({\n                    type: 'sender',\n                    message: 'app-deploy-uploaded',\n                    value: {\n                        status: true,\n                        issues: self.hardUploadErrors.length > 0\n                    }\n                });\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }\n        );\n    }\n\n    uploadFile(input, output) {\n        let self = this;\n\n        this.connection.put(\n            input,\n            output,\n            function (err) {\n                if (err) {\n                    console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${output}`);\n                    console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n                    setTimeout(() => {\n                        if(!self.softUploadErrors[input]) {\n                            self.softUploadErrors[input] = 1;\n                        } else {\n                            self.softUploadErrors[input]++;\n                        }\n\n                        if(self.softUploadErrors[input] <= 5) {\n                            self.uploadFile(input, output);\n                        } else {\n                            self.hardUploadErrors.push(input);\n\n                            self.deployment.currentOperationNumber++;\n                            console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`);\n                            self.deployment.progressOfUploading += self.deployment.progressPerFile;\n\n                            process.send({\n                                type: 'web-contents',\n                                message: 'app-uploading-progress',\n                                value: {\n                                    progress: 8 + Math.floor(self.deployment.progressOfUploading),\n                                    operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                                }\n                            });\n\n                            self.deployment.uploadFile();\n                        }\n                    }, 500);\n                } else {\n                    self.deployment.currentOperationNumber++;\n                    console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`);\n                    self.deployment.progressOfUploading += self.deployment.progressPerFile;\n\n                    process.send({\n                        type: 'web-contents',\n                        message: 'app-uploading-progress',\n                        value: {\n                            progress: 8 + Math.floor(self.deployment.progressOfUploading),\n                            operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                        }\n                    });\n\n                    self.deployment.uploadFile();\n                }\n            }\n        );\n    }\n\n    uploadDirectory(input, output) {\n        let self = this;\n\n        this.connection.mkdir(\n            output,\n            true,\n            function (err) {\n                if (err) {\n                    console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`);\n                    console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n                    setTimeout(() => {\n                        if(!self.softUploadErrors[input]) {\n                            self.softUploadErrors[input] = 1;\n                        } else {\n                            self.softUploadErrors[input]++;\n                        }\n\n                        if(self.softUploadErrors[input] <= 5) {\n                            self.uploadDirectory(input, output);\n                        } else {\n                            self.hardUploadErrors.push(input);\n\n                            self.deployment.currentOperationNumber++;\n                            console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`);\n                            self.deployment.progressOfUploading += self.deployment.progressPerFile;\n\n                            process.send({\n                                type: 'web-contents',\n                                message: 'app-uploading-progress',\n                                value: {\n                                    progress: 8 + Math.floor(self.deployment.progressOfUploading),\n                                    operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                                }\n                            });\n\n                            self.deployment.uploadFile();\n                        }\n                    }, 500);\n                } else {\n                    self.deployment.currentOperationNumber++;\n                    console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`);\n                    self.deployment.progressOfUploading += self.deployment.progressPerFile;\n\n                    process.send({\n                        type: 'web-contents',\n                        message: 'app-uploading-progress',\n                        value: {\n                            progress: 8 + Math.floor(self.deployment.progressOfUploading),\n                            operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                        }\n                    });\n\n                    self.deployment.uploadFile();\n                }\n            }\n        );\n    }\n\n    removeFile(input) {\n        let self = this;\n\n        this.connection.delete(\n            input,\n            function (err) {\n                self.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n\n                if (err) {\n                    console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`);\n                    console.log(`[${ new Date().toUTCString() }] ${err}`);\n                }\n\n                self.deployment.progressOfDeleting += self.deployment.progressPerFile;\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8 + Math.floor(self.deployment.progressOfDeleting),\n                        operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                    }\n                });\n\n                self.deployment.removeFile();\n            }\n        );\n    }\n\n    removeDirectory(input) {\n        let self = this;\n\n        this.connection.rmdir(\n            input,\n            true,\n            function (err) {\n                self.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n\n                if (err) {\n                    console.log(`[${ new Date().toUTCString() }] ERROR REMOVE DIR: ${input}`);\n                    console.log(`[${ new Date().toUTCString() }] ${err}`);\n                }\n                \n                self.deployment.progressOfDeleting += self.deployment.progressPerFile;\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8 + Math.floor(self.deployment.progressOfDeleting),\n                        operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                    }\n                });\n\n                self.deployment.removeFile();\n            }\n        );\n    }\n\n    async testConnection(app, deploymentConfig, siteName, uuid) {\n        let client = new ftpClient();\n        let waitForTimeout = true;\n        let ftpPassword = deploymentConfig.password;\n        let account = slug(siteName);\n        let secureConnection = false;\n\n        if (uuid) {\n            account = uuid;\n        }\n\n        if(ftpPassword === 'publii ' + account) {\n            ftpPassword = await passwordSafeStorage.getPassword('publii', account);\n        }\n\n        if(deploymentConfig.protocol !== 'ftp') {\n            secureConnection = true;\n        }\n\n        let connectionParams = {\n            host: deploymentConfig.server,\n            port: deploymentConfig.port,\n            user: deploymentConfig.username,\n            password: ftpPassword,\n            secure: secureConnection,\n            secureOptions: {\n                host: deploymentConfig.server,\n                port: deploymentConfig.port,\n                user: deploymentConfig.username,\n                password: ftpPassword,\n                rejectUnauthorized: deploymentConfig.rejectUnauthorized\n            },\n            connTimeout: 10000,\n            pasvTimeout: 10000\n        };\n\n        let testFilePath = normalizePath(path.join(app.sitesDir, siteName, 'input', 'publii.test'));\n        fs.writeFileSync(testFilePath, 'It is a test file. You can remove it.');\n        client.connect(connectionParams);\n\n        client.on('ready', () => {\n            waitForTimeout = false;\n\n            client.put(\n                normalizePath(testFilePath),\n                normalizePath(path.join(deploymentConfig.path, 'publii.test')),\n                (err) => {   \n                    if (err) {\n                        app.mainWindow.webContents.send('app-deploy-test-write-error');\n\n                        if (fs.existsSync(testFilePath)) {\n                            fs.unlinkSync(testFilePath);\n                        }\n\n                        client.destroy();\n                        return;   \n                    }\n                    \n                    client.delete(\n                        normalizePath(path.join(deploymentConfig.path, 'publii.test')),\n                        (err) => {\n                            if (err) {\n                                app.mainWindow.webContents.send('app-deploy-test-write-error');\n                                \n                                if (fs.existsSync(testFilePath)) {\n                                    fs.unlinkSync(testFilePath);\n                                }\n\n                                client.destroy();\n                                return;\n                            }\n\n                            app.mainWindow.webContents.send('app-deploy-test-success');\n                            \n                            if (fs.existsSync(testFilePath)) {\n                                fs.unlinkSync(testFilePath);\n                            }\n\n                            client.destroy();\n                        }\n                    );\n                }\n            );\n        });\n\n        if (fs.existsSync(testFilePath)) {\n            fs.unlinkSync(testFilePath);\n        }\n\n        client.on('error', function(err) {\n            if(waitForTimeout) {\n                waitForTimeout = false;\n                client.destroy();\n                app.mainWindow.webContents.send('app-deploy-test-error', { \n                    message: stripTags((err.message).toString())\n                });\n            }\n        });\n\n        setTimeout(function() {\n            if(waitForTimeout === true) {\n                client.destroy();\n                app.mainWindow.webContents.send('app-deploy-test-error');\n            }\n        }, 15000);\n    }\n\n    connectionDebugger(message) {\n        if(message.indexOf(\"[connection] > 'PASS \") > -1) {\n            message = '[connection] > PASS ******************************';\n        }\n\n        message = `[${ new Date().toUTCString() }] ${message}`;\n        console.log(message);\n    }\n}\n\nmodule.exports = FTP;\n"
  },
  {
    "path": "app/back-end/modules/deploy/git.js",
    "content": "/*\n * Class used to upload files to the Github Pages\n */\n\nconst fs = require('fs-extra');\nconst gitClient = require('isomorphic-git')\nconst http = require('isomorphic-git/http/node')\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst stripTags = require('striptags');\n\nclass Git {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.repositoryURL = '';\n        this.user = '';\n        this.password = '';\n        this.branch = '';\n        this.commitAuthor = '';\n        this.commitEmail = '';\n        this.commitMessage = '';\n    }\n\n    async initConnection() {\n        let account = slug(this.deployment.siteConfig.name);\n        this.repositoryURL = this.deployment.siteConfig.deployment.git.url;\n        this.user = this.deployment.siteConfig.deployment.git.user;\n        this.password = this.deployment.siteConfig.deployment.git.password;\n        this.branch = this.deployment.siteConfig.deployment.git.branch;\n        this.commitAuthor = this.deployment.siteConfig.deployment.git.commitAuthor;\n        this.commitEmail = this.deployment.siteConfig.deployment.git.commitEmail;\n        this.commitMessage = this.deployment.siteConfig.deployment.git.commitMessage;\n        \n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        if (this.password === 'publii-git-password ' + account) {\n            this.password = await passwordSafeStorage.getPassword('publii-git-password', account);\n        }\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 6,\n                operations: false\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        this.deployment.setInput();\n        await this.deploy();\n    }\n\n    async testConnection(app, deploymentConfig, siteName, uuid) {\n        this.waitForTimeout = true;\n        let account = slug(siteName);\n        let username = deploymentConfig.git.user;\n        let password = deploymentConfig.git.password;\n        let url = deploymentConfig.git.url;\n        \n        if (uuid) {\n            account = uuid;\n        }\n\n        if (password === 'publii-git-password ' + account) {\n            password = await passwordSafeStorage.getPassword('publii-git-password', account);\n        }\n\n        let authObject = {\n            username,\n            password\n        };\n\n        let timeoutCheck = setTimeout(function () {\n            if(this.waitForTimeout === true) {\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: {\n                        translation: 'core.server.requestTimeout'\n                    }\n                });\n\n                this.waitForTimeout = false;\n            }\n        }, 20000);\n\n        try {\n            await gitClient.getRemoteInfo({\n                http,\n                url,\n                onAuth: () => authObject,\n                onAuthFailure: () => {\n                    app.mainWindow.webContents.send('app-deploy-test-error', {\n                        noAdditionalMessage: true,\n                        message: {\n                            translation: 'core.server.tokenOrServerAddressInvalid'\n                        }\n                    });\n                    this.waitForTimeout = false;\n                    clearTimeout(timeoutCheck);\n                },\n                onAuthSuccess: () => {\n                    app.mainWindow.webContents.send('app-deploy-test-success');\n                    this.waitForTimeout = false;\n                    clearTimeout(timeoutCheck);\n                }\n            });\n        } catch (e) {\n            console.log('Cannot connect to the git repository: ', e);\n\n            if (e.data && e.data.response) {\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: stripTags((e.data.response).toString())\n                });\n            } else {\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: stripTags((e).toString())\n                });\n            }\n        }\n    }\n\n    async prepareToSync (siteConfig, siteName, dir, sendProgressCallback) {\n        let account = slug(siteName);\n        let username = siteConfig.deployment.git.user;\n        let password = siteConfig.deployment.git.password;\n        let url = siteConfig.deployment.git.url;\n        let branch = siteConfig.deployment.git.branch;\n        let commitAuthor = siteConfig.deployment.git.commitAuthor;\n        let commitEmail = siteConfig.deployment.git.commitEmail;\n        \n        if (siteConfig.uuid) {\n            account = siteConfig.uuid;\n        }\n\n        if (password === 'publii-git-password ' + account) {\n            password = await passwordSafeStorage.getPassword('publii-git-password', account);\n        }\n\n        let authObject = {\n            username,\n            password\n        };\n\n        try {\n            let repo = { fs, dir };\n\n            await gitClient.init({\n                fs,\n                dir,\n                defaultBranch: branch\n            });\n\n            console.log('[i] Git debug: repository URL = ', url, ' branch = ' + branch, ' path = ' + dir);\n\n            const hasProperOrigin = await this.hasCorrectOrigin(repo, url);\n\n            if (!hasProperOrigin) {\n                await gitClient.addRemote({ \n                    fs, \n                    dir, \n                    url, \n                    remote: 'origin'\n                });\n\n                console.log('[i] Git debug: need to add origin');\n                await this.hasCorrectOrigin(repo, url);\n            } else {\n                console.log('[i] Git debug: not needed to add origin')\n            }\n\n            console.log('[i] Git debug: origin checked');\n\n            let info = await gitClient.getRemoteInfo({\n                http,\n                url,\n                onAuth: () => authObject\n            });\n\n            console.log('[i] Git debug: remote info = ' + JSON.stringify(info));\n\n            await gitClient.fetch({\n                fs,\n                http,\n                dir,\n                url,\n                ref: branch,\n                depth: 1,\n                singleBranch: true,\n                onMessage: message => {\n                    sendProgressCallback(1, message);\n                    console.log('[i] Git debug: ' + message);\n                },\n                onProgress: event => {\n                    sendProgressCallback(1, event.phase + '(' + event.loaded + ')');\n                    console.log('[i] Git debug: ' + event.phase + '(' + event.loaded + ')')\n                },\n                onAuth: () => authObject\n            });\n\n            await gitClient.checkout({ \n                fs, \n                dir, \n                ref: branch,\n                noCheckout: true,\n                onProgress: event => {\n                    sendProgressCallback(1, event.phase + '(' + event.loaded + ')');\n                    console.log('[i] Git debug: ' + event.phase + '(' + event.loaded + ')')\n                },\n            });\n\n            await gitClient.pull({ \n                http, \n                fs, \n                dir, \n                ref: branch,\n                remote: 'origin',\n                author: {\n                    name: commitAuthor, \n                    email: commitEmail\n                },\n                onMessage: message => {\n                    sendProgressCallback(1, message);\n                    console.log('[i] Git debug: ' + message);\n                },\n                onProgress: event => {\n                    sendProgressCallback(1, event.phase + '(' + event.loaded + ')');\n                    console.log('[i] Git debug: ' + event.phase + '(' + event.loaded + ')')\n                },\n                onAuth: () => authObject\n            });\n\n            console.log('[i] Git debug: pull done');\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ERROR: ${err}`);\n\n            if (err.toString().indexOf('MergeNotSupportedError') > -1) {\n                return 'merge-error';\n            }\n        }\n\n        return 'ok';\n    }\n\n    async deploy() {\n        try {\n            let dir = this.deployment.inputDir;\n            let repo = { fs, dir };\n            let authObject = {\n                username: this.user,\n                password: this.password\n            };\n\n            await gitClient.statusMatrix(repo).then((status) =>\n                Promise.all(\n                    status.map(([filepath, , worktreeStatus]) => {\n                        if (filepath.substr(-9) === '.DS_Store' || filepath.substr(-9) === 'Thumbs.db') {\n                            console.log('[i] Git debug: skip system files');\n                        } else {\n                            if (worktreeStatus) {\n                                gitClient.add({ ...repo, filepath });\n                                console.log('[i] Git debug: add = ', filepath);\n                            } else { \n                                gitClient.remove({ ...repo, filepath });\n                                console.log('[i] Git debug: remove = ', filepath);\n                            }\n                        }\n                    })\n                )\n            );\n\n            console.log('[i] Git debug: git add done');\n\n            const changesMatrix = await gitClient.statusMatrix(repo);\n            const hasChanges = changesMatrix.some(row => {\n                return row[1] !== 1 || row[2] !== 1;\n            });\n\n            console.log('[i] Git debug: changes exists = ', hasChanges);\n\n            if (hasChanges) {\n                await gitClient.commit({ \n                    fs, \n                    dir, \n                    author: {\n                        name: this.commitAuthor,\n                        email: this.commitEmail\n                    },\n                    message: this.commitMessage\n                });\n\n                console.log('[i] Git debug: commit done');\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        message: 'Pushing changes to remote...',\n                        progress: 50,\n                        operations: [0, 1]\n                    }\n                });\n\n                await gitClient.push({\n                    http,\n                    fs,\n                    dir,\n                    remote: 'origin',\n                    ref: this.branch,\n                    onAuth: () => (authObject),\n                    onMessage: message => {\n                        console.log(`[i] Git debug - message: ${message}`);\n                    },\n                    onProgress: event => {\n                        console.log(`[i] Git debug - event: ${event}`);\n                    }\n                });\n\n                console.log('[i] Git debug: push done');\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        message: 'Push operation completed',\n                        progress: 99,\n                        operations: [2, 2]\n                    }\n                });\n            }\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 100,\n                    operations: false\n                }\n            });\n\n            process.send({\n                type: 'sender',\n                message: 'app-deploy-uploaded',\n                value: {\n                    progress: 100,\n                    status: true\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ERROR: ${err}`);\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: 'Critical error: ' + stripTags((err).toString())\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        }\n    }\n\n    async hasCorrectOrigin (repo, originToCheck) {\n        let remotes = await gitClient.listRemotes(repo);\n\n        console.log('[i] Git debug: remotes = ' + JSON.stringify(remotes));\n    \n        if (!remotes) {\n            return false;\n        }\n    \n        for (let i = 0; i < remotes.length; i++) {\n            if (remotes[i].url === originToCheck) {\n                return true;\n            }\n        }\n    \n        return false;\n    }\n}\n\nmodule.exports = Git;\n"
  },
  {
    "path": "app/back-end/modules/deploy/github-pages.js",
    "content": "/*\n * Class used to upload files to the Github Pages\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst { Octokit } = require(\"@octokit/rest\");\nconst list = require('ls-all');\nconst crypto = require('crypto');\nconst countFiles = require('count-files');\nconst moment = require('moment');\nconst normalizePath = require('normalize-path');\nconst stripTags = require('striptags');\n\nclass GithubPages {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.serverURL = '';\n\n        if (this.deployment.siteConfig) {\n            this.serverURL = this.deployment.siteConfig.deployment.github.server;\n        } else {\n            this.serverURL = this.deployment.github.server;\n        }\n\n        this.connection = false;\n        this.client = new Octokit({\n            baseUrl: `https://${this.serverURL}`,\n            request: {\n                timeout: 30000\n            },\n            userAgent: \"Publii\"\n        });\n        this.token = '';\n        this.repository = '';\n        this.user = '';\n        this.branch = '';\n        this.filesToUpdate = 0;\n        this.filesUpdated = 0;\n        this.waitForTimeout = false;\n        this.uploadedBlobs = {};\n    }\n\n    async initConnection() {\n        let self = this;\n        this.token = this.deployment.siteConfig.deployment.github.token;\n        this.repository = this.deployment.siteConfig.deployment.github.repo;\n        this.user = this.deployment.siteConfig.deployment.github.user;\n        this.branch = 'heads/' + this.deployment.siteConfig.deployment.github.branch;\n        this.parallelOperations = parseInt(this.deployment.siteConfig.deployment.github.parallelOperations, 10);\n        this.apiRateLimiting = !!this.deployment.siteConfig.deployment.github.apiRateLimiting;\n        this.waitForTimeout = true;\n        let account = slug(this.deployment.siteConfig.name);\n\n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        if (this.token === 'publii-gh-token ' + account) {\n            this.token = await passwordSafeStorage.getPassword('publii-gh-token', account);\n        }\n\n        this.client = new Octokit({\n            auth: this.token,\n            baseUrl: `https://${this.serverURL}`,\n            request: {\n                timeout: 30000\n            },\n            userAgent: \"Publii\"\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 6,\n                operations: false\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        self.deployment.setInput();\n        self.deployment.setOutput(true);\n\n        /*\n         * Create CNAME file if necessary\n         */\n        if (this.deployment.siteConfig.domain.indexOf('github.io') === -1) {\n            let cnameFilePath = path.join(self.deployment.inputDir, 'CNAME');\n            let domainName = this.deployment.siteConfig.domain;\n\n            if (domainName.indexOf('//') > -1) {\n                domainName = domainName.split('//')[1];\n            }\n\n            if (domainName.indexOf('/') === 0) {\n                domainName = domainName.slice(1);\n            }\n\n            fs.writeFileSync(\n                cnameFilePath,\n                domainName\n            );\n        }\n\n        countFiles(self.deployment.inputDir, async function (err, results) {\n            let numberOfFiles = parseInt(results.files + results.dirs, 10);\n\n            if(numberOfFiles > 4000) {\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error',\n                    value: {\n                        additionalMessage: {\n                            translation: 'core.server.tooManyFilesInfo',\n                            translationVars: {\n                                numberOfFiles: numberOfFiles\n                            }\n                        }\n                    }\n                });\n\n                return;\n            }\n\n            try {\n                if (self.apiRateLimiting) {\n                    let result = self.getAPIRateLimit();\n\n                    if(result.remaining < 10) {\n                        process.send({\n                            type: 'web-contents',\n                            message: 'app-connection-error',\n                            value: {\n                                additionalMessage: {\n                                    translation: 'core.server.requestLimitExceededInfo',\n                                    translationVars: {\n                                        remaining: parseInt(result.remaining, 10),\n                                        resetTime: moment(parseInt(result.reset * 1000, 10)).format('MMMM Do YYYY, h:mm:ss a')\n                                    }\n                                }\n                            }\n                        });\n\n                        return;\n                    }\n                }\n\n                await self.deploy();\n            } catch (err) {\n                console.log(`[${ new Date().toUTCString() }] ERROR: ${JSON.stringify(err)}`);\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error',\n                    value: {\n                        additionalMessage: 'E2 ' + stripTags((err).toString())\n                    }\n                });\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }\n        });\n\n        setTimeout(function() {\n            if(this.waitForTimeout === true) {\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error',\n                    value: {\n                        additionalMessage: {\n                            translation: 'core.server.requestTimeout'\n                        }\n                    }\n                });\n            }\n        }, 15000);\n    }\n\n    async testConnection(app, deploymentConfig, siteName, uuid) {\n        let token = deploymentConfig.github.token;\n        let repository = deploymentConfig.github.repo;\n        let user = deploymentConfig.github.user;\n        let branch = 'heads/' + deploymentConfig.github.branch;\n        let account = slug(siteName);\n        this.waitForTimeout = true;\n\n        if (uuid) {\n            account = uuid;\n        }\n\n        if(token === 'publii-gh-token ' + account) {\n            token = await passwordSafeStorage.getPassword('publii-gh-token', account);\n        }\n\n        this.client = new Octokit({\n            auth: token,\n            baseUrl: `https://${this.serverURL}`,\n            request: {\n                timeout: 30000\n            },\n            userAgent: \"Publii\"\n        });\n\n        this.apiRequest(\n            {\n                owner: user,\n                repo: repository,\n                ref: branch\n            },\n            (api) => api.rest.git.getRef,\n            (result) => {\n                if(result.data && result.data.object) {\n                    return result.data.object.sha;\n                }\n\n                return null;\n            }\n        ).then(result => {\n            if(result === null) {\n                this.waitForTimeout = false;\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: {\n                        translation: 'core.server.branchDoesNotExist'\n                    }\n                });\n\n                return;\n            }\n\n            this.waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-success');\n        }).catch(err => {\n            err = JSON.parse(err);\n            this.waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-error', {\n                message: stripTags((err.message).toString())\n            });\n        });\n\n        setTimeout(function() {\n            if(this.waitForTimeout === true) {\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: {\n                        translation: 'core.server.requestTimeout'\n                    }\n                });\n\n                this.waitForTimeout = false;\n            }\n        }, 10000);\n    }\n\n    async deploy() {\n        let self = this;\n\n        try {\n            let commitSHA;\n            this.uploadedBlobs = {};\n            commitSHA = await this.getLatestSHA();\n            let treeSHA = await this.getTreeSHA(commitSHA);\n            let remoteTree = await this.getTreeData(treeSHA);\n            let trees = await this.listFolderFiles(remoteTree);\n            let finalTree = await this.getNewTreeBasedOnDiffs(trees.remoteTree, trees.localTree);\n            finalTree = await this.createBlobs(finalTree, false);\n            finalTree = await this.updateBlobsList(finalTree);\n            let sha = await this.createTree(finalTree);\n            sha = await this.createCommit(sha, commitSHA);\n            let result = await this.createReference(sha);\n\n            if(result === false) {\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n\n                return;\n            }\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 100,\n                    operations: false\n                }\n            });\n\n            process.send({\n                type: 'sender',\n                message: 'app-deploy-uploaded',\n                value: {\n                    progress: 100,\n                    status: true\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] ERROR: ${JSON.stringify(err)}`);\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: 'E1 ' + stripTags(JSON.stringify(err))\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        }\n    }\n\n    async apiRequest(requestData, method, extractor) {\n        try {\n            let data = await method(this.client)(requestData);\n            let result = extractor ? extractor(data) : data;\n            return result;\n        } catch (err) {\n            console.log(`[${ new Date().toUTCString() }] (i) TRIED AGAIN: ${method.toString()} - ${requestData.filePath}`);\n            \n            try {\n                let data = await method(this.client)(requestData);\n                let result = extractor ? extractor(data) : data;\n                return result;\n            } catch (retryErr) {\n                console.log(`[${ new Date().toUTCString() }] (i) TRIED AGAIN FAIL: ${method.toString()} - ${requestData.filePath}`);\n                throw retryErr;\n            }\n        }\n    }\n\n    getAPIRateLimit() {\n        return this.apiRequest(\n            {},\n            (api) => api.rest.rateLimit.get,\n            (result) => result.data.resources.core\n        );\n    }\n\n    getLatestSHA() {\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8,\n                message: 'core.server.getInfoAboutLatestCommit'\n            }\n        });\n\n        return this.apiRequest(\n            {\n                owner: this.user,\n                repo: this.repository,\n                ref: this.branch\n            },\n            (api) => api.rest.git.getRef,\n            (result) => {\n                this.waitForTimeout = false;\n\n                if(result.data && result.data.object) {\n                    return result.data.object.sha;\n                }\n\n                return null;\n            }\n        );\n    }\n\n    getTreeSHA(latestCommitSHA) {\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8,\n                message: 'core.server.retrievingHandlerOfRemoteFilesTree'\n            }\n        });\n\n        return this.apiRequest(\n            {\n                owner: this.user,\n                repo: this.repository,\n                commit_sha: latestCommitSHA\n            },\n            (api) => api.rest.git.getCommit,\n            (result) => result.data.tree.sha\n        );\n    }\n\n    getTreeData(treeSHA) {\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8,\n                message: {\n                    translation: 'core.server.retrievingRemoteFilesTree'\n                }\n            }\n        });\n\n        return this.apiRequest(\n            {\n                owner: this.user,\n                repo: this.repository,\n                tree_sha: treeSHA,\n                recursive: 1\n            },\n            (api) => api.rest.git.getTree,\n            (result) => result.data.tree\n        );\n    }\n\n    async getNewTreeBasedOnDiffs(remoteTree, localTree) {\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8,\n                message: {\n                    translation: 'core.server.preparingFilesTreeToUpload'\n                }\n            }\n        });\n\n        this.filesToUpdate = 0;\n        this.filesUpdated = 0;\n\n        for(let localFile of localTree) {\n            let remoteFile = this.findRemoteFile(localFile.path, remoteTree);\n\n            if(remoteFile === false) {\n                localFile.getBlob = true;\n                this.filesToUpdate++;\n                continue;\n            }\n\n            if(localFile.sha === false) {\n                if(remoteFile.size !== localFile.size) {\n                    localFile.getBlob = true;\n                    this.filesToUpdate++;\n                    continue;\n                }\n\n                localFile.sha = remoteFile.sha;\n                continue;\n            }\n\n            if(localFile.sha !== remoteFile.sha) {\n                localFile.getBlob = true;\n                this.filesToUpdate++;\n                continue;\n            }\n        }\n\n        return localTree;\n    }\n\n    findRemoteFile(filePath, remoteTree) {\n        for(let remoteFile of remoteTree) {\n            if(remoteFile.path === filePath) {\n                return remoteFile;\n            }\n        }\n\n        return false;\n    }\n\n    async listFolderFiles(remoteTree) {\n        return list([this.deployment.inputDir], {\n            recurse: true,\n            flatten: true\n        }).then(files => {\n            let localTree = files.filter(file => !file.mode.dir)\n                                 .map(file => {\n                                     let calculatedHash = false;\n                                     let fileSize = fs.statSync(file.path).size;\n\n                                     if(!this.isBinaryFile(file.path)) {\n                                         let fileContent = FileHelper.readFileSync(file.path);\n                                         fileSize = fileContent.length;\n                                         calculatedHash = crypto.createHash('sha1')\n                                                                .update(\"blob \" + fileSize + \"\\0\" + fileContent)\n                                                                .digest('hex');\n                                     }\n\n                                     return {\n                                         fullPath: normalizePath(file.path),\n                                         path: normalizePath(path.relative(this.deployment.inputDir, file.path)),\n                                         mode: file.mode.exec ? '100755' : '100644',\n                                         type: 'blob',\n                                         size: fileSize,\n                                         sha: calculatedHash,\n                                         encoding: 'base64',\n                                         getBlob: false\n                                     };\n                                 })\n                                 .filter(file => this.isNecessaryFile(file.path));\n            return {\n                localTree: localTree,\n                remoteTree: remoteTree\n            };\n        });\n    }\n\n    createBlob(filePath) {\n        let fileContent = FileHelper.readFileSync(filePath, { encoding: 'base64' });\n        console.log(`[${ new Date().toUTCString() }] CREATE BLOB: ${filePath}`);\n\n        return this.apiRequest(\n            {\n                owner: this.user,\n                repo: this.repository,\n                encoding: 'base64',\n                content: fileContent,\n                filePath: filePath\n            },\n            (api) => api.rest.git.createBlob,\n            (result) => {\n                this.uploadedBlobs[filePath] = result.data.sha;\n                console.log(`[${ new Date().toUTCString() }] CREATED BLOB: ${filePath} - ${result.data.sha}`);\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8 + (Math.floor((this.filesUpdated / this.filesToUpdate) * 100) - 8),\n                        operations: [this.filesUpdated, this.filesToUpdate]\n                    }\n                });\n\n                this.filesUpdated++;\n\n                return result.data.sha;\n            }\n        );\n    }\n\n    updateBlobsList(files) {\n        let counterOfFilesToUpload = 0;\n        let output = files.map(file => {\n            if(this.uploadedBlobs[file.fullPath]) {\n                file.sha = this.uploadedBlobs[file.fullPath];\n                file.getBlob = false;\n            }\n\n            if(file.getBlob) {\n                counterOfFilesToUpload++;\n            }\n\n            return file;\n        });\n\n        return output;\n    }\n\n    async createBlobs(files, reuploadSession = false) {\n        if (this.apiRateLimiting) {\n            let result = await this.getAPIRateLimit();\n\n            if(result.remaining < this.filesToUpdate + 10) {\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error',\n                    value: {\n                        additionalMessage: {\n                            translation: 'core.server.requestLimitExceededInfo',\n                            translationVars: {\n                                remaining: parseInt(result.remaining, 10),\n                                resetTime: moment(parseInt(result.reset * 1000, 10)).format('MMMM Do YYYY, h:mm:ss a')\n                            }\n                        }\n                    }\n                });\n\n                return [];\n            }\n        }\n\n        let filesToUpdate = [];\n\n        for (let i = 0; i < files.length; i++) {\n            let file = files[i];\n\n            if(file.getBlob) {\n                filesToUpdate.push(i);\n            }\n        }\n\n        for (let i = 0; i < filesToUpdate.length; i += this.parallelOperations) {\n            let requests = [];\n\n            for (let j = 0; j < this.parallelOperations; j++) {\n                let index = filesToUpdate[i + j];\n\n                if (typeof index === 'number') {\n                    let file = files[index];\n                    requests.push(this.createBlob(file.fullPath).then((sha) => {\n                        file = Object.assign({}, file, {\n                            sha: sha,\n                            getBlob: false\n                        });\n                    }));\n                }\n            }\n\n            await Promise.all(requests);\n        }\n\n        return files;\n    }\n\n    createTree(tree) {\n        if(!tree || !tree.length) {\n            return [];\n        }\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8 + (Math.floor((this.filesUpdated / this.filesToUpdate) * 100) - 8),\n                operations: [this.filesUpdated, this.filesToUpdate]\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 95,\n                message: {\n                    translation: 'core.server.creatingNewRemoteFilesTree'\n                }\n            }\n        });\n\n        let logPath = path.join(this.deployment.appDir, 'github-tree.txt');\n        fs.writeFileSync(logPath, JSON.stringify(tree, null, 4));\n\n        return this.apiRequest(\n            {\n                owner: this.user,\n                repo: this.repository,\n                tree: tree\n            },\n            (api) => api.rest.git.createTree,\n            (result) => result.data.sha\n        );\n    }\n\n    createCommit(tree, parentSHA) {\n        if(!tree.length) {\n            return '';\n        }\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 95,\n                message: {\n                    translation: 'core.server.creatingNewRemoteFilesTree'\n                }\n            }\n        });\n\n        return this.apiRequest(\n            {\n                owner: this.user,\n                repo: this.repository,\n                message: 'Updated from Publii',\n                tree: tree,\n                parents: [parentSHA]\n            },\n            (api) => api.rest.git.createCommit,\n            (result) => result.data.sha\n        );\n    }\n\n    createReference(sha) {\n        if(sha === '') {\n            return false;\n        }\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 99,\n                message: {\n                    translation: 'core.server.finishingDeploymentProcess'\n                }\n            }\n        });\n\n        return this.apiRequest(\n            {\n                owner: this.user,\n                repo: this.repository,\n                sha: sha,\n                ref: this.branch\n            },\n            (api) => api.rest.git.updateRef\n        );\n    }\n\n    isBinaryFile(fullPath) {\n        let extension = path.parse(fullPath).ext;\n        let nonBinaryExtensions = [\n            '.html',\n            '.htm',\n            '.xml',\n            '.json',\n            '.css',\n            '.js',\n            '.map',\n            '.svg'\n        ];\n\n        if(nonBinaryExtensions.indexOf(extension) > -1) {\n            return false;\n        }\n\n        return true;\n    }\n\n    isNecessaryFile(filePath) {\n        let filename = path.parse(filePath).base;\n        let unnecessaryFiles = [\n            '.DS_Store',\n            'thumbs.db'\n        ];\n\n        if(unnecessaryFiles.indexOf(filename) > -1) {\n            return false;\n        }\n\n        return true;\n    }\n}\n\nmodule.exports = GithubPages;\n"
  },
  {
    "path": "app/back-end/modules/deploy/gitlab-pages.js",
    "content": "/*\n * Class used to upload files to the Github Pages\n */\n\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst { Gitlab } = require('@gitbeaker/node');\nconst stripTags = require('striptags');\n\nclass GitlabPages {\n    constructor (deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.connection = false;\n        this.repository = '';\n        this.user = '';\n        this.branch = '';\n        this.projectID = '';\n        this.filesToUpdate = 0;\n        this.filesUpdated = 0;\n        this.waitForTimeout = false;\n        this.uploadedBlobs = {};\n        this.remoteFilesList = [];\n        this.filesToRemove = [];\n        this.filesToUpdate = [];\n        this.filesToUpload = [];\n        this.binaryFilesToUpdate = [];\n        this.binaryFilesToUpload = [];\n        this.binaryProgressOffset = 0;\n        this.binaryFilesUploadedCount = 0;\n        this.binaryFilesToUploadCount = 0;\n        this.currentUploadProgress = 0;\n    }\n\n    async testConnection (app, deploymentConfig, siteName, uuid) {\n        let repository = deploymentConfig.gitlab.repo;\n        let branchName = deploymentConfig.gitlab.branch;\n        let token = deploymentConfig.gitlab.token;\n        let account = slug(siteName);\n        this.waitForTimeout = true;\n\n        if (uuid) {\n            account = uuid;\n        }\n\n        if (token === 'publii-gl-token ' + account) {\n            token = await passwordSafeStorage.getPassword('publii-gl-token', account);\n        }\n\n        this.client = new Gitlab({\n            host: deploymentConfig.gitlab.server,\n            token: token,\n            rejectUnauthorized: deploymentConfig.rejectUnauthorized\n        });\n\n        this.client.Projects.all({\n            owned: true,\n            maxPages: 1,\n            perPage: 1\n        }).then(project => {\n            this.client.Projects.all({\n                search: repository,\n                owned: true,\n                maxPages: 1,\n                perPage: 1\n            }).then(projects => {\n                let projectID = projects[0].id;\n\n                // Detect a case when repository name is only similar to the provided repository name (not equal)\n                if (projects[0].name !== repository && projects[0].path !== repository) {\n                    this.waitForTimeout = false;\n                    app.mainWindow.webContents.send('app-deploy-test-error', {\n                        message: {\n                            translation: 'core.server.repositoryDoesNotExist'\n                        }\n                    });\n\n                    return;\n                }\n\n                if(!projectID) {\n                    this.waitForTimeout = false;\n                    app.mainWindow.webContents.send('app-deploy-test-error', {\n                        message: {\n                            translation: 'core.server.repositoryDoesNotExist'\n                        }\n                    });\n\n                    return;\n                }\n\n                this.client.Branches.show(projectID, branchName).then(branch => {\n                    if(!branch) {\n                        this.waitForTimeout = false;\n                        app.mainWindow.webContents.send('app-deploy-test-error', {\n                            message: {\n                                translation: 'core.server.branchDoesNotExist'\n                            }\n                        });\n\n                        return;\n                    }\n\n                    this.waitForTimeout = false;\n                    app.mainWindow.webContents.send('app-deploy-test-success');\n                }).catch(err => {\n                    this.waitForTimeout = false;\n                    app.mainWindow.webContents.send('app-deploy-test-error', {\n                        message: {\n                            translation: 'core.server.branchDoesNotExist'\n                        }\n                    });\n                });\n            }).catch(err => {\n                this.waitForTimeout = false;\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: {\n                        translation: 'core.server.repositoryDoesNotExist'\n                    }\n                });\n            });\n        }).catch(err => {\n            this.waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-error', {\n                message: {\n                    translation: 'core.server.tokenOrServerAddressInvalid'\n                }\n            });\n        });\n\n        setTimeout(function() {\n            if(this.waitForTimeout === true) {\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: {\n                        translation: 'core.server.requestTimeout'\n                    }\n                });\n\n                this.waitForTimeout = false;\n            }\n        }, 10000);\n    }\n\n    async initConnection () {\n        this.repository = this.deployment.siteConfig.deployment.gitlab.repo;\n        this.user = this.deployment.siteConfig.deployment.gitlab.user;\n        this.branch = this.deployment.siteConfig.deployment.gitlab.branch;\n        this.remoteFilesList = [];\n        this.filesToRemove = [];\n        this.filesToUpdate = [];\n        this.filesToUpload = [];\n        this.waitForTimeout = true;\n        let token = this.deployment.siteConfig.deployment.gitlab.token;\n        let account = slug(this.deployment.siteConfig.name);\n\n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        if (token === 'publii-gl-token ' + account) {\n            token = await passwordSafeStorage.getPassword('publii-gl-token', account);\n        }\n\n        this.client = new Gitlab({\n            host: this.deployment.siteConfig.deployment.gitlab.server,\n            token: token,\n            rejectUnauthorized: this.deployment.siteConfig.deployment.rejectUnauthorized\n        });\n\n        this.setUploadProgress(6);\n        console.log(`[${ new Date().toUTCString() }] (!) CLIENT CREATED`);\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        this.deployment.setInput();\n        this.deployment.setOutput(true);\n        this.deployment.prepareLocalFilesList();\n        this.setUploadProgress(7);\n        this.downloadFilesList();\n    }\n\n    downloadFilesList () {\n        this.client.Projects.all({\n            search: this.repository,\n            owned: true,\n            maxPages: 1,\n            perPage: 1\n        }).then(projects => {\n            this.projectID = projects[0].id;\n\n            this.client.RepositoryFiles.showRaw(this.projectID, 'publii-files.json', this.branch).then(response => {\n                let remoteListToCheck = '';\n\n                if (typeof response === 'Buffer') {\n                    remoteListToCheck = response.toString();\n                } else {\n                    remoteListToCheck = response;\n                }\n\n                try {\n                    if (remoteListToCheck.length) {\n                        this.remoteFilesList = JSON.parse(remoteListToCheck);\n                    } else {\n                        this.remoteFilesList = [];\n                    }\n                } catch (e) {\n                    this.remoteFilesList = [];\n                }\n\n                this.deployment.checkLocalListWithRemoteList(response);\n                console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILE DOWNLOADED`);\n            }).catch(err => {\n                console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILE NOT DOWNLOADED`);\n                console.log(`[${ new Date().toUTCString() }] (!) ERROR WHILE DOWNLOADING publii-files.json: ${err.error}`);\n                this.deployment.compareFilesList(false);\n            });\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] downloadFilesList: ${err.error}`);\n            console.warn(`[${ new Date().toUTCString() }] ${err}`);\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: stripTags((err.message).toString())\n                }\n            });\n        });\n    }\n\n    startSync () {\n        this.setUploadProgress(8);\n        this.removeFiles();\n    }\n\n    removeFiles () {\n        // Create a commit to remove all unnecessary files\n        if (this.deployment.filesToRemove.length) {\n            this.filesToRemove = [];\n            console.log(`[${ new Date().toUTCString() }] (!) FILES TO REMOVE DETECTED`);\n\n            for (let i = 0; i < this.deployment.filesToRemove.length; i++) {\n                let filePath = this.deployment.filesToRemove[i].path;\n\n                this.filesToRemove.push({\n                    'action': 'delete',\n                    'file_path': this.getPrefix(filePath) + filePath\n                });\n            }\n\n            return this.makeCommit(this.filesToRemove, this.updateTextFiles.bind(this), '[skip ci] Publii - remove files');\n        }\n\n        console.log(`[${ new Date().toUTCString() }] (!) NO FILES TO REMOVE DETECTED`);\n        return this.updateTextFiles();\n    }\n\n    updateTextFiles () {\n        this.setUploadProgress(10);\n\n        // Create a commit to update all non-binary files\n        if (this.deployment.filesToUpload.length) {\n            this.filesToUpdate = [];\n            let existingFilesList = this.remoteFilesList.map(file => file.path);\n\n            for (let i = 0; i < this.deployment.filesToUpload.length; i++) {\n                if (existingFilesList.indexOf(this.deployment.filesToUpload[i].path) === -1) {\n                    continue;\n                }\n\n                if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) {\n                    continue;\n                }\n\n                if (!this.isNecessaryFile(this.deployment.filesToUpload[i].path)) {\n                    continue;\n                }\n\n                let filePath = this.deployment.filesToUpload[i].path;\n                \n                this.filesToUpdate.push({\n                    'action': 'update',\n                    'file_path': this.getPrefix(filePath) + filePath,\n                    'encoding': 'base64',\n                    'content': this.readFile(path.join(this.deployment.inputDir, this.deployment.filesToUpload[i].path))\n                });\n            }\n\n            if (this.filesToUpdate.length) {\n                console.log(`[${ new Date().toUTCString() }] (!) TEXT FILES TO UPDATE DETECTED`);\n                return this.makeCommit(this.filesToUpdate, this.uploadTextFiles.bind(this), '[skip ci] Publii - update non-binary files');\n            }\n        }\n\n        console.log(`[${ new Date().toUTCString() }] (!) NO TEXT FILES TO UPDATE DETECTED`);\n        this.uploadTextFiles();\n    }\n\n    uploadTextFiles () {\n        this.setUploadProgress(12);\n\n        // Create a commit to upload all non-binary files\n        if (this.deployment.filesToUpload.length) {\n            this.filesToUpdate = [];\n            let existingFilesList = this.remoteFilesList.map(file => file.path);\n\n            for (let i = 0; i < this.deployment.filesToUpload.length; i++) {\n                if (existingFilesList.indexOf(this.deployment.filesToUpload[i].path) > -1) {\n                    continue;\n                }\n\n                if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) {\n                    continue;\n                }\n\n                if (!this.isNecessaryFile(this.deployment.filesToUpload[i].path)) {\n                    continue;\n                }\n\n                let filePath = this.deployment.filesToUpload[i].path;\n                \n                this.filesToUpdate.push({\n                    'action': 'create',\n                    'file_path': this.getPrefix(filePath) + filePath,\n                    'encoding': 'base64',\n                    'content': this.readFile(path.join(this.deployment.inputDir, this.deployment.filesToUpload[i].path))\n                });\n            }\n\n            if (this.filesToUpdate.length) {\n                console.log(`[${ new Date().toUTCString() }] (!) TEXT FILES TO UPLOAD DETECTED`);\n                return this.makeCommit(this.filesToUpdate, this.createBinaryFilesList.bind(this), '[skip ci] Publii - upload non-binary files');\n            }\n        }\n\n        console.log(`[${ new Date().toUTCString() }] (!) NO TEXT FILES TO UPLOAD DETECTED`);\n        this.createBinaryFilesList();\n    }\n\n    createBinaryFilesList () {\n        this.setUploadProgress(15);\n        this.binaryFilesToUpdate = [];\n        this.binaryFilesToUpload = [];\n\n        if (this.deployment.filesToUpload.length) {\n            let existingFilesList = this.remoteFilesList.map(file => file.path);\n\n            for (let i = 0; i < this.deployment.filesToUpload.length; i++) {\n                if (existingFilesList.indexOf(this.deployment.filesToUpload[i].path) > -1) {\n                    if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) {\n                        console.log(`[${ new Date().toUTCString() }] (!) BINARY FILE TO UPDATE DETECTED`);\n                        let filePath = this.deployment.filesToUpload[i].path;\n\n                        this.binaryFilesToUpdate.push({\n                            'action': 'update',\n                            'file_path': this.getPrefix(filePath) + filePath,\n                            'encoding': 'base64'\n                        });\n                    }\n                } else {\n                    if (this.isBinaryFile(this.deployment.filesToUpload[i].path)) {\n                        console.log(`[${ new Date().toUTCString() }] (!) BINARY FILE TO UPLOAD DETECTED`);\n                        let filePath = this.deployment.filesToUpload[i].path;\n\n                        this.binaryFilesToUpload.push({\n                            'action': 'create',\n                            'file_path': this.getPrefix(filePath) + filePath,\n                            'encoding': 'base64'\n                        });\n                    }\n                }\n            }\n        }\n\n        this.binaryProgressOffset = 82 / (this.binaryFilesToUpdate.length + this.binaryFilesToUpload.length);\n        this.binaryFilesUploadedCount = 0;\n        this.binaryFilesToUploadCount = this.binaryFilesToUpdate.length;\n        this.currentUploadProgress = 15;\n        this.updateBinaryFiles();\n    }\n\n    updateBinaryFiles () {\n        if (this.binaryFilesToUpdate.length) {\n            let commits = [];\n            let progress = this.currentUploadProgress;\n\n            for (let i = 1; i <= 10 && this.binaryFilesToUpdate.length; i++) {\n                progress = progress + this.binaryProgressOffset;\n                this.binaryFilesUploadedCount++;\n                let commit = this.binaryFilesToUpdate.shift();\n                let fixedPath = commit.file_path.indexOf('public/') === 0 ? commit.file_path.substr(6) : commit.file_path;\n                commit.content = this.readFile(path.join(this.deployment.inputDir, fixedPath));\n                commits.push(commit);\n            }\n\n            let operations = [this.binaryFilesUploadedCount, this.binaryFilesToUploadCount];\n            this.setUploadProgress(progress, operations);\n            console.log(`[${ new Date().toUTCString() }] (!) BINARY FILES UPDATED`);\n            this.makeCommit(commits, this.updateBinaryFiles.bind(this), '[skip ci] Publii - update ' + commits.length + ' files');\n            return;\n        }\n\n        this.binaryFilesUploaded = 0;\n        this.binaryFilesToUploadCount = this.binaryFilesToUpload.length;\n        this.uploadBinaryFiles();\n    }\n\n    uploadBinaryFiles () {\n        if (this.binaryFilesToUpload.length) {\n            let commits = [];\n            let progress = this.currentUploadProgress;\n\n            for (let i = 1; i <= 10 && this.binaryFilesToUpload.length; i++) {\n                progress = progress + this.binaryProgressOffset;\n                this.binaryFilesUploadedCount++;\n                let commit = this.binaryFilesToUpload.shift();\n                let fixedPath = commit.file_path.indexOf('public/') === 0 ? commit.file_path.substr(6) : commit.file_path;\n                commit.content = this.readFile(path.join(this.deployment.inputDir, fixedPath));\n                commits.push(commit);\n            }\n\n            let operations = [this.binaryFilesUploadedCount, this.binaryFilesToUploadCount];\n            this.setUploadProgress(progress, operations);\n            console.log(`[${ new Date().toUTCString() }] (!) BINARY FILES UPLOADED`);\n            this.makeCommit(commits, this.uploadBinaryFiles.bind(this), '[skip ci] Publii - upload ' + commits.length + ' files');\n            return;\n        }\n\n        this.updateFilesListFile();\n    }\n\n    updateFilesListFile () {\n        this.setUploadProgress(98);\n        let localFilesListPath = path.join(this.deployment.inputDir, 'files.publii.json');\n        let localFilesContent = FileHelper.readFileSync(localFilesListPath);\n        let actionType = 'create';\n        let commit = [];\n\n        if (this.remoteFilesList.length) {\n            console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILES SHOULD BE UPDATED`);\n            actionType = 'update';\n        }\n\n        commit.push({\n            'action': actionType,\n            'file_path': 'publii-files.json',\n            'encoding': 'base64',\n            'content': Buffer.from(localFilesContent).toString('base64')\n        });\n\n        console.log(`[${ new Date().toUTCString() }] (!) REMOTE FILES LIST UPDATED`);\n        return this.makeCommit(commit, this.finishSync.bind(this), 'Publii - upload remote files list');\n    }\n\n    finishSync () {\n        this.setUploadProgress(100);\n\n        process.send({\n            type: 'sender',\n            message: 'app-deploy-uploaded',\n            value: {\n                status: true\n            }\n        });\n    }\n\n    makeCommit (operations, nextOperationCallback, commitMessage = 'Publii - deployment') {\n        this.client.Commits.create(this.projectID, this.branch, commitMessage, operations).then(res => {\n            return nextOperationCallback();\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] (!) COMMIT ERROR: ${JSON.stringify(err)}`);\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: stripTags((err.message).toString())\n                }\n            });\n        });\n    }\n\n    setUploadProgress (progress, operations = false) {\n        this.currentUploadProgress = progress;\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: progress,\n                operations: operations\n            }\n        });\n    }\n\n    readFile (filePath) {\n        return Buffer.from(FileHelper.readFileSync(filePath)).toString('base64');\n    }\n\n    isBinaryFile (fullPath) {\n        let extension = path.parse(fullPath).ext;\n        let nonBinaryExtensions = [\n            '.html',\n            '.htm',\n            '.xml',\n            '.json',\n            '.css',\n            '.js',\n            '.map',\n            '.svg'\n        ];\n\n        if(nonBinaryExtensions.indexOf(extension) > -1) {\n            return false;\n        }\n\n        return true;\n    }\n\n    getPrefix (fileNameToBePrefixed) {\n        let prefix = 'public';\n\n        if (fileNameToBePrefixed[0] !== '/') {\n            prefix = 'public/';\n        }\n\n        return prefix;\n    }\n\n    isNecessaryFile (filePath) {\n        let filename = path.parse(filePath).base;\n\n        if(filename.substr(0,1) === '.') {\n            return false;\n        }\n\n        return true;\n    }\n}\n\nmodule.exports = GitlabPages;\n"
  },
  {
    "path": "app/back-end/modules/deploy/google-cloud.js",
    "content": "/*\n * Class used to upload files to the FTP(S) server\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst { Storage } = require('@google-cloud/storage');\nconst normalizePath = require('normalize-path');\nconst stripTags = require('striptags');\n\nclass GoogleCloud {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.connection = false;\n        this.debugOutput = [];\n        this.econnresetCounter = 0;\n        this.softUploadErrors = {};\n        this.hardUploadErrors = [];\n    }\n\n    async initConnection() {\n        let self = this;\n        let bucketName = this.deployment.siteConfig.deployment.google.bucket;\n        let keyFilePath = normalizePath(this.deployment.siteConfig.deployment.google.key);\n        this.prefix = this.deployment.siteConfig.deployment.google.prefix;\n\n        if(!fs.existsSync(keyFilePath)) {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error'\n            });\n\n            return;\n        }\n\n        let keyData = require(keyFilePath); \n\n        let gcs = new Storage({\n            projectId: keyData.project_id,\n            credentials: {\n                client_email: keyData.client_email,\n                private_key: keyData.private_key\n            }\n        });\n\n        this.connection = gcs.bucket(bucketName);\n        this.connection.setMetadata({\n            website: {\n                mainPageSuffix: \"index.html\",\n                notFoundPage: \"404.html\"\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 6,\n                operations: false\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-success'\n        });\n\n        self.deployment.setInput();\n        self.deployment.setOutput(true);\n        self.deployment.prepareLocalFilesList();\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 7,\n                operations: false\n            }\n        });\n\n        self.downloadFilesList();\n    }\n\n    downloadFilesList() {\n        let self = this;\n        let fileToDownload = normalizePath(path.join(this.deployment.outputDir, 'files.publii.json'));\n\n        if(typeof this.prefix === 'string' && this.prefix !== '') {\n            fileToDownload = normalizePath(path.join(this.deployment.outputDir, this.prefix, 'files.publii.json'));\n        }\n\n        this.connection.file(fileToDownload).download({\n            destination: path.join(self.deployment.configDir, 'temp-files-remote.json')\n        }, function(err) {\n            if (!err) {\n                let downloadedFilePath = path.join(self.deployment.configDir, 'temp-files-remote.json');\n                let downloadedFile = FileHelper.readFileSync(downloadedFilePath);\n                self.deployment.checkLocalListWithRemoteList(downloadedFile);\n            } else {\n                self.deployment.compareFilesList(false);\n            }\n        });\n    }\n\n    uploadNewFileList() {\n        let self = this;\n        let fileToUpload = normalizePath(path.join(this.deployment.inputDir, 'files.publii.json'));\n        let fileDestination = 'files.publii.json';\n\n        if(typeof this.prefix === 'string' && this.prefix !== '') {\n            fileDestination = normalizePath(path.join(this.prefix, 'files.publii.json'));\n        }\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 99,\n                operations: [self.deployment.currentOperationNumber ,self.deployment.operationsCounter]\n            }\n        });\n\n        this.connection.upload(fileToUpload, {\n            destination: fileDestination\n        }, function(err) {\n            console.log(`[${ new Date().toUTCString() }] -> files.publii.json`);\n\n            if (err) {\n                console.log(`[${ new Date().toUTCString() }] ${err}`);\n            }\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 100,\n                    operations: false\n                }\n            });\n\n            process.send({\n                type: 'sender',\n                message: 'app-deploy-uploaded',\n                value: {\n                    status: true\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n    }\n\n    uploadFile(input, output) {\n        let self = this;\n\n        if (typeof this.prefix === 'string' && this.prefix !== '') {\n            output = normalizePath(path.join(this.prefix, output));\n        }\n\n        if (output[0] === '/') {\n            output = output.substr(1);\n        }\n\n        this.connection.upload(input, {\n            destination: output,\n            public: true\n        }, function(err) {\n            if (err) {\n                console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${output}`);\n                console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n                setTimeout(() => {\n                    if(!self.softUploadErrors[input]) {\n                        self.softUploadErrors[input] = 1;\n                    } else {\n                        self.softUploadErrors[input]++;\n                    }\n\n                    if(self.softUploadErrors[input] <= 5) {\n                        self.uploadFile(input, output);\n                    } else {\n                        self.hardUploadErrors.push(input);\n\n                        self.deployment.currentOperationNumber++;\n                        console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${output}`);\n                        self.deployment.progressOfUploading += self.deployment.progressPerFile;\n\n                        process.send({\n                            type: 'web-contents',\n                            message: 'app-uploading-progress',\n                            value: {\n                                progress: 8 + Math.floor(self.deployment.progressOfUploading),\n                                operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                            }\n                        });\n\n                        self.deployment.uploadFile();\n                    }\n                }, 500);\n            } else {\n                self.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`);\n                self.deployment.progressOfUploading += self.deployment.progressPerFile;\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8 + Math.floor(self.deployment.progressOfUploading),\n                        operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                    }\n                });\n\n                self.deployment.uploadFile();\n            }\n        });\n    }\n\n    uploadDirectory(input, output) {\n        this.deployment.uploadFile();\n    }\n\n    removeFile(input) {\n        let self = this;\n\n        if(typeof this.prefix === 'string' && this.prefix !== '') {\n            input = normalizePath(path.join(this.prefix, input));\n        }\n\n        if(input[0] === '/') {\n            input = input.substr(1);\n        }\n\n        this.connection.file(input).delete(function (err) {\n            self.deployment.currentOperationNumber++;\n            console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n\n            if (err) {\n                console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`);\n                console.log(`[${ new Date().toUTCString() }] ${err}`);\n            }\n\n            self.deployment.progressOfDeleting += self.deployment.progressPerFile;\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 8 + Math.floor(self.deployment.progressOfDeleting),\n                    operations: [self.deployment.currentOperationNumber ,self.deployment.operationsCounter]\n                }\n            });\n\n            self.deployment.removeFile();\n        });\n    }\n\n    removeDirectory(input) {\n        this.deployment.removeFile();\n    }\n\n    async testConnection(app, deploymentConfig, siteName) {\n        let bucketName = deploymentConfig.google.bucket;\n        let keyFilePath = normalizePath(deploymentConfig.google.key);\n        let waitForTimeout = true;\n\n        if(!fs.existsSync(keyFilePath)) {\n            waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-error');\n            return;\n        }\n\n        let keyData = require(keyFilePath); \n\n        let gcs = new Storage({\n            projectId: keyData.project_id,\n            credentials: {\n                client_email: keyData.client_email,\n                private_key: keyData.private_key\n            }\n        });\n\n        let bucket = gcs.bucket(bucketName);\n\n        bucket.getMetadata().then(data => {\n            waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-success');\n        }).catch(err => {\n            waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-error', {\n                message: stripTags((err.message).toString())\n            });\n        });\n\n        setTimeout(function() {\n            if(waitForTimeout === true) {\n                app.mainWindow.webContents.send('app-deploy-test-error');\n            }\n        }, 15000);\n    }\n}\n\nmodule.exports = GoogleCloud;\n"
  },
  {
    "path": "app/back-end/modules/deploy/libraries/netlify-api.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst util = require('util');\nconst crypto = require('crypto');\nconst normalizePath = require('normalize-path');\nconst asyncReadFile = util.promisify(fs.readFile);\n\nclass NetlifyAPI {\n    constructor (settings, events = {}) {\n        this.userAgent = 'Publii';\n        this.apiUrl = 'https://api.netlify.com/api/v1/';\n        this.accessToken = settings.accessToken || '';\n        this.siteID = settings.siteID || '';\n        this.inputDir = settings.inputDir || '';\n        this.events = {\n            onStart: events.onStart || this.noop,\n            onProgress: events.onProgress || this.noop,\n            onError: events.onError || this.noop,\n            onFinish: events.onFinish || this.noop,\n        }\n    }\n\n    async deploy () {\n        let localFilesList = await this.prepareLocalFilesList();\n        let deployData = await this.makeApiRequest('POST', 'sites/:site_id/deploys', localFilesList);\n        let deployID = deployData.id;\n        let hashesOfFilesToUpload = deployData.required;\n        let filesToUpload = this.getFilesToUpload(localFilesList, hashesOfFilesToUpload);\n        this.events.onStart(filesToUpload.length);\n        \n        for (let i = 0; i < filesToUpload.length; i++) {\n            let filePath = filesToUpload[i];\n            \n            try {\n                let apiResponse = await this.uploadFile(filePath, deployID);\n\n                if (!apiResponse) {\n                    return Promise.reject(apiResponse);\n                }\n            } catch (e) {\n                try {\n                    let apiResponse = await this.uploadFile(filePath, deployID);\n\n                    if (!apiResponse) {\n                        return Promise.reject(apiResponse);\n                    }\n                } catch (e) {\n                    return Promise.reject(false);\n                }\n            }\n\n            this.events.onProgress(i);\n        }\n\n        this.events.onFinish();\n        return Promise.resolve(true);\n    }\n\n    async prepareLocalFilesList () {\n        let tempFileList = this.readDirRecursiveSync(this.inputDir);\n        let fileList = {};\n\n        for(let filePath of tempFileList) {\n            // Skip directories\n            if(fs.lstatSync(path.join(this.inputDir, filePath)).isDirectory()) {\n                continue;\n            }\n\n            let fileHash = await this.getFileHash(path.join(this.inputDir, filePath));\n            let fileKey = ('/' + filePath).replace(/\\/\\//gmi, '/');\n            fileList[fileKey] = fileHash;\n        }\n\n        // Save the files list\n        return Promise.resolve({ files: fileList });\n    }\n\n    async makeApiRequest (method, endpoint, data) {\n        let endpointUrl = this.apiUrl + endpoint.replace(':site_id', this.siteID);\n        let headers = new Headers({\n            'User-Agent': 'Publii',\n            'Authorization': `Bearer ${this.accessToken}`,\n            'Content-Type': 'application/json'\n        });\n    \n        let options = {\n            method: method,\n            headers: headers,\n            body: JSON.stringify(data),\n        };\n    \n        if (method.toUpperCase() === 'GET') {\n            delete options.body;\n        }\n\n        let fetchTimeoutPromise = new Promise((resolve, reject) => {\n            let timeoutId = setTimeout(() => {\n                clearTimeout(timeoutId);\n                reject(new Error('Fetch request timeout'));\n            }, 15000);\n        });\n\n        try {\n            let response = await Promise.race([fetch(endpointUrl, options), fetchTimeoutPromise]);\n\n            if (response.ok) {\n                return await response.json();\n            } else {\n                let error = new Error(`Fetch HTTP Error: ${response.status}`);\n                error.response = response;\n                throw error;\n            }\n        } catch (error) {\n            console.log(`Request failed (URL: ${endpointUrl}): ${error.message}`);\n        }\n    }\n\n    async uploadFile (filePath, deployID) {\n        let endpointUrl = this.apiUrl + 'deploys/' + deployID + '/files' + filePath;\n        let fullFilePath = this.getFilePath(this.inputDir, filePath, true);\n        let fileContent = await asyncReadFile(fullFilePath);\n        let headers = new Headers({\n            'User-Agent': 'Publii',\n            'Authorization': `Bearer ${this.accessToken}`,\n            'Content-Type': 'application/octet-stream',\n            'Content-Length': fileContent.length\n        });\n    \n        let options = {\n            method: 'PUT',\n            headers: headers,\n            body: fileContent,\n        };\n\n        let fetchTimeoutPromise = new Promise((resolve, reject) => {\n            let timeoutId = setTimeout(() => {\n                clearTimeout(timeoutId);\n                reject(new Error('Fetch request timeout'));\n            }, 15000);\n        });\n\n        try {\n            let response = await Promise.race([fetch(endpointUrl, options), fetchTimeoutPromise]);\n\n            if (response.ok) {\n                return await response.json();\n            } else {\n                let error = new Error(`Fetch HTTP Error: ${response.status}`);\n                error.response = response;\n                throw error;\n            }\n        } catch (error) {\n            console.log(`Request failed (URL: ${endpointUrl}): ${error.message}`);\n        }\n\n        return false;\n    }\n\n    getFilesToUpload (filesList, hashesToUpload) {\n        let filePaths = Object.keys(filesList.files);\n        let filesToUpload = [];\n        let foundedHashes = [];\n\n        for (let i = 0; i < filePaths.length; i++) {\n            let filePath = filePaths[i];\n            \n            if (hashesToUpload.indexOf(filesList.files[filePath]) > -1) {\n                filesToUpload.push(filePath.replace(/\\/\\//gmi, '/'));\n                foundedHashes.push(filesList.files[filePath]);\n            }\n        }\n\n        return filesToUpload;\n    }\n\n    getFileHash (fileName) {\n        return new Promise((resolve, reject) => {\n            let shaSumCalculator = crypto.createHash('sha1');\n\n            try {\n                let fileStream = fs.ReadStream(fileName);\n                fileStream.on('data', fileContentChunk => shaSumCalculator.update(fileContentChunk));\n                fileStream.on('end', () => resolve(shaSumCalculator.digest('hex')));\n            } catch (error) {\n                return reject('');\n            }\n        });\n    }\n\n    readDirRecursiveSync(dir, fileList) {\n        let files = fs.readdirSync(dir);\n        fileList = fileList || [];\n\n        files.forEach(file => {\n            if (this.fileIsDirectory(dir, file)) {\n                fileList = this.readDirRecursiveSync(path.join(dir, file), fileList);\n                return;\n            } \n            \n            if (this.fileIsNotExcluded(file)) {\n                fileList.push(this.getFilePath(dir, file));\n            }\n        });\n\n        return fileList;\n    };\n\n    fileIsDirectory (dir, file) {\n        return fs.statSync(path.join(dir, file)).isDirectory();\n    }\n\n    fileIsNotExcluded (file) {\n        return file.indexOf('.') !== 0 || file === '.htaccess' || file === '.htpasswd' || file === '_redirects';\n    }\n\n    getFilePath (dir, file, includeInputDir = false) {\n        if (!includeInputDir) {\n            dir = dir.replace(this.inputDir, '')\n        }\n\n        return normalizePath( path.join( dir, file ) );\n    }\n    \n    noop () {\n        return false;\n    }\n\n    async testConnection () {\n        let testData = await this.makeApiRequest('GET', 'sites/:site_id/');\n\n        if (testData.body && testData.id) {\n            return Promise.resolve(true);\n        }\n\n        return Promise.reject(false);\n    }\n}\n\nmodule.exports = NetlifyAPI;\n"
  },
  {
    "path": "app/back-end/modules/deploy/manual.js",
    "content": "/*\n * Manual deployment class\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst slug = require('./../../helpers/slug');\nconst archiver = require('archiver');\nconst Utils = require('./../../helpers/utils');\n\nclass ManualDeployment {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n    }\n\n    async initConnection() {\n        this.deployment.setInput();\n        this.deployment.prepareLocalFilesList();\n        \n        switch(this.deployment.siteConfig.deployment.manual.output) {\n            case 'catalog': this.returnCatalog(); break;\n            case 'zip-archive': this.returnZipArchive(); break;\n            case 'tar-archive': this.returnTarArchive(); break;\n            default:\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n                break;\n        }\n    }\n\n    returnCatalog() {\n        let outputBaseDir = this.deployment.siteConfig.deployment.manual.outputDirectory;\n        let outputDirName = slug(this.deployment.siteName) + '-files'; \n\n        if (outputBaseDir && !Utils.dirExists(outputBaseDir)) {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: {\n                        translation: 'core.archive.destinationNotExists'\n                    }\n                }\n            });\n\n            return;\n        }\n\n        if (!outputBaseDir) {\n            outputBaseDir = path.join(this.deployment.sitesDir, this.deployment.siteName);\n        }\n\n        let outputPath = path.join(outputBaseDir, outputDirName);\n\n        if (outputPath !== '') {\n            if (Utils.dirExists(outputPath)) {\n                fs.emptyDirSync(outputPath);\n            }\n\n            fs.copy(this.deployment.inputDir, outputPath, {\n                filter: (src, dest) => {\n                    if (src.substr(-9) === '.DS_Store' || src.substr(-9) === 'Thumbs.db') {\n                        return false;\n                    }\n\n                    return true;\n                }\n            }).then(() => this.endDeployment('catalog', outputPath));\n            \n            return;\n        }\n\n        this.endDeployment('catalog', this.deployment.inputDir);\n    }\n\n    returnZipArchive() {\n        let self = this;\n        let backupFile = path.join(\n            this.deployment.sitesDir,\n            this.deployment.siteName,\n            slug(this.deployment.siteName) + '.zip'\n        );\n\n        if(this.deployment.siteConfig.deployment.manual.outputDirectory !== '') {\n            backupFile = path.join(this.deployment.siteConfig.deployment.manual.outputDirectory, slug(this.deployment.siteName) + '-files.zip');\n        }\n\n        let output = fs.createWriteStream(backupFile);\n        let archive = archiver('zip');\n\n        output.on('error', function (err) {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: {\n                        translation: 'core.archive.errorDuringCreatingZIP'\n                    }\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n\n        output.on('close', function () {\n            self.endDeployment('zip-archive', backupFile);\n        });\n\n        archive.on('error', function (err) {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: {\n                        translation: 'core.archive.errorDuringCreatingZIP'\n                    }\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n\n        archive.pipe(output);\n        archive.directory(this.deployment.inputDir, '/');\n        archive.finalize();\n    }\n\n    returnTarArchive() {\n        let self = this;\n        let backupFile = path.join(\n            this.deployment.sitesDir,\n            this.deployment.siteName,\n            slug(this.deployment.siteName) + '.tar'\n        );\n\n        if(this.deployment.siteConfig.deployment.manual.outputDirectory !== '') {\n            backupFile = path.join(this.deployment.siteConfig.deployment.manual.outputDirectory, slug(this.deployment.siteName) + '-files.tar');\n        }\n\n        let output = fs.createWriteStream(backupFile);\n        let archive = archiver('tar');\n\n        output.on('error', function (err) {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: {\n                        translation: 'core.archive.errorDuringCreatingTAR'\n                    }\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n\n        output.on('close', function () {\n            self.endDeployment('tar-archive', backupFile);\n        });\n\n        archive.on('error', function (err) {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: {\n                        translation: 'core.archive.errorDuringCreatingTAR'\n                    }\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n\n        archive.pipe(output);\n        archive.directory(this.deployment.inputDir, '/');\n        archive.finalize();\n    }\n\n    endDeployment(type, pathToOutput) {\n        process.send({\n            type: 'web-contents',\n            message: 'app-deploy-uploaded',\n            value: {\n                status: true,\n                type: type,\n                path: pathToOutput\n            }\n        });\n\n        setTimeout(function () {\n            process.kill(process.pid, 'SIGTERM');\n        }, 1000);\n    }\n}\n\nmodule.exports = ManualDeployment;\n"
  },
  {
    "path": "app/back-end/modules/deploy/netlify.js",
    "content": "/*\n * Class used to upload files to the Netlify\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst NetlifyAPI = require('./libraries/netlify-api');\nconst stripTags = require('striptags');\n\nclass Netlify {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.connection = false;\n        this.debugOutput = [];\n    }\n\n    async initConnection() {\n        let client;\n        let localDir;\n        let siteID = this.deployment.siteConfig.deployment.netlify.id;\n        let token = this.deployment.siteConfig.deployment.netlify.token;\n        let account = slug(this.deployment.siteConfig.name);\n\n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        if(siteID === 'publii-netlify-id ' + account) {\n            siteID = await passwordSafeStorage.getPassword('publii-netlify-id', account);\n        }\n\n        if(token === 'publii-netlify-token ' + account) {\n            token = await passwordSafeStorage.getPassword('publii-netlify-token', account);\n        }\n\n        this.deployment.setInput();\n        this.deployment.setOutput(true);\n        localDir = this.deployment.inputDir;\n\n        client = new NetlifyAPI({\n            accessToken: (token).toString().trim(),\n            siteID: (siteID).toString().trim(),\n            inputDir: localDir\n        }, {\n            onStart: this.onStart.bind(this),\n            onProgress: this.onProgress.bind(this),\n            onError: this.onError.bind(this)\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 6,\n                operations: false\n            }\n        });\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        let results = client.deploy();\n        results.then(res => {\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 100,\n                    operations: false\n                }\n            });\n\n            process.send({\n                type: 'sender',\n                message: 'app-deploy-uploaded',\n                value: {\n                    status: true\n                }\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] Netlify ERROR: ${err}`);\n            this.onError(err);\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n    }\n\n    onStart (totalFiles) {\n        this.deployment.operationsCounter = parseInt(totalFiles, 10);\n        this.deployment.progressPerFile = 90.0 / this.deployment.operationsCounter;\n        this.deployment.currentOperationNumber = 0;\n        this.deployment.progressOfUploading = 0;\n    }\n\n    onError (apiResponse = false) {\n        if (typeof apiResponse === 'boolean' || !apiResponse.body) {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error'\n            });\n        } else {\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error',\n                value: {\n                    additionalMessage: stripTags((JSON.parse(apiResponse.body).message).toString())\n                }\n            });\n        }\n\n        setTimeout(function () {\n            process.kill(process.pid, 'SIGTERM');\n        }, 1000);\n    }\n\n    onProgress(currentFile) {\n        if (currentFile < this.deployment.currentOperationNumber) {\n            return;\n        }\n\n        this.deployment.currentOperationNumber = currentFile;\n        this.deployment.progressOfUploading = this.deployment.currentOperationNumber * this.deployment.progressPerFile;\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 8 + Math.floor(this.deployment.progressOfUploading),\n                operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter]\n            }\n        });\n    }\n\n    async testConnection(app, deploymentConfig, siteName, uuid) {\n        let client;\n        let siteID = deploymentConfig.netlify.id;\n        let token = deploymentConfig.netlify.token;\n        let account = slug(siteName);\n        let waitForTimeout = true;\n\n        if (uuid) {\n            account = uuid;\n        }\n\n        if(siteID === 'publii-netlify-id ' + account) {\n            siteID = await passwordSafeStorage.getPassword('publii-netlify-id', account);\n        }\n\n        if(token === 'publii-netlify-token ' + account) {\n            token = await passwordSafeStorage.getPassword('publii-netlify-token', account);\n        }\n\n        client = new NetlifyAPI({\n            accessToken: (token).toString().trim(),\n            siteID: (siteID).toString().trim(),\n            inputDir: ''\n        });\n\n        try {\n            await client.testConnection();\n            waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-success');\n        } catch (err) {\n            waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-error', {\n                message: stripTags((err.message).toString())\n            });\n        }\n\n        setTimeout(function() {\n            if(waitForTimeout === true) {\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: {\n                        translation: 'core.server.requestTimeout'\n                    }\n                });\n            }\n        }, 10000);\n    }\n}\n\nmodule.exports = Netlify;\n"
  },
  {
    "path": "app/back-end/modules/deploy/s3.js",
    "content": "/*\n * Class used to upload files to the S3 bucket\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst { \n    S3Client, \n    ListObjectsCommand, \n    GetObjectCommand, \n    PutObjectCommand, \n    DeleteObjectCommand \n} = require(\"@aws-sdk/client-s3\");\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst mime = require('mime');\nconst stripTags = require('striptags');\n\nclass S3 {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.connection = false;\n        this.econnresetCounter = 0;\n        this.waitForTimeout = false;\n        this.softUploadErrors = {};\n        this.hardUploadErrors = [];\n    }\n\n    async initConnection() {\n        let s3Provider = this.deployment.siteConfig.deployment.s3.provider;\n        let s3Endpoint = this.deployment.siteConfig.deployment.s3.endpoint;\n        let s3Id = this.deployment.siteConfig.deployment.s3.id;\n        let s3Key = this.deployment.siteConfig.deployment.s3.key;\n        let region = this.deployment.siteConfig.deployment.s3.region;\n        let customRegion = this.deployment.siteConfig.deployment.s3.customRegion;\n        let account = slug(this.deployment.siteConfig.name);\n        this.bucket = this.deployment.siteConfig.deployment.s3.bucket;\n        this.prefix = this.deployment.siteConfig.deployment.s3.prefix;\n        this.waitForTimeout = true;\n\n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        if (s3Id === 'publii-s3-id ' + account) {\n            s3Id = await passwordSafeStorage.getPassword('publii-s3-id', account);\n        }\n\n        if (s3Key === 'publii-s3-key ' + account) {\n            s3Key = await passwordSafeStorage.getPassword('publii-s3-key', account);\n        }\n\n        if (s3Provider !== 'aws' && typeof s3Endpoint === 'string' && s3Endpoint.indexOf('://') === -1) {\n            s3Endpoint = 'https://' + s3Endpoint;\n        }\n\n        let connectionParams;\n\n        if (s3Provider === 'aws') {\n            connectionParams = {\n                credentials: {\n                    accessKeyId: s3Id,\n                    secretAccessKey: s3Key,\n                },\n                region: region\n            }\n        } else {\n            connectionParams = {\n                credentials: {\n                    accessKeyId: s3Id,\n                    secretAccessKey: s3Key,\n                },\n                endpoint: s3Endpoint,\n                region: customRegion\n            }\n        }\n\n        this.connection = new S3Client(connectionParams);\n        this.sendProgress(6, false);\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-connection-in-progress'\n        });\n\n        let params = {\n            Bucket: this.bucket,\n            Prefix: this.prefix,\n            MaxKeys: 1\n        };\n\n        try {\n            await this.connection.send(new ListObjectsCommand(params));\n            this.waitForTimeout = false;\n          \n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-success'\n            });\n          \n            this.deployment.setInput();\n            this.deployment.setOutput(true);\n            this.deployment.prepareLocalFilesList();\n            this.sendProgress(7, false);\n          \n            await this.downloadFilesList();\n        } catch (err) {\n            this.onError(err);\n        }\n\n        setTimeout(() => {\n            if(this.waitForTimeout === true) {\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error'\n                });\n\n                setTimeout(() => {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }\n        }, 20000);\n    }\n\n    async downloadFilesList() {\n        let fileName = 'files.publii.json';\n\n        if (typeof this.prefix === 'string' && this.prefix !== '') {\n            fileName = this.prefix + fileName;\n        }\n\n        let params = {\n            Bucket: this.bucket,\n            Key: fileName,\n        };\n\n        try {\n            let data = await this.connection.send(new GetObjectCommand(params));\n            console.log(`[${new Date().toUTCString()}] <- files.publii.json`);\n            this.sendProgress(8, false);\n            let remoteFile = await this.s3streamToString(data.Body);\n            this.deployment.checkLocalListWithRemoteList(remoteFile);\n          } catch (err) {\n            console.log(`[${new Date().toUTCString()}] <- files.publii.json`);\n        \n            if (err.name !== 'NoSuchKey') {\n                this.onError(err);\n                return;\n            }\n        \n            this.sendProgress(8, false);\n            this.deployment.compareFilesList(false);\n        }\n    }\n\n    async uploadNewFileList() {    \n        this.sendProgress(99);\n        let fileName = 'files.publii.json';\n        \n        if (typeof this.prefix === 'string' && this.prefix !== '') {\n            fileName = this.prefix + fileName;\n        }\n        \n        let filePath = path.join(this.deployment.inputDir, 'files.publii.json');\n    \n        fs.readFile(filePath, async (err, fileContent) => {\n            if (err) {\n                this.onError(err);\n                return;\n            }\n        \n            let fileACL = this.deployment.siteConfig.deployment.s3.acl || 'public-read';\n        \n            let params = {\n                ACL: fileACL,\n                Body: fileContent,\n                Bucket: this.bucket,\n                Key: fileName,\n                ContentType: mime.getType(fileName) || 'application/json'\n            };\n    \n            try {\n                await this.connection.send(new PutObjectCommand(params));\n                console.log(`[${new Date().toUTCString()}] -> ${fileName}`);\n                this.sendProgress(100, false);\n        \n                process.send({\n                    type: 'sender',\n                    message: 'app-deploy-uploaded',\n                    value: {\n                        status: true,\n                        issues: this.hardUploadErrors.length > 0\n                    }\n                });\n        \n                setTimeout(() => {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            } catch (uploadErr) {\n                console.log(`[${new Date().toUTCString()}] -> ${fileName}`);\n                this.onError(uploadErr);\n            }\n        });\n    }\n\n    /**\n     * Uploads file\n     */\n    async uploadFile() {\n        if (this.deployment.filesToUpload.length > 0) {\n            let fileToUpload = this.deployment.filesToUpload.pop();\n            fileToUpload.path = this.prepareFilePath(fileToUpload.path);\n\n            if (fileToUpload.type === 'file') {\n                await this.uploadFileObject(fileToUpload.path);\n            } else {\n                await this.uploadFile();\n            }\n        } else {\n            this.sendProgress(98);\n            await this.uploadNewFileList();\n        }\n    }\n\n    async uploadFileObject(input) {\n        let filePath = path.join(this.deployment.inputDir, input);\n        \n        fs.readFile(filePath, async (err, fileContent) => {\n            if (err) {\n                this.onError(err);\n                return;\n            }\n\n            let fileName = input;\n            \n            if (typeof this.prefix === 'string' && this.prefix !== '') {\n                fileName = this.prefix + fileName;\n            }\n\n            let fileACL = this.deployment.siteConfig.deployment.s3.acl || 'public-read';\n            let htmlCacheControl = this.deployment.siteConfig.deployment.s3.htmlCacheControl || 'no-cache, no-store';\n            let otherCacheControl = this.deployment.siteConfig.deployment.s3.otherCacheControl || 'public, max-age=2592000';\n            let fileExtension = path.extname(fileName).substring(1);\n            let cacheControl = fileExtension === 'html' ? htmlCacheControl : otherCacheControl;\n            let params = {\n                ACL: fileACL,\n                Body: fileContent,\n                Bucket: this.bucket,\n                Key: fileName,\n                CacheControl: cacheControl,\n                ContentType: mime.getType(fileExtension) || 'application/octet-stream'\n            };\n\n            try {\n                await this.connection.send(new PutObjectCommand(params));\n                this.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${fileName}`);\n                this.deployment.progressOfUploading += this.deployment.progressPerFile;\n                this.sendProgress(8 + Math.floor(this.deployment.progressOfUploading));\n                await this.uploadFile();\n            } catch (uploadErr) {\n                this.onError(uploadErr, true);\n\n                setTimeout(async () => {\n                    if (!this.softUploadErrors[input]) {\n                        this.softUploadErrors[input] = 1;\n                    } else {\n                        this.softUploadErrors[input]++;\n                    }\n\n                    if (this.softUploadErrors[input] <= 5) {\n                        await this.uploadFileObject(input);\n                    } else {\n                        this.hardUploadErrors.push(input);\n                        this.deployment.currentOperationNumber++;\n                        console.log(`[${ new Date().toUTCString() }] UPL HARD ERR ${input} -> ${fileName}`);\n                        this.deployment.progressOfUploading += this.deployment.progressPerFile;\n                        this.sendProgress(8 + Math.floor(this.deployment.progressOfUploading));\n                        await this.uploadFile();\n                    }\n                }, 500);\n            }\n        });\n    }\n\n    async removeFile() {\n        if (this.deployment.filesToRemove.length > 0) {\n            let fileToRemove = this.deployment.filesToRemove.pop();\n            fileToRemove.path = this.prepareFilePath(fileToRemove.path);\n\n            if(fileToRemove.type === 'file') {\n                await this.removeFileObject(fileToRemove.path);\n            } else {\n                await this.removeFile();\n            }\n        } else {\n            this.sendProgress(8 + Math.floor(this.deployment.progressOfUploading));\n            await this.uploadFile();\n        }\n    }\n\n    async removeFileObject(input) {\n        let params = {\n            Bucket: this.bucket,\n            Key: input\n        };\n    \n        try {\n            await this.connection.send(new DeleteObjectCommand(params));\n            this.deployment.currentOperationNumber++;\n            console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n            this.deployment.progressOfDeleting += this.deployment.progressPerFile;\n            this.sendProgress(8 + Math.floor(this.deployment.progressOfDeleting));\n            await this.removeFile();\n        } catch (err) {\n            // Handle case when specific file no longer exists in the bucket - don't block sync\n            if (err.name === 'NoSuchKey') {\n                this.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] DEL ${input} - NoSuchKey`);\n                this.deployment.progressOfDeleting += this.deployment.progressPerFile;\n                this.sendProgress(8 + Math.floor(this.deployment.progressOfDeleting));\n                await this.removeFile();\n                return;\n            }\n\n            console.error(`[${new Date().toUTCString()}] Error deleting ${input}`, err);\n            this.onError(err, true);\n        }\n    }\n\n    onError(err, silentMode = false) {\n        console.log(`[${ new Date().toUTCString() }] S3 ERROR: ${err.message}`);\n\n        if(this.waitForTimeout && !silentMode) {\n            this.waitForTimeout = false;\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error'\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        }\n    }\n\n    prepareFilePath(filePath) {\n        if (filePath[0] && filePath[0] === '/') {\n            filePath = filePath.substr(1);\n        }\n\n        return filePath;\n    }\n\n    async testConnection(app, deploymentConfig, siteName, uuid) {\n        let s3Provider = deploymentConfig.s3.provider;\n        let s3Endpoint = deploymentConfig.s3.endpoint;\n        let s3Id = deploymentConfig.s3.id;\n        let s3Key = deploymentConfig.s3.key;\n        let bucket = deploymentConfig.s3.bucket;\n        let prefix = deploymentConfig.s3.prefix;\n        let region = deploymentConfig.s3.region;\n        let customRegion = deploymentConfig.s3.customRegion;\n        let account = slug(siteName);\n        let waitForTimeout = true;\n\n        if (uuid) {\n            account = uuid;\n        }\n\n        if (s3Id === 'publii-s3-id ' + account) {\n            s3Id = await passwordSafeStorage.getPassword('publii-s3-id', account);\n        }\n\n        if (s3Key === 'publii-s3-key ' + account) {\n            s3Key = await passwordSafeStorage.getPassword('publii-s3-key', account);\n        }\n\n        let connectionParams;\n\n        if (s3Provider === 'aws') {\n            connectionParams = {\n                credentials: {\n                    accessKeyId: s3Id,\n                    secretAccessKey: s3Key,\n                },\n                region: region\n            }\n        } else {\n            connectionParams = {\n                credentials: {\n                    accessKeyId: s3Id,\n                    secretAccessKey: s3Key,\n                },\n                endpoint: s3Endpoint,\n                region: customRegion\n            }\n        }\n\n        this.connection = new S3Client(connectionParams);\n\n        let testParams = {\n            Bucket: bucket,\n            Prefix: prefix,\n            MaxKeys: 1\n        };\n\n        try {\n            await this.connection.send(new ListObjectsCommand(testParams));\n        } catch (err) {\n            waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-error', {\n                message: stripTags((err.message).toString())\n            });\n\n            return;\n        }\n\n        waitForTimeout = false;\n        app.mainWindow.webContents.send('app-deploy-test-success');\n\n        setTimeout(function() {\n            if (waitForTimeout === true) {\n                app.mainWindow.webContents.send('app-deploy-test-error', {\n                    message: {\n                        translation: 'core.server.requestTimeout'\n                    }\n                });\n            }\n        }, 10000);\n    }\n\n    sendProgress (progress, showOperations = true) {\n        let operations = [this.deployment.currentOperationNumber, this.deployment.operationsCounter];\n\n        if (!showOperations) {\n            operations = false;\n        }\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress,\n                operations\n            }\n        });\n    }\n\n    async s3streamToString (stream) {\n        let chunks = [];\n        \n        for await (let chunk of stream) {\n            chunks.push(chunk);\n        }\n\n        return Buffer.concat(chunks).toString('utf-8');\n    }\n}\n\nmodule.exports = S3;\n"
  },
  {
    "path": "app/back-end/modules/deploy/sftp.js",
    "content": "/*\n * Class used to upload files to the SFTP server\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst sftpClient = require('ssh2-sftp-client');\nconst passwordSafeStorage = require('keytar');\nconst slug = require('./../../helpers/slug');\nconst normalizePath = require('normalize-path');\n\nclass SFTP {\n    constructor(deploymentInstance = false) {\n        this.deployment = deploymentInstance;\n        this.connection = false;\n    }\n\n    async initConnection() {\n        let waitForTimeout = true;\n        let ftpPassword = this.deployment.siteConfig.deployment.password;\n        let passphrase = this.deployment.siteConfig.deployment.passphrase;\n        let account = slug(this.deployment.siteConfig.name);\n        this.connection = new sftpClient();\n\n        if (this.deployment.siteConfig.uuid) {\n            account = this.deployment.siteConfig.uuid;\n        }\n\n        if(ftpPassword === 'publii ' + account) {\n            ftpPassword = await passwordSafeStorage.getPassword('publii', account);\n        }\n\n        if(passphrase === 'publii-passphrase ' + account) {\n            passphrase = await passwordSafeStorage.getPassword('publii-passphrase', account);\n        }\n\n        let connectionSettings = {\n            host: this.deployment.siteConfig.deployment.server,\n            port: this.deployment.siteConfig.deployment.port,\n            username: this.deployment.siteConfig.deployment.username\n        };\n\n        if(this.deployment.siteConfig.deployment.protocol === 'sftp') {\n            connectionSettings.password = ftpPassword;\n        } else {\n            let keyPath = this.deployment.siteConfig.deployment.sftpkey;\n\n            if(passphrase !== '') {\n                connectionSettings.passphrase = passphrase;\n            }\n\n            connectionSettings.privateKey = FileHelper.readFileSync(keyPath);\n        }\n\n        this.connection.connect(connectionSettings).then(() => {\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 6,\n                    operations: false\n                }\n            });\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-in-progress'\n            });\n\n            waitForTimeout = false;\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-success'\n            });\n\n            this.deployment.setInput();\n            this.deployment.setOutput();\n            this.deployment.prepareLocalFilesList();\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 7,\n                    operations: false\n                }\n            });\n\n            this.downloadFilesList();\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] ERR (1): ${err}`);\n            this.connection.end();\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-connection-error'\n            });\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n\n        setTimeout(() => {\n            if(waitForTimeout === true) {\n                this.connection.end();\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-connection-error'\n                });\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }\n        }, 20000);\n    }\n\n    downloadFilesList() {\n        this.connection.get(\n            normalizePath(path.join(this.deployment.outputDir, 'files.publii.json'))\n        ).then((stream) => {\n            console.log(`[${ new Date().toUTCString() }] <- files.publii.json`);\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 8,\n                    operations: false\n                }\n            });\n\n            this.deployment.checkLocalListWithRemoteList(stream);\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] ERR (2): ${err} (${err.stack}) [<- files.publii.json]`);\n            \n            try {\n                this.deployment.compareFilesList(false);\n            } catch (err) {\n                console.log(`[${ new Date().toUTCString() }] ERR (3): ${err} (${err.stack}) [<- files.publii.json]`);\n            }\n        });\n    }\n\n    uploadNewFileList() {\n        let self = this;\n\n        process.send({\n            type: 'web-contents',\n            message: 'app-uploading-progress',\n            value: {\n                progress: 99,\n                operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n            }\n        });\n\n        this.connection.put(\n            normalizePath(path.join(self.deployment.inputDir, 'files.publii.json')),\n            normalizePath(path.join(self.deployment.outputDir, 'files.publii.json')),\n        ).then(() => {\n            this.connection.chmod(normalizePath(path.join(this.deployment.outputDir, 'files.publii.json')), 0o644).then(() => {\n                console.log(`[${ new Date().toUTCString() }] -> files.publii.json`);\n                this.connection.end();\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 100,\n                        operations: false\n                    }\n                });\n\n                process.send({\n                    type: 'sender',\n                    message: 'app-deploy-uploaded',\n                    value: {\n                        status: true\n                    }\n                });\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            }).catch(err => {\n                this.connection.end();\n                console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n                setTimeout(function () {\n                    process.kill(process.pid, 'SIGTERM');\n                }, 1000);\n            });\n        }).catch(err => {\n            this.connection.end();\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n            setTimeout(function () {\n                process.kill(process.pid, 'SIGTERM');\n            }, 1000);\n        });\n    }\n\n    uploadFile(input, output) {\n        this.connection.put(input, output).then(() => {\n            this.connection.chmod(output, 0o644).then(() => {\n                this.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`);\n                this.deployment.progressOfUploading += this.deployment.progressPerFile;\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8 + Math.floor(this.deployment.progressOfUploading),\n                        operations: [\n                            this.deployment.currentOperationNumber, \n                            this.deployment.operationsCounter\n                        ]\n                    }\n                });\n\n                this.deployment.uploadFile();\n            }).catch(err => {\n                console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${normalizePath(input)}`);\n                console.log(`[${ new Date().toUTCString() }] ${err}`);\n                this.deployment.uploadFile();\n            });\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD FILE: ${normalizePath(input)}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n            this.deployment.uploadFile();\n        });\n    }\n\n    uploadDirectory(input, output) {\n        this.connection.mkdir(output, true).then(() => {\n            this.connection.chmod(output, 0o755).then(() => {\n                this.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] UPL ${input} -> ${output}`);\n                this.deployment.progressOfUploading += this.deployment.progressPerFile;\n\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8 + Math.floor(this.deployment.progressOfUploading),\n                        operations: [this.deployment.currentOperationNumber, this.deployment.operationsCounter]\n                    }\n                });\n\n                this.deployment.uploadFile();\n            }).catch(err => {\n                this.deployment.currentOperationNumber++;\n                console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`);\n                console.log(`[${ new Date().toUTCString() }] ${err}`);\n    \n                this.deployment.progressOfUploading += this.deployment.progressPerFile;\n                process.send({\n                    type: 'web-contents',\n                    message: 'app-uploading-progress',\n                    value: {\n                        progress: 8 + Math.floor(this.deployment.progressOfUploading),\n                        operations: [\n                            this.deployment.currentOperationNumber, \n                            this.deployment.operationsCounter\n                        ]\n                    }\n                });\n    \n                this.deployment.uploadFile();\n            });\n        }).catch(err => {\n            this.deployment.currentOperationNumber++;\n            console.log(`[${ new Date().toUTCString() }] ERROR UPLOAD DIR: ${output}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n            this.deployment.progressOfUploading += this.deployment.progressPerFile;\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 8 + Math.floor(this.deployment.progressOfUploading),\n                    operations: [\n                        this.deployment.currentOperationNumber, \n                        this.deployment.operationsCounter\n                    ]\n                }\n            });\n\n            this.deployment.uploadFile();\n        });\n    }\n\n    removeFile(input) {\n        let self = this;\n\n        this.connection.delete(input).then(function (result) {\n            self.deployment.currentOperationNumber++;\n            console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n            self.deployment.progressOfDeleting += self.deployment.progressPerFile;\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 8 + Math.floor(self.deployment.progressOfDeleting),\n                    operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                }\n            });\n\n            self.deployment.removeFile();\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] ERROR REMOVE FILE: ${input}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n            self.deployment.removeFile();\n        });\n    }\n\n    removeDirectory(input) {\n        let self = this;\n\n        this.connection.rmdir(input, true).then(function (result) {\n            self.deployment.currentOperationNumber++;\n            console.log(`[${ new Date().toUTCString() }] DEL ${input}`);\n            self.deployment.progressOfDeleting += self.deployment.progressPerFile;\n\n            process.send({\n                type: 'web-contents',\n                message: 'app-uploading-progress',\n                value: {\n                    progress: 8 + Math.floor(self.deployment.progressOfDeleting),\n                    operations: [self.deployment.currentOperationNumber, self.deployment.operationsCounter]\n                }\n            });\n\n            self.deployment.removeFile();\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] ERROR REMOVE DIR ${input}`);\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n            self.deployment.removeFile();\n        });\n    }\n\n    async testConnection(app, deploymentConfig, siteName, siteConfig) {\n        let client = new sftpClient();\n        let waitForTimeout = true;\n        let ftpPassword = deploymentConfig.password;\n        let passphrase = deploymentConfig.passphrase;\n        let account = slug(siteName);\n\n        if (siteConfig.uuid) {\n            account = siteConfig.uuid;\n        }\n\n        if(ftpPassword === 'publii ' + account) {\n            ftpPassword = await passwordSafeStorage.getPassword('publii', account);\n        }\n\n        if(passphrase === 'publii-passphrase ' + account) {\n            passphrase = await passwordSafeStorage.getPassword('publii-passphrase', account);\n        }\n\n        let connectionSettings = {\n            host: deploymentConfig.server,\n            port: deploymentConfig.port,\n            username: deploymentConfig.username\n        };\n\n        if(deploymentConfig.protocol === 'sftp') {\n            connectionSettings.password = ftpPassword;\n        } else {\n            let keyPath = deploymentConfig.sftpkey;\n\n            if(passphrase !== '') {\n                connectionSettings.passphrase = passphrase;\n            }\n\n            connectionSettings.privateKey = FileHelper.readFileSync(keyPath);\n        }\n\n        let testFilePath = normalizePath(path.join(app.sitesDir, siteName, 'input', 'publii.test'));\n\n        client.connect(connectionSettings).then(() => {\n            return client.list('/');\n        }).then(data => {\n            fs.writeFileSync(testFilePath, 'It is a test file. You can remove it.');\n\n            client.put(\n                testFilePath,\n                normalizePath(path.join(deploymentConfig.path, 'publii.test'))\n            ).then(() => { \n                client.chmod(normalizePath(path.join(deploymentConfig.path, 'publii.test')), 0o644).then(() => {\n                    client.delete(\n                        normalizePath(path.join(deploymentConfig.path, 'publii.test'))\n                    ).then(() => {\n                        app.mainWindow.webContents.send('app-deploy-test-success');\n                        \n                        if (fs.existsSync(testFilePath)) {\n                            fs.unlinkSync(testFilePath);\n                        }\n    \n                        client.end().catch(err => console.log('SFTP session end error'));\n                    }).catch(() => {\n                        app.mainWindow.webContents.send('app-deploy-test-write-error');\n                        \n                        if (fs.existsSync(testFilePath)) {\n                            fs.unlinkSync(testFilePath);\n                        }\n    \n                        client.end().catch(err => console.log('SFTP session end error'));\n                    });\n                }).catch(err => {\n                    app.mainWindow.webContents.send('app-deploy-test-write-error');\n                   \n                    if (fs.existsSync(testFilePath)) {\n                        fs.unlinkSync(testFilePath);\n                    }\n    \n                    client.end().catch(err => console.log('SFTP session end error'));\n                });\n            }).catch(err => {\n                app.mainWindow.webContents.send('app-deploy-test-write-error');\n               \n                if (fs.existsSync(testFilePath)) {\n                    fs.unlinkSync(testFilePath);\n                }\n\n                client.end().catch(err => console.log('SFTP session end error'));\n            });\n\n            waitForTimeout = false;\n            app.mainWindow.webContents.send('app-deploy-test-success');\n        }).catch(err => {\n            console.log(`[${ new Date().toUTCString() }] ${err}`);\n\n            if(waitForTimeout) {\n                waitForTimeout = false;\n                client.end().catch(err => console.log('SFTP session end error'));\n                app.mainWindow.webContents.send('app-deploy-test-error');\n            }\n        });\n\n        setTimeout(function() {\n            if(waitForTimeout === true) {\n                client.end().catch(err => console.log('SFTP session end error'));\n                app.mainWindow.webContents.send('app-deploy-test-error');\n            }\n        }, 15000);\n    }\n}\n\nmodule.exports = SFTP;\n"
  },
  {
    "path": "app/back-end/modules/import/automatic-paragraphs.js",
    "content": "function automaticParagraphs (inputText) {\n    let rules = [\n        [/<br \\/>/gmi, '<br>'],\n        [/<object[\\s\\S]+?<\\/object>/g, matchingText => matchingText.replace(/[\\r\\n]+/g, '')],\n        [/<[^<>]+>/g, matchingText => matchingText.replace(/[\\r\\n]+/g, ' ')],\n        [/<(pre|script|style)[^>]*>[\\s\\S]+?<\\/\\1>/g, matchingText => matchingText.replace(/(\\r\\n|\\n)/g, '<restore-line-break>')],\n        [/(<(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)/gi, \"\\n$1\"],\n        [/(<\\/(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)>)/gi, \"$1\\n\\n\"],\n        [/([\\s\\S]+?)\\n\\n/g, \"<p>$1</p>\\n\"],\n        [/<p>\\s*(<\\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)\\s*<\\/p>/gi, '$1'],\n        [/<p>(<li.+?)<\\/p>/gi, '$1'],\n        [/<p>\\s*(<\\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)/gi, '$1'],\n        [/(<\\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)(?: [^>]*)?>)\\s*<\\/p>/gi, '$1'],\n        [/(<\\/?(?:address|aside|blockquote|caption|dd|div|dl|dt|figcaption|figure|h1|h2|h3|h4|h5|h6|header|hr|legend|li|ol|p|pre|section|table|tbody|td|tfoot|th|thead|tr|ul)[^>]*>)\\s*<br>/gi, '$1'],\n        [/<br>(\\s*<\\/?(?:dd|div|dl|dt|li|ol|p|pre|td|th|ul)>)/gi, '$1'],\n        [/(<(?:dd|div|th|td)[^>]*>)(.*?)<\\/p>/g, (match, offset, content) => (content.match(/<p( [^>]*)?>/)) ? match : offset + '<p>' + content + '</p>'],\n        [/<restore-line-break>/g, \"\\n\"]\n        [/<p><\\/p>/gi, '']\n    ];\n\n    inputText = inputText + \"\\n\\n\";\n\n    for (let i = 0; i < rules.length; i++) {\n        if (!rules[i]) {\n            continue;\n        }\n        \n        inputText = inputText.replace(rules[i][0], rules[i][1]);\n    }\n  \n    return inputText;\n}\n\nmodule.exports = automaticParagraphs;\n"
  },
  {
    "path": "app/back-end/modules/import/import.js",
    "content": "/*\n * Class used to import data from WP to Publii using WXR file\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst os = require('os');\nconst WxrParser = require('./wxr-parser');\nconst Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3');\nconst DBUtils = require('../../helpers/db.utils.js');\n\nclass Import {\n    /**\n     * Creates an instance\n     *\n     * @param appInstance\n     * @param siteName\n     * @param filePath\n     */\n    constructor(appInstance, siteName, filePath) {\n        this.appInstance = appInstance;\n        this.siteName = siteName;\n        this.filePath = filePath;\n        this.connectWithDB();\n\n        this.parser = new WxrParser(appInstance, siteName);\n        this.parser.loadFile(this.filePath);\n    }\n\n    /**\n     * Creates DB instance for the importer\n     */\n    connectWithDB() {\n        if(!this.appInstance) {\n            return;\n        }\n\n        const dbPath = path.join(this.appInstance.sitesDir, this.siteName, 'input', 'db.sqlite');\n\n        if (this.appInstance.db) {\n            try {\n                this.appInstance.db.close();\n            } catch (e) {\n                console.log('[WP IMPORT] DB already closed');\n            }\n        }\n\n        this.appInstance.db = new DBUtils(new Database(dbPath));\n    }\n\n    /**\n     * Checks the file\n     *\n     * @returns {*}\n     */\n    checkFile() {\n        if (this.parser.isWXR()) {\n            try {\n                let result = this.parser.getWxrStats();\n\n                if (result) {\n                    return {\n                        status: 'success',\n                        message: result\n                    };\n                }\n\n                return {\n                    status: 'error',\n                    message: 'An error occurred during parsing selected WXR file'\n                };\n            } catch (e) {\n                return {\n                    status: 'error',\n                    message: 'An error occurred during parsing selected WXR file'\n                };\n            }\n        }\n\n        return {\n            status: 'error',\n            message: 'Selected file is not a proper WXR file.'\n        };\n    }\n\n    /**\n     * Imports data from the given WXR file\n     *\n     * @param importAuthors\n     * @param usedTaxonomy\n     * @returns {{status: string, message: boolean}}\n     */\n    importFile(importAuthors, usedTaxonomy, autop, postTypes) {\n        console.log('(i) Import started');\n        this.parser.setConfig(importAuthors, usedTaxonomy, autop, postTypes);\n        this.parser.importAuthorsData();\n        this.parser.importTagsData();\n        this.parser.getImageURLs();\n        this.parser.importPostsData();\n        this.parser.importPagesData();\n        this.parser.importImages();\n    }\n}\n\nmodule.exports = Import;\n"
  },
  {
    "path": "app/back-end/modules/import/wxr-parser.js",
    "content": "const fs = require('fs');\nconst url = require('url');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst moment = require('moment');\nconst { XMLParser } = require('fast-xml-parser');\nconst download = require('image-downloader');\nconst automaticParagraphs = require('./automatic-paragraphs.js');\nconst slug = require('./../../helpers/slug');\nconst Author = require('./../../author.js');\nconst Tag = require('./../../tag.js');\nconst Post = require('./../../post.js');\nconst Page = require('./../../page.js');\nconst Utils = require('./../../helpers/utils.js');\n\n/**\n * Class used to parse WXR files\n */\nclass WxrParser {\n    /**\n     * Create an instance\n     *\n     * @param appInstance\n     * @param siteName\n     */\n    constructor(appInstance, siteName) {\n        this.appInstance = appInstance;\n        this.siteName = siteName;\n        this.importAuthors = false;\n        this.autop = false;\n        this.usedTaxonomy = 'tags';\n        this.postTypes = [];\n        this.temp = {\n            authors: [],\n            posts: [],\n            pages: [],\n            tags: [],\n            images: [],\n            mapping: {\n                authors: [],\n                tags: [],\n                images: [],\n                posts: [],\n                pages: []\n            },\n            imagesQueue: {}\n        };\n    }\n\n    /**\n     * Load WXR file and parse it\n     *\n     * @param filePath\n     */\n    loadFile(filePath) {\n        this.filePath = filePath;\n        this.fileContent = FileHelper.readFileSync(this.filePath, 'utf8');\n        this.fileContent = this.fileContent.trim();\n        this.parseFile();\n    }\n\n    /**\n     * Check if loaded WXR file is a WXR file\n     *\n     * @returns {boolean}\n     */\n    isWXR() {\n        if(path.parse(this.filePath).ext !== '.xml') {\n            return false;\n        }\n\n        if(\n            this.fileContent.indexOf('<!-- generator=\"WordPress') === -1 &&\n            this.fileContent.indexOf('<wp:wxr_version>') === -1\n        ) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Transform XML to JSON\n     *\n     * @returns {boolean}\n     */\n    parseFile() {\n        let results = false;\n        try {\n            let xmlParser = new XMLParser({\n                ignoreAttributes: false,\n                attributeNamePrefix : \"@_\"\n            });\n            results = xmlParser.parse(this.fileContent);\n        } catch(e) {\n            console.log('An error occurred:', e);\n            return false;\n        }\n\n        this.parsedContent = results;\n\n        return true;\n    }\n\n    /**\n     * Analyzes WXR content and returns its stats\n     *\n     * @returns {{authors: number, categories: number, tags: number, images: number, posts: number}}\n     */\n    getWxrStats() {\n        let authors = this.parsedContent.rss.channel['wp:author'];\n        let categories = this.parsedContent.rss.channel['wp:category'];\n        let tags = this.parsedContent.rss.channel['wp:tag'];\n        let items = this.parsedContent.rss.channel['item'];\n\n        if (!Array.isArray(items)) {\n            items = [items];\n        }\n\n        let postTypes = this.getPostTypes(items);\n\n        let stats = {\n            authors: this.getItemsCount(authors),\n            categories: this.getItemsCount(categories),\n            tags: this.getItemsCount(tags),\n            types: {\n                image: this.getItemsCount(items, 'attachment'),\n                post: this.getItemsCount(items, 'post'),\n                page: this.getItemsCount(items, 'page')\n            }\n        };\n\n        for(let postType of postTypes) {\n            stats.types[postType] = this.getItemsCount(items, postType);\n        }\n\n        return stats;\n    }\n\n    /**\n     * Return number of items of given type\n     *\n     * @param items\n     * @param filterType\n     * @returns {number}\n     */\n    getItemsCount(items, filterType = false) {\n        if(filterType) {\n            items = items.filter(item => item['wp:post_type'] === filterType);\n        }\n\n        if(items && (items.length || items.length === 0)) {\n            return items.length;\n        }\n\n        if(typeof items === 'object') {\n            return 1;\n        }\n\n        return 0;\n    }\n\n    /**\n     * Detects post types (without default post types)\n     *\n     * @param items\n     * @returns {Array}\n     */\n    getPostTypes(items) {\n        let skippedTypes = ['post', 'page', 'attachment', 'nav_menu_item'];\n        let foundedTypes = [];\n\n        for(let item of items) {\n            if(skippedTypes.indexOf(item['wp:post_type']) !== -1) {\n                continue;\n            }\n\n            if(foundedTypes.indexOf(item['wp:post_type']) !== -1) {\n                continue;\n            }\n\n            foundedTypes.push(item['wp:post_type']);\n        }\n\n        return foundedTypes;\n    }\n\n    /**\n     * Set configuration of parser and importer\n     *\n     * @param authors\n     * @param taxonomy\n     * @param autop\n     * @param postTypes\n     */\n    setConfig(authors, taxonomy, autop, postTypes) {\n        this.importAuthors = false;\n        this.usedTaxonomy = taxonomy;\n        this.autop = autop;\n        this.postTypes = postTypes;\n\n        if(authors === 'wp-authors') {\n            this.importAuthors = true;\n        }\n\n        console.log('(i) CONFIG:');\n        console.log('- Import authors: ' + this.importAuthors);\n        console.log('- Used taxonomy: ' + this.usedTaxonomy);\n        console.log('- Use autop: '+ this.autop + \"\\n\\n\");\n        console.log('- Post types: '+ this.postTypes.toString() + \"\\n\\n\");\n    }\n\n    /**\n     * Import authors related data\n     */\n    importAuthorsData() {\n        // If authors import is disabled - skip authors import\n        if(!this.importAuthors) {\n            return;\n        }\n\n        // get all authors items\n        let authors = this.parsedContent.rss.channel['wp:author'];\n\n        if(!authors.length) {\n            this.createAuthor(authors, 0, 1);\n            return;\n        }\n\n        for(let i = 0; i < authors.length; i++) {\n            this.createAuthor(authors[i], i, authors.length);\n        }\n    }\n\n    /**\n     * Creates an author\n     *\n     * @param authorData\n     * @param index\n     * @param totalNumber\n     */\n    createAuthor(authorData, index, totalNumber) {\n        let authorUsername = slug(authorData['wp:author_login']);\n        // For each author item insert author object\n        let newAuthor = new Author(this.appInstance, {\n            id: 0,\n            site: this.siteName,\n            name: authorData['wp:author_display_name'],\n            username: authorUsername,\n            config: JSON.stringify({\n                email: authorData['wp:author_email'],\n                avatar: '',\n                useGravatar: false,\n                description: '',\n                metaTitle: '',\n                metaDescription: '',\n                template: ''\n            }),\n            additionalData: ''\n        }, false);\n\n        let newAuthorResult = newAuthor.save();\n        let authors = newAuthorResult.authors;\n        let newAuthorResultFiltered = authors.filter(author => author.username === authorUsername);\n\n        // Store tag ID in the internal array AS:\n        // wp:tag_slug -> tag ID in Publii\n        this.temp.authors[authorUsername] = newAuthorResultFiltered[0].id;\n        this.temp.mapping.authors[authorData['wp:author_id']] = newAuthorResultFiltered[0].id;\n\n        process.send({\n            type: 'progress',\n            message: {\n                translation: 'core.wpImport.authorsProgressInfo',\n                translationVars: {\n                    progress: (index + 1),\n                    total: totalNumber\n                }\n            }\n        });\n\n        console.log('-> Imported author (' + (index + 1) + ' / ' + totalNumber + '): ' + authorUsername);\n    }\n\n    /**\n     * Import tags related data\n     */\n    importTagsData() {\n        let items = false;\n\n        if(this.usedTaxonomy === 'tags') {\n            items = this.parsedContent.rss.channel['wp:tag'];\n        } else {\n            items = this.parsedContent.rss.channel['wp:category'];\n        }\n\n        if(items && !items.length) {\n            return;\n        }\n\n        if(items && items.length) {\n            for (let i = 0; i < items.length; i++) {\n                this.createTag(items[i], i, items.length);\n            }\n        }\n    }\n\n    /**\n     * Creates tag\n     *\n     * @param tagData\n     * @param index\n     * @param totalNumber\n     */\n    createTag(tagData, index, totalNumber) {\n        let itemName = '';\n        let itemSlug = '';\n\n        if(this.usedTaxonomy === 'tags') {\n            itemName = (tagData['wp:tag_name']).toString();\n            itemSlug = (tagData['wp:tag_slug']).toString();\n        } else {\n            itemName = (tagData['wp:cat_name']).toString();\n            itemSlug = (tagData['wp:category_nicename']).toString();\n        }\n\n        // For each author item insert author object\n        let newItem = new Tag(this.appInstance, {\n            id: 0,\n            site: this.siteName,\n            name: itemName,\n            slug: itemSlug,\n            description: '',\n            additionalData: ''\n        }, false);\n\n        let newItemResult = newItem.save();\n\n        if(!newItemResult.tags) {\n            itemSlug += '-2';\n\n            newItem = new Tag(this.appInstance, {\n                id: 0,\n                site: this.siteName,\n                name: itemName,\n                slug: itemSlug,\n                description: '',\n                additionalData: ''\n            });\n\n            newItemResult = newItem.save();\n\n            if(!newItemResult.tags) {\n                return;\n            }\n        }\n\n        let newItemResultFiltered = newItemResult.tags.filter(tag => tag.slug === slug(itemSlug));\n        this.temp.tags[itemSlug] = newItemResultFiltered[0].id;\n        this.temp.mapping.tags[tagData['wp:term_id']] = newItemResultFiltered[0].id;\n\n        process.send({\n            type: 'progress',\n            message: {\n                translation: 'core.wpImport.tagsProgressInfo',\n                translationVars: {\n                    progress: (index + 1),\n                    total: totalNumber\n                }\n            }\n        });\n\n        console.log('-> Imported tag (' + (index + 1) + ' / ' + totalNumber + '): ' + itemName);\n    }\n\n    /**\n     * Import posts data\n     */\n    importPostsData() {\n        let posts = this.parsedContent.rss.channel['item'];\n        let newPost;\n\n        posts = Array.isArray(posts) ? posts : [posts];\n        posts = posts && posts.length ? posts.filter(item => this.postTypes.indexOf(item['wp:post_type']) !== -1 && item['wp:post_type'] !== 'page') : false;\n\n        if(!posts) {\n            return;\n        }\n\n        let untitledPostsCount = 1;\n\n        for(let i = 0; i < posts.length; i++) {\n            if (!posts[i].title) {\n                console.log('(!) Empty post title detected - fallback to \"Untitled #X\" title');\n                posts[i].title = 'Untitled #' + untitledPostsCount++;\n            }\n\n            // For each post item insert post object\n            let postImages = this.getPostImages(posts[i]['content:encoded']);\n            let postSlug = slug(posts[i].title);\n            let postAuthor = this.temp.authors[slug(posts[i]['dc:creator'])];\n            let postText = this.preparePostText(posts[i]['content:encoded'], postImages);\n            let postStatus = posts[i]['wp:status'] === 'draft' ? 'draft' : 'published'\n            let postTags = '';\n            let postTitle = (posts[i].title).toString();\n\n            if(posts[i]['category'] && (posts[i]['category'].length || posts[i]['category'] instanceof Object)) {\n                let tags = false;\n\n                if (!posts[i]['category'].length || typeof posts[i]['category'] === 'string') {\n                    posts[i]['category'] = [posts[i]['category']];\n                }\n\n                if(this.usedTaxonomy === 'tags') {\n                    tags = posts[i]['category'].filter(item => item['@_domain'] === 'post_tag');\n                } else {\n                    tags = posts[i]['category'].filter(item => item['@_domain'] === 'category');\n                }\n\n                postTags = [...new Set(tags.map(tag => tag['#text']))];\n            }\n\n            if(!this.importAuthors) {\n                postAuthor = '1';\n            }\n\n            newPost = new Post(this.appInstance, {\n                id: 0,\n                site: this.siteName,\n                title: postTitle,\n                slug: postSlug,\n                author: postAuthor,\n                status: postStatus,\n                tags: postTags,\n                text: postText,\n                creationDate: moment(posts[i]['wp:post_date']).format('x'),\n                modificationDate: moment().format('x'),\n                template: '',\n                additionalData: '',\n                postViewSettings: ''\n            }, false);\n\n            let newPostResult = newPost.save();\n            let newPostID = newPostResult.postID;\n\n            this.temp.posts[postSlug] = newPostID;\n            this.temp.mapping.posts[posts[i]['wp:post_id']] = newPostID;\n\n            // Create queue for download images\n            if(postImages.length) {\n                this.temp.imagesQueue[newPostID] = postImages;\n            }\n\n            let featuredImage = this.getFeaturedPostImage(posts[i]);\n            let fileName = false;\n\n            if(featuredImage) {\n                fileName = path.parse(featuredImage).base;\n\n                if(!this.temp.imagesQueue[newPostID]) {\n                    this.temp.imagesQueue[newPostID] = [];\n                }\n\n                this.temp.imagesQueue[newPostID].push(featuredImage);\n            }\n\n            if(fileName) {\n                let featuredPostImageSqlQuery = newPost.db.prepare(`INSERT INTO posts_images VALUES(NULL, @newPostID, @fileName, '', '', @config)`);\n                featuredPostImageSqlQuery.run({\n                    newPostID: newPostID,\n                    fileName: fileName,\n                    config: '{\"alt\":\"\",\"caption\":\"\",\"credits\":\"\"}'\n                });\n\n                let featuredPostID = newPost.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n                let featuredPostIdUpdate = newPost.db.prepare(`UPDATE posts SET featured_image_id = @featuredPostID WHERE id = @newPostID`);\n\n                featuredPostIdUpdate.run({\n                    featuredPostID,\n                    newPostID\n                });\n            }\n\n            process.send({\n                type: 'progress',\n                message: {\n                    translation: 'core.wpImport.postsProgressInfo',\n                    translationVars: {\n                        progress: (i + 1),\n                        total: posts.length\n                    }\n                }\n            });\n\n            console.log('-> Imported post (' + (i+1) + ' / ' + posts.length + '): ' + postTitle);\n        }\n    }\n\n    /**\n     * Import pages data\n     */\n    importPagesData() {\n        if (this.postTypes.indexOf('page') === -1) {\n            console.log('(!) Pages import is disabled');\n            return;\n        }\n\n        let pages = this.parsedContent.rss.channel['item'];\n        let newPage;\n        pages = Array.isArray(pages) ? pages : [pages];\n        pages = pages && pages.length ? pages.filter(item => item['wp:post_type'] === 'page') : false;\n\n        if(!pages) {\n            console.log('(!) No pages to import');\n            return;\n        }\n\n        let untitledPagesCount = 1;\n\n        console.log('(X) pages:', pages);\n\n        for(let i = 0; i < pages.length; i++) {\n            if (!pages[i].title) {\n                console.log('(!) Empty page title detected - fallback to \"Untitled #X\" title');\n                pages[i].title = 'Untitled #' + untitledPagesCount++;\n            }\n\n            // For each page item insert post object\n            let pageImages = this.getPostImages(pages[i]['content:encoded']);\n            let pageSlug = slug(pages[i].title);\n            let pageAuthor = this.temp.authors[slug(pages[i]['dc:creator'])];\n            let pageText = this.preparePostText(pages[i]['content:encoded'], pageImages);\n            let pageStatus = pages[i]['wp:status'] === 'draft' ? 'draft,is-page' : 'published,is-page'\n            let pageTitle = (pages[i].title).toString();\n\n            if(!this.importAuthors) {\n                pageAuthor = '1';\n            }\n\n            newPage = new Page(this.appInstance, {\n                id: 0,\n                site: this.siteName,\n                title: pageTitle,\n                slug: pageSlug,\n                author: pageAuthor,\n                status: pageStatus,\n                text: pageText,\n                creationDate: moment(pages[i]['wp:post_date']).format('x'),\n                modificationDate: moment().format('x'),\n                template: '',\n                additionalData: '',\n                pageViewSettings: ''\n            }, false);\n\n            let newPageResult = newPage.save();\n            let newPageID = newPageResult.pageID;\n\n            this.temp.pages[pageSlug] = newPageID;\n            this.temp.mapping.pages[pages[i]['wp:post_id']] = newPageID;\n\n            // Create queue for download images\n            if(pageImages.length) {\n                this.temp.imagesQueue[newPageID] = pageImages;\n            }\n\n            let featuredImage = this.getFeaturedPostImage(pages[i]);\n            let fileName = false;\n\n            if(featuredImage) {\n                fileName = path.parse(featuredImage).base;\n\n                if(!this.temp.imagesQueue[newPageID]) {\n                    this.temp.imagesQueue[newPageID] = [];\n                }\n\n                this.temp.imagesQueue[newPageID].push(featuredImage);\n            }\n\n            if(fileName) {\n                let featuredPageImageSqlQuery = newPage.db.prepare(`INSERT INTO posts_images VALUES(NULL, @newPageID, @fileName, '', '', @config)`);\n                featuredPageImageSqlQuery.run({\n                    newPageID: newPageID,\n                    fileName: fileName,\n                    config: '{\"alt\":\"\",\"caption\":\"\",\"credits\":\"\"}'\n                });\n\n                let featuredPageID = newPage.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n                let featuredPageIdUpdate = newPage.db.prepare(`UPDATE posts SET featured_image_id = @featuredPageID WHERE id = @newPageID`);\n\n                featuredPageIdUpdate.run({\n                    featuredPageID,\n                    newPageID\n                });\n            }\n\n            process.send({\n                type: 'progress',\n                message: {\n                    translation: 'core.wpImport.pagesProgressInfo',\n                    translationVars: {\n                        progress: (i + 1),\n                        total: pages.length\n                    }\n                }\n            });\n\n            console.log('-> Imported page (' + (i+1) + ' / ' + pages.length + '): ' + pageTitle);\n        }\n    }\n\n    /**\n     * Create array with all available images for download\n     */\n    getImageURLs() {\n        let items = this.parsedContent.rss.channel['item'];\n        items = Array.isArray(items) ? items : [items];\n\n        if (items && items.length) {\n            items = items.filter(item => item['wp:post_type'] === 'attachment');\n\n            for (let item of items) {\n                this.temp.images[item['wp:post_id']] = item['wp:attachment_url'];\n            }\n        }\n    }\n\n    /**\n     * Retrieve images connected with a given post text\n     *\n     * @param postText\n     */\n    getPostImages(postText) {\n        let postImages = [];\n        let regex = /<img.*?src=\"(.*?)\"/g;\n        let regexResult = null;\n\n        // Get images from the content\n        do {\n            regexResult = regex.exec(postText);\n\n            if(regexResult !== null) {\n                let postImage = regexResult[1];\n                postImages.push(postImage);\n            }\n        } while(regexResult);\n\n        return postImages;\n    }\n\n    /**\n     * Retrieve featured post image\n     *\n     * @param postObject\n     * @returns {boolean}\n     */\n    getFeaturedPostImage(postObject) {\n        let featuredImage = false;\n\n        if(!postObject['wp:postmeta'] || !postObject['wp:postmeta'].length) {\n            return;\n        }\n\n        // Get featured image\n        for(let postMeta of postObject['wp:postmeta']) {\n            if(postMeta['wp:meta_key'] === '_thumbnail_id') {\n                let featuredImageID = postMeta['wp:meta_value'];\n\n                if(this.temp.images[featuredImageID]) {\n                    featuredImage = this.temp.images[featuredImageID];\n                }\n            }\n        }\n\n        return featuredImage;\n    }\n\n    /**\n     * Import images data\n     */\n    importImages() {\n        let postIDs = Object.keys(this.temp.imagesQueue);\n        let imagesQueue = [];\n        let destinationPath = path.join(\n            this.appInstance.sitesDir,\n            this.siteName,\n            'input',\n            'media',\n            'posts'\n        );\n        this.downloadImagesProgress = 0;\n        this.totalImages = this.countImages();\n\n        for(let i = 0; i < postIDs.length; i++) {\n            let imagesForPost = this.temp.imagesQueue[postIDs[i]];\n            imagesForPost = [...new Set(imagesForPost)];\n\n            for(let j = 0; j < imagesForPost.length; j++) {\n                let img = imagesForPost[j];\n                imagesQueue.push({\n                    postID: postIDs[i],\n                    imgUrl: img\n                });\n            }\n        }\n\n        this.downloadImages(imagesQueue, destinationPath);\n    }\n\n    /**\n     * Downloads images from queue\n     *\n     * @param imagesQueue\n     * @param destinationPath\n     */\n    downloadImages(imagesQueue, destinationPath) {\n        if(imagesQueue.length === 0) {\n            this.finishImport();\n            return;\n        }\n\n        let nextImg = imagesQueue.shift();\n        let dirPath = path.join(destinationPath, (nextImg.postID).toString());\n\n        if(!Utils.dirExists(dirPath)) {\n            fs.mkdirSync(dirPath, { recursive: true });\n        }\n\n        let image = nextImg.imgUrl;\n        let imageFileName = url.parse(image);\n\n        if(imageFileName && imageFileName.pathname && imageFileName.protocol) {\n            imageFileName = path.basename(imageFileName.pathname);\n\n            download.image({\n                url: image.replace(imageFileName, encodeURIComponent(imageFileName)),\n                dest: path.join(dirPath, imageFileName),\n                headers: {\n                    'User-Agent': 'Publii'\n                }\n            }).then(({filename, image}) => {\n                this.downloadImagesProgress++;\n\n                process.send({\n                    type: 'progress',\n                    message: {\n                        translation: 'core.wpImport.imagesProgressInfo',\n                        translationVars: {\n                            progress: this.downloadImagesProgress,\n                            total: this.totalImages\n                        }\n                    }\n                });\n\n                console.log('-> Downloaded image: ' + filename);\n\n                setTimeout(() => {\n                    this.downloadImages(imagesQueue, destinationPath);\n                }, 250);\n            }).catch(err => {\n                this.downloadImagesProgress++;\n\n                process.send({\n                    type: 'progress',\n                    message: {\n                        translation: 'core.wpImport.imageDownloadError',\n                        translationVars: {\n                            image: image\n                        }\n                    }\n                });\n\n                console.log('(!) An error occurred during downloading the image: ' + image);\n                console.log(err);\n\n                setTimeout(() => {\n                    this.downloadImages(imagesQueue, destinationPath);\n                }, 250);\n            });\n        } else {\n            console.log('(!!) An error occurred during downloading the image: ' + image);\n\n            setTimeout(() => {\n                this.downloadImages(imagesQueue, destinationPath);\n            }, 250);\n        }\n    }\n\n    /**\n     * Counts images to download\n     */\n    countImages() {\n        let postIDs = Object.keys(this.temp.imagesQueue);\n        let sum = 0;\n\n        for(let i = 0; i < postIDs.length; i++) {\n            sum += [...new Set(this.temp.imagesQueue[postIDs[i]])].length;\n        }\n\n        return sum;\n    }\n\n    /**\n     * Prepares post text to import\n     *\n     * @param text\n     */\n    preparePostText(text, images) {\n        // Case when content is empty\n        if(typeof text !== 'string') {\n            return '';\n        }\n\n        // Replace images with #DOMAIN_NAME#\n        if(images.length) {\n            for (let image of images) {\n                let imageFileName = url.parse(image);\n\n                if(imageFileName && imageFileName.pathname) {\n                    imageFileName = path.basename(imageFileName.pathname);\n                    text = text.split(image).join('#DOMAIN_NAME#' + imageFileName);\n                }\n            }\n        }\n\n        // Remove [caption] from content\n        text = text.replace(/\\[caption.*?\\]/g, '');\n        text = text.replace(/\\[\\/caption\\]/g, '');\n\n        // Replace <!-- more --> with Publii separator\n        text = text.replace(/<!--more-->/g, '<hr id=\"read-more\">');\n\n        if(this.autop) {\n            console.log('(i) Used automatic paragraphs for the post content');\n            text = automaticParagraphs(text);\n        }\n\n        return text;\n    }\n\n    /**\n     * Finishing import process\n     */\n    finishImport() {\n        process.send({\n            type: 'result',\n            status: 'success',\n            message: true\n        });\n\n        console.log('(i) Import is done');\n\n        setTimeout(function() {\n            process.exit();\n        }, 1000);\n    }\n}\n\nmodule.exports = WxrParser;\n"
  },
  {
    "path": "app/back-end/modules/plugins/plugins-api.js",
    "content": "class PluginsAPI {\n    constructor () {\n        this.events = {\n            app: {},\n            site: {}\n        };\n    }\n\n    /**\n     * Add \n     */\n    _add (scope, place, callback, priority) {\n        if (!this.events[scope][place]) {\n            this.events[scope][place] = [{ priority, callback }];\n        } else {\n            this.events[scope][place].push({ priority, callback });\n        }\n\n        this.events[scope][place].sort(this.sortByPriority);\n    }\n\n    addSiteEvent (event, callback, priority) {\n        this._add('site', event, callback, priority);\n    }\n\n    addAppEvent (event, callback, priority) {\n        this._add('app', event, callback, priority);\n    }\n\n    /**\n     * Get\n     */\n    _get (scope, place) {\n        if (!this.events[scope][place]) {\n            return [];\n        }\n\n        return this.events[scope][place];\n    }\n\n    getSiteEvents (event) {\n        return this._get('site', event);\n    }\n\n    getAppEvents (event) {\n        return this._get('app', event);\n    }\n\n    /**\n     * Remove \n     */\n    _remove (scope, place, callback, priority) {\n        if (!this.events[scope][place]) {\n            return;\n        }\n\n        this.events[scope][place] = this.events[scope][place].filter(insertion => insertion.callback !== callback && insertion.priority !== priority);\n    }\n\n    removeSiteEvent (event, callback, priority) {\n        this._remove('site', event, callback, priority);\n    }\n\n    removeAppEvent (event, callback, priority) {\n        this._remove('app', event, callback, priority);\n    }\n\n    /**\n     * Reset\n     */\n    _reset (scope) {\n        this.events[scope] = {}\n    }\n\n    resetSiteEvents () {\n        this._reset('site');\n    }\n\n    resetAppEvents () {\n        this._reset('app');\n    }\n\n    /**\n     * Helpers \n     */\n    sortByPriority (itemA, itemB) {\n        return itemA.priority - itemB.priority;\n    }\n}\n\nmodule.exports = PluginsAPI;\n"
  },
  {
    "path": "app/back-end/modules/plugins/plugins-helpers.js",
    "content": "const path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\n\nclass PluginsHelpers {\n    // Returns a list of active plugins for given site plugins config file;\n    static getActivePluginsList (sitePluginsConfigPath) {\n        let fileContent;\n        let allPlugins;\n        let activePlugins = [];\n\n        try {\n            fileContent = FileHelper.readFileSync(sitePluginsConfigPath);\n            fileContent = fileContent.toString();\n            fileContent = JSON.parse(fileContent);\n        } catch (e) {\n            console.log('(!) Unable to find site plugins config JSON');\n            return [];\n        }\n\n        allPlugins = Object.keys(fileContent);\n\n        for (let i = 0; i < allPlugins.length; i++) {\n            let pluginName = allPlugins[i];\n\n            if (fileContent[pluginName]) {\n                activePlugins.push(pluginName);\n            }\n        }\n\n        return activePlugins;\n    }\n\n    // Returns a list of files which should be copied to the website\n    static getPluginFrontEndFiles (pluginName, pluginsDir) {\n        let pluginConfigPath = path.join(pluginsDir, pluginName, 'plugin.json');\n        let pluginConfig;\n\n        try {\n            pluginConfig = FileHelper.readFileSync(pluginConfigPath);\n            pluginConfig = pluginConfig.toString();\n            pluginConfig = JSON.parse(pluginConfig);\n        } catch (e) {\n            console.log('(!) Unable to read plugin config file (plugin.json): ' + pluginName);\n            return [];\n        }\n\n        if (pluginConfig.assets && pluginConfig.assets.front) {\n            pluginConfig.assets.front = pluginConfig.assets.front.map(fileName => fileName.split('/'));\n            return pluginConfig.assets.front.map(fileName => ({\n                input: path.join(pluginsDir, pluginName, 'front-assets', ...fileName),\n                output: fileName.join('/')\n            }));\n        }\n\n        return [];\n    }\n}\n\nmodule.exports = PluginsHelpers;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/404.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\n\n/**\n * Class used create context\n * for the homepage theme view\n */\n\nclass RendererContext404 extends RendererContext {\n    /**\n     * Loading data used in the view\n     */\n    loadData() {\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.homepage;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n        this.pages = this.renderer.commonData.pages;\n        this.metaTitle = this.siteConfig.advanced.errorMetaTitle.replace(/%sitename/g, siteName);\n        this.metaDescription = this.siteConfig.advanced.errorMetaDescription.replace(/%sitename/g, siteName);\n\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n        }\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.tags = this.tags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    /**\n     * Preparing the loaded data\n     */\n    prepareData() {\n        this.title = this.siteConfig.name;\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n    }\n\n    /**\n     * Setting context for the view\n     */\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.siteConfig.advanced.metaRobotsError;\n\n        if(this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            tags: this.tags,\n            pages: this.pages,\n            mainTags: this.mainTags,\n            authors: this.authors,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    /**\n     * Getting context for the view\n     *\n     * @returns {object} - context for the view\n     */\n    getContext() {\n        this.setContext();\n\n        return this.context;\n    }\n}\n\nmodule.exports = RendererContext404;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/author.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context');\nconst slug = require('./../../../helpers/slug');\nconst RendererHelpers = require('./../helpers/helpers.js');\nconst path = require('path');\n\n/**\n * Class used create context\n * for the author theme views\n */\n\nclass RendererContextAuthor extends RendererContext {\n    loadData() {\n        // Prepare query data\n        this.authorID = parseInt(this.authorID, 10);\n        this.postsNumber = parseInt(this.postsNumber, 10);\n        this.offset = parseInt(this.offset, 10);\n\n        // Retrieve author data\n        this.author = this.renderer.cachedItems.authors[this.authorID];\n\n        // Retrieve post\n        let includeFeaturedPosts = '';\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('authorsIncludeFeaturedInPosts', this.themeConfig) === false;\n\n        if (shouldSkipFeaturedPosts) {\n            includeFeaturedPosts = 'status NOT LIKE \\'%featured%\\' AND';\n        }\n\n        if(this.postsNumber === -1) {\n            this.postsNumber = 999;\n        }\n\n        if(this.postsNumber === 0) {\n            this.posts = false;\n        } else {\n            this.posts = this.db.prepare(`\n                SELECT\n                    id\n                FROM\n                    posts\n                WHERE\n                    status LIKE '%published%' AND\n                    status NOT LIKE '%hidden%' AND\n                    status NOT LIKE '%trashed%' AND\n                    status NOT LIKE '%is-page%' AND\n                    ${includeFeaturedPosts}\n                    authors LIKE @authorID\n                ORDER BY\n                    ${this.postsOrdering}\n                LIMIT\n                    @postsNumber\n                OFFSET\n                    @offset\n            `).all({\n                authorID: this.authorID.toString(),\n                postsNumber: this.postsNumber,\n                offset: this.offset\n            });\n        }\n\n        this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.author;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n        this.pages = this.renderer.commonData.pages;\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.tags = this.tags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    prepareData() {\n        this.title = 'Author: ' + this.author.name;\n        this.posts = this.posts || [];\n        this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('authorsIncludeFeaturedInPosts', this.themeConfig) === false;\n        let featuredPostsNumber = RendererHelpers.getRendererOptionValue('authorsFeaturedPostsNumber', this.themeConfig);\n\n        // Remove featured posts from posts if featured posts allowed\n        if (shouldSkipFeaturedPosts && (featuredPostsNumber > 0 || featuredPostsNumber === -1)) {\n            let featuredPostsIds = this.featuredPosts.map(post => post.id);\n            this.posts = this.posts.filter(post => featuredPostsIds.indexOf(post.id) === -1);\n        }\n\n        // Prepare meta data\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        this.metaTitle = this.siteConfig.advanced.authorMetaTitle.replace(/%authorname/g, this.author.name)\n                                                                 .replace(/%sitename/g, siteName);\n        this.metaDescription = this.siteConfig.advanced.authorMetaDescription.replace(/%authorname/g, this.author.name)\n                                                                             .replace(/%sitename/g, siteName);\n        this.metaRobots = false;\n        this.hasCustomCanonicalUrl = false;\n        this.canonicalUrl = '';\n\n        let metaData = this.author.config;\n        let additionalData = this.author.additionalData;\n\n        if (metaData && metaData.metaTitle) {\n            this.metaTitle = metaData.metaTitle.replace(/%authorname/g, this.author.name)\n                                               .replace(/%sitename/g, siteName);\n        }\n\n        if (metaData && metaData.metaDescription) {\n            this.metaDescription = metaData.metaDescription.replace(/%authorname/g, this.author.name)\n                                                           .replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n        }\n\n        if (additionalData && additionalData.metaRobots) {\n            this.metaRobots = additionalData.metaRobots;\n        }\n\n        if (additionalData && additionalData.canonicalUrl) {\n            this.canonicalUrl = additionalData.canonicalUrl;\n            this.hasCustomCanonicalUrl = true;\n            this.metaRobots = '';\n        }\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.siteConfig.advanced.metaRobotsAuthors;\n        \n        if (this.metaRobots !== false) {\n            metaRobotsValue = this.metaRobots;\n        }\n\n        if (this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            author: this.author,\n            posts: this.posts,\n            pages: this.pages,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            tags: this.tags,\n            mainTags: this.mainTags,\n            authors: this.authors,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            hasCustomCanonicalUrl: this.hasCustomCanonicalUrl,\n            canonicalUrl: this.canonicalUrl,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    getContext(authorID, offset = 0, postsNumber = 999) {\n        this.offset = offset;\n        this.postsNumber = postsNumber;\n        this.authorID = authorID;\n        this.setContext();\n\n        return this.context;\n    }\n}\n\nmodule.exports = RendererContextAuthor;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/feed.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\nconst URLHelper = require('./../helpers/url.js');\nconst ContentHelper = require('./../helpers/content.js');\nconst normalizePath = require('normalize-path');\n\n/**\n * Class used create context\n * for the feed theme view\n */\n\nclass RendererContextFeed extends RendererContext {\n    loadData() {\n        // prepare query variables\n        this.postsNumber = parseInt(this.postsNumber, 10);\n        this.offset = parseInt(this.offset, 10);\n        // Handle \"only featured posts\" mode\n        let featuredPostsCondition = '';\n\n        if (this.siteConfig.advanced.feed.showOnlyFeatured) {\n            featuredPostsCondition = 'status LIKE \\'%featured%\\' AND';\n        } else if (this.siteConfig.advanced.feed.excludeFeatured) {\n            featuredPostsCondition = 'status NOT LIKE \\'%featured%\\' AND';\n        }\n\n        // Retrieve post\n        this.posts = this.db.prepare(`\n            SELECT\n                *\n            FROM\n                posts\n            WHERE\n                status LIKE '%published%' AND\n                ${featuredPostsCondition}\n                status NOT LIKE '%hidden%' AND\n                status NOT LIKE '%is-page%' AND\n                status NOT LIKE '%trashed%' AND\n                status NOT LIKE '%excluded_homepage%'\n            ORDER BY\n                created_at DESC\n            LIMIT\n                @postsNumber\n            OFFSET\n                @offset\n        `).all({\n            postsNumber: this.postsNumber,\n            offset: this.offset\n        });\n    }\n\n    prepareData() {\n        let self = this;\n        this.posts = this.posts || [];\n        this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]);\n\n        this.posts = this.posts.map(post => {\n            let contentMode = self.siteConfig.advanced.feed.showFullText ? 'fullText' : 'excerpt';\n\n            return {\n                title: post.title,\n                url: post.url,\n                author: this.getAuthor('post', post.id),\n                text: contentMode === 'fullText' ? post.text : false,\n                excerpt: post.excerpt,\n                createdAt: post.createdAt,\n                modifiedAt: post.createdAt > post.modifiedAt ? post.createdAt : post.modifiedAt, // Get higher date - created_at or modified_at\n                categories: this.getPostCategories(post.id),\n                thumbnail: this.getPostThumbnail(post.id)\n            }\n        });\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let siteOwnerData = this.renderer.cachedItems.authors[1];\n        let logoUrl = normalizePath(this.themeConfig.config.logo);\n        let siteName = this.siteConfig.name;\n\n        if (this.siteConfig.advanced.feed.title === 'customTitle') {\n            siteName = this.siteConfig.advanced.feed.titleValue;\n        } else {\n            if (this.siteConfig.displayName) {\n                siteName = this.siteConfig.displayName;\n            }\n        }\n\n        if(logoUrl !== '') {\n            logoUrl = normalizePath(this.siteConfig.domain) + '/' + normalizePath(this.themeConfig.config.logo);\n            logoUrl = URLHelper.fixProtocols(logoUrl);\n        }\n\n        this.context = {\n            siteName: siteName,\n            siteAuthor: siteOwnerData,\n            siteDomain: this.siteConfig.domain,\n            siteLogo: logoUrl,\n            updatedDateType: this.siteConfig.advanced.feed.updatedDateType,\n            siteLastUpdate: this.getLastUpdateDate(),\n            posts: this.posts\n        };\n    }\n\n    getContext(postsNumber = 10) {\n        this.offset = 0;\n        this.postsNumber = postsNumber;\n        this.setContext();\n\n        return this.context;\n    }\n\n    getLastUpdateDate() {\n        let latestDate = 0;\n\n        for(let i = 0; i < this.posts.length; i++) {\n\n            if(this.posts[i].modifiedAt > latestDate) {\n                latestDate = this.posts[i].modifiedAt;\n            }\n        }\n\n        return latestDate;\n    }\n\n    getPostCategories(postID) {\n        let tags = this.db.prepare(`\n            SELECT\n                t.name AS name\n            FROM\n                tags AS t\n            LEFT JOIN\n                posts_tags AS pt\n                ON\n                pt.tag_id = t.id\n            WHERE\n                pt.post_id = @postID\n                AND (\n        \t\t\t(json_valid(t.additional_data) AND json_extract(t.additional_data, '$.isHidden') = false)\n        \t\t\tOR t.additional_data IS NULL\n        \t\t\tOR t.additional_data = ''\n    \t\t\t)\n            ORDER BY\n                name DESC\n        `).all({\n            postID: postID\n        });\n\n        return tags;\n    }\n\n    getPostThumbnail(postID) {\n        if(!this.siteConfig.advanced.feed.showFeaturedImage) {\n            return false;\n        }\n\n        let thumbnailUrl = '';\n        let thumbnailAlt = '';\n        let thumbnail = this.db.prepare(`\n            SELECT\n                pi.url AS url,\n                pi.additional_data AS additionalData\n            FROM\n                posts_images AS pi\n            WHERE\n                pi.post_id = @postID\n            LIMIT 1\n        `).get({\n            postID: postID            \n        });\n\n        if(thumbnail && thumbnail.url) {\n            let additionalData = { alt: '' };\n            thumbnailUrl = this.siteConfig.domain + '/media/posts/' + postID + '/' + thumbnail.url;\n\n            try {\n                additionalData = JSON.parse(thumbnail.additionalData);\n                thumbnailAlt = additionalData.alt;\n            } catch (e) {\n                console.log('Malformed thumnail additional data');\n            }\n        } else {\n            return false;\n        }\n\n        return {\n            url: thumbnailUrl,\n            alt: thumbnailAlt\n        };\n    }\n\n    /**\n     *\n     * Function used to retrieve an author data\n     *\n     * @param dataType (string) - 'post' or 'author'\n     * @param id (int) - ID of post or author\n     *\n     * @return object - author data\n     *\n     */\n    getAuthor(dataType, id) {\n        let authorID = id;\n\n        if(dataType === 'post') {\n            let result = this.db.prepare(`SELECT authors FROM posts WHERE id = @id LIMIT 1;`).get({ id: id });\n            \n            if (result && result.authors) {\n                authorID = parseInt(result.authors, 10);\n            } else {\n                authorID = 1;\n            }\n        }\n\n        return this.renderer.cachedItems.authors[authorID];\n    }\n}\n\nmodule.exports = RendererContextFeed;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/home.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\nconst RendererHelpers = require('./../helpers/helpers.js');\n\n/**\n * Class used create context\n * for the homepage theme view\n */\n\nclass RendererContextHome extends RendererContext {\n    loadData() {\n        // prepare query variables\n        this.postsNumber = parseInt(this.postsNumber, 10);\n        this.offset = parseInt(this.offset, 10);\n\n        // Retrieve post\n        let includeFeaturedPosts = '';\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('includeFeaturedInPosts', this.themeConfig) === false;\n\n        if (shouldSkipFeaturedPosts) {\n            includeFeaturedPosts = 'status NOT LIKE \\'%featured%\\' AND';\n        }\n\n        if(this.postsNumber === -1) {\n            this.postsNumber = 999;\n        }\n\n        if(this.postsNumber === 0) {\n            this.posts = false;\n        } else {\n            this.posts = this.db.prepare(`\n                SELECT\n                    *\n                FROM\n                    posts\n                WHERE\n                    ${includeFeaturedPosts}\n                    status LIKE '%published%' AND\n                    status NOT LIKE '%hidden%' AND\n                    status NOT LIKE '%trashed%' AND\n                    status NOT LIKE '%is-page%' AND\n                    status NOT LIKE '%excluded_homepage%'\n                ORDER BY\n                    ${this.postsOrdering}\n                LIMIT\n                    @postsNumber\n                OFFSET\n                    @offset\n            `).all({\n                postsNumber: this.postsNumber,\n                offset: this.offset  \n            });\n        }\n\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n\n        if (\n            !this.siteConfig.advanced.usePageAsFrontpage && \n            this.siteConfig.advanced.urls.postsPrefix &&\n            this.renderer.menuContext.indexOf('frontpage') > -1\n        ) {\n            this.metaTitle = this.siteConfig.advanced.homepageMetaTitle.replace(/%sitename/g, siteName);\n            this.metaDescription = this.siteConfig.advanced.homepageMetaDescription.replace(/%sitename/g, siteName);\n        }\n\n        this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.pages = this.renderer.commonData.pages;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.homepage;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.tags = this.tags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    prepareData() {\n        this.title = this.siteConfig.name;\n        this.posts = this.posts || [];\n        this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('includeFeaturedInPosts', this.themeConfig) == false;\n        let featuredPostsNumber = RendererHelpers.getRendererOptionValue('featuredPostsNumber', this.themeConfig);\n\n        // Remove featured posts from posts if featured posts not allowed\n        if (shouldSkipFeaturedPosts && (featuredPostsNumber > 0 || featuredPostsNumber === -1)) {\n            let featuredPostsIds = this.featuredPosts.map(post => post.id);\n            this.posts = this.posts.filter(post => featuredPostsIds.indexOf(post.id) === -1);\n        }\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.siteConfig.advanced.metaRobotsIndex;\n\n        if(this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            posts: this.posts,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            tags: this.tags,\n            mainTags: this.mainTags,\n            pages: this.pages,\n            authors: this.authors,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    getContext(offset = 0, postsNumber = 999) {\n        this.offset = offset;\n        this.postsNumber = postsNumber;\n        this.setContext();\n\n        return this.context;\n    }\n\n    getPostsNumber() {\n        let includeFeaturedPosts = '';\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('includeFeaturedInPosts', this.themeConfig) === false;\n\n        if (shouldSkipFeaturedPosts) {\n            includeFeaturedPosts = 'AND status NOT LIKE \\'%featured%\\'';\n        }\n\n        let results = this.db.prepare(`\n            SELECT\n                COUNT(id)\n            FROM\n                posts\n            WHERE\n                status LIKE '%published%' AND\n                status NOT LIKE '%hidden%' AND\n                status NOT LIKE '%trashed%' AND \n                status NOT LIKE '%is-page%' AND \n                status NOT LIKE '%excluded_homepage%'\n                ${includeFeaturedPosts}\n            GROUP BY\n                id\n        `).all();\n\n        if(!results || !results.length) {\n            return 0;\n        }\n\n        return results.length;\n    }\n}\n\nmodule.exports = RendererContextHome;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/page-preview.js",
    "content": "// Necessary packages\nconst fs = require('fs');\nconst path = require('path');\nconst sizeOf = require('image-size');\nconst sqlString = require('sqlstring');\nconst normalizePath = require('normalize-path');\nconst slug = require('./../../../helpers/slug');\nconst RendererContext = require('../renderer-context.js');\nconst RendererHelpers = require('./../helpers/helpers.js');\nconst URLHelper = require('../helpers/url.js');\nconst ContentHelper = require('../helpers/content.js');\n\n/**\n * Class used create context\n * for the single page theme previews\n */\n\nclass RendererContextPagePreview extends RendererContext {\n    loadData() {\n        // Prepare data\n        this.pageID = parseInt(this.renderer.postData.postID, 10);\n        this.title = this.renderer.postData.title;\n        this.pageImage = this.renderer.postData.featuredImage;\n        this.editor = this.renderer.postData.additionalData.editor;\n        // Retrieve all tags\n        this.allTags = this.getAllTags();\n        // Retrieve menu data\n        this.menus = this.getMenus();\n    }\n\n    prepareData() {\n        let pageURL = this.siteConfig.domain + '/preview.html';\n        let preparedText = this.prepareContent(this.renderer.postData.text, this.renderer.postData.id);\n        let hasCustomExcerpt = false;\n        let readmoreMatches = preparedText.match(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n\n        if (readmoreMatches && readmoreMatches.length) {\n            hasCustomExcerpt = true;\n        }\n\n        this.page = {\n            id: this.renderer.postData.id,\n            title: this.renderer.postData.title,\n            slug: this.renderer.postData.slug,\n            author: this.renderer.cachedItems.authors[this.renderer.postData.author],\n            url: pageURL,\n            text: preparedText.replace(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi, ''),\n            excerpt: ContentHelper.prepareExcerpt(this.themeConfig.config.excerptLength, preparedText),\n            createdAt: this.renderer.postData.creationDate,\n            modifiedAt: this.renderer.postData.modificationDate,\n            status: this.renderer.postData.status,\n            featuredImage: {},\n            hasGallery: preparedText.indexOf('class=\"gallery') !== -1,\n            isFeatured: this.renderer.postData.status.indexOf('featured') > -1,\n            isHidden: this.renderer.postData.status.indexOf('hidden') > -1,\n            isExcludedOnHomepage: this.renderer.postData.status.indexOf('excluded_homepage') > -1,\n            hasGallery: preparedText.indexOf('class=\"gallery') !== -1,\n            template: this.renderer.postData.template,\n            hasCustomExcerpt: hasCustomExcerpt\n        };\n\n        if (this.pageImage) {\n            this.page.featuredImage = this.getPageFeaturedImages(this.page.id, true);\n        }\n\n        this.metaTitle = 'It is an example value for the preview mode';\n        this.metaDescription = 'It is an example value for the preview mode';\n        this.metaRobots = 'It is an example value for the preview mode';\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.metaRobots;\n\n        if(this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            page: this.page,\n            tags: this.allTags,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus.assigned\n        };\n    }\n\n    getContext(pageID) {\n        this.pageID = pageID;\n        this.setContext();\n\n        return this.context;\n    }\n\n    getPageFeaturedImages(pageID, mainPage = false) {\n        let pageImage = false;\n\n        // Retrieve post image\n        if(mainPage === true) {\n            pageImage = {\n                id: 0,\n                url: this.renderer.postData.featuredImageFilename,\n                additional_data: JSON.stringify(this.renderer.postData.featuredImageData)\n            };\n        } else {\n            pageImage = this.db.prepare(`\n                SELECT\n                    pi.id AS id,\n                    pi.url AS url,\n                    pi.additional_data AS additional_data\n                FROM\n                    posts as p\n                LEFT JOIN\n                    posts_images as pi\n                    ON\n                    p.featured_image_id = pi.id\n                WHERE\n                    p.id = @pageID\n                ORDER BY\n                    pi.id DESC\n                LIMIT 1\n            `).get({\n                pageID: pageID\n            });\n        }\n\n        if (pageImage && pageImage.url) {\n            let url = '';\n            let alt = '';\n            let caption = '';\n            let credits = '';\n            let imageDimensions = false;\n\n            if (pageImage.additional_data) {\n                let data = JSON.parse(pageImage.additional_data);\n                let pageDirectory = pageID;\n\n                if(pageDirectory === 0) {\n                    pageDirectory = 'temp';\n                }\n\n                let imagePath = URLHelper.createImageURL(this.inputDir, pageDirectory, pageImage.url);\n                let domain = this.siteConfig.domain;\n\n                url = URLHelper.createImageURL(domain, pageDirectory, pageImage.url);\n                alt = data.alt;\n                caption = data.caption;\n                credits = data.credits;\n\n                try {\n                    imageDimensions = sizeOf(imagePath);\n                } catch(e) {\n                    console.log('page-preview.js: wrong image path - missing dimensions');\n                    imageDimensions = false;\n                }\n            } else {\n                return false;\n            }\n\n            let featuredImageSrcSet = false;\n            let featuredImageSizes = false;\n\n            if(!this.isGifOrSvg(url)) {\n                let useWebp = false;\n\n                if (this.renderer.siteConfig?.advanced?.forceWebp) {\n                    useWebp = true;\n                }\n\n                featuredImageSrcSet = ContentHelper.getFeaturedImageSrcset(url, this.themeConfig, useWebp);\n                featuredImageSizes = ContentHelper.getFeaturedImageSizes(this.themeConfig);\n            } else {\n                featuredImageSrcSet = '';\n                featuredImageSizes = '';\n            }\n\n            let featuredImageData = {\n                id: pageImage.id,\n                url: url,\n                alt: alt,\n                caption: caption,\n                credits: credits,\n                height: imageDimensions.height,\n                width: imageDimensions.width,\n                srcset: featuredImageSrcSet,\n                sizes: featuredImageSizes\n            };\n\n            // Create alternative names for dimensions\n            let dimensions = false;\n\n            if (\n                this.themeConfig.files &&\n                this.themeConfig.files.responsiveImages\n            ) {\n                if (\n                    this.themeConfig.files.responsiveImages.featuredImages &&\n                    this.themeConfig.files.responsiveImages.featuredImages.dimensions\n                ) {\n                    dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions;\n                } else if (\n                    this.themeConfig.files.responsiveImages.contentImages &&\n                    this.themeConfig.files.responsiveImages.contentImages.dimensions\n                ) {\n                    dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions;\n                }\n\n                if (dimensions) {\n                    let dimensionNames = Object.keys(dimensions);\n\n                    for (let dimensionName of dimensionNames) {\n                        let base = path.parse(url).base;\n                        let filename = path.parse(url).name;\n                        let extension = path.parse(url).ext;\n                        let newFilename = filename + '-' + dimensionName + extension;\n                        let capitalizedDimensionName = dimensionName.charAt(0).toUpperCase() + dimensionName.slice(1);\n\n                        if(!this.isGifOrSvg(url)) {\n                            featuredImageData['url' + capitalizedDimensionName] = url.replace(base, newFilename);\n                        } else {\n                            featuredImageData['url' + capitalizedDimensionName] = url;\n                        }\n                    }\n                }\n            }\n\n            return featuredImageData;\n        }\n\n        return false;\n    }\n\n    prepareContent(originalText, pageID) {\n        let self = this;\n        let domain = normalizePath(self.siteConfig.domain);\n        domain = URLHelper.fixProtocols(domain);\n\n        // Get media URL\n        let pageDirectory = pageID;\n\n        if(pageDirectory === 0) {\n            pageDirectory = 'temp';\n        }\n\n        let domainMediaPath = domain + '/media/posts/' + pageDirectory + '/';\n\n        // Replace domain name constat with real URL to media directory\n        let preparedText = originalText.split('#DOMAIN_NAME#').join(domainMediaPath);\n        preparedText = ContentHelper.parseText(preparedText, this.editor);\n\n        // Remove TOC plugin ID attributes when TOC does not exist\n        if (preparedText.indexOf('class=\"post__toc') === -1) {\n            preparedText = preparedText.replace(/\\sid=\"mcetoc_[a-z0-9]*?\"/gmi, ''); \n        }\n\n        // Reduce download=\"download\" to download\n        preparedText = preparedText.replace(/download=\"download\"/gmi, 'download');\n\n        // Remove content for AMP or non-AMP depending from ampMode value\n        preparedText = preparedText.replace(/<publii-amp>.*<\\/publii-amp>/gmi, '');\n        preparedText = preparedText.replace(/<publii-non-amp>/gmi, '');\n        preparedText = preparedText.replace(/<\\/publii-non-amp>/gmi, '');\n\n        // Remove read more text\n        preparedText = preparedText.replace(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi, '');\n\n        // Remove the last empty paragraph\n        preparedText = preparedText.replace(/<p>&nbsp;<\\/p>\\s?$/gmi, '');\n\n        let useWebp = false;\n\n        if (this.renderer.siteConfig?.advanced?.forceWebp) {\n            useWebp = true;\n        }\n\n        // Find all images and add srcset and sizes attributes\n        if (this.siteConfig.responsiveImages) {\n            preparedText = preparedText.replace(/<img.*?src=\"(.*?)\"/gmi, function(matches, url) {\n                if(\n                    ContentHelper.getContentImageSrcset(url, self.themeConfig, useWebp) !== false &&\n                    !(\n                        url.toLowerCase().indexOf('.jpg') === -1 &&\n                        url.toLowerCase().indexOf('.jpeg') === -1 &&\n                        url.toLowerCase().indexOf('.png') === -1 && \n                        url.toLowerCase().indexOf('.webp') === -1\n                    ) &&\n                    url.toLowerCase().indexOf('/gallery/') === -1\n                ) {\n                    if(ContentHelper.getContentImageSizes(self.themeConfig)) {\n                        return matches +\n                            ' sizes=\"' + ContentHelper.getContentImageSizes(self.themeConfig) + '\"' +\n                            ' srcset=\"' + ContentHelper.getContentImageSrcset(url, self.themeConfig, useWebp) + '\" ';\n                    } else {\n                        return matches +\n                            ' srcset=\"' + ContentHelper.getContentImageSrcset(url, self.themeConfig, useWebp) + '\" ';\n                    }\n                } else {\n                    return matches;\n                }\n            });\n        }\n\n        // Add loading=\"lazy\" attributes to img, video, audio, iframe tags\n        if (self.siteConfig.advanced.mediaLazyLoad) {\n            preparedText = preparedText.replace(/<img\\s/gmi, '<img loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<video\\s/gmi, '<video loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<audio\\s/gmi, '<audio loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<iframe\\s/gmi, '<iframe loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<img\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<img$1');\n            preparedText = preparedText.replace(/<video\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<video$1');\n            preparedText = preparedText.replace(/<audio\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<audio$1');\n            preparedText = preparedText.replace(/<iframe\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<iframe$1');\n        }\n\n        if (this.editor === 'tinymce' || this.editor === 'markdown') {\n            // Wrap images with classes into <figure>\n            preparedText = preparedText.replace(/(<p.*?>\\s*?)?<img[^>]*?(class=\".*?\").*?>(\\s*?<\\/p>)?/gmi, function(matches, p1, classes) {\n                return '<figure ' + classes + '>' + matches.replace('</p>', '').replace(/<p.*?>/, '').replace(classes, '') + '</figure>';\n            });\n\n            // Fix some specific syntax cases for double figure elements\n            preparedText = preparedText.replace(/<figure contenteditable=\"false\">[\\s]*?<figure class=\"post__image\">([\\s\\S]*?)<\\/figure>[\\s]*?<\\/figure>/gmi, '<figure class=\"post__image\">$1</figure>');\n            preparedText = preparedText.replace(/<figure contenteditable=\"false\">[\\s]*?<figure class=\"post__image\">([\\s\\S]*?)<\\/figure>[\\s]*?<figcaption contentEditable=\"true\">([\\s\\S]*?)<\\/figcaption>[\\s]*?<\\/figure>/gmi, '<figure class=\"post__image\">$1<figcaption>$2</figcaption></figure>');\n        }\n\n        // Remove contenteditable attributes\n        preparedText = preparedText.replace(/contentEditable=\".*?\"/gi, '');\n\n        if (this.editor === 'tinymce') {\n            // Wrap galleries with classes into div with gallery-wrapper CSS class\n            preparedText = preparedText.replace(/<div class=\"gallery([\\s\\S]*?)\"[\\s\\S]*?<\\/div>?/gmi, function(matches, classes) {\n                return '<div class=\"gallery-wrapper' + classes + '\">' + matches.replace(classes, '') + '</div>';\n            });\n        }\n\n        // Remove paragraphs around <iframe>'s\n        preparedText = preparedText.replace(/\\<p\\>\\<iframe/gmi, '<iframe');\n        preparedText = preparedText.replace(/\\<\\/iframe\\>\\<\\/p\\>/gmi, '</iframe>');\n\n        // Wrap iframes into <div class=\"post__iframe\">\n        preparedText = preparedText.replace(/(?<!<figure[\\s\\S]*?class=\"post__video\">[\\s\\S]*?)(<iframe.*?>[\\s\\S]*?<\\/iframe>)/gmi, function(matches) {\n            if (matches.indexOf('data-responsive=\"false\"') > -1) {\n                return matches;\n            }\n            \n            return '<div class=\"post__iframe\">' + matches + '</div>';\n        });\n\n        // Remove CDATA sections inside scripts added by TinyMCE\n        preparedText = preparedText.replace(/\\<script\\>\\/\\/ \\<\\!\\[CDATA\\[/g, '<script>');\n        preparedText = preparedText.replace(/\\/\\/ \\]\\]\\>\\<\\/script\\>/g, '</script>');\n\n        return preparedText;\n    }\n\n    /**\n     * Detects if image is a GIF or SVG\n     */\n    isGifOrSvg(url) {\n        if(url.slice(-4) === '.gif' || url.slice(-4) === '.svg') {\n            return true;\n        }\n\n        return false;\n    }\n}\n\nmodule.exports = RendererContextPagePreview;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/page.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\nconst stripTags = require('striptags');\n\n/**\n * Class used create context\n * for the single page theme views\n */\nclass RendererContextPage extends RendererContext {\n    loadData() {\n        // Retrieve meta data\n        let metaDataQuery = this.db.prepare(`SELECT value FROM posts_additional_data WHERE post_id = @pageID AND key = '_core'`);\n        this.metaData = metaDataQuery.get({ pageID: this.pageID});\n        this.allTags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.homepage;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n        this.pages = this.renderer.commonData.pages;\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.allTags = this.allTags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    prepareData() {\n        this.page = this.renderer.cachedItems.pages[this.pageID];\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n        this.metaTitle = this.siteConfig.advanced.pageMetaTitle;\n        this.metaDescription = this.siteConfig.advanced.pageMetaDescription;\n        this.canonicalUrl = this.page.url;\n        this.hasCustomCanonicalUrl = false;\n        this.metaRobots = '';\n\n        if (this.siteConfig.advanced.pageMetaDescription === '') {\n            this.metaDescription = stripTags(this.page.excerpt).replace(/\\n/gmi, '');\n        }\n\n        if(this.metaData && this.metaData.value) {\n            let results = JSON.parse(this.metaData.value);\n\n            if (results.metaTitle) {\n                this.metaTitle = results.metaTitle;\n            }\n\n            if (results.metaDesc) {\n                this.metaDescription = results.metaDesc;\n            }\n\n            if (results.metaRobots) {\n                this.metaRobots = results.metaRobots;\n            }\n\n            if (results.canonicalUrl) {\n                this.canonicalUrl = results.canonicalUrl;\n                this.hasCustomCanonicalUrl = true;\n                this.metaRobots = '';\n            }\n        }\n\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n        }\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.metaRobots;\n\n        if(this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        let siteName = this.siteConfig.name;\n\n        if (this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        // Detect if the page title is empty\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.pageMetaTitle.replace(/%pagetitle/g, this.page.title)\n                                                                   .replace(/%sitename/g, siteName)\n                                                                   .replace(/%authorname/g, this.page.author.name);\n        } else {\n            this.metaTitle = this.metaTitle.replace(/%pagetitle/g, this.page.title)\n                                           .replace(/%sitename/g, siteName)\n                                           .replace(/%authorname/g, this.page.author.name);\n        }\n\n        // If still meta title is empty - use page title\n        if (this.metaTitle === '') {\n            this.metaTitle = this.page.title;\n        }\n\n        this.metaDescription = this.metaDescription.replace(/%pagetitle/g, this.page.title)\n                                                    .replace(/%sitename/g, siteName)\n                                                    .replace(/%authorname/g, this.page.author.name);\n\n        this.context = {\n            title: this.metaTitle,\n            page: this.page,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            tags: this.allTags,\n            mainTags: this.mainTags,\n            authors: this.authors,\n            pages: this.pages,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            hasCustomCanonicalUrl: this.hasCustomCanonicalUrl,\n            canonicalUrl: this.canonicalUrl,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    getContext(pageID) {\n        this.pageID = pageID;\n        this.setContext();\n\n        return this.context;\n    }\n}\n\nmodule.exports = RendererContextPage;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/post-preview.js",
    "content": "// Necessary packages\nconst fs = require('fs');\nconst path = require('path');\nconst sizeOf = require('image-size');\nconst sqlString = require('sqlstring');\nconst normalizePath = require('normalize-path');\nconst slug = require('./../../../helpers/slug');\nconst RendererContext = require('../renderer-context.js');\nconst RendererHelpers = require('./../helpers/helpers.js');\nconst URLHelper = require('../helpers/url.js');\nconst ContentHelper = require('../helpers/content.js');\n\n/**\n * Class used create context\n * for the single post theme previews\n */\n\nclass RendererContextPostPreview extends RendererContext {\n    loadData() {\n        // Prepare data\n        this.postID = parseInt(this.renderer.postData.postID, 10);\n        this.title = this.renderer.postData.title;\n        this.postImage = this.renderer.postData.featuredImage;\n        this.editor = this.renderer.postData.additionalData.editor;\n\n        // Retrieve post tags\n        if(this.renderer.postData.tags === '') {\n            this.tags = false;\n        } else {\n            this.tags = this.renderer.postData.tags;\n        }\n\n        // Retrieve all tags\n        this.allTags = this.getAllTags();\n\n        // Retrieve menu data\n        this.menus = this.getMenus();\n    }\n\n    prepareData() {\n        let postURL = this.siteConfig.domain + '/preview.html';\n        let preparedText = this.prepareContent(this.renderer.postData.text, this.renderer.postData.id);\n        let hasCustomExcerpt = false;\n        let readmoreMatches = preparedText.match(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n\n        if (readmoreMatches && readmoreMatches.length) {\n            hasCustomExcerpt = true;\n        }\n\n        this.post = {\n            id: this.renderer.postData.id,\n            title: this.renderer.postData.title,\n            slug: this.renderer.postData.slug,\n            author: this.renderer.cachedItems.authors[this.renderer.postData.author],\n            url: postURL,\n            text: preparedText.replace(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi, ''),\n            excerpt: ContentHelper.prepareExcerpt(this.themeConfig.config.excerptLength, preparedText),\n            createdAt: this.renderer.postData.creationDate,\n            modifiedAt: this.renderer.postData.modificationDate,\n            status: this.renderer.postData.status,\n            featuredImage: {},\n            hasGallery: preparedText.indexOf('class=\"gallery') !== -1,\n            isFeatured: this.renderer.postData.status.indexOf('featured') > -1,\n            isHidden: this.renderer.postData.status.indexOf('hidden') > -1,\n            isExcludedOnHomepage: this.renderer.postData.status.indexOf('excluded_homepage') > -1,\n            hasGallery: preparedText.indexOf('class=\"gallery') !== -1,\n            template: this.renderer.postData.template,\n            hasCustomExcerpt: hasCustomExcerpt\n        };\n\n        if(this.postImage) {\n            this.post.featuredImage = this.getPostFeaturedImages(this.post.id, true);\n        }\n\n        if(this.tags) {\n            this.tags = this.tags.map(tag => {\n                return {\n                    id: 0,\n                    name: tag,\n                    slug: '',\n                    description: 'It is an example description for the preview mode',\n                    additionalData: '',\n                    postsNumber: 0,\n                    url: '#'\n                };\n            });\n\n            this.tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name));\n        }\n\n        this.metaTitle = 'It is an example value for the preview mode';\n        this.metaDescription = 'It is an example value for the preview mode';\n        this.metaRobots = 'It is an example value for the preview mode';\n        this.post.tags = this.tags;\n\n        // load related posts\n        this.loadRelatedPosts();\n\n        // load previous and next posts (only on the visible posts)\n        let renderPrevNextPosts = RendererHelpers.getRendererOptionValue('renderPrevNextPosts', this.themeConfig);\n        \n        if(!renderPrevNextPosts || this.post.status.indexOf('hidden') > -1) {\n            this.nextPost = false;\n            this.previousPost = false;\n        } else {\n            if(this.renderer.postData.id !== 0) {\n                this.loadPost('next', false);\n            }\n\n            this.loadPost('previous', false);\n        }\n\n        // load previous and next similar posts (only on the visible posts)\n        let renderSimilarPosts = RendererHelpers.getRendererOptionValue('renderSimilarPosts', this.themeConfig);\n\n        if(!renderSimilarPosts || this.post.status.indexOf('hidden') > -1) {\n            this.nextSimilarPost = false;\n            this.previousSimilarPost = false;\n        } else {\n            if(this.renderer.postData.id !== 0) {\n                this.loadPost('next', true);\n            }\n\n            this.loadPost('previous', true);\n        }\n    }\n\n    loadPost(type, similarPost = false) {\n        let postType = similarPost ? type + 'SimilarPost' : type + 'Post';\n        let operator = type === 'previous' ? '<=' : '>=';\n        let postData = false;\n        let temporaryPostsOrdering = this.postsOrdering;\n\n        // For the next posts we have to reverse the results\n        if(type === 'next') {\n            if(temporaryPostsOrdering.indexOf('ASC') > -1) {\n                temporaryPostsOrdering = temporaryPostsOrdering.replace('ASC', 'DESC');\n            } else {\n                temporaryPostsOrdering = temporaryPostsOrdering.replace('DESC', 'ASC');\n            }\n        }\n\n        this.post.createdAt = parseInt(this.post.createdAt, 10);\n        this.post.id = parseInt(this.post.id, 10);\n\n        let tagsCondition = '';\n\n        if(similarPost) {\n            let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : [];\n\n            if(tags.length) {\n                tagsCondition = ' AND pt.tag_id IN(' + tags.join(',') + ') ';\n            }\n\n            // Retrieve post\n            postData = this.db.prepare(`\n                SELECT\n                    p.id AS id\n                FROM\n                    posts AS p\n                LEFT JOIN\n                    posts_tags AS pt\n                    ON\n                    p.id = pt.post_id\n                WHERE\n                    p.created_at ${operator} ${this.post.createdAt} AND\n                    p.id != @postID AND\n                    p.status LIKE '%published%' AND\n                    p.status NOT LIKE '%trashed%' AND\n                    p.status NOT LIKE '%is-page%' AND\n                    p.status NOT LIKE '%hidden%'\n                    ${tagsCondition}\n                GROUP BY\n                    p.id\n                ORDER BY\n                    ${temporaryPostsOrdering}\n                LIMIT 1\n            `).get({\n                postID: this.post.id\n            });\n        } else {\n            // Retrieve post\n            postData = this.db.prepare(`\n                SELECT\n                    id\n                FROM\n                    posts\n                WHERE\n                    created_at ${operator} ${this.post.createdAt} AND\n                    id != @postID AND\n                    status LIKE '%published%' AND\n                    status NOT LIKE '%trashed%' AND\n                    status NOT LIKE '%is-page%' AND\n                    status NOT LIKE '%hidden%'\n                ORDER BY\n                    ${temporaryPostsOrdering}\n                LIMIT 1\n            `).get({\n                postID: this.post.id\n            });\n        }\n\n        if(!postData || !postData.id) {\n            return false;\n        }\n\n        this[postType] = this.renderer.cachedItems.posts[postData.id];\n    }\n\n    loadRelatedPosts() {\n        let renderRelatedPosts = RendererHelpers.getRendererOptionValue('renderRelatedPosts', this.themeConfig);\n        let relatedPostsNumberFromConfig = RendererHelpers.getRendererOptionValue('relatedPostsNumber', this.themeConfig);\n\n        if (!renderRelatedPosts || relatedPostsNumberFromConfig === 0) {\n            this.relatedPosts = [];\n            return;\n        }\n\n        this.post.id = parseInt(this.post.id, 10);\n        let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : [];\n        let relatedPostsNumber = 5;\n        let tagsCondition = '';\n        let postTitleConditions = [];\n        let conditions = [];\n\n        if (relatedPostsNumberFromConfig) {\n            relatedPostsNumber = relatedPostsNumberFromConfig;\n        }\n\n        // Get tags\n        if(tags.length) {\n            tagsCondition = ' pt.tag_id IN(' + tags.join(',') + ') ';\n            conditions.push(tagsCondition);\n        }\n\n        // Get words to compare (with length bigger than 3 chars)\n        let stringsToCompare = this.post.title.split(' ');\n        stringsToCompare = stringsToCompare.filter(word => word.length > 3);\n\n        if(stringsToCompare.length) {\n            for (let toCompare of stringsToCompare) {\n                postTitleConditions.push(' LOWER(p.title) LIKE LOWER(\\'%' + sqlString.escape(toCompare).replace(/'/g, '').replace(/\"/g, '') + '%\\') ')\n            }\n\n            postTitleConditions = '(' + postTitleConditions.join('OR') + ')';\n            conditions.push(postTitleConditions);\n        }\n\n        if(conditions.length > 1) {\n            conditions = conditions.join(' OR ');\n            conditions = ' ( ' + conditions + ' ) ';\n        }\n\n        if(conditions.length) {\n            conditions = ' AND ' + conditions;\n        }\n\n        // Retrieve post\n        let postsData = this.db.prepare(`\n            SELECT\n                p.id AS id\n            FROM\n                posts AS p\n            LEFT JOIN\n                posts_tags AS pt\n                ON\n                p.id = pt.post_id\n            WHERE\n                p.id != @postID AND\n                p.status LIKE '%published%' AND\n                p.status NOT LIKE '%trashed%' AND\n                p.status NOT LIKE '%is-page%' AND\n                p.status NOT LIKE '%hidden%'\n                ${conditions}\n            GROUP BY\n                p.id\n            LIMIT @relatedPostsNumber\n        `).all({\n            postID: this.post.id,\n            relatedPostsNumber: relatedPostsNumber\n        });\n\n        this.relatedPosts = [];\n\n        if(!postsData || !postsData.length) {\n            return false;\n        }\n\n        for(let i = 0; i < postsData.length; i++) {\n            this.relatedPosts[i] = this.renderer.cachedItems.posts[postsData[i].id];\n        }\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.metaRobots;\n\n        if(this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            post: this.post,\n            tags: this.allTags,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            previousPost: this.previousPost,\n            nextPost: this.nextPost,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus.assigned\n        };\n    }\n\n    getContext(postID) {\n        this.postID = postID;\n        this.setContext();\n\n        return this.context;\n    }\n\n    getPostFeaturedImages(postID, mainPost = false) {\n        let postImage = false;\n\n        // Retrieve post image\n        if(mainPost === true) {\n            postImage = {\n                id: 0,\n                url: this.renderer.postData.featuredImageFilename,\n                additional_data: JSON.stringify(this.renderer.postData.featuredImageData)\n            };\n        } else {\n            postImage = this.db.prepare(`\n                SELECT\n                    pi.id AS id,\n                    pi.url AS url,\n                    pi.additional_data AS additional_data\n                FROM\n                    posts as p\n                LEFT JOIN\n                    posts_images as pi\n                    ON\n                    p.featured_image_id = pi.id\n                WHERE\n                    p.id = @postID\n                ORDER BY\n                    pi.id DESC\n                LIMIT 1\n            `).get({\n                postID: postID\n            });\n        }\n\n        if (postImage && postImage.url) {\n            let url = '';\n            let alt = '';\n            let caption = '';\n            let credits = '';\n            let imageDimensions = false;\n\n            if (postImage.additional_data) {\n                let data = JSON.parse(postImage.additional_data);\n                let postDirectory = postID;\n\n                if(postDirectory === 0) {\n                    postDirectory = 'temp';\n                }\n\n                let imagePath = URLHelper.createImageURL(this.inputDir, postDirectory, postImage.url);\n                let domain = this.siteConfig.domain;\n\n                url = URLHelper.createImageURL(domain, postDirectory, postImage.url);\n                alt = data.alt;\n                caption = data.caption;\n                credits = data.credits;\n                try {\n                    imageDimensions = sizeOf(imagePath);\n                } catch(e) {\n                    console.log('post-preview.js: wrong image path - missing dimensions');\n                    imageDimensions = false;\n                }\n            } else {\n                return false;\n            }\n\n            let featuredImageSrcSet = false;\n            let featuredImageSizes = false;\n\n            if(!this.isGifOrSvg(url)) {\n                let useWebp = false;\n\n                if (this.renderer.siteConfig?.advanced?.forceWebp) {\n                    useWebp = true;\n                }\n\n                featuredImageSrcSet = ContentHelper.getFeaturedImageSrcset(url, this.themeConfig, useWebp);\n                featuredImageSizes = ContentHelper.getFeaturedImageSizes(this.themeConfig);\n            } else {\n                featuredImageSrcSet = '';\n                featuredImageSizes = '';\n            }\n\n            let featuredImageData = {\n                id: postImage.id,\n                url: url,\n                alt: alt,\n                caption: caption,\n                credits: credits,\n                height: imageDimensions.height,\n                width: imageDimensions.width,\n                srcset: featuredImageSrcSet,\n                sizes: featuredImageSizes\n            };\n\n            // Create alternative names for dimensions\n            let dimensions = false;\n\n            if (\n                this.themeConfig.files &&\n                this.themeConfig.files.responsiveImages\n            ) {\n                if (\n                    this.themeConfig.files.responsiveImages.featuredImages &&\n                    this.themeConfig.files.responsiveImages.featuredImages.dimensions\n                ) {\n                    dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions;\n                } else if (\n                    this.themeConfig.files.responsiveImages.contentImages &&\n                    this.themeConfig.files.responsiveImages.contentImages.dimensions\n                ) {\n                    dimensions = this.themeConfig.files.responsiveImages.featuredImages.dimensions;\n                }\n\n                if (dimensions) {\n                    let dimensionNames = Object.keys(dimensions);\n\n                    for (let dimensionName of dimensionNames) {\n                        let base = path.parse(url).base;\n                        let filename = path.parse(url).name;\n                        let extension = path.parse(url).ext;\n                        let newFilename = filename + '-' + dimensionName + extension;\n                        let capitalizedDimensionName = dimensionName.charAt(0).toUpperCase() + dimensionName.slice(1);\n\n                        if(!this.isGifOrSvg(url)) {\n                            featuredImageData['url' + capitalizedDimensionName] = url.replace(base, newFilename);\n                        } else {\n                            featuredImageData['url' + capitalizedDimensionName] = url;\n                        }\n                    }\n                }\n            }\n\n            return featuredImageData;\n        }\n\n        return false;\n    }\n\n    prepareContent(originalText, postID) {\n        let self = this;\n        let domain = normalizePath(self.siteConfig.domain);\n        domain = URLHelper.fixProtocols(domain);\n\n        // Get media URL\n        let postDirectory = postID;\n\n        if(postDirectory === 0) {\n            postDirectory = 'temp';\n        }\n\n        let domainMediaPath = domain + '/media/posts/' + postDirectory + '/';\n\n        // Replace domain name constat with real URL to media directory\n        let preparedText = originalText.split('#DOMAIN_NAME#').join(domainMediaPath);\n        preparedText = ContentHelper.parseText(preparedText, this.editor);\n\n        // Remove TOC plugin ID attributes when TOC does not exist\n        if (preparedText.indexOf('class=\"post__toc') === -1) {\n            preparedText = preparedText.replace(/\\sid=\"mcetoc_[a-z0-9]*?\"/gmi, ''); \n        }\n\n        // Reduce download=\"download\" to download\n        preparedText = preparedText.replace(/download=\"download\"/gmi, 'download');\n\n        // Remove content for AMP or non-AMP depending from ampMode value\n        preparedText = preparedText.replace(/<publii-amp>.*<\\/publii-amp>/gmi, '');\n        preparedText = preparedText.replace(/<publii-non-amp>/gmi, '');\n        preparedText = preparedText.replace(/<\\/publii-non-amp>/gmi, '');\n\n        // Remove read more text\n        preparedText = preparedText.replace(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi, '');\n\n        // Remove the last empty paragraph\n        preparedText = preparedText.replace(/<p>&nbsp;<\\/p>\\s?$/gmi, '');\n\n        let useWebp = false;\n\n        if (this.renderer.siteConfig?.advanced?.forceWebp) {\n            useWebp = true;\n        }\n\n        // Find all images and add srcset and sizes attributes\n        if (this.siteConfig.responsiveImages) {\n            preparedText = preparedText.replace(/<img.*?src=\"(.*?)\"/gmi, function(matches, url) {\n                if(\n                    ContentHelper.getContentImageSrcset(url, self.themeConfig, useWebp) !== false &&\n                    !(\n                        url.toLowerCase().indexOf('.jpg') === -1 &&\n                        url.toLowerCase().indexOf('.jpeg') === -1 &&\n                        url.toLowerCase().indexOf('.png') === -1 && \n                        url.toLowerCase().indexOf('.webp') === -1\n                    ) &&\n                    url.toLowerCase().indexOf('/gallery/') === -1\n                ) {\n                    if(ContentHelper.getContentImageSizes(self.themeConfig)) {\n                        return matches +\n                            ' sizes=\"' + ContentHelper.getContentImageSizes(self.themeConfig) + '\"' +\n                            ' srcset=\"' + ContentHelper.getContentImageSrcset(url, self.themeConfig, useWebp) + '\" ';\n                    } else {\n                        return matches +\n                            ' srcset=\"' + ContentHelper.getContentImageSrcset(url, self.themeConfig, useWebp) + '\" ';\n                    }\n                } else {\n                    return matches;\n                }\n            });\n        }\n\n        // Add loading=\"lazy\" attributes to img, video, audio, iframe tags\n        if (self.siteConfig.advanced.mediaLazyLoad) {\n            preparedText = preparedText.replace(/<img\\s/gmi, '<img loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<video\\s/gmi, '<video loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<audio\\s/gmi, '<audio loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<iframe\\s/gmi, '<iframe loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<img\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<img$1');\n            preparedText = preparedText.replace(/<video\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<video$1');\n            preparedText = preparedText.replace(/<audio\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<audio$1');\n            preparedText = preparedText.replace(/<iframe\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<iframe$1');\n        }\n\n        if (this.editor === 'tinymce' || this.editor === 'markdown') {\n            // Wrap images with classes into <figure>\n            preparedText = preparedText.replace(/(<p.*?>\\s*?)?<img[^>]*?(class=\".*?\").*?>(\\s*?<\\/p>)?/gmi, function(matches, p1, classes) {\n                return '<figure ' + classes + '>' + matches.replace('</p>', '').replace(/<p.*?>/, '').replace(classes, '') + '</figure>';\n            });\n\n            // Fix some specific syntax cases for double figure elements\n            preparedText = preparedText.replace(/<figure contenteditable=\"false\">[\\s]*?<figure class=\"post__image\">([\\s\\S]*?)<\\/figure>[\\s]*?<\\/figure>/gmi, '<figure class=\"post__image\">$1</figure>');\n            preparedText = preparedText.replace(/<figure contenteditable=\"false\">[\\s]*?<figure class=\"post__image\">([\\s\\S]*?)<\\/figure>[\\s]*?<figcaption contentEditable=\"true\">([\\s\\S]*?)<\\/figcaption>[\\s]*?<\\/figure>/gmi, '<figure class=\"post__image\">$1<figcaption>$2</figcaption></figure>');\n        }\n\n        // Remove contenteditable attributes\n        preparedText = preparedText.replace(/contentEditable=\".*?\"/gi, '');\n\n        if (this.editor === 'tinymce') {\n            // Wrap galleries with classes into div with gallery-wrapper CSS class\n            preparedText = preparedText.replace(/<div class=\"gallery([\\s\\S]*?)\"[\\s\\S]*?<\\/div>?/gmi, function(matches, classes) {\n                return '<div class=\"gallery-wrapper' + classes + '\">' + matches.replace(classes, '') + '</div>';\n            });\n        }\n\n        // Remove paragraphs around <iframe>'s\n        preparedText = preparedText.replace(/\\<p\\>\\<iframe/gmi, '<iframe');\n        preparedText = preparedText.replace(/\\<\\/iframe\\>\\<\\/p\\>/gmi, '</iframe>');\n\n        // Wrap iframes into <div class=\"post__iframe\">\n        preparedText = preparedText.replace(/(?<!<figure[\\s\\S]*?class=\"post__video\">[\\s\\S]*?)(<iframe.*?>[\\s\\S]*?<\\/iframe>)/gmi, function(matches) {\n            if (matches.indexOf('data-responsive=\"false\"') > -1) {\n                return matches;\n            }\n            \n            return '<div class=\"post__iframe\">' + matches + '</div>';\n        });\n\n        // Remove CDATA sections inside scripts added by TinyMCE\n        preparedText = preparedText.replace(/\\<script\\>\\/\\/ \\<\\!\\[CDATA\\[/g, '<script>');\n        preparedText = preparedText.replace(/\\/\\/ \\]\\]\\>\\<\\/script\\>/g, '</script>');\n\n        return preparedText;\n    }\n\n    /**\n     * Detects if image is a GIF or SVG\n     */\n    isGifOrSvg(url) {\n        if(url.slice(-4) === '.gif' || url.slice(-4) === '.svg') {\n            return true;\n        }\n\n        return false;\n    }\n}\n\nmodule.exports = RendererContextPostPreview;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/post.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\nconst RendererHelpers = require('./../helpers/helpers.js');\nconst sqlString = require('sqlstring');\nconst stripTags = require('striptags');\n\n/**\n * Class used create context\n * for the single post theme views\n */\nclass RendererContextPost extends RendererContext {\n    loadData() {\n        // Retrieve meta data\n        let metaDataQuery = this.db.prepare(`SELECT value FROM posts_additional_data WHERE post_id = @postID AND key = '_core'`);\n        this.metaData = metaDataQuery.get({ postID: this.postID});\n        this.allTags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.homepage;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n        this.pages = this.renderer.commonData.pages;\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.allTags = this.allTags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    prepareData() {\n        this.post = this.renderer.cachedItems.posts[this.postID];\n        this.post.tags = this.post.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n        this.metaTitle = this.siteConfig.advanced.postMetaTitle;\n        this.metaDescription = this.siteConfig.advanced.postMetaDescription;\n        this.canonicalUrl = this.post.url;\n        this.hasCustomCanonicalUrl = false;\n        this.metaRobots = '';\n\n        if (this.siteConfig.advanced.postMetaDescription === '') {\n            this.metaDescription = stripTags(this.post.excerpt).replace(/\\n/gmi, '');\n        }\n\n        if(this.metaData && this.metaData.value) {\n            let results = JSON.parse(this.metaData.value);\n\n            if (results.metaTitle) {\n                this.metaTitle = results.metaTitle;\n            }\n\n            if (results.metaDesc) {\n                this.metaDescription = results.metaDesc;\n            }\n\n            if (results.metaRobots) {\n                this.metaRobots = results.metaRobots;\n            }\n\n            if (results.canonicalUrl) {\n                this.canonicalUrl = results.canonicalUrl;\n                this.hasCustomCanonicalUrl = true;\n                this.metaRobots = '';\n            }\n        }\n\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n        }\n\n        // load related posts\n        this.loadRelatedPosts();\n\n        // load previous and next posts (only on the visible posts)\n        let renderPrevNextPosts = RendererHelpers.getRendererOptionValue('renderPrevNextPosts', this.themeConfig);\n        \n        if(!renderPrevNextPosts || this.post.status.indexOf('hidden') > -1) {\n            this.nextPost = false;\n            this.previousPost = false;\n        } else {\n            this.loadPost('next', false);\n            this.loadPost('previous', false);\n        }\n\n        // load previous and next similar posts (only on the visible posts)\n        let renderSimilarPosts = RendererHelpers.getRendererOptionValue('renderSimilarPosts', this.themeConfig);\n\n        if(!renderSimilarPosts || this.post.status.indexOf('hidden') > -1) {\n            this.nextSimilarPost = false;\n            this.previousSimilarPost = false;\n        } else {\n            this.loadPost('next', true);\n            this.loadPost('previous', true);\n        }\n    }\n\n    loadPost(type, similarPost = false) {\n        let postType = similarPost ? type + 'SimilarPost' : type + 'Post';\n        let operator = type === 'previous' ? '<=' : '>=';\n        let postData = false;\n        let temporaryPostsOrdering = this.postsOrdering;\n\n        // Reverse operator when post ordering is reversed\n        if(temporaryPostsOrdering.indexOf('ASC') > -1) {\n            if(operator === '>=') {\n                operator = '<=';\n            } else {\n                operator = '>=';\n            }\n        }\n\n        // For the next posts we have to reverse the results\n        if(type === 'next') {\n            if(temporaryPostsOrdering.indexOf('ASC') > -1) {\n                temporaryPostsOrdering = temporaryPostsOrdering.replace('ASC', 'DESC');\n            } else {\n                temporaryPostsOrdering = temporaryPostsOrdering.replace('DESC', 'ASC');\n            }\n        }\n\n        this.post.createdAt = parseInt(this.post.createdAt, 10);\n        this.post.id = parseInt(this.post.id, 10);\n\n        let tagsCondition = '';\n        let sortColumn = this.siteConfig.advanced.postsListingOrderBy;\n\n        if (typeof sortColumn !== 'string') {\n            sortColumn = 'created_at';\n        }\n\n        let sortField = sortColumn;\n\n        if (sortColumn === 'modified_at') {\n            sortField = 'modifiedAt';\n        } else if (sortColumn === 'created_at') {\n            sortField = 'createdAt';\n        }\n\n        if(similarPost) {\n            let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : [];\n\n            if(tags.length) {\n                tagsCondition = ' AND pt.tag_id IN(' + tags.join(',') + ') ';\n            }\n\n            let sortCondition = `p.${sortColumn} ${operator} ${this.post[sortField]}`;\n\n            if (sortColumn === 'title') {\n                sortCondition = `p.${sortColumn} ${operator} \"${this.post[sortField].replace(/\"/gmi, '')}\"`;\n            }\n\n            // Retrieve post\n            postData = this.db.prepare(`\n                SELECT\n                    p.id AS id\n                FROM\n                    posts AS p\n                LEFT JOIN\n                    posts_tags AS pt\n                    ON\n                    p.id = pt.post_id\n                WHERE\n                    ${sortCondition} AND\n                    p.id != @postID AND\n                    p.status LIKE '%published%' AND\n                    p.status NOT LIKE '%trashed%'AND\n                    p.status NOT LIKE '%is-page%' AND\n                    p.status NOT LIKE '%hidden%'\n                    ${tagsCondition}\n                GROUP BY\n                    p.id\n                ORDER BY\n                    ${temporaryPostsOrdering}\n                LIMIT 1\n            `).get({\n                postID: this.post.id\n            });\n        } else {\n            // Retrieve post\n            let sortCondition = `${sortColumn} ${operator} ${this.post[sortField]}`;\n\n            if (sortColumn === 'title') {\n                sortCondition = `${sortColumn} ${operator} \"${this.post[sortField].replace(/\"/gmi, '')}\"`;\n            }\n\n            try {\n                postData = this.db.prepare(`\n                    SELECT\n                        id\n                    FROM\n                        posts\n                    WHERE\n                        ${sortCondition} AND\n                        id != @postID AND\n                        status LIKE '%published%' AND\n                        status NOT LIKE '%trashed%' AND\n                        status NOT LIKE '%is-page%' AND\n                        status NOT LIKE '%hidden%'\n                    ORDER BY\n                        ${temporaryPostsOrdering}\n                    LIMIT 1\n                `).get({\n                    postID: this.post.id\n                });\n            } catch (err) {\n                console.log('ERR', err);\n            }\n        }\n\n        if(!postData || !postData.id) {\n            return false;\n        }\n\n        this[postType] = this.renderer.cachedItems.posts[postData.id];\n    }\n\n    loadRelatedPosts() {\n        let renderRelatedPosts = RendererHelpers.getRendererOptionValue('renderRelatedPosts', this.themeConfig);\n        let relatedPostsNumberFromConfig = RendererHelpers.getRendererOptionValue('relatedPostsNumber', this.themeConfig);\n\n        if (!renderRelatedPosts || relatedPostsNumberFromConfig === 0) {\n            this.relatedPosts = [];\n            return;\n        }\n\n        this.post.id = parseInt(this.post.id, 10);\n        let tags = this.post.tags ? this.post.tags.map(tag => parseInt(tag.id, 10)) : [];\n        let relatedPostsNumber = 5;\n        let postTitleConditions = [];\n        let conditions = [];\n        let conditionsLowerPriority = [];\n\n        if (relatedPostsNumberFromConfig) {\n            relatedPostsNumber = relatedPostsNumberFromConfig;\n        }\n\n        // Get tags\n        if(tags.length) {\n            conditions.push(' pt.tag_id IN(' + tags.join(',') + ') ');\n            conditionsLowerPriority.push(' pt.tag_id NOT IN(' + tags.join(',') + ') ');\n        }\n\n        // Get words to compare (with length bigger than 3 chars)\n        if (['titles', 'titles-and-tags'].indexOf(this.siteConfig.advanced.relatedPostsCriteria) > -1) {\n            let stringsToCompare = this.post.title.split(' ');\n            stringsToCompare = stringsToCompare.filter(word => word.length > 3);\n\n            if(stringsToCompare.length) {\n                for (let toCompare of stringsToCompare) {\n                    postTitleConditions.push(' LOWER(p.title) LIKE LOWER(\\'%' + sqlString.escape(toCompare).replace(/'/g, '').replace(/\"/g, '') + '%\\') ')\n                }\n\n                postTitleConditions = '(' + postTitleConditions.join('OR') + ')';\n                conditions.push(postTitleConditions);\n                conditionsLowerPriority.push(postTitleConditions);\n            }\n        }\n\n        if(conditions.length > 1) {\n            conditions = conditions.join(' AND ');\n            conditions = ' ( ' + conditions + ' ) ';\n        }\n\n        if(conditions.length) {\n            conditions = ' AND ' + conditions;\n        }\n\n        if(conditionsLowerPriority.length > 1) {\n            conditionsLowerPriority = conditionsLowerPriority.join(' AND ');\n            conditionsLowerPriority = ' ( ' + conditionsLowerPriority + ' ) ';\n        }\n\n        if(conditionsLowerPriority.length) {\n            conditionsLowerPriority = ' AND ' + conditionsLowerPriority;\n        }\n\n        // Get related posts ordering\n        let ordering = ' subquery.id DESC ';\n\n        if (this.siteConfig.advanced.relatedPostsOrder === 'id-asc') {\n            ordering = ' subquery.id ASC ';\n        } else if (this.siteConfig.advanced.relatedPostsOrder === 'random') {\n            ordering = ' RANDOM() ';\n        }\n\n        // Use second query\n        let secondQuery = '';\n\n        if (this.siteConfig.advanced.relatedPostsIncludeAllPosts) {\n            secondQuery = `\n                UNION\n                SELECT\n                    p.id AS id,\n                    2 AS priority\n                FROM\n                    posts AS p\n                LEFT JOIN\n                    posts_tags AS pt\n                    ON\n                    p.id = pt.post_id\n                WHERE\n                    p.id != @postID AND\n                    p.status LIKE '%published%' AND\n                    p.status NOT LIKE '%trashed%' AND\n                    p.status NOT LIKE '%is-page%' AND\n                    p.status NOT LIKE '%hidden%'\n                    ${conditionsLowerPriority}\n            `;\n        }\n\n        // Retrieve post\n        let postsData = this.db.prepare(`\n            SELECT \n                subquery.id AS id\n            FROM\n                (\n                    SELECT\n                        p.id AS id,\n                        1 AS priority\n                    FROM\n                        posts AS p\n                    LEFT JOIN\n                        posts_tags AS pt\n                        ON\n                        p.id = pt.post_id\n                    WHERE\n                        p.id != @postID AND\n                        p.status LIKE '%published%' AND\n                        p.status NOT LIKE '%trashed%' AND\n                        p.status NOT LIKE '%is-page%' AND\n                        p.status NOT LIKE '%hidden%'\n                        ${conditions}\n                    ${secondQuery}\n                    GROUP BY\n                        p.id\n                    ORDER BY\n                        priority ASC\n                    LIMIT @relatedPostsNumber\n                ) AS subquery\n            ORDER BY\n                ${ordering}\n        `).all({\n            postID: this.post.id,\n            relatedPostsNumber: relatedPostsNumber * 2\n        });\n\n        this.relatedPosts = [];\n\n        if (!postsData || !postsData.length) {\n            return false;\n        }\n\n        postsData = postsData.map(postData => postData.id);\n        postsData = [...new Set(postsData)]; \n\n        if (postsData.length > relatedPostsNumber) {\n            postsData = postsData.slice(0, relatedPostsNumber);\n        }\n\n        for(let i = 0; i < postsData.length; i++) {\n            this.relatedPosts[i] = this.renderer.cachedItems.posts[postsData[i]];\n        }\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.metaRobots;\n\n        if(this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        // Detect if the post title is empty\n        if(this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.postMetaTitle.replace(/%posttitle/g, this.post.title)\n                                                                   .replace(/%sitename/g, siteName)\n                                                                   .replace(/%authorname/g, this.post.author.name);\n        } else {\n            this.metaTitle = this.metaTitle.replace(/%posttitle/g, this.post.title)\n                                           .replace(/%sitename/g, siteName)\n                                           .replace(/%authorname/g, this.post.author.name);\n        }\n\n        // If still meta title is empty - use post title\n        if(this.metaTitle === '') {\n            this.metaTitle = this.post.title;\n        }\n\n        this.metaDescription = this.metaDescription.replace(/%posttitle/g, this.post.title)\n                                                    .replace(/%sitename/g, siteName)\n                                                    .replace(/%authorname/g, this.post.author.name);\n\n        this.context = {\n            title: this.metaTitle,\n            post: this.post,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            relatedPosts: this.relatedPosts,\n            tags: this.allTags,\n            mainTags: this.mainTags,\n            authors: this.authors,\n            pages: this.pages,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            hasCustomCanonicalUrl: this.hasCustomCanonicalUrl,\n            canonicalUrl: this.canonicalUrl,\n            previousPost: this.previousPost,\n            previousSimilarPost: this.previousSimilarPost,\n            nextPost: this.nextPost,\n            nextSimilarPost: this.nextSimilarPost,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    getContext(postID) {\n        this.postID = postID;\n        this.setContext();\n\n        return this.context;\n    }\n}\n\nmodule.exports = RendererContextPost;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/search.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\n\n/**\n * Class used create context\n * for the search theme view\n */\n\nclass RendererContextSearch extends RendererContext {\n    /**\n     * Loading data used in the view\n     */\n    loadData() {\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        this.metaTitle = this.siteConfig.advanced.searchMetaTitle.replace(/%sitename/g, siteName);\n        this.metaDescription = this.siteConfig.advanced.searchMetaDescription.replace(/%sitename/g, siteName);\n\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n        }\n\n        this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.homepage;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n        this.pages = this.renderer.commonData.pages;\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.tags = this.tags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    /**\n     * Preparing the loaded data\n     */\n    prepareData() {\n        this.title = this.siteConfig.name;\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n    }\n\n    /**\n     * Setting context for the view\n     */\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.siteConfig.advanced.metaRobotsSearch;\n\n        if (this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            tags: this.tags,\n            mainTags: this.mainTags,\n            authors: this.authors,\n            pages: this.pages,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    /**\n     * Getting context for the view\n     *\n     * @returns {object} - context for the view\n     */\n    getContext() {\n        this.setContext();\n\n        return this.context;\n    }\n}\n\nmodule.exports = RendererContextSearch;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/tag.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\nconst RendererHelpers = require('./../helpers/helpers.js');\n\n/**\n * Class used create context\n * for the single tag theme views\n */\n\nclass RendererContextTag extends RendererContext {\n    loadData() {\n        // Prepare query data\n        this.postsNumber = parseInt(this.postsNumber, 10);\n        this.offset = parseInt(this.offset, 10);\n        // Retrieve tag data\n        this.tag = this.renderer.cachedItems.tags[this.tagID];\n\n        // Retrieve post\n        let includeFeaturedPosts = '';\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('tagsIncludeFeaturedInPosts', this.themeConfig) === false;\n\n        if (shouldSkipFeaturedPosts) {\n            includeFeaturedPosts = 'p.status NOT LIKE \\'%featured%\\' AND';\n        }\n\n        if(this.postsNumber === -1) {\n            this.postsNumber = 999;\n        }\n\n        if(this.postsNumber === 0) {\n            this.posts = false;\n        } else {\n            this.posts = this.db.prepare(`\n                SELECT\n                    id\n                FROM\n                    posts AS p\n                LEFT JOIN\n                    posts_tags AS pt\n                ON\n                    p.id = pt.post_id\n                WHERE\n                    p.status LIKE '%published%' AND\n                    p.status NOT LIKE '%hidden%' AND\n                    p.status NOT LIKE '%trashed%' AND\n                    p.status NOT LIKE '%is-page%' AND\n                    ${includeFeaturedPosts}\n                    pt.tag_id = @tagID\n                ORDER BY\n                    ${this.postsOrdering}\n                LIMIT\n                    @postsNumber\n                OFFSET\n                    @offset\n            `).all({\n                tagID: this.tagID,\n                postsNumber: this.postsNumber,\n                offset: this.offset\n            });\n        }\n\n        this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.tag;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n        this.pages = this.renderer.commonData.pages;\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.tags = this.tags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    prepareData() {\n        let siteName = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        this.title = this.siteConfig.advanced.tagMetaTitle\n                                                    .replace(/%tagname/g, this.tag.name)\n                                                    .replace(/%sitename/g, siteName);\n\n        this.posts = this.posts || [];\n        this.posts = this.posts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('tagsIncludeFeaturedInPosts', this.themeConfig) === false;\n        let featuredPostsNumber = RendererHelpers.getRendererOptionValue('tagsFeaturedPostsNumber', this.themeConfig);\n\n        // Remove featured posts from posts if featured posts allowed\n        if (shouldSkipFeaturedPosts && (featuredPostsNumber > 0 || featuredPostsNumber === -1)) {\n            let featuredPostsIds = this.featuredPosts.map(post => post.id);\n            this.posts = this.posts.filter(post => featuredPostsIds.indexOf(post.id) === -1);\n        }\n\n        // Prepare meta data\n        this.metaTitle = this.siteConfig.advanced.tagMetaTitle.replace(/%tagname/g, this.tag.name)\n                                                              .replace(/%sitename/g, siteName);\n        this.metaDescription = this.siteConfig.advanced.tagMetaDescription.replace(/%tagname/g, this.tag.name)\n                                                                          .replace(/%sitename/g, siteName);\n        this.metaRobots = false;\n        this.hasCustomCanonicalUrl = false;\n        this.canonicalUrl = '';\n\n        let metaData = this.tag.additionalData;\n\n        if (metaData && metaData.metaTitle) {\n            this.metaTitle = metaData.metaTitle.replace(/%tagname/g, this.tag.name)\n                                               .replace(/%sitename/g, siteName);\n        }\n\n        if(metaData && metaData.metaDescription) {\n            this.metaDescription = metaData.metaDescription.replace(/%tagname/g, this.tag.name)\n                                                           .replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n        }\n\n        if (metaData && metaData.metaRobots) {\n            this.metaRobots = metaData.metaRobots;\n        }\n\n        if (metaData && metaData.canonicalUrl) {\n            this.canonicalUrl = metaData.canonicalUrl;\n            this.hasCustomCanonicalUrl = true;\n            this.metaRobots = '';\n        }\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.siteConfig.advanced.metaRobotsTags;\n\n        if (this.metaRobots !== false) {\n            metaRobotsValue = this.metaRobots;\n        }\n\n        if (this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            tag: this.tag,\n            posts: this.posts,\n            pages: this.pages,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            tags: this.tags,\n            mainTags: this.mainTags,\n            authors: this.authors,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            hasCustomCanonicalUrl: this.hasCustomCanonicalUrl,\n            canonicalUrl: this.canonicalUrl,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    getContext(tagID, offset = 0, postsNumber = 999) {\n        this.offset = offset;\n        this.postsNumber = postsNumber;\n        this.tagID = tagID;\n        this.setContext();\n\n        return this.context;\n    }\n}\n\nmodule.exports = RendererContextTag;\n"
  },
  {
    "path": "app/back-end/modules/render-html/contexts/tags.js",
    "content": "// Necessary packages\nconst RendererContext = require('../renderer-context.js');\n\n/**\n * Class used create context\n * for the tags list theme view\n */\nclass RendererContextTags extends RendererContext {\n    loadData() {\n        this.tags = this.renderer.commonData.tags.filter(tag => tag.additionalData.isHidden !== true);\n        this.mainTags = this.renderer.commonData.mainTags.filter(maintag => maintag.additionalData.isHidden !== true);\n        this.menus = this.renderer.commonData.menus;\n        this.unassignedMenus = this.renderer.commonData.unassignedMenus;\n        this.authors = this.renderer.commonData.authors;\n        this.featuredPosts = this.renderer.commonData.featuredPosts.tag;\n        this.hiddenPosts = this.renderer.commonData.hiddenPosts;\n        this.pages = this.renderer.commonData.pages;\n\n        // mark tags as main tags\n        let mainTagsIds = this.mainTags.map(tag => tag.id);\n        this.tags = this.tags.map(tag => {\n            tag.isMainTag = mainTagsIds.includes(tag.id);\n            return tag;\n        });\n    }\n\n    prepareData() {\n        let siteName = this.siteConfig.name;\n\n        if (this.siteConfig.displayName) {\n            siteName = this.siteConfig.displayName;\n        }\n\n        this.title = this.siteConfig.advanced.tagsMetaTitle.replace(/%sitename/g, siteName);\n        this.featuredPosts = this.featuredPosts || [];\n        this.featuredPosts = this.featuredPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.hiddenPosts = this.hiddenPosts || [];\n        this.hiddenPosts = this.hiddenPosts.map(post => this.renderer.cachedItems.posts[post.id]);\n        this.pages = this.pages || [];\n        this.pages = this.pages.map(page => this.renderer.cachedItems.pages[page.id]);\n\n        // Prepare meta data\n        this.metaTitle = this.siteConfig.advanced.tagsMetaTitle.replace(/%sitename/g, siteName);\n        this.metaDescription = this.siteConfig.advanced.tagsMetaDescription.replace(/%sitename/g, siteName);\n\n        if (this.metaTitle === '') {\n            this.metaTitle = this.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteName);\n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription.replace(/%sitename/g, siteName);\n        }\n    }\n\n    setContext() {\n        this.loadData();\n        this.prepareData();\n\n        let metaRobotsValue = this.siteConfig.advanced.metaRobotsTagsList;\n\n        if (this.siteConfig.advanced.noIndexThisPage) {\n            metaRobotsValue = 'noindex,nofollow';\n        }\n\n        this.context = {\n            title: this.metaTitle !== '' ? this.metaTitle : this.title,\n            featuredPosts: this.featuredPosts,\n            hiddenPosts: this.hiddenPosts,\n            pages: this.pages,\n            tags: this.tags,\n            tagsNumber: this.tags.length,\n            mainTags: this.mainTags,\n            authors: this.authors,\n            metaTitleRaw: this.metaTitle,\n            metaDescriptionRaw: this.metaDescription,\n            metaRobotsRaw: metaRobotsValue,\n            siteOwner: this.renderer.cachedItems.authors[1],\n            menus: this.menus,\n            unassignedMenus: this.unassignedMenus\n        };\n    }\n\n    getContext () {\n        this.setContext();\n        return this.context;\n    }\n}\n\nmodule.exports = RendererContextTags;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/_modules.js",
    "content": "/*\n * Module which loads all Handlebar's helpers\n */\nmodule.exports = {\n    assetHelper: require('./asset.js').assetHelper,\n    CSSHelper: require('./css.js'),\n    fontHelper: require('./font.js').fontHelper,\n    dateHelper: require('./date.js'),\n    is: require('./is.js'),\n    isNot: require('./is-not.js'),\n    isCurrentPage: require('./is-current-page.js'),\n    encodeUrl: require('./encode-url.js'),\n    encodeUrlFragment: require('./encode-url-fragment.js'),\n    JSHelper: require('./js.js'),\n    metaDescription: require('./meta-description.js'),\n    metaRobotsHelper: require('./meta-robots.js'),\n    pageURLHelper: require('./page-url.js'),\n    menuURLHelper: require('./menu-url.js'),\n    menuItemClassesHelper: require('./menu-item-classes.js'),\n    feedLinkHelper: require('./feed-link.js').feedLinkHelper,\n    socialMetaTagsHelper: require('./social-meta-tags.js'),\n    publiiHeadHelper: require('./publii-head.js'),\n    publiiFooterHelper: require('./publii-footer.js'),\n    gdprScriptBlockerHelper: require('./gdpr-script-blocker.js'),\n    checkIf: require('./check-if.js'),\n    checkIfAny: require('./check-if-any.js'),\n    checkIfAll: require('./check-if-all.js'),\n    checkIfNone: require('./check-if-none.js'),\n    isEmpty: require('./is-empty.js'),\n    isNotEmpty: require('./is-not-empty.js'),\n    jsonLDHelper: require('./json-ld.js'),\n    canonicalLinkHelper: require('./canonical-link.js'),\n    imageDimensionsHelper: require('./image-dimensions.js'),\n    responsiveSrcSetHelper: require('./responsive-srcset.js').responsiveSrcSetHelper,\n    responsiveSizesHelper: require('./responsive-sizes.js').responsiveSizesHelper,\n    responsiveImageAttributesHelper: require('./responsive-image-attributes.js'),\n    translateHelper: require('./translate.js').translateHelper,\n    math: require('./math.js'),\n    jsonify: require('./jsonify.js'),\n    reverse: require('./reverse.js'),\n    orderby: require('./orderby.js'),\n    getPageHelper: require('./get-page.js'),\n    getPagesHelper: require('./get-pages.js'),\n    getPagesByCustomFieldHelper: require('./get-pages-by-custom-field.js'),\n    getPostHelper: require('./get-post.js'),\n    getPostsHelper: require('./get-posts.js'),\n    getPostByTagsHelper: require('./get-post-by-tags.js'),\n    getPostsByTagsHelper: require('./get-posts-by-tags.js'),\n    getPostsByCustomFieldsHelper: require('./get-posts-by-custom-field.js'),\n    getTagHelper: require('./get-tag.js'),\n    getTagsHelper: require('./get-tags.js'),\n    concatenate: require('./concatenate.js'),\n    contains: require('./contains.js'),\n    join: require('./join.js'),\n    lazyloadHelper: require('./lazyload.js'),\n    getAuthorHelper: require('./get-author.js'),\n    getAuthorsHelper: require('./get-authors.js')\n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/asset.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper function for generating asset URL based on a given path\n *\n * @param {string} path\n *\n * @returns {string} URL to the assets directory\n */\n\nfunction asset(filePath) {\n    let url = [\n        this.siteConfig.domain,\n        this.themeConfig.files.assetsPath,\n        filePath\n    ].join('/');\n    \n    url = Handlebars.Utils.escapeExpression(url);\n\n    return new Handlebars.SafeString(url);\n}\n\n/**\n * Helper for loading asset files\n *\n * @returns {string} URL to the asset\n */\nfunction assetHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('asset', asset.bind(rendererInstance));\n}\n\nmodule.exports = {\n    assetHelper: assetHelper,\n    __asset: asset\n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/canonical-link.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for creating canonical link\n *\n * {{canonicalLink}}\n *\n * @returns {string} - <link> element with canonical URL\n */\nfunction canonicalLinkHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('canonicalLink', function (context) {\n        // If current page is not indexed - skip canonical link\n        if ((context.data.root.metaRobotsRaw.indexOf('noindex') > -1 || context.data.root.metaRobotsRaw.indexOf('nofollow') > -1) && !context.data.root.hasCustomCanonicalUrl) {\n            return '';\n        }\n\n        if (\n            Array.isArray(context.data.context) &&\n            context.data.context[0] && (\n                (\n                    rendererInstance.siteConfig.advanced.homepageNoIndexPagination &&\n                    context.data.context.indexOf('index-pagination') !== -1\n                ) || (\n                    rendererInstance.siteConfig.advanced.tagNoIndexPagination &&\n                    context.data.context.indexOf('tag-pagination') !== -1\n                ) || (\n                    rendererInstance.siteConfig.advanced.authorNoIndexPagination &&\n                    context.data.context.indexOf('author-pagination') !== -1\n                )\n            )\n        ) {\n            return '';\n        }\n\n        let pageUrl = context.data.website.pageUrl;\n\n        // If current page is a post - check for canonical URL\n        if (context.data.root.canonicalUrl) {\n            pageUrl = context.data.root.canonicalUrl;\n        }\n\n        // Remove index.html from the end of URL\n        pageUrl = pageUrl.replace(/index\\.html$/, '', pageUrl);\n\n        // Add trailing slash if not exists\n        if(pageUrl[pageUrl.length - 1] !== '/' && pageUrl.substr(-5) !== '.html') {\n            pageUrl = pageUrl + '/';\n        }\n\n        let output = '<link rel=\"canonical\" href=\"' + pageUrl + '\">';\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = canonicalLinkHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/check-if-all.js",
    "content": "/**\n * Helper for creating conditions where all\n * elements are connected using AND operator\n *\n * {{#checkIfAll value1 value2 ... valueN}}\n *    ...\n * {{/checkIfAll}}\n *\n * @returns {callback} - true if all provided values are true otherwhise false\n */\nfunction checkIfAll() {\n    let args = Array.from(arguments);\n    let options = args.pop();\n    let allLength = args.length;\n    let result = args.filter(argument => !!argument === true);\n\n    if(result.length > 0 && result.length === allLength) {\n        return options.fn(this);\n    }\n\n    return options.inverse(this);\n}\n\nmodule.exports = checkIfAll;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/check-if-any.js",
    "content": "/**\n * Helper for creating conditions where all\n * elements are connected using OR operator\n *\n * {{#checkIfAny value1 value2 ... valueN}}\n *    ...\n * {{/checkIfAny}}\n *\n * @returns {callback} -  true if at least one of the provided values is true otherwhise false\n */\nfunction checkIfAny() {\n    let args = Array.from(arguments);\n    let options = args.pop();\n    let result = args.filter(argument => !!argument == true);\n\n    if(result.length > 0) {\n        return options.fn(this);\n    }\n\n    return options.inverse(this);\n}\n\nmodule.exports = checkIfAny;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/check-if-none.js",
    "content": "/**\n * Helper for creating conditions where all\n * elements are connected using AND operator\n * and are all false\n *\n * {{#checkIfNone value1 value2 ... valueN}}\n *    ...\n * {{/checkIfNone}}\n *\n * @returns {callback|boolean} - true if any of the provided values is true otherwhise false\n */\nfunction checkIfNone() {\n    let args = Array.from(arguments);\n    let options = args.pop();\n    let result = args.filter(argument => !!argument === true);\n\n    if(result.length === 0) {\n        return options.fn(this);\n    }\n\n    return options.inverse(this);\n}\n\nmodule.exports = checkIfNone;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/check-if.js",
    "content": "/**\n * Helper for creating conditions\n *\n * {{#checkIf author '&&' avatar}}\n *    ...\n * {{/checkIf}}\n *\n * Available operators:\n * '==', '!=', '===', '!==', '&&', '||', '<', '<=', '>', '>=',\n * 'and', 'or', 'equal', 'strictEqual', 'different', 'strictDifferent', 'lesser', 'lesserEqual', 'greater', 'greaterEqual', 'contains', 'notContains'\n *\n * @returns {callback} \n */\nfunction checkIf(v1, operator, v2, options) {\n    if(v1 === undefined || v2 === undefined) {\n        return;\n    }\n\n    switch (operator) {\n        case \"&&\":\n        case \"and\":\n            return (v1 && v2) ? options.fn(this) : options.inverse(this);\n        case \"||\":\n        case \"or\":\n            return (v1 || v2) ? options.fn(this) : options.inverse(this);\n        case \"==\":\n        case \"equal\":\n            return (v1 == v2) ? options.fn(this) : options.inverse(this);\n        case \"===\":\n        case \"strictEqual\":\n            return (v1 === v2) ? options.fn(this) : options.inverse(this);\n        case \"!=\":\n        case \"different\":\n            return (v1 != v2) ? options.fn(this) : options.inverse(this);\n        case \"!==\":\n        case \"strictDifferent\":\n            return (v1 !== v2) ? options.fn(this) : options.inverse(this);\n        case \"<\":\n        case \"lesser\":\n            return (v1 < v2) ? options.fn(this) : options.inverse(this);\n        case \">\":\n        case \"greater\":\n            return (v1 > v2) ? options.fn(this) : options.inverse(this);\n        case \"<=\":\n        case \"lesserEqual\":\n            return (v1 <= v2) ? options.fn(this) : options.inverse(this);\n        case \">=\":\n        case \"greaterEqual\":\n            return (v1 >= v2) ? options.fn(this) : options.inverse(this);\n        case \"contains\": \n            if (typeof v1 !== 'string') {\n                return;\n            }\n\n            if (typeof v2 === 'number') {\n                return (v1.split(',').map(v1item => +v1item).indexOf(v2) > -1) ? options.fn(this) : options.inverse(this);    \n            } \n\n            return (v1.split(',').indexOf(v2) > -1) ? options.fn(this) : options.inverse(this);\n        case \"notContains\": \n            if (typeof v1 !== 'string') {\n                return;\n            }\n\n            if (typeof v2 === 'number') {\n                return (v1.split(',').map(v1item => +v1item).indexOf(v2) === -1) ? options.fn(this) : options.inverse(this);    \n            } \n\n            return (v1.split(',').indexOf(v2) === -1) ? options.fn(this) : options.inverse(this);\n    }\n\n    return;\n}\n\nmodule.exports = checkIf;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/concatenate.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for concatenating values\n *\n * {{concatenate 'abc' 3 'def'}}\n *\n * @returns {callback}\n */\nfunction concatenate () {\n    let inputs = Array.from(arguments);\n    inputs.pop();\n\n    return inputs.join('');\n}\n\nmodule.exports = concatenate;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/contains.js",
    "content": "/**\n * Helper for checking if a specifc value is inside comma-separated string\n *\n * {{#contains 'abc' 'abc,def'}}\n *\n * @returns {callback}\n */\nfunction contains (needle, haystack, options) {\n    if (needle === undefined || haystack === undefined) {\n        return;\n    }\n\n    if (typeof haystack === 'object' && haystack.string) {\n        haystack = haystack.string;\n    }\n\n    haystack = haystack.split(',');\n\n    if (typeof needle === 'number') {\n        haystack = haystack.map(n => parseInt(n, 10));\n    }\n\n    if (haystack.indexOf(needle) > -1) {\n        return options.fn(this);\n    }\n\n    return options.inverse(this)\n}\n\nmodule.exports = contains;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/css.js",
    "content": "const fs = require('fs');\nconst FileHelper = require('./../../../../helpers/file.js');\nconst crypto = require('crypto');\nconst path = require('path');\nconst memoize = require('fast-memoize');\n\n/**\n *\n * Helper function used to calculate MD5 sum of the given file contents\n *\n * @param {string} localPath - path to the file\n * @param {string} overridedLocalPath - path to the overrided version of the file\n *\n * @returns {string} - MD5 sum based on the given file contents\n */\nfunction getMD5(localPath, overridedLocalPath) {\n    let fileContent = '';\n    \n    if (fs.existsSync(overridedLocalPath)) {\n        fileContent = FileHelper.readFileSync(overridedLocalPath);\n    } else {\n        fileContent = FileHelper.readFileSync(localPath);\n    }\n\n    return crypto.createHash('md5').update(fileContent).digest('hex');\n}\n\nconst memoizedMD5 = memoize(getMD5);\n\n/**\n * Helper for loading CSS files from the assets directory\n *\n * It also adds MD5 sum hash as a v= param for preventing browser cache\n *\n * {{css \"filepath.css\"}}\n *\n * @returns {string} path to the CSS file with assigned v= param based on the file MD5 sum\n */\nfunction CSSHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('css', function (filename) {\n        let md5Sum = '';\n        let localPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase(), rendererInstance.themeConfig.files.assetsPath, 'css', filename);\n        let overridedLocalPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase() + '-override', rendererInstance.themeConfig.files.assetsPath, 'css', filename);\n        let versionSuffix = '';\n\n        if (rendererInstance.siteConfig.advanced.versionSuffix) {\n            md5Sum = memoizedMD5(localPath, overridedLocalPath);\n            versionSuffix = '?v=' + md5Sum;\n        }\n\n        let url = [\n            rendererInstance.siteConfig.domain,\n            rendererInstance.themeConfig.files.assetsPath,\n            'css',\n            filename + versionSuffix\n        ].join('/');\n\n        return new Handlebars.SafeString(url);\n    });\n}\n\nmodule.exports = CSSHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/date.js",
    "content": "const Handlebars = require('handlebars');\nconst moment = require('moment');\n\n/**\n * Helper for creating customized date output\n *\n * {{date timestamp \"format\"}}\n * \n * {{date timestamp \"format\" true}}\n * \n * {{date timestamp \"format\" false \"en\"}}\n *\n * Format compatible with moment().js: http://momentjs.com/\n * \n * The last param allows us to not return safe string which can be helpful for time comparision\n *\n * @returns {string} - date formatted using specified format\n *\n */\nfunction dateHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('date', function (timestamp, dateFormat, returnRawText = false, overrideDateLanguage = false) {\n        // If date format is not a string - then it is empty or is an object with\n        // options for the helper\n        if(typeof dateFormat !== 'string') {\n            dateFormat = 'MMM Do YYYY';\n        }\n\n        let originalMomentLanguage = moment.locale();\n\n        if (overrideDateLanguage) {\n            moment.locale(overrideDateLanguage);\n        }\n\n        if(!rendererInstance.siteConfig.language || (rendererInstance.siteConfig.domain !== '' && rendererInstance.siteConfig.domain !== 'en-us')) {\n            moment.locale(rendererInstance.siteConfig.language);\n        }\n\n        let output = moment(timestamp).format(dateFormat);\n\n        if (overrideDateLanguage) {\n            moment.locale(originalMomentLanguage);\n        }\n\n        if (returnRawText) {\n            return output;\n        }\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = dateHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/encode-url-fragment.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for creating encoded output inside URLs\n *\n * {{encodeUrlFragment \"text\"}}\n *\n * @returns {string} - text prepared to be displayed as a part of URLs\n */\nfunction encodeUrlFragment(text) {\n    let output = encodeURIComponent(text);\n\n    return new Handlebars.SafeString(output);\n}\n\nmodule.exports = encodeUrlFragment;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/encode-url.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for creating encoded output of URLs\n *\n * Useful for the social media scripts where you have to put\n * full URL to your website as an URL param\n *\n * {{encodeUrl \"text\"}}\n *\n * @returns {string} - text prepared to be displayed as an URL inside other URLs\n */\nfunction encodeUrl(text) {\n    let output = encodeURI(text);\n\n    return new Handlebars.SafeString(output);\n}\n\nmodule.exports = encodeUrl;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/feed-link.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for creating link element with feed URL (for RSS and JSON)\n *\n * {{feedLink}}\n *\n * @returns {string} <link> elements with URL to the RSS/JSON feeds\n */\nfunction feedLink() {\n    let output = '';\n\n    if (!this.siteConfig.deployment || !this.siteConfig.deployment.relativeUrls) {\n        let feedTitle = this.siteConfig.displayName;\n\n        if (\n            this.siteConfig.advanced && \n            this.siteConfig.advanced.feed && \n            this.siteConfig.advanced.feed.title === 'customTitle'\n        ) {\n            feedTitle = this.siteConfig.advanced.feed.titleValue;\n        }\n\n        let rssFeedTitle = '';\n        let jsonFeedTitle = '';\n\n        if (feedTitle) {\n            rssFeedTitle = 'title=\"' + Handlebars.Utils.escapeExpression(feedTitle) + ' - RSS\"';\n            jsonFeedTitle = 'title=\"' + Handlebars.Utils.escapeExpression(feedTitle) + ' - JSON\"';\n        }\n\n        if (this.siteConfig.advanced.feed.enableRss) {\n            let rssUrl = Handlebars.Utils.escapeExpression(this.siteConfig.domain + '/feed.xml');\n            output += '<link rel=\"alternate\" type=\"application/atom+xml\" href=\"' + rssUrl + '\" ' + rssFeedTitle + '>' + \"\\n\";\n        }\n\n        if (this.siteConfig.advanced.feed.enableJson) {\n            let jsonUrl = Handlebars.Utils.escapeExpression(this.siteConfig.domain + '/feed.json');\n            output += '<link rel=\"alternate\" type=\"application/json\" href=\"' + jsonUrl + '\" ' + jsonFeedTitle + '>' + \"\\n\";\n        }\n    }\n\n    return new Handlebars.SafeString(output);\n}\n\nfunction feedLinkHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('feedLink', feedLink.bind(rendererInstance));\n}\n\nmodule.exports = {\n    __feedLink: feedLink,\n    feedLinkHelper: feedLinkHelper\n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/font.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper function for generating Google Fonts API URL based on a given font\n *\n * @param {string} path to the Google Font font\n *\n * @returns {string} URL to the Google Fonts API for a given font\n */\n\nfunction font(path) {\n    let url = 'https://fonts.googleapis.com/css?family=' + path;\n    url = Handlebars.Utils.escapeExpression(url);\n\n    return new Handlebars.SafeString(url);\n}\n\n/**\n * Helper for loading font files from the Google Fonts directory\n *\n * {{font @config.visual.font}}\n *\n * @returns {string} URL to the Google Fonts API for a given font\n */\nfunction fontHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('font', font.bind(rendererInstance));\n}\n\nmodule.exports = {\n    fontHelper: fontHelper,\n    __font: font\n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/gdpr-script-blocker.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for creating proper script type value according to the\n *\n * {{gdprScriptBlocker \"group-name\"}}\n *\n * @returns {string} type attribute value with a proper value if GDRP is enabled or not\n */\nfunction gdprScriptBlockerHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('gdprScriptBlocker', function (groupName) {\n        let output  = 'text/javascript';\n\n        if (rendererInstance.siteConfig.advanced.gdpr.enabled) {\n            output = 'gdpr-blocker/' + groupName;\n        }\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = gdprScriptBlockerHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-author.js",
    "content": "/**\n * Helper for loading author data\n *\n * {{#getAuthor AUTHOR_ID}}\n *    <p>{{ name }}</p>\n * {{/getAuthor}}\n * \n * {{#getAuthor \"AUTHOR_USERNAME\"}}\n *    <p>{{ name }}</p>\n * {{/getAuthor}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\n function getAuthorHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getAuthor', function (selectedAuthor, options) {\n        if (!rendererInstance.contentStructure.authors) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let authorData; \n        \n        if (typeof selectedAuthor === 'number') {\n            authorData = rendererInstance.contentStructure.authors.filter(author => author.id === selectedAuthor);\n        } else {\n            authorData = rendererInstance.contentStructure.authors.filter(author => author.username === selectedAuthor);\n        }\n\n        if(!authorData.length) {\n            return '';\n        }\n\n        return options.fn(authorData[0]);\n    });\n}\n\nmodule.exports = getAuthorHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-authors.js",
    "content": "/**\n * Helper for loading authors data\n *\n * {{#getAuthors \"AUTHOR_ID_1,AUTHOR_ID_2,AUTHOR_ID_N\" \"prefix\" \"suffix\"}}\n * <li>{{ name }}</li>\n * {{/getAuthors}}\n *\n * Authors are ordered by the ID order in the string.\n *\n * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output.\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\n function getAuthorsHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getAuthors', function (authorsIDs, prefix, suffix, options) {\n        if (!rendererInstance.contentStructure.authors) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let content = '';\n        authorsIDs = authorsIDs.split(',').map(n => parseInt(n, 10));\n\n        for (let i = 0; i < authorsIDs.length; i++) {\n            let authorData = rendererInstance.contentStructure.authors.filter(author => author.id === authorsIDs[i]);\n\n            if (authorData.length) {\n                options.data.index = i;\n                content += options.fn(authorData[0]);\n            }\n        }\n\n        if(content === '') {\n            return '';\n        }\n\n        content = [prefix, content, suffix].join('');\n\n        return content;\n    });\n}\n\nmodule.exports = getAuthorsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-page.js",
    "content": "/**\n * Helper for loading page data\n *\n * {{#getPage PAGE_ID}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPage}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPageHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPage', function (pageID, options) {\n        if (!rendererInstance.contentStructure.pages) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let pageData = rendererInstance.contentStructure.pages.filter(page => page.id === pageID);\n\n        if(!pageData.length) {\n            return '';\n        }\n\n        return options.fn(pageData[0]);\n    });\n}\n\nmodule.exports = getPageHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-pages-by-custom-field.js",
    "content": "/**\n * Helper for loading pages data which contains a specific custom fields\n * \n * QueryString options:\n * * count - how many pages should be included in the result\n * * customField - which custom field should be used\n * * customFieldValue - which value of custom field is expected\n * * customFieldCompare - default: 'equals', other available values: 'not-equals', 'greater', 'greater-equals', 'lesser', 'lesser-equals' (for numeric values), 'starts-with', 'ends-with', 'contains', 'not-contains' (for string values)\n * * excluded - which pages should be excluded\n * * offset - how many pages to skip\n * * orderby - order field or customField\n * * ordering - order direction - asc, desc, random\n * * orderbyCompareLanguage - if orderby=customField, you can specify in which language ordering will be done.\n * \n * {{#getPagesByCustomFields \"count=5&customField=test&customFieldValue=10&customFieldCompare=not-equals&excluded=1,2&offset=10&orderby=modified_at&ordering=desc\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPagesByCustomFields}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPagesByCustomFieldsHelper (rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPagesByCustomFields', function (queryString, options) {\n        if (!rendererInstance.contentStructure.pages) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let count;\n        let offset = 0;\n        let orderby = false;\n        let ordering = 'desc';\n        let customField = false;\n        let customFieldValue = false;\n        let customFieldCompare = 'equals';\n        let compareLanguage = false;\n\n        queryString = queryString.split('&').map(pair => pair.split('='));\n        let queryStringData = {};\n\n        for (let i = 0; i < queryString.length; i++) {\n            let key = queryString[i][0];\n            let value = queryString[i][1];\n            queryStringData[key] = value;\n        }\n\n        if (queryStringData['count']) {\n            count = parseInt(queryStringData['count'], 10);\n\n            if (count === -1) {\n                count = 999;\n            }\n        }\n\n        if (queryStringData['excluded']) {\n            excludedPages = queryStringData['excluded'];\n        }\n\n        if (queryStringData['customField']) {\n            customField = queryStringData['customField'];\n        }\n\n        if (queryStringData['customFieldValue']) {\n            customFieldValue = queryStringData['customFieldValue'];\n        }\n\n        if (queryStringData['customFieldCompare']) {\n            customFieldCompare = queryStringData['customFieldCompare'];\n        }\n\n        if (queryStringData['offset']) {\n            offset = parseInt(queryStringData['offset']);\n        }\n\n        if (queryStringData['orderby']) {\n            orderby = queryStringData['orderby'];\n        }\n\n        if (queryStringData['ordering']) {\n            ordering = queryStringData['ordering'];\n        }\n\n        if (queryStringData['orderbyCompareLanguage']) {\n            compareLanguage = queryStringData['orderbyCompareLanguage'];\n        }\n\n        let pagesData;\n        let content = '';\n        let filteredPages = JSON.parse(JSON.stringify(rendererInstance.contentStructure.pages));\n\n        if (typeof excludedPages === 'number' || (typeof excludedPages === 'string' && excludedPages !== '')) {\n            if (typeof excludedPages === 'number') {\n                let excludedPage = excludedPages;\n                filteredPages = filteredPages.filter(page => page.id !== excludedPage)\n            } else {\n                excludedPages = excludedPages.split(',').map(n => parseInt(n, 10));\n                filteredPages = filteredPages.filter(page => excludedPages.indexOf(page.id) === -1);\n            }\n        }\n\n        filteredPages = filteredPages.filter(page => {\n            if (!page.pageViewConfig[customField]) {\n                return false;\n            }\n\n            switch (customFieldCompare) {\n                case 'equals': \n                    return page.pageViewConfig[customField] == customFieldValue;\n                case 'not-equals': \n                    return page.pageViewConfig[customField] != customFieldValue;\n                case 'greater': \n                    return parseInt(page.pageViewConfig[customField], 10) > parseInt(customFieldValue, 10);\n                case 'greater-equals': \n                    return parseInt(page.pageViewConfig[customField], 10) >= parseInt(customFieldValue, 10);\n                case 'lesser': \n                    return parseInt(page.pageViewConfig[customField], 10) < parseInt(customFieldValue, 10);\n                case 'lesser-equals': \n                    return parseInt(page.pageViewConfig[customField], 10) <= parseInt(customFieldValue, 10);\n                case 'starts-with': \n                    return page.pageViewConfig[customField].indexOf(customFieldValue) === 0;\n                case 'ends-with': \n                    return page.pageViewConfig[customField].lastIndexOf(customFieldValue) === page.pageViewConfig[customField].length - customFieldValue.length;\n                case 'contains': \n                    return page.pageViewConfig[customField].indexOf(customFieldValue) !== -1;\n                case 'not-contains': \n                    return page.pageViewConfig[customField].indexOf(customFieldValue) === -1;\n            }\n        });\n\n        pagesData = filteredPages;\n\n        if (orderby && ordering && ordering !== 'random') {\n            pagesData.sort((itemA, itemB) => {\n                if (orderby === 'customField') {\n                    if (isNaN(itemA.pageViewConfig[customField]) && isNaN(itemB.pageViewConfig[customField])) {\n                        if (ordering === 'asc') {\n                            if (compareLanguage) {\n                                return itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField], compareLanguage);\n                            } else {\n                                return itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField]);\n                            }\n                        } else {\n                            if (compareLanguage) {\n                                return -(itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField], compareLanguage));\n                            } else {\n                                return -(itemA.pageViewConfig[customField].localeCompare(itemB.pageViewConfig[customField]));\n                            }\n                        }\n                    } else {\n                        if (ordering === 'asc') {\n                            return parseInt(itemA.pageViewConfig[customField], 10) - parseInt(itemB.pageViewConfig[customField], 10);\n                        } else {\n                            return parseInt(itemB.pageViewConfig[customField], 10) - parseInt(itemA.pageViewConfig[customField], 10);\n                        }\n                    }\n                }\n\n                if (orderby !== 'customField') {\n                    if(typeof itemA[orderby] === 'string') {\n                        if (ordering === 'asc') {\n                            return itemA[orderby].localeCompare(itemB[orderby]);\n                        } else {\n                            return -(itemA[orderby].localeCompare(itemB[orderby]));\n                        }\n                    } else {\n                        if (ordering === 'asc') {\n                            return itemA[orderby] - itemB[orderby];\n                        } else {\n                            return itemB[orderby] - itemA[orderby];\n                        }\n                    }\n                }\n            });\n        } else if (ordering === 'random') {\n            pagesData.sort(() => 0.5 - Math.random());\n        }\n\n        for (let i = offset; i < count + offset; i++) {\n            if (pagesData.length >= i + 1) {\n                options.data.index = i;\n                content += options.fn(pagesData[i]);\n            } else {\n                break;\n            }\n        }\n\n        if (content === '') {\n            return '';\n        }\n\n        return content;\n    });\n}\n\nmodule.exports = getPagesByCustomFieldsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-pages.js",
    "content": "/**\n * Helper for loading posts data\n *\n * {{#getPages \"PAGE_ID_1,PAGE_ID_2,PAGE_ID_N\" \"prefix\" \"suffix\"}}\n * <article>\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * </article>\n * {{/getPages}}\n *\n * Pages are ordered by the ID order in the string.\n *\n * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output.\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPagesHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPages', function (pageIDs, prefix, suffix, options) {\n        if (!rendererInstance.contentStructure.pages) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let content = '';\n        pageIDs = pageIDs.split(',').map(n => parseInt(n, 10));\n\n        for (let i = 0; i < pageIDs.length; i++) {\n            let pageData = rendererInstance.contentStructure.pages.filter(page => page.id === pageIDs[i]);\n\n            if (pageData.length) {\n                options.data.index = i;\n                content += options.fn(pageData[0]);\n            }\n        }\n\n        if (content === '') {\n            return '';\n        }\n\n        content = [prefix, content, suffix].join('');\n\n        return content;\n    });\n}\n\nmodule.exports = getPagesHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-post-by-tags.js",
    "content": "/**\n * Helper for loading single post data which contains a specific tag(s) - specified by tag ID or tag slugs separated by comma\n *\n * Get post which contains a tag with given ID\n * \n * {{#getPostByTags TAG_ID1 \"\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostByTags}}\n * \n * Get post which contains one of the given tag slugs\n * \n * {{#getPostByTags \"TAG_SLUG1,TAG_SLUG2\" \"\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostByTags}}\n * \n * Get post which contains one of the given tag slugs excluding posts with ID equal to 1 or 2\n * \n * {{#getPostByTags \"TAG_SLUG1,TAG_SLUG2\" \"1,2\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostByTags}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPostByTagsHelper (rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPostByTags', function (selectedTags, excludedPosts, options) {\n        if (!rendererInstance.contentStructure.posts) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let postData;\n        let filteredPosts = JSON.parse(JSON.stringify(rendererInstance.contentStructure.posts));\n\n        if (typeof excludedPosts === 'number' || typeof excludedPosts === 'string' && excludedPosts !== '') {\n            if (typeof excludedPosts === 'number') {\n                let excludedPost = excludedPosts;\n                filteredPosts = filteredPosts.filter(post => post.id !== excludedPost)\n            } else {\n                excludedPosts = excludedPosts.split(',').map(n => parseInt(n, 10));\n                filteredPosts = filteredPosts.filter(post => excludedPosts.indexOf(post.id) === -1);\n            }\n        }\n\n        if (typeof selectedTags === 'number') {\n            let tagID = selectedTags;\n            postData = filteredPosts.filter(post => post.tags.filter(tag => tag.id === tagID).length || post.hiddenTags.filter(tag => tag.id === tagID).length);            \n        } else {\n            let tagsSlugs = selectedTags.split(',');\n            postData = filteredPosts.filter(post => post.tags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length || post.hiddenTags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length);\n        }\n\n        if(!postData.length) {\n            return '';\n        }\n\n        return options.fn(postData[0]);\n    });\n}\n\nmodule.exports = getPostByTagsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-post.js",
    "content": "/**\n * Helper for loading post data\n *\n * {{#getPost POST_ID}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPost}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPostHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPost', function (postID, options) {\n        if (!rendererInstance.contentStructure.posts) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let postData = rendererInstance.contentStructure.posts.filter(post => post.id === postID);\n\n        if(!postData.length) {\n            return '';\n        }\n\n        return options.fn(postData[0]);\n    });\n}\n\nmodule.exports = getPostHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-posts-by-custom-field.js",
    "content": "/**\n * Helper for loading posts data which contains a specific custom fields\n * \n * QueryString options:\n * * count - how many posts should be included in the result\n * * allowed - which post statuses should be included\n * * customField - which custom field should be used\n * * customFieldValue - which value of custom field is expected\n * * customFieldCompare - default: 'equals', other available values: 'not-equals', 'greater', 'greater-equals', 'lesser', 'lesser-equals' (for numeric values), 'starts-with', 'ends-with', 'contains', 'not-contains' (for string values)\n * * excluded - which posts should be excluded\n * * excluded_status - which posts statuses should be excluded\n * * offset - how many posts to skip\n * * orderby - order field or customField\n * * ordering - order direction - asc, desc, random\n * * orderbyCompareLanguage - if orderby=customField, you can specify in which language ordering will be done.\n * \n * {{#getPostsByCustomFields \"count=5&allowed=hidden,featured&customField=test&customFieldValue=10&customFieldCompare=not-equals&excluded=1,2&offset=10&orderby=modified_at&ordering=desc\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostsByCustomFields}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPostsByCustomFieldsHelper (rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPostsByCustomFields', function (queryString, options) {\n        if (!rendererInstance.contentStructure.posts) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let count;\n        let offset = 0;\n        let allowedStatus = 'any';\n        let excludedStatus = '';\n        let orderby = false;\n        let ordering = 'desc';\n        let customField = false;\n        let customFieldValue = false;\n        let customFieldCompare = 'equals';\n        let compareLanguage = false;\n\n        queryString = queryString.split('&').map(pair => pair.split('='));\n        let queryStringData = {};\n\n        for (let i = 0; i < queryString.length; i++) {\n            let key = queryString[i][0];\n            let value = queryString[i][1];\n            queryStringData[key] = value;\n        }\n\n        if (queryStringData['count']) {\n            count = parseInt(queryStringData['count'], 10);\n\n            if (count === -1) {\n                count = 999;\n            }\n        }\n\n        if (queryStringData['excluded']) {\n            excludedPosts = queryStringData['excluded'];\n        }\n\n        if (queryStringData['excluded_status']) {\n            excludedStatus = queryStringData['excluded_status'];\n        }\n\n        if (queryStringData['customField']) {\n            customField = queryStringData['customField'];\n        }\n\n        if (queryStringData['customFieldValue']) {\n            customFieldValue = queryStringData['customFieldValue'];\n        }\n\n        if (queryStringData['customFieldCompare']) {\n            customFieldCompare = queryStringData['customFieldCompare'];\n        }\n\n        if (queryStringData['allowed']) {\n            allowedStatus = queryStringData['allowed'];\n        }\n\n        if (queryStringData['offset']) {\n            offset = parseInt(queryStringData['offset']);\n        }\n\n        if (queryStringData['orderby']) {\n            orderby = queryStringData['orderby'];\n        }\n\n        if (queryStringData['ordering']) {\n            ordering = queryStringData['ordering'];\n        }\n\n        if (queryStringData['orderbyCompareLanguage']) {\n            compareLanguage = queryStringData['orderbyCompareLanguage'];\n        }\n\n        let postsData;\n        let content = '';\n        let filteredPosts = JSON.parse(JSON.stringify(rendererInstance.contentStructure.posts));\n\n        if (typeof excludedPosts === 'number' || (typeof excludedPosts === 'string' && excludedPosts !== '')) {\n            if (typeof excludedPosts === 'number') {\n                let excludedPost = excludedPosts;\n                filteredPosts = filteredPosts.filter(post => post.id !== excludedPost)\n            } else {\n                excludedPosts = excludedPosts.split(',').map(n => parseInt(n, 10));\n                filteredPosts = filteredPosts.filter(post => excludedPosts.indexOf(post.id) === -1);\n            }\n        }\n\n        if (allowedStatus !== 'any') {\n            allowedStatus = allowedStatus.split(',');\n\n            if (excludedStatus !== '') {\n                excludedStatus = excludedStatus.split(',');\n            }\n\n            filteredPosts = filteredPosts.filter(post => {\n                if (excludedStatus) {\n                    for (let i = 0; i < excludedStatus.length; i++) {\n                        if (post.status.indexOf(excludedStatus[i]) > -1) {\n                            return false;\n                        }\n                    }\n                }\n                \n                for (let i = 0; i < allowedStatus.length; i++) {\n                    if (post.status.indexOf(allowedStatus[i]) > -1) {\n                        return true;\n                    }\n                }\n\n                return false;\n            });\n        } else if (allowedStatus === 'any' && excludedStatus !== '') {\n            excludedStatus = excludedStatus.split(',');\n\n            filteredPosts = filteredPosts.filter(post => {\n                for (let i = 0; i < excludedStatus.length; i++) {\n                    if (post.status.indexOf(excludedStatus[i]) > -1) {\n                        return false;\n                    }\n                }\n\n                return true;\n            });\n        }\n\n        filteredPosts = filteredPosts.filter(post => {\n            if (!post.postViewConfig[customField]) {\n                return false;\n            }\n\n            switch (customFieldCompare) {\n                case 'equals': \n                    return post.postViewConfig[customField] == customFieldValue;\n                case 'not-equals': \n                    return post.postViewConfig[customField] != customFieldValue;\n                case 'greater': \n                    return parseInt(post.postViewConfig[customField], 10) > parseInt(customFieldValue, 10);\n                case 'greater-equals': \n                    return parseInt(post.postViewConfig[customField], 10) >= parseInt(customFieldValue, 10);\n                case 'lesser': \n                    return parseInt(post.postViewConfig[customField], 10) < parseInt(customFieldValue, 10);\n                case 'lesser-equals': \n                    return parseInt(post.postViewConfig[customField], 10) <= parseInt(customFieldValue, 10);\n                case 'starts-with': \n                    return post.postViewConfig[customField].indexOf(customFieldValue) === 0;\n                case 'ends-with': \n                    return post.postViewConfig[customField].lastIndexOf(customFieldValue) === post.postViewConfig[customField].length - customFieldValue.length;\n                case 'contains': \n                    return post.postViewConfig[customField].indexOf(customFieldValue) !== -1;\n                case 'not-contains': \n                    return post.postViewConfig[customField].indexOf(customFieldValue) === -1;\n            }\n        });\n\n        postsData = filteredPosts;\n\n        if (orderby && ordering && ordering !== 'random') {\n            postsData.sort((itemA, itemB) => {\n                if (orderby === 'customField') {\n                    if (isNaN(itemA.postViewConfig[customField]) && isNaN(itemB.postViewConfig[customField])) {\n                        if (ordering === 'asc') {\n                            if (compareLanguage) {\n                                return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage);\n                            } else {\n                                return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField]);\n                            }\n                        } else {\n                            if (compareLanguage) {\n                                return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage));\n                            } else {\n                                return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField]));\n                            }\n                        }\n                    } else {\n                        if (ordering === 'asc') {\n                            return parseInt(itemA.postViewConfig[customField], 10) - parseInt(itemB.postViewConfig[customField], 10);\n                        } else {\n                            return parseInt(itemB.postViewConfig[customField], 10) - parseInt(itemA.postViewConfig[customField], 10);\n                        }\n                    }\n                }\n\n                if (orderby !== 'customField') {\n                    if(typeof itemA[orderby] === 'string') {\n                        if (ordering === 'asc') {\n                            return itemA[orderby].localeCompare(itemB[orderby]);\n                        } else {\n                            return -(itemA[orderby].localeCompare(itemB[orderby]));\n                        }\n                    } else {\n                        if (ordering === 'asc') {\n                            return itemA[orderby] - itemB[orderby];\n                        } else {\n                            return itemB[orderby] - itemA[orderby];\n                        }\n                    }\n                }\n            });\n        } else if (ordering === 'random') {\n            postsData.sort(() => 0.5 - Math.random());\n        }\n\n        for (let i = offset; i < count + offset; i++) {\n            if (postsData.length >= i + 1) {\n                options.data.index = i;\n                content += options.fn(postsData[i]);\n            } else {\n                break;\n            }\n        }\n\n        if (content === '') {\n            return '';\n        }\n\n        return content;\n    });\n}\n\nmodule.exports = getPostsByCustomFieldsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-posts-by-tags.js",
    "content": "/**\n * Helper for loading posts data which contains a specific tag(s) - specified by tag ID or tag slugs separated by comma\n *\n * Get up to five posts which contains a tag with given ID\n * \n * {{#getPostsByTags 5 TAG_ID1 \"\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostByTags}}\n * \n * Get up to five posts which contains one of the given tag slugs\n * \n * {{#getPostsByTags 5 \"TAG_SLUG1,TAG_SLUG2\" \"\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostsByTags}}\n * \n * Get up to five posts which contains one of the given tag slugs excluding posts with ID equal to 1 or 2\n * \n * {{#getPostsByTags 5 \"TAG_SLUG1,TAG_SLUG2\" \"1,2\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostsByTags}}\n * \n * QueryString options:\n * * count - how many posts should be included in the result\n * * allowed - which post statuses should be included\n * * tags - which tags should be used\n * * excluded - which posts should be excluded\n * * excluded_status - which posts statuses should be excluded\n * * offset - how many posts to skip\n * * orderby - order field or customField\n * * ordering - order direction - asc, desc, random\n * * customField - use when orderby=customField - name of the field to be used for ordering\n * * orderbyCompareLanguage - if orderby=customField, you can specify in which language ordering will be done.\n * * tag_as - specify if we select by tag id or slug\n * * operator - (OR or AND as value) - defines how the tags should be selected (post must have all tags at once time - AND, or one of them - OR)\n * \n * {{#getPostsByTags \"count=5&allowed=hidden,featured&tag_as=id&tags=1,2,3&excluded=1,2&offset=10&orderby=modified_at&ordering=desc&operator=AND\"}}\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * {{/getPostsByTags}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPostsByTagsHelper (rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPostsByTags', function (queryString, selectedTags, excludedPosts, options) {\n        if (!rendererInstance.contentStructure.posts) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let count;\n        let offset = 0;\n        let allowedStatus = 'any';\n        let excludedStatus = '';\n        let orderby = false;\n        let ordering = 'desc';\n        let customField = false;\n        let compareLanguage = false;\n        let tagAs = 'slug';\n        let operator = 'OR';\n\n        if (typeof queryString === 'string' && queryString.indexOf('=') > -1) {\n            options = selectedTags; // have to override option with second argument as query string syntax uses only one argument\n            queryString = queryString.split('&').map(pair => pair.split('='));\n            let queryStringData = {};\n\n            for (let i = 0; i < queryString.length; i++) {\n                let key = queryString[i][0];\n                let value = queryString[i][1];\n                queryStringData[key] = value;\n            }\n\n            if (queryStringData['count']) {\n                count = parseInt(queryStringData['count'], 10);\n\n                if (count === -1) {\n                    count = 999;\n                }\n            }\n\n            if (queryStringData['excluded']) {\n                excludedPosts = queryStringData['excluded'];\n            }\n\n            if (queryStringData['excluded_status']) {\n                excludedStatus = queryStringData['excluded_status'];\n            }\n\n            if (queryStringData['tags']) {\n                selectedTags = queryStringData['tags'];\n            }\n\n            if (queryStringData['allowed']) {\n                allowedStatus = queryStringData['allowed'];\n            }\n\n            if (queryStringData['offset']) {\n                offset = parseInt(queryStringData['offset']);\n            }\n\n            if (queryStringData['orderby']) {\n                orderby = queryStringData['orderby'];\n            }\n\n            if (queryStringData['ordering']) {\n                ordering = queryStringData['ordering'];\n            }\n\n            if (queryStringData['customField']) {\n                customField = queryStringData['customField'];\n            }\n\n            if (queryStringData['orderbyCompareLanguage']) {\n                compareLanguage = queryStringData['orderbyCompareLanguage'];\n            }\n\n            if (queryStringData['tag_as']) {\n                tagAs = queryStringData['tag_as'];\n            }\n\n            if (queryStringData['operator']) {\n                operator = queryStringData['operator'];\n\n                if (operator !== 'AND' && operator !== 'OR') {\n                    operator = 'OR';\n                }\n            }\n        } else {\n            if (queryString === -1 || queryString === '-1') {\n                count = 999;\n            } else {\n                count = parseInt(queryString, 10);\n            }\n        }\n\n        let postsData;\n        let content = '';\n        let filteredPosts = JSON.parse(JSON.stringify(rendererInstance.contentStructure.posts));\n\n        if (typeof excludedPosts === 'number' || (typeof excludedPosts === 'string' && excludedPosts !== '')) {\n            if (typeof excludedPosts === 'number') {\n                let excludedPost = excludedPosts;\n                filteredPosts = filteredPosts.filter(post => post.id !== excludedPost)\n            } else {\n                excludedPosts = excludedPosts.split(',').map(n => parseInt(n, 10));\n                filteredPosts = filteredPosts.filter(post => excludedPosts.indexOf(post.id) === -1);\n            }\n        }\n\n        if (allowedStatus !== 'any') {\n            allowedStatus = allowedStatus.split(',');\n\n            if (excludedStatus !== '') {\n                excludedStatus = excludedStatus.split(',');\n            }\n\n            filteredPosts = filteredPosts.filter(post => {\n                if (excludedStatus) {\n                    for (let i = 0; i < excludedStatus.length; i++) {\n                        if (post.status.indexOf(excludedStatus[i]) > -1) {\n                            return false;\n                        }\n                    }\n                }\n                \n                for (let i = 0; i < allowedStatus.length; i++) {\n                    if (post.status.indexOf(allowedStatus[i]) > -1) {\n                        return true;\n                    }\n                }\n\n                return false;\n            });\n        } else if (allowedStatus === 'any' && excludedStatus !== '') {\n            excludedStatus = excludedStatus.split(',');\n\n            filteredPosts = filteredPosts.filter(post => {\n                for (let i = 0; i < excludedStatus.length; i++) {\n                    if (post.status.indexOf(excludedStatus[i]) > -1) {\n                        return false;\n                    }\n                }\n\n                return true;\n            });\n        }\n\n        if (typeof selectedTags === 'number') {\n            let tagID = selectedTags;\n            postsData = filteredPosts.filter(post => post.tags.filter(tag => tag.id === tagID).length || post.hiddenTags.filter(tag => tag.id === tagID).length);    \n        } else if (typeof selectedTags === 'string') {\n            if (tagAs === 'slug') {\n                let tagsSlugs = [...new Set(selectedTags.split(','))];\n\n                if (operator === 'OR') {\n                    postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length || post.hiddenTags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length);\n                } else if (operator === 'AND') {\n                    postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length + post.hiddenTags.filter(tag => tagsSlugs.indexOf(tag.slug) > -1).length === tagsSlugs.length);\n                }\n            } else {\n                let tagsIDs = [...new Set(selectedTags.split(',').map(id => parseInt(id, 10)))];\n\n                if (operator === 'OR') {\n                    postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length || post.hiddenTags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length);\n                } else if (operator === 'AND') {\n                    postsData = filteredPosts.filter(post => post.tags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length + post.hiddenTags.filter(tag => tagsIDs.indexOf(tag.id) > -1).length === tagsIDs.length);\n                }\n            }\n        } else {\n            postsData = filteredPosts;\n        }\n\n        if (orderby && ordering && ordering !== 'random') {\n            postsData.sort((itemA, itemB) => {\n                if (orderby === 'customField' && customField) {\n                    if (isNaN(itemA.postViewConfig[customField]) && isNaN(itemB.postViewConfig[customField])) {\n                        if (ordering === 'asc') {\n                            if (compareLanguage) {\n                                return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage);\n                            } else {\n                                return itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField]);\n                            }\n                        } else {\n                            if (compareLanguage) {\n                                return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField], compareLanguage));\n                            } else {\n                                return -(itemA.postViewConfig[customField].localeCompare(itemB.postViewConfig[customField]));\n                            }\n                        }\n                    } else {\n                        if (ordering === 'asc') {\n                            return parseInt(itemA.postViewConfig[customField], 10) - parseInt(itemB.postViewConfig[customField], 10);\n                        } else {\n                            return parseInt(itemB.postViewConfig[customField], 10) - parseInt(itemA.postViewConfig[customField], 10);\n                        }\n                    }\n                }\n\n                if (orderby !== 'customField') {\n                    if(typeof itemA[orderby] === 'string') {\n                        if (ordering === 'asc') {\n                            return itemA[orderby].localeCompare(itemB[orderby]);\n                        } else {\n                            return -(itemA[orderby].localeCompare(itemB[orderby]));\n                        }\n                    } else {\n                        if (ordering === 'asc') {\n                            return itemA[orderby] - itemB[orderby];\n                        } else {\n                            return itemB[orderby] - itemA[orderby];\n                        }\n                    }\n                }\n            });\n        } else if (ordering === 'random') {\n            postsData.sort(() => 0.5 - Math.random());\n        }\n\n        for (let i = offset; i < count + offset; i++) {\n            if (postsData.length >= i + 1) {\n                options.data.index = i;\n                content += options.fn(postsData[i]);\n            } else {\n                break;\n            }\n        }\n\n        if (content === '') {\n            return '';\n        }\n\n        return content;\n    });\n}\n\nmodule.exports = getPostsByTagsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-posts.js",
    "content": "/**\n * Helper for loading posts data\n *\n * {{#getPosts \"POST_ID_1,POST_ID_2,POST_ID_N\" \"prefix\" \"suffix\"}}\n * <article>\n *    <h2>{{ title }}</h2>\n *    <div>{{{ excerpt }}}</div>\n * </article>\n * {{/getPosts}}\n *\n * Posts are ordered by the ID order in the string.\n *\n * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output.\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getPostsHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getPosts', function (postIDs, prefix, suffix, options) {\n        if (!rendererInstance.contentStructure.posts) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let content = '';\n        postIDs = postIDs.split(',').map(n => parseInt(n, 10));\n\n        for (let i = 0; i < postIDs.length; i++) {\n            let postData = rendererInstance.contentStructure.posts.filter(post => post.id === postIDs[i]);\n\n            if (postData.length) {\n                options.data.index = i;\n                content += options.fn(postData[0]);\n            }\n        }\n\n        if(content === '') {\n            return '';\n        }\n\n        content = [prefix, content, suffix].join('');\n\n        return content;\n    });\n}\n\nmodule.exports = getPostsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-tag.js",
    "content": "/**\n * Helper for loading tag data\n *\n * {{#getTag TAG_ID}}\n *    <p>{{ name }}</p>\n * {{/getTag}}\n * \n * {{#getTag \"TAG_SLUG\"}}\n *    <p>{{ name }}</p>\n * {{/getTag}}\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getTagHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getTag', function (selectedTag, options) {\n        if (!rendererInstance.contentStructure.tags) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let tagData; \n        \n        if (typeof selectedTag === 'number') {\n            tagData = rendererInstance.contentStructure.tags.filter(tag => tag.id === selectedTag);\n        } else {\n            tagData = rendererInstance.contentStructure.tags.filter(tag => tag.slug === selectedTag);\n        }\n\n        if(!tagData.length) {\n            return '';\n        }\n\n        return options.fn(tagData[0]);\n    });\n}\n\nmodule.exports = getTagHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/get-tags.js",
    "content": "/**\n * Helper for loading tags data\n *\n * {{#getTags \"TAG_ID_1,TAG_ID_2,TAG_ID_N\" \"prefix\" \"suffix\"}}\n * <li>{{ name }}</li>\n * {{/getTags}}\n *\n * Tags are ordered by the ID order in the string.\n *\n * The second parameter creates HTML prefix, the third parameter creates HTML suffix for the generated output.\n *\n * IMPORTANT: It requires availability of the @website.contentStructure global variable\n */\nfunction getTagsHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('getTags', function (tagsIDs, prefix, suffix, options) {\n        if (!rendererInstance.contentStructure.tags) {\n            return 'Error: @website.contentStructure global variable is not available.';\n        }\n\n        let content = '';\n        tagsIDs = tagsIDs.split(',').map(n => parseInt(n, 10));\n\n        for (let i = 0; i < tagsIDs.length; i++) {\n            let tagData = rendererInstance.contentStructure.tags.filter(tag => tag.id === tagsIDs[i]);\n\n            if (tagData.length) {\n                options.data.index = i;\n                content += options.fn(tagData[0]);\n            }\n        }\n\n        if(content === '') {\n            return '';\n        }\n\n        content = [prefix, content, suffix].join('');\n\n        return content;\n    });\n}\n\nmodule.exports = getTagsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/image-dimensions.js",
    "content": "const Handlebars = require('handlebars');\nconst sizeOf = require('image-size');\nconst path = require('path');\nconst normalizePath = require('normalize-path');\n\n/**\n * Helper for creating width/height attributes from the provided image\n *\n * {{imageDimensions @config.custom.imageOptionName}}\n *\n * @returns {string} - string with width and height attributes based on a given image\n */\nfunction imageDimensionsHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('imageDimensions', function (url) {\n        if (!url) {\n            return '';\n        }\n\n        url = normalizePath(url);\n        let basicUrl = normalizePath(rendererInstance.siteConfig.domain);\n        let basicDir = normalizePath(rendererInstance.inputDir);\n        let imagePath = url.replace(basicUrl, '');\n        imagePath = path.join(basicDir, imagePath);\n        let output = '';\n\n        try {\n            let dimensions = sizeOf(imagePath);\n\n            if(dimensions) {\n                output = ' width=\"' + dimensions.width + '\" height=\"' + dimensions.height + '\" ';\n            }\n        } catch(e) {\n            console.log('Image dimensions HSB helper: wrong image path - missing dimensions');\n        }\n\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = imageDimensionsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/is-current-page.js",
    "content": "/**\n * Helper for checking if a given page number is a current page\n *\n * Useful in pagination\n *\n * {{#isCurrentPage @pagination.currentPage this}}\n *\n * @returns {callback}\n */\nfunction isCurrentPage(current, iteration, options) {\n    if (current === iteration) {\n        return options.fn(this);\n    }\n\n    return options.inverse(this);\n}\n\nmodule.exports = isCurrentPage;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/is-empty.js",
    "content": "/**\n * Helper for checking if collection is empty\n *\n * {{#isEmpty value}}\n *    ...\n * {{/isEmpty}}\n *\n * @returns {callback}\n */\nfunction isEmpty(obj, options) {\n    if(Object.keys(obj).length === 0) {\n        return options.fn(this);\n    } else {\n        return options.inverse(this);\n    }\n}\n\nmodule.exports = isEmpty;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/is-not-empty.js",
    "content": "/**\n * Helper for checking if collection is not empty\n *\n * {{#isNotEmpty value}}\n *    ...\n * {{/isNotEmpty}}\n *\n * @returns {callback}\n *\n */\nfunction isNotEmpty(obj, options) {\n    if(Object.keys(obj).length === 0) {\n        return options.inverse(this);\n    } else {\n        return options.fn(this);\n    }\n}\n\nmodule.exports = isNotEmpty;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/is-not.js",
    "content": "/**\n * Helper for detecting contexts - opposite to #is helper\n *\n * {{#isNot 'index'}}\n *\n * {{#isNot 'tag,post'}}\n *\n * Available phrases: index, blogindex, tag, post, page, author, 404, search, pagination, index-pagination, tag-pagination, author-pagination\n *\n * @returns {callback}\n */\nfunction isNot (conditional, options) {\n    let contextIsCorrect = false;\n    let contextsToCheck = conditional.split(',');\n    contextsToCheck = contextsToCheck.map(context => context.trim());\n\n    for (let context of contextsToCheck) {\n        if (options.data.context.indexOf(context) > -1) {\n            contextIsCorrect = true;\n            break;\n        }\n    }\n\n    if (!contextIsCorrect) {\n        return options.fn(this);\n    } else {\n        return options.inverse(this);\n    }\n}\n\nmodule.exports = isNot;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/is.js",
    "content": "/**\n * Helper for detecting contexts\n *\n * {{#is 'index'}}\n *\n * {{#is 'tag,post'}}\n *\n * Available phrases: index, blogindex, tag, post, page, author, 404, search, pagination, index-pagination, tag-pagination, author-pagination\n *\n * @returns {callback}\n */\nfunction is(conditional, options) {\n    let contextIsCorrect = false;\n    let contextsToCheck = conditional.split(',');\n    contextsToCheck = contextsToCheck.map(context => context.trim());\n\n    for (let context of contextsToCheck) {\n        if (options.data.context.indexOf(context) > -1) {\n            contextIsCorrect = true;\n            break;\n        }\n    }\n\n    if (contextIsCorrect) {\n        return options.fn(this);\n    } else {\n        return options.inverse(this);\n    }\n}\n\nmodule.exports = is;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/join.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for concatenating values with a specific separator\n *\n * {{join ',' 'a' 1 'b'}}\n *\n * @returns {callback}\n */\nfunction join () {\n    let inputs = Array.from(arguments);\n    let separator = inputs.shift();\n    inputs.pop();\n\n    return inputs.join(separator);\n}\n\nmodule.exports = join;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/js.js",
    "content": "const fs = require('fs');\nconst FileHelper = require('./../../../../helpers/file.js');\nconst crypto = require('crypto');\nconst path = require('path');\nconst memoize = require('fast-memoize');\n\n/**\n *\n * Helper function used to calculate MD5 sum of the given file contents\n *\n * @param {string} localPath - path to the file\n * @param {string} overridedLocalPath - path to the overrided version of the file\n *\n * @returns {string} - MD5 sum based on the given file contents\n */\nfunction getMD5(localPath, overridedLocalPath) {\n    let fileContent = '';\n    \n    if (fs.existsSync(overridedLocalPath)) {\n        fileContent = FileHelper.readFileSync(overridedLocalPath);\n    } else {\n        fileContent = FileHelper.readFileSync(localPath);\n    }\n\n    return crypto.createHash('md5').update(fileContent).digest('hex');\n}\n\nconst memoizedMD5 = memoize(getMD5);\n\n/**\n * Helper for loading JS files from the assets directory\n *\n * It also adds MD5 sum hash as a v= param for preventing browser cache\n *\n * {{js \"filepath.js\"}}\n *\n * @returns {string} - path to the JS file with v= param based on the MD5 sume of the given file\n */\nfunction JSHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('js', function (filename) {\n        let md5Sum = '';\n        let localPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase(), rendererInstance.themeConfig.files.assetsPath, 'js', filename);\n        let overridedLocalPath = path.join(rendererInstance.inputDir, 'themes', rendererInstance.themeConfig.name.toLowerCase() + '-override', rendererInstance.themeConfig.files.assetsPath, 'js', filename);\n\n        let versionSuffix = '';\n\n        if (rendererInstance.siteConfig.advanced.versionSuffix) {\n            md5Sum = memoizedMD5(localPath, overridedLocalPath);\n            versionSuffix = '?v=' + md5Sum;\n        }\n\n        let url = [\n            rendererInstance.siteConfig.domain,\n            rendererInstance.themeConfig.files.assetsPath,\n            'js',\n            filename + versionSuffix\n        ].join('/');\n\n        return new Handlebars.SafeString(url);\n    });\n}\n\nmodule.exports = JSHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/json-ld.js",
    "content": "const Handlebars = require('handlebars');\nconst moment = require('moment');\nconst sizeOf = require('image-size');\nconst path = require('path');\nconst URLHelper = require('../../helpers/url');\n\n/**\n * Helper for creating JSON-LD data\n *\n * {{jsonLD}}\n *\n * @returns {string} <script> tag with JSON-LD data inside it\n */\nfunction jsonLDHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('jsonLD', function(context) {\n        let output = '';\n        let jsonLDObject = {};\n        let momentOriginalLocale = moment.locale();\n        moment.locale('en');\n        jsonLDObject['@context'] = 'http://schema.org';\n\n        // Detect if the page is not a post page\n        if (context.data.context.indexOf('post') === -1 && context.data.context.indexOf('page') === -1) {\n            jsonLDObject['@type'] = 'Organization';\n            jsonLDObject['name'] = context.data.website.name;\n\n            if(context.data.website.logo) {\n                jsonLDObject['logo'] = context.data.website.logo;\n            }\n\n            jsonLDObject['url'] = context.data.website.url;\n\n            if(context.data.config.custom && context.data.config.custom.socialButtons) {\n                jsonLDObject['sameAs'] = [];\n\n                if(context.data.config.custom.socialFacebook) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialFacebook);\n                }\n\n                if(context.data.config.custom.socialTwitter) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialTwitter);\n                }\n\n                if(context.data.config.custom.socialGoogleplus) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialGoogleplus);\n                }\n\n                if(context.data.config.custom.socialInstagram) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialInstagram);\n                }\n\n                if(context.data.config.custom.socialLinkedin) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialLinkedin);\n                }\n\n                if(context.data.config.custom.socialVimeo) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialVimeo);\n                }\n\n                if(context.data.config.custom.socialPinterest) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialPinterest);\n                }\n\n                if(context.data.config.custom.socialYoutube) {\n                    jsonLDObject['sameAs'].push(context.data.config.custom.socialYoutube);\n                }\n            }\n        } else { // JSON-LD for posts/pages\n            let itemData = context.data.root.post;\n\n            if (!itemData) {\n                itemData = context.data.root.page;\n            }\n\n            jsonLDObject['@type'] = 'Article';\n            jsonLDObject['mainEntityOfPage'] = {\n                \"@type\": \"WebPage\",\n                \"@id\": itemData.url,\n            };\n            jsonLDObject['headline'] = itemData.title;\n            jsonLDObject['datePublished'] = moment(itemData.createdAt).format('YYYY-MM-DDTHH:mmZ');\n            jsonLDObject['dateModified'] = moment(itemData.modifiedAt).format('YYYY-MM-DDTHH:mmZ');\n\n            if (itemData.featuredImage.url || context.data.website.logo) {\n                let imageUrl = '';\n\n                if (itemData.featuredImage && itemData.featuredImage.url) {\n                    imageUrl = itemData.featuredImage.url;\n                } else {\n                    imageUrl = context.data.website.logo;\n                }\n\n                let websiteUrl = context.data.website.url;\n                let imagePath = URLHelper.transformAssetURLIntoPath(rendererInstance.inputDir, imageUrl, websiteUrl);\n                let imageDimensions = {\n                    height: false,\n                    width: false\n                };\n\n                try {\n                    if (path.extname(imageUrl)) {\n                        imageDimensions = sizeOf(imagePath);\n                    } else {\n                        imageDimensions = {\n                            height: false,\n                            width: false\n                        };\n                    }\n                } catch(e) {\n                    console.log('JSON-LD HBS helper: wrong image path - missing dimensions: ', imageUrl);\n\n                    imageDimensions = {\n                        height: false,\n                        width: false\n                    };\n                }\n\n                jsonLDObject['image'] = {\n                    \"@type\": \"ImageObject\",\n                    \"url\": imageUrl,\n                    \"height\": imageDimensions.height,\n                    \"width\": imageDimensions.width\n                };\n            }\n\n            if (context.data.root.metaDescriptionRaw) {\n                jsonLDObject['description'] = context.data.root.metaDescriptionRaw.replace(/\"/g, \"'\");\n            } else {\n                jsonLDObject['description'] = itemData.excerpt;\n            }\n\n            if (itemData.author && itemData.author.name) {\n                jsonLDObject['author'] = {\n                    \"@type\": \"Person\",\n                    \"name\": itemData.author.name\n                };\n\n                let authorPagesEnabled = true;\n                let renderAuthorPages = rendererInstance.themeConfig.renderer && rendererInstance.themeConfig.renderer['createAuthorPages'];\n\n                if (rendererInstance.themeConfig.customConfig && typeof rendererInstance.themeConfig.customConfig['createAuthorPages'] !== 'undefined') {\n                    renderAuthorPages = rendererInstance.themeConfig.customConfig['createAuthorPages'];\n                }\n\n                if (\n                    (\n                        rendererInstance.themeConfig.supportedFeatures && \n                        rendererInstance.themeConfig.supportedFeatures.authorPages === false\n                    ) || renderAuthorPages === false\n                ) {\n                    authorPagesEnabled = false;\n                }\n\n                if (authorPagesEnabled) {\n                    let authorUrl = context.data.website.baseUrl + context.data.config.site.urls.authorsPrefix + '/' + itemData.author.username + '/';\n\n                    if (context.data.config.site.urls.postsPrefix && context.data.config.site.urls.authorsPrefixAfterPostsPrefix) {\n                        authorUrl = context.data.website.baseUrl + context.data.config.site.urls.postsPrefix + '/' + context.data.config.site.urls.authorsPrefix + '/' + itemData.author.username + '/';\n                    }\n                    \n                    jsonLDObject['author']['url'] = authorUrl;\n                }\n            }\n\n            jsonLDObject['publisher'] = {\n                \"@type\": \"Organization\",\n                \"name\": context.data.root.siteOwner.name\n            };\n\n            if(context.data.website.logo) {\n                let logoUrl = context.data.website.logo;\n                let websiteUrl = context.data.website.url;\n                let imagePath = URLHelper.transformAssetURLIntoPath(rendererInstance.inputDir, logoUrl, websiteUrl);\n                let imageDimensions = {\n                    height: false,\n                    width: false\n                };\n\n                try {\n                    imageDimensions = sizeOf(imagePath);\n                } catch(e) {\n                    console.log('JSON-LD HBS helper: wrong image path - missing dimensions');\n\n                    imageDimensions = {\n                        height: false,\n                        width: false\n                    };\n                }\n\n                if (logoUrl.trim() !== '') {\n                    jsonLDObject['publisher']['logo'] = {\n                        \"@type\": \"ImageObject\",\n                        \"url\": logoUrl,\n                        \"height\": imageDimensions.height,\n                        \"width\": imageDimensions.width,\n                    };\n                }\n            }\n        }\n\n        if (rendererInstance.plugins.hasModifiers('jsonLD')) {\n            jsonLDObject = rendererInstance.plugins.runModifiers('jsonLD', rendererInstance, jsonLDObject); \n        }\n\n        output += '<script type=\"application/ld+json\">';\n        output += JSON.stringify(jsonLDObject);\n        output += '</script>';\n        moment.locale(momentOriginalLocale);\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = jsonLDHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/jsonify.js",
    "content": "/**\n * Helper for creating JSON-safe strings\n *\n * {{{jsonify string|object}}}\n *\n * @returns {string} string prepared for use in JSON\n */\nfunction jsonify(content) {\n    return JSON.stringify(content);\n}\n\nmodule.exports = jsonify;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/lazyload.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for creating loading attributes\n *\n * {{ lazyload eager }}\n *\n * @returns {string} loading attribute with a value specified as a param\n */\nfunction lazyloadHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('lazyload', function(value) {\n        let output = '';\n\n        if (rendererInstance.siteConfig.advanced.mediaLazyLoad && ['eager', 'lazy', 'auto'].indexOf(value) > -1) {\n            output = ' loading=\"' + value + '\"';\n        }\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = lazyloadHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/math.js",
    "content": "/**\n * Helper for doing a simple math operations\n *\n * {{math @index '+' 1}}\n *\n * {{math @index '-' 1}}\n *\n * Available phrases operations: +, -, *, /, %\n *\n * @returns {number} - value based on the operation result\n */\nfunction math(a, operator, b) {\n    a = parseFloat(a);\n    b = parseFloat(b);\n\n    return {\n        \"+\": a + b,\n        \"-\": a - b,\n        \"*\": a * b,\n        \"/\": a / b,\n        \"%\": a % b\n    }[operator];\n}\n\nmodule.exports = math;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/menu-item-classes.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helpers for creating CSS classes in menu\n *\n * {{menuItemClasses}}\n * {{menuItemClasses \"active\"}}\n * {{menuItemClasses \"active\" \"active-parent\"}}\n * {{menuItemClasses \"active\" \"active-parent\" \"has-submenu\"}}\n *\n * {{menuItemClassesRaw}}\n * {{menuItemClassesRaw \"active\"}}\n * {{menuItemClassesRaw \"active\" \"active-parent\"}}\n * {{menuItemClassesRaw \"active\" \"active-parent\" \"has-submenu\"}}\n *\n * @returns {string} CSS class names for the given menu item\n */\nfunction menuItemClassesHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('menuItemClasses', function(activeClass, activeParentClass, hasSubmenuClass) {\n        let context = rendererInstance.menuContext;\n        let output = [];\n\n        // If there is no arguments Handlebars will push\n        // context as an argument which will cause\n        // that the default params won't work\n        if (typeof activeClass !== 'string') {\n            activeClass = 'active';\n        }\n\n        if (typeof activeParentClass !== 'string') {\n            activeParentClass = 'active-parent';\n        }\n\n        if (typeof hasSubmenuClass !== 'string') {\n            hasSubmenuClass = 'has-submenu';\n        }\n\n        // Check for the state of the menu item\n        if (hasActiveChild(this.items, context)) {\n            output.push(activeParentClass);\n        } else if (\n            (this.type === 'blogpage' && context[0] === 'blogpage') ||\n            (this.type === 'frontpage' && context[0] === 'frontpage') ||\n            (this.type === 'frontpage' && context[0] === 'page' && context[2] && context[2] === 'page-' + rendererInstance.siteConfig.advanced.pageAsFrontpage) ||\n            (this.type === 'tags' && context[0] === 'tags') ||\n            (this.type === context[0] && this.link === context[1])\n        ) {\n            output.push(activeClass);\n        }\n\n        if (this.cssClass !== '') {\n            output.push(this.cssClass);\n        }\n\n        if (this.items.length) {\n            output.push(hasSubmenuClass);\n        }\n\n        // Prepare output\n        if (output.length) {\n            output = ' class=\"' + output.join(' ') + '\"';\n            return new Handlebars.SafeString(output);\n        }\n\n        return '';\n    });\n\n    Handlebars.registerHelper('menuItemClassesRaw', function(activeClass, activeParentClass, hasSubmenuClass) {\n        let context = rendererInstance.menuContext;\n        let output = [];\n\n        // If there is no arguments Handlebars will push\n        // context as an argument which will cause\n        // that the default params won't work\n        if (typeof activeClass !== 'string') {\n            activeClass = 'active';\n        }\n\n        if (typeof activeParentClass !== 'string') {\n            activeParentClass = 'active-parent';\n        }\n\n        if (typeof hasSubmenuClass !== 'string') {\n            hasSubmenuClass = 'has-submenu';\n        }\n\n        // Check for the state of the menu item\n        if (hasActiveChild(this.items, context)) {\n            output.push(activeParentClass);\n        } else if (\n            (this.type === 'frontpage' && context[0] === 'frontpage') ||\n            (this.type === 'tags' && context[0] === 'tags') ||\n            (this.type === context[0] && this.link === context[1])\n        ) {\n            output.push(activeClass);\n        }\n\n        if (this.cssClass !== '') {\n            output.push(this.cssClass);\n        }\n\n        if (this.items.length) {\n            output.push(hasSubmenuClass);\n        }\n\n        // Prepare output\n        if (output.length) {\n            output = output.join(' ');\n            return new Handlebars.SafeString(output);\n        }\n\n        return '';\n    });\n}\n\n/**\n * Private function for finding the active\n * childrens (for the adding active-parent CSS class purpose)\n *\n * @param {object} items - items from the menu\n * @param {string} context - name of the context\n *\n * @returns {boolean} - true if the given submenu has an active menu item, otherwise false\n */\nfunction hasActiveChild(items, context) {\n    let result = false;\n\n    for (let i = 0; i < items.length; i++) {\n        if (\n            (items[i].type === 'frontpage' && context[0] === 'frontpage') ||\n            (items[i].type === 'tags' && context[0] === 'tags') ||\n            (items[i].type === context[0] && items[i].link === context[1])\n        ) {\n            return true;\n        }\n\n        if (items[i].items.length) {\n            result = hasActiveChild(items[i].items, context);\n\n            if (result) {\n                return true;\n            }\n        }\n    }\n\n    return result;\n}\n\nmodule.exports = menuItemClassesHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/menu-url.js",
    "content": "const Handlebars = require('handlebars');\nconst slug = require('./../../../../helpers/slug');\n\n/**\n * Helper for creating URLs in menu\n *\n * {{menuUrl}}\n *\n * Available types:\n * - post\n * - page\n * - tag\n * - frontpage\n * - blogpage\n * - tags\n * - external\n * - internal\n *\n * @returns {string} - URL for the current menu item\n */\nfunction menuURLHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('menuUrl', function() {\n        let output = '';\n        let baseUrl = rendererInstance.siteConfig.domain;\n\n        // Link to the single post pages\n        if (this.type === 'post') {\n            if (rendererInstance.siteConfig.advanced.urls.cleanUrls) {\n                if (rendererInstance.siteConfig.advanced.urls.postsPrefix) {\n                    output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + this.link + '/';\n                } else {\n                    output = baseUrl + '/' + this.link + '/';\n                }\n                // In the preview mode we have to load URLs with\n                // index.html as filesystem on OS doesn't behave\n                // as the server environment and not redirect to\n                // a proper URL\n                if(rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                    output += 'index.html';\n                }\n            } else {\n                if (rendererInstance.siteConfig.advanced.urls.postsPrefix) {\n                    output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + this.link + '.html';\n                } else {\n                    output = baseUrl + '/' + this.link + '.html';\n                }\n            }\n        }\n\n        // Link to the single page\n        if (this.type === 'page') {\n            let parentItems = rendererInstance.cachedItems.pagesStructureHierarchy[this.linkID];\n            let pageSlug = this.link;\n\n            if (rendererInstance.siteConfig.advanced.usePageAsFrontpage && rendererInstance.siteConfig.advanced.pageAsFrontpage === this.linkID) {\n                output = baseUrl + '/';\n\n                // In the preview mode we have to load URLs with\n                // index.html as filesystem on OS doesn't behave\n                // as the server environment and not redirect to\n                // a proper URL\n                if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                    output += 'index.html';\n                }   \n            } else {\n                if (rendererInstance.siteConfig.advanced.urls.cleanUrls && parentItems && parentItems.length) {\n                    let slugs = [];\n\n                    for (let i = 0; i < parentItems.length; i++) {\n                        if (rendererInstance.cachedItems.pages[parentItems[i]]) {\n                            slugs.push(rendererInstance.cachedItems.pages[parentItems[i]].slug);\n                        }\n                    }\n\n                    slugs.push(this.link);\n                    pageSlug = slugs.join('/');\n                }\n\n                if (rendererInstance.siteConfig.advanced.urls.cleanUrls) {\n                    output = baseUrl + '/' + pageSlug + '/';\n                    // In the preview mode we have to load URLs with\n                    // index.html as filesystem on OS doesn't behave\n                    // as the server environment and not redirect to\n                    // a proper URL\n                    if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                        output += 'index.html';\n                    }\n                } else {\n                    output = baseUrl + '/' + pageSlug + '.html';\n                }\n            }\n        }\n\n        // Link to the tag pages\n        if (this.type === 'tag') {\n            output = baseUrl + '/' + this.link + '/';\n\n            if (rendererInstance.siteConfig.advanced.urls.tagsPrefix !== '') {\n                output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/' + this.link + '/';\n            }\n\n            if (rendererInstance.siteConfig.advanced.urls.postsPrefix && rendererInstance.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/' + this.link + '/';\n            }\n\n            // In the preview mode we have to load URLs with\n            // index.html as filesystem on OS doesn't behave\n            // as the server environment and not redirect to\n            // a proper URL\n            if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                output += 'index.html';\n            }\n        }\n\n        // Link to the author pages\n        if (this.type === 'author') {\n            output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.authorsPrefix + '/' + slug(this.link) + '/';\n\n            if (rendererInstance.siteConfig.advanced.urls.postsPrefix && rendererInstance.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) {\n                output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + rendererInstance.siteConfig.advanced.urls.authorsPrefix + '/' + slug(this.link) + '/';\n            }\n\n            // In the preview mode we have to load URLs with\n            // index.html as filesystem on OS doesn't behave\n            // as the server environment and not redirect to\n            // a proper URL\n            if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                output += 'index.html';\n            }\n        }\n\n        // Link to the frontpage - just the page domain name\n        if (this.type === 'frontpage') {\n            output = baseUrl + '/';\n\n            // In the preview mode we have to load URLs with\n            // index.html as filesystem on OS doesn't behave\n            // as the server environment and not redirect to\n            // a proper URL\n            if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                output += 'index.html';\n            }\n        }\n\n        // Link to the blogpage - just the page domain name or page with posts prefix\n        if (this.type === 'blogpage') {\n            output = baseUrl + '/';\n\n            if (rendererInstance.siteConfig.advanced.urls.postsPrefix) {\n                output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/';\n            }\n\n            // In the preview mode we have to load URLs with\n            // index.html as filesystem on OS doesn't behave\n            // as the server environment and not redirect to\n            // a proper URL\n            if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                output += 'index.html';\n            }\n        }\n\n        // Link to the tags list - just the page domain name with tags prefix\n        if (this.type === 'tags') {\n            output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/';\n\n            if (rendererInstance.siteConfig.advanced.urls.postsPrefix && rendererInstance.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                output = baseUrl + '/' + rendererInstance.siteConfig.advanced.urls.postsPrefix + '/' + rendererInstance.siteConfig.advanced.urls.tagsPrefix + '/';\n            }\n\n            // In the preview mode we have to load URLs with\n            // index.html as filesystem on OS doesn't behave\n            // as the server environment and not redirect to\n            // a proper URL\n            if (rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n                output += 'index.html';\n            }\n        }\n\n        // External links which should start with protocol\n        if (this.type === 'external') {\n            output = this.link;\n        }\n\n        // Internal links which should start with the page domain name\n        if (this.type === 'internal') {\n            output = baseUrl + '/' + this.link;\n        }\n\n        output = Handlebars.Utils.escapeExpression(output);\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = menuURLHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/meta-description.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for generating meta_description\n *\n * {{metaDescription}}\n *\n * @returns {string} - <meta> tag with meta description\n */\nfunction metaDescription(options) {\n    if (options.data.root.metaDescriptionRaw !== '') {\n        if (options.data.root.metaRobotsRaw.indexOf('noindex') !== -1) {\n            return '';\n        }\n\n        let output = '<meta name=\"description\" content=\"' + options.data.root.metaDescriptionRaw.replace(/\"/g, \"'\") + '\">';\n        return new Handlebars.SafeString(output);\n    }\n\n    return '';\n}\n\nmodule.exports = metaDescription;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/meta-robots.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for generating meta_robots\n *\n * {{metaRobots}}\n *\n * @returns {string} <meta> tag with the meta robots value\n */\nfunction metaRobotsHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('metaRobots', function (options) {\n        // If canonical is set - skip meta robots tag\n        if (options.data.root.hasCustomCanonicalUrl) {\n            return '';\n        }\n\n        if (\n            Array.isArray(options.data.context) &&\n            options.data.context[0] && (\n                (\n                    rendererInstance.siteConfig.advanced.homepageNoIndexPagination &&\n                    options.data.context.indexOf('index-pagination') !== -1\n                ) || (\n                    rendererInstance.siteConfig.advanced.tagNoIndexPagination &&\n                    options.data.context.indexOf('tag-pagination') !== -1\n                ) || (\n                    rendererInstance.siteConfig.advanced.authorNoIndexPagination &&\n                    options.data.context.indexOf('author-pagination') !== -1\n                )\n            )\n        ) {\n            return new Handlebars.SafeString('<meta name=\"robots\" content=\"noindex, follow\">');\n        }\n\n        if (options.data.root.metaRobotsRaw === 'index, follow') {\n            return '';\n        }\n\n        if (options.data.root.metaRobotsRaw !== '') {\n            return new Handlebars.SafeString('<meta name=\"robots\" content=\"' + options.data.root.metaRobotsRaw + '\">');\n        }\n\n        return '';\n    });\n}\n\nmodule.exports = metaRobotsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/not-contains.js",
    "content": "/**\n * Helper for checking if a specifc value is not inside comma-separated string\n *\n * {{#notContains 'abc' 'abc,def'}}\n *\n * @returns {callback}\n */\nfunction notContains (needle, haystack, options) {\n    if (needle === undefined || haystack === undefined) {\n        return;\n    }\n\n    if (typeof haystack === 'object' && haystack.string) {\n        haystack = haystack.string;\n    }\n\n    haystack = haystack.split(',');\n\n    if (typeof needle === 'number') {\n        haystack = haystack.map(n => parseInt(n, 10));\n    }\n\n    if (haystack.indexOf(needle) === -1) {\n        return options.fn(this);\n    }\n\n    return options.inverse(this)\n}\n\nmodule.exports = notContains;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/orderby.js",
    "content": "/**\n * Helper used to order collection\n *\n * @param collection\n * @param field\n * @param direction\n * @param langForLocaleCompare\n */\n\nfunction orderby (collection, field, direction, langForLocaleCompare = false) {\n    collection.sort((itemA, itemB) => {\n        if (typeof itemA[field] === 'string') {\n            if (langForLocaleCompare) {\n                if (direction === 'ASC') {\n                    return itemA[field].localeCompare(itemB[field], langForLocaleCompare);\n                } else {\n                    return -1 * itemA[field].localeCompare(itemB[field], langForLocaleCompare);\n                }\n            }\n\n            if (direction === 'ASC') {\n                return itemA[field].localeCompare(itemB[field]);\n            } else {\n                return -1 * itemA[field].localeCompare(itemB[field]);\n            }\n        } else {\n            if (direction === 'ASC') {\n                return itemA[field] - itemB[field];\n            } else {\n                return itemB[field] - itemA[field];\n            }\n        }\n    });\n}\n\nmodule.exports = orderby;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/page-url.js",
    "content": "/**\n * Helper for creating pagination URL\n *\n * {{pageUrl @pagination.context NUMBER}}\n *\n * @returns {string} URL for the specific page in the pagination for the given context\n */\nfunction pageURLHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('pageUrl', function (context, number) {\n        let path = [rendererInstance.siteConfig.domain];\n        number = parseInt(number, 10);\n\n        // Skip context for homepage\n        if (context !== '') {\n            path.push(context);\n        }\n\n        if (context === '' && rendererInstance.siteConfig.advanced.urls.postsPrefix) {\n            path.push(rendererInstance.siteConfig.advanced.urls.postsPrefix);\n        }\n\n        // Skip page/X for URLs in page = 1\n        if (number > 1) {\n            path.push(rendererInstance.siteConfig.advanced.urls.pageName);\n            path.push(number);\n        }\n\n        if(rendererInstance.previewMode || rendererInstance.siteConfig.advanced.urls.addIndex) {\n            path.push('index.html');\n        }\n\n        // Connect the URL parts\n        path = path.join('/');\n\n        // Add trailing slash only if adding index.html is disabled and there is no preview mode active\n        if (!rendererInstance.previewMode && !rendererInstance.siteConfig.advanced.urls.addIndex) {\n            path += '/';\n        }\n\n        let url = Handlebars.Utils.escapeExpression(path);\n\n        return new Handlebars.SafeString(url);\n    });\n}\n\nmodule.exports = pageURLHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/publii-footer.js",
    "content": "const Handlebars = require('handlebars');\nconst Gdpr = require('./../../helpers/gdpr.js');\n\n/**\n * Helper for creating additional useful tags\n *\n * {{{ publiiFooter }}}\n *\n * @returns {string} content of the Publii-specific element like GDPR popup\n */\nfunction publiiFooterHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('publiiFooter', function (context) {\n        let output  = '';\n\n        if (rendererInstance.plugins.hasInsertions('publiiFooter')) {\n            output += \"\\n\";\n            output += rendererInstance.plugins.runInsertions('publiiFooter', rendererInstance);\n        }\n\n        if (rendererInstance.siteConfig.advanced.gdpr.enabled) {\n            output += Gdpr.popupHtmlOutput(rendererInstance.siteConfig.advanced.gdpr, rendererInstance);\n            output += Gdpr.popupJsOutput(rendererInstance.siteConfig.advanced.gdpr);\n        }\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = publiiFooterHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/publii-head.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper for creating additional useful tags\n *\n * {{publiiHead}}\n *\n * @returns {string} content of the Publii-specific meta tags\n */\nfunction publiiHeadHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('publiiHead', function (context) {\n        let output  = '<meta name=\"generator\" content=\"Publii Open-Source CMS for Static Site\">';\n\n        if (\n            rendererInstance.themeConfig.supportedFeatures &&\n            rendererInstance.themeConfig.supportedFeatures.embedConsents &&\n            rendererInstance.siteConfig.advanced.gdpr.enabled &&\n            rendererInstance.siteConfig.advanced.gdpr.allowAdvancedConfiguration &&\n            rendererInstance.siteConfig.advanced.gdpr.embedConsents && \n            rendererInstance.siteConfig.advanced.gdpr.embedConsents.length\n        ) {\n            let configRevision = '';\n            let configTTL = 0;\n\n            if (rendererInstance.siteConfig.advanced.gdpr.cookieSettingsRevision) {\n                configRevision = '-v' + parseInt(rendererInstance.siteConfig.advanced.gdpr.cookieSettingsRevision, 10);\n            }\n\n            if (rendererInstance.siteConfig.advanced.gdpr.cookieSettingsTTL) {\n                configTTL = parseInt(rendererInstance.siteConfig.advanced.gdpr.cookieSettingsTTL, 10);\n            }\n\n            output += `\n            <script>\n            window.publiiEmbedConsentCheck = function (cookieGroup) {\n                var configName = 'publii-gdpr-allowed-cookies${configRevision}';\n                var lastConfigSave = localStorage.getItem('publii-gdpr-cookies-config-save-date');\n                var configIsFresh = false;\n\n                if (lastConfigSave !== null) {\n                    lastConfigSave = parseInt(lastConfigSave, 10);\n\n                    if (lastConfigSave === 0) {\n                        configIsFresh = true;\n                    } else if (${configTTL} > 0 && +new Date() - lastConfigSave < ${configTTL} * 24 * 60 * 60 * 1000) {\n                        configIsFresh = true;\n                    }\n                }\n\n                if (!configIsFresh) {\n                    return;\n                }\n\n                var config = localStorage.getItem(configName);\n\n                if (config && config.indexOf(cookieGroup) > -1) {\n                    var iframesToUnlock = document.querySelectorAll('.pec-wrapper[data-consent-group-id=\"' + cookieGroup + '\"]');\n\n                    for (var i = 0; i < iframesToUnlock.length; i++) {\n                        var iframeWrapper = iframesToUnlock[i];\n\n                        if (iframeWrapper.getAttribute('data-consent-given') === 'true') {\n                            continue;\n                        }\n\n                        iframeWrapper.setAttribute('data-consent-given', 'true');\n                        iframeWrapper.querySelector('.pec-overlay').classList.remove('is-active');\n                        iframeWrapper.querySelector('.pec-overlay').setAttribute('aria-hidden', 'true');\n                        var iframe = iframeWrapper.querySelector('iframe');\n                        iframe.setAttribute('src', iframe.getAttribute('data-consent-src'));\n                    }\n                }\n            }\n            </script>\n            `;\n        }\n\n        if (rendererInstance.plugins.hasInsertions('publiiHead')) {\n            output += \"\\n\";\n            output += rendererInstance.plugins.runInsertions('publiiHead', rendererInstance);\n        }\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = publiiHeadHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/responsive-image-attributes.js",
    "content": "const Handlebars = require('handlebars');\nconst responsiveSrcSet = require('./responsive-srcset.js');\nconst responsiveSizes = require('./responsive-sizes.js');\n\n/**\n * Helper for sizes attribute for the images from options\n *\n * {{responsiveImageAttributes @config.custom.imageOptionName [type] [group]}}\n * \n * {{responsiveImageAttributes 'featuredImage' srcset.post sizes.post}}\n * {{responsiveImageAttributes 'tagImage' srcset.post sizes.post}}\n * {{responsiveImageAttributes 'authorImage' srcset.post sizes.post}}\n *\n * @returns {string} - string with the srcset and sizes attributes\n */\nfunction responsiveImageAttributesHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('responsiveImageAttributes', function (firstParam, secondParam, thirdParam) {\n        if (!firstParam) { \n            return '';\n        }\n        \n        if (\n            firstParam === 'featuredImage' || \n            firstParam === 'tagImage' || \n            firstParam === 'authorImage'\n        ) {\n            if (secondParam && thirdParam) {\n                return new Handlebars.SafeString('srcset=\"' + secondParam + '\" sizes=\"' + thirdParam + '\"');\n            }\n\n            return '';\n        }\n\n        let srcSet = responsiveSrcSet.returnSrcSetAttribute.bind(rendererInstance)(firstParam, secondParam, thirdParam);\n\n        if (typeof secondParam !== 'string') {\n            if (firstParam.indexOf('/media/authors/') > -1) {\n                secondParam = 'authorImages';\n            } else if (firstParam.indexOf('/media/tags/') > -1) {\n                secondParam = 'tagImages';\n            } else if (firstParam.indexOf('/media/posts/') > -1 || firstParam.indexOf('/media/pages/') > -1) {\n                secondParam = 'contentImages';\n            } else if (firstParam.indexOf('/media/website/') > -1) {\n                secondParam = 'optionImages';\n            }\n        }\n\n        let sizes = responsiveSizes.returnSizesAttribute.bind(rendererInstance)(secondParam, thirdParam);\n\n        if (srcSet) {\n            return new Handlebars.SafeString(srcSet + ' ' + sizes);\n        }\n\n        return new Handlebars.SafeString(srcSet);\n    });\n}\n\nmodule.exports = responsiveImageAttributesHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/responsive-sizes.js",
    "content": "const Handlebars = require('handlebars');\nconst UtilsHelper = require('./../../../../helpers/utils.js');\n\n/**\n * Helper for sizes attribute for the images from options\n *\n * {{responsiveSizes type [group]}}\n *\n * @returns {string} - string with the sizes attribute\n */\nfunction responsiveSizesHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('responsiveSizes', returnSizesAttribute.bind(rendererInstance));\n}\n\nfunction returnSizesAttribute (type, group) {\n    if (!UtilsHelper.responsiveImagesConfigExists(this.themeConfig)) {\n        return '';\n    }\n\n    let output = '';\n    let responsiveConfig = this.themeConfig.files.responsiveImages;\n    let useType = false;\n    let useGroup = false;\n\n    if (typeof type === \"string\") {\n        useType = true;\n    }\n\n    if (typeof group === \"string\") {\n        useGroup = true;\n    }\n\n    if (!useType) {\n        return '';\n    }\n    \n    if (useGroup && responsiveConfig[type] && responsiveConfig[type].sizes && responsiveConfig[type].sizes[group]) {\n        output = ' sizes=\"' + responsiveConfig[type].sizes[group] + '\" ';  \n    } else if (!useGroup && responsiveConfig[type] && responsiveConfig[type].sizes) {\n        output = ' sizes=\"' + responsiveConfig[type].sizes + '\" ';\n    }\n\n    return new Handlebars.SafeString(output);\n}\n\nmodule.exports = {\n    responsiveSizesHelper,\n    returnSizesAttribute\n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/responsive-srcset.js",
    "content": "const Handlebars = require('handlebars');\nconst path = require('path');\nconst normalizePath = require('normalize-path');\nconst UtilsHelper = require('./../../../../helpers/utils.js');\nconst URLHelper = require('./../../helpers/url.js');\n\n/**\n * Helper for srcset attribute from the provided image\n *\n * {{responsiveSrcSet @config.custom.imageOptionName [type] [group]}}\n *\n * @returns {string} - string with the srcset attribute\n */\nfunction responsiveSrcSetHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('responsiveSrcSet', returnSrcSetAttribute.bind(rendererInstance));\n}\n\nfunction returnSrcSetAttribute (url, type, group) {\n    if (!url) {\n        return;\n    }\n\n    url = URLHelper.fixProtocols(normalizePath(url));\n    let output = '';\n    let dimensions = false;\n    let dimensionsData = false;\n\n    if (!UtilsHelper.responsiveImagesConfigExists(this.themeConfig)) {\n        return output;\n    }\n\n    // skip GIF and SVG images\n    if (url.slice(-4) === '.gif' || url.slice(-4) === '.svg') {\n        return output;\n    }\n\n    if (typeof type !== \"string\") {\n        type = false;\n    }\n\n    if (typeof group !== \"string\") {\n        group = false;\n    }\n\n    if (!type) {\n        if (url.indexOf('/media/authors/') > -1) {\n            type = 'authorImages';\n        } else if (url.indexOf('/media/tags/') > -1) {\n            type = 'tagImages';\n        } else if (url.indexOf('/media/posts/') > -1 || url.indexOf('/media/pages/') > -1) {\n            type = 'contentImages';\n        } else if (url.indexOf('/media/website/') > -1) {\n            type = 'optionImages';\n        }\n    }\n\n    if (UtilsHelper.responsiveImagesConfigExists(this.themeConfig, type)) {\n        dimensions = UtilsHelper.responsiveImagesDimensions(this.themeConfig, type, group);\n        dimensionsData = UtilsHelper.responsiveImagesData(this.themeConfig, type, group);\n    }\n\n    if (!dimensions) {\n        return;\n    }\n\n    let srcset = [];\n\n    for(let name of dimensions) {\n        let filename = url.split('/');\n        filename = filename[filename.length-1];\n        let filenameFile = path.parse(filename).name;\n        let filenameExtension = path.parse(filename).ext;\n        let useWebp = false;\n\n        if (this.siteConfig?.advanced?.forceWebp) {\n            useWebp = true;\n        }\n\n        if (useWebp) {\n            filenameExtension = '.webp';\n        }\n\n        let baseUrlWithoutFilename = url.replace(filename, '');\n        let responsiveImage = baseUrlWithoutFilename + 'responsive/' + filenameFile + '-' + name + filenameExtension;\n        srcset.push(responsiveImage + ' ' + dimensionsData[name].width + 'w');\n    }\n\n    output = ' srcset=\"' + srcset.join(' ,') + '\" ';\n\n    return new Handlebars.SafeString(output);\n}\n\nmodule.exports = {\n    responsiveSrcSetHelper,\n    returnSrcSetAttribute\n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/reverse.js",
    "content": "/**\n * Helper used to reverse collection\n *\n * @param collection\n */\n\nfunction reverse (collection) {\n    collection.reverse();\n}\n\nmodule.exports = reverse;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/social-meta-tags.js",
    "content": "const stripTags = require('striptags');\nconst sizeOf = require('image-size');\nconst path = require('path');\nconst normalizePath = require('normalize-path');\n\n/**\n * Helper for creating Open Graph and Twitter Cars metatags\n *\n * {{socialMetaTags}}\n *\n * @returns {string} - meta tags for Open Graph and Twitter\n */\nfunction socialMetaTagsHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('socialMetaTags', function (contextData) {\n        if (rendererInstance.siteConfig.deployment.relativeUrls) {\n            return new Handlebars.SafeString('');\n        }\n\n        let output = '';\n        let openGraphEnabled = rendererInstance.siteConfig.advanced.openGraphEnabled;\n        let openGraphImage = rendererInstance.siteConfig.advanced.openGraphImage;\n        let siteName = contextData.data.website.name;\n        let image = '';\n        let title = '';\n        let description = '';\n        let openGraphType = 'website';\n        let twitterUsername = rendererInstance.siteConfig.advanced.twitterUsername;\n        let twitterCardsType = rendererInstance.siteConfig.advanced.twitterCardsType;\n        let twitterCardsEnabled = rendererInstance.siteConfig.advanced.twitterCardsEnabled;\n\n        // Get SEO title if exists\n        if(\n            rendererInstance.siteConfig.advanced.metaTitle &&\n            rendererInstance.siteConfig.advanced.metaTitle != ''\n        ) {\n            let siteNameValue = rendererInstance.siteConfig.name;\n\n            if(rendererInstance.siteConfig.displayName) {\n                siteNameValue = rendererInstance.siteConfig.displayName;\n            }\n\n            siteName = rendererInstance.siteConfig.advanced.metaTitle.replace(/%sitename/g, siteNameValue);\n        }\n\n        if (\n            !rendererInstance.siteConfig.advanced.usePageAsFrontpage && \n            rendererInstance.siteConfig.advanced.urls.postsPrefix !== '' &&\n            contextData.data.context.indexOf('index') > -1 && \n            rendererInstance.siteConfig.advanced.homepageMetaTitle\n        ) {\n            let siteNameValue = rendererInstance.siteConfig.name;\n\n            if (rendererInstance.siteConfig.displayName) {\n                siteNameValue = rendererInstance.siteConfig.displayName;\n            }\n\n            siteName = rendererInstance.siteConfig.advanced.homepageMetaTitle.replace(/%sitename/g, siteNameValue);\n        } \n        \n        if(contextData.data.context.indexOf('post') === -1 && contextData.data.context.indexOf('page') === -1) {\n            // Get tag values according to the current context - listing or single post page\n            // Data for the index/tag listing page\n            image = contextData.data.website.logo;\n            title = siteName;\n            description = contextData.data.root.metaDescriptionRaw;\n\n            if (contextData.data.context.indexOf('tag') !== -1) {\n                title = contextData.data.root.tag.name;\n\n                if (rendererInstance.siteConfig.advanced.usePageTitleInsteadItemName) {\n                    title = contextData.data.root.title;\n                }\n            }\n\n            if (contextData.data.context.indexOf('author') !== -1) {\n                title = contextData.data.root.author.name;\n\n                if (rendererInstance.siteConfig.advanced.usePageTitleInsteadItemName) {\n                    title = contextData.data.root.title;\n                }\n            }\n        } else {\n            // Data for the single post or page\n            let itemData = contextData.data.root.post;\n\n            if (!itemData) {\n                itemData = contextData.data.root.page;\n            }\n\n            image = itemData.featuredImage.url;\n            openGraphType = 'article';\n\n            if(!image) {\n                image = contextData.data.website.logo;\n            }\n\n            title = itemData.title;\n\n            if (rendererInstance.siteConfig.advanced.usePageTitleInsteadItemName) {\n                title = contextData.data.root.title;\n            }\n\n            description = contextData.data.root.metaDescriptionRaw;\n\n            if(description === '') {\n                description = itemData.excerpt;\n            }\n        }\n\n        // Get fallback image if available\n        if (openGraphImage && openGraphImage !== '' && image === contextData.data.website.logo) {\n            image = openGraphImage;\n        }\n\n        // Generate Open Graph tags\n        if (openGraphEnabled) {\n            output += '<meta property=\"og:title\" content=\"' + title.replace(/\"/g, \"'\") + '\">';\n\n            if (image) {\n                output += '<meta property=\"og:image\" content=\"' + image + '\">';\n            }\n\n            let ogImageDimensions = false;\n            let imageLocalPath = image;\n\n            try {\n                let baseLocalPath = path.join(rendererInstance.sitesDir, rendererInstance.siteName, 'input', 'media');\n                baseLocalPath = normalizePath(baseLocalPath);\n                imageLocalPath = normalizePath(image)\n                imageLocalPath = imageLocalPath.split('/media/');\n                \n                if (imageLocalPath[1]) {\n                    imageLocalPath = path.join(baseLocalPath, imageLocalPath[1]);\n                    ogImageDimensions = sizeOf(imageLocalPath);\n                }\n            } catch(e) {\n                console.log('OG image - wrong image path - missing dimensions', imageLocalPath);\n                ogImageDimensions = false;\n            }\n\n            if (ogImageDimensions) {\n                output += '<meta property=\"og:image:width\" content=\"' + ogImageDimensions.width + '\">';\n                output += '<meta property=\"og:image:height\" content=\"' + ogImageDimensions.height + '\">'\n            }\n\n            output += '<meta property=\"og:site_name\" content=\"' + siteName.replace(/\"/g, \"'\") + '\">';\n            output += '<meta property=\"og:description\" content=\"' + stripTags(description).replace(/\"/g, \"'\") + '\">';\n            output += '<meta property=\"og:url\" content=\"' + contextData.data.website.pageUrl + '\">';\n            output += '<meta property=\"og:type\" content=\"' + openGraphType + '\">';\n\n            if (rendererInstance.siteConfig.advanced.openGraphAppId !== '') {\n                output += '<meta property=\"fb:app_id\" content=\"' + rendererInstance.siteConfig.advanced.openGraphAppId + '\">';\n            }\n        }\n\n        // If user set Twitter username - generate Twitter Cards tags\n        if(twitterCardsEnabled && twitterUsername && twitterUsername !== '') {\n            if(twitterUsername.indexOf('@') !== 0) {\n                twitterUsername = '@' + twitterUsername;\n            }\n\n            output += '<meta name=\"twitter:card\" content=\"' + twitterCardsType + '\">';\n            output += '<meta name=\"twitter:site\" content=\"' + twitterUsername + '\">';\n            output += '<meta name=\"twitter:title\" content=\"' + title.replace(/\"/g, \"'\") + '\">';\n            output += '<meta name=\"twitter:description\" content=\"' + stripTags(description).replace(/\"/g, \"'\") + '\">';\n\n            if(image) {\n                output += '<meta name=\"twitter:image\" content=\"' + image + '\">';\n            }\n        }\n\n        if (rendererInstance.plugins.hasModifiers('socialMetaTags')) {\n            output = rendererInstance.plugins.runModifiers('socialMetaTags', rendererInstance, output); \n        }\n\n        return new Handlebars.SafeString(output);\n    });\n}\n\nmodule.exports = socialMetaTagsHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/check-if-all.spec.js",
    "content": "const assert = require('assert');\nconst checkIfAll = require('../check-if-all.js');\n\ndescribe('Handlebars - checkIfAll block helper', function() {\n    // Below object emulates Handlebars behaviour\n    let optionsParam = {\n        fn: () => true,\n        inverse: () => false\n    };\n\n    it('should return false when there is no arguments', function() {\n        assert.equal(false, checkIfAll(optionsParam));\n    });\n\n    it('should return false for falsy values arguments', function() {\n        assert.equal(false, checkIfAll('', optionsParam));\n        assert.equal(false, checkIfAll(\"\", optionsParam));\n        assert.equal(false, checkIfAll(NaN, optionsParam));\n        assert.equal(false, checkIfAll(null, optionsParam));\n        assert.equal(false, checkIfAll(undefined, optionsParam));\n        assert.equal(false, checkIfAll(0, optionsParam));\n        assert.equal(false, checkIfAll(false, optionsParam));\n    });\n\n    it('should return true if all arguments are true', function() {\n        assert.equal(true, checkIfAll('a', optionsParam));\n        assert.equal(true, checkIfAll('a', 'b', optionsParam));\n        assert.equal(true, checkIfAll('a', 'b', 'c', optionsParam));\n        assert.equal(true, checkIfAll('a', 'b', 'c', 'd', optionsParam));\n        assert.equal(true, checkIfAll('a', 1, 10, true, optionsParam));\n        assert.equal(true, checkIfAll('lorem', 1, 'ipsum', [1], {a: 1}, optionsParam));\n    });\n\n    it('should return proper false when at least argument is false', function() {\n        assert.equal(false, checkIfAll('a', false, optionsParam));\n        assert.equal(false, checkIfAll(false, 1, true, optionsParam));\n        assert.equal(false, checkIfAll(true, false, true, optionsParam));\n        assert.equal(false, checkIfAll('lorem', [1], false, optionsParam));\n    });\n\n    it('should return false when all arguments are false or falsy values', function() {\n        assert.equal(false, checkIfAll(false, optionsParam));\n        assert.equal(false, checkIfAll(false, false, optionsParam));\n        assert.equal(false, checkIfAll(false, false, false, optionsParam));\n        assert.equal(false, checkIfAll(false, undefined, optionsParam));\n        assert.equal(false, checkIfAll(false, null, 0, optionsParam));\n        assert.equal(false, checkIfAll('', \"\", NaN, null, undefined, 0, false, optionsParam));\n    });\n\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/check-if-any.spec.js",
    "content": "const assert = require('assert');\nconst checkIfAny = require('../check-if-any.js');\n\ndescribe('Handlebars - checkIfAny block helper', function() {\n    // Below object emulates Handlebars behaviour\n    let optionsParam = {\n        fn: () => true,\n        inverse: () => false\n    };\n\n    it('should return false when there is no arguments', function() {\n        assert.equal(false, checkIfAny(optionsParam));\n    });\n\n    it('should return false for falsy values arguments', function() {\n        assert.equal(false, checkIfAny('', optionsParam));\n        assert.equal(false, checkIfAny(\"\", optionsParam));\n        assert.equal(false, checkIfAny(NaN, optionsParam));\n        assert.equal(false, checkIfAny(null, optionsParam));\n        assert.equal(false, checkIfAny(undefined, optionsParam));\n        assert.equal(false, checkIfAny(0, optionsParam));\n        assert.equal(false, checkIfAny(false, optionsParam));\n    });\n\n    it('should return true if all arguments are true', function() {\n        assert.equal(true, checkIfAny('a', optionsParam));\n        assert.equal(true, checkIfAny('a', 'b', optionsParam));\n        assert.equal(true, checkIfAny('a', 'b', 'c', optionsParam));\n        assert.equal(true, checkIfAny('a', 'b', 'c', 'd', optionsParam));\n        assert.equal(true, checkIfAny('a', 1, 10, true, optionsParam));\n        assert.equal(true, checkIfAny('lorem', 1, 'ipsum', [1], {a: 1}, optionsParam));\n    });\n\n    it('should return true when at least argument is true', function() {\n        assert.equal(true, checkIfAny('a', false, optionsParam));\n        assert.equal(true, checkIfAny(false, 1, true, optionsParam));\n        assert.equal(true, checkIfAny(true, false, true, optionsParam));\n        assert.equal(true, checkIfAny('lorem', [1], false, optionsParam));\n    });\n\n    it('should return false when all arguments are false or falsy values', function() {\n        assert.equal(false, checkIfAny(false, optionsParam));\n        assert.equal(false, checkIfAny(false, false, optionsParam));\n        assert.equal(false, checkIfAny(false, false, false, optionsParam));\n        assert.equal(false, checkIfAny(false, undefined, optionsParam));\n        assert.equal(false, checkIfAny(false, null, 0, optionsParam));\n        assert.equal(false, checkIfAny('', \"\", NaN, null, undefined, 0, false, optionsParam));\n    });\n\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/check-if-none.spec.js",
    "content": "const assert = require('assert');\nconst checkIfNone = require('../check-if-none.js');\n\ndescribe('Handlebars - checkIfNone block helper', function() {\n    // Below object emulates Handlebars behaviour\n    let optionsParam = {\n        fn: () => true,\n        inverse: () => false\n    };\n\n    it('should return true when there is no arguments', function() {\n        assert.equal(true, checkIfNone(optionsParam));\n    });\n\n    it('should return true for falsy values arguments', function() {\n        assert.equal(true, checkIfNone('', optionsParam));\n        assert.equal(true, checkIfNone(\"\", optionsParam));\n        assert.equal(true, checkIfNone(NaN, optionsParam));\n        assert.equal(true, checkIfNone(null, optionsParam));\n        assert.equal(true, checkIfNone(undefined, optionsParam));\n        assert.equal(true, checkIfNone(0, optionsParam));\n        assert.equal(true, checkIfNone(false, optionsParam));\n    });\n\n    it('should return false if all arguments are true', function() {\n        assert.equal(false, checkIfNone('a', optionsParam));\n        assert.equal(false, checkIfNone('a', 'b', optionsParam));\n        assert.equal(false, checkIfNone('a', 'b', 'c', optionsParam));\n        assert.equal(false, checkIfNone('a', 'b', 'c', 'd', optionsParam));\n        assert.equal(false, checkIfNone('a', 1, 10, true, optionsParam));\n        assert.equal(false, checkIfNone('lorem', 1, 'ipsum', [1], {a: 1}, optionsParam));\n    });\n\n    it('should return false when at least argument is true', function() {\n        assert.equal(false, checkIfNone('a', false, optionsParam));\n        assert.equal(false, checkIfNone(false, 1, true, optionsParam));\n        assert.equal(false, checkIfNone(true, false, true, optionsParam));\n        assert.equal(false, checkIfNone('lorem', [1], false, optionsParam));\n    });\n\n    it('should return true when all arguments are false or falsy values', function() {\n        assert.equal(true, checkIfNone(false, optionsParam));\n        assert.equal(true, checkIfNone(false, false, optionsParam));\n        assert.equal(true, checkIfNone(false, false, false, optionsParam));\n        assert.equal(true, checkIfNone(false, undefined, optionsParam));\n        assert.equal(true, checkIfNone(false, null, 0, optionsParam));\n        assert.equal(true, checkIfNone('', \"\", NaN, null, undefined, 0, false, optionsParam));\n    });\n\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/check-if.spec.js",
    "content": "const assert = require('assert');\nconst checkIf = require('../check-if.js');\n\ndescribe('Handlebars - checkIf block helper', function() {\n    // Below object emulates Handlebars behaviour\n    let optionsParam = {\n        fn: () => true,\n        inverse: () => false\n    };\n\n    it('should return false for too few arguments', function() {\n        assert.equal(undefined, checkIf('a'));\n        assert.equal(undefined, checkIf('a', '=='));\n    });\n\n    it('should return false for non-existing operator', function() {\n        assert.equal(undefined, checkIf('a', 'LOL', 'b'));\n    });\n\n    it('should return proper value for the `==` operator', function() {\n        assert.equal(true, checkIf('a', '==', 'a', optionsParam));\n        assert.equal(true, checkIf('1', '==', 1, optionsParam));\n        assert.equal(false, checkIf('a', '==', 'b', optionsParam));\n        assert.equal(false, checkIf(10, '==', 11, optionsParam));\n    });\n\n    it('should return proper value for the `equal` operator', function() {\n        assert.equal(true, checkIf('a', 'equal', 'a', optionsParam));\n        assert.equal(true, checkIf('1', 'equal', 1, optionsParam));\n        assert.equal(false, checkIf('a', 'equal', 'b', optionsParam));\n        assert.equal(false, checkIf(10, 'equal', 11, optionsParam));\n    });\n\n    it('should return proper value for the `===` operator', function() {\n        assert.equal(true, checkIf('a', '===', 'a', optionsParam));\n        assert.equal(false, checkIf('1', '===', 1, optionsParam));\n        assert.equal(true, checkIf(10, '===', 10, optionsParam));\n        assert.equal(false, checkIf(10, '===', 11, optionsParam));\n    });\n\n    it('should return proper value for the `strictEqual` operator', function() {\n        assert.equal(true, checkIf('a', 'strictEqual', 'a', optionsParam));\n        assert.equal(false, checkIf('1', 'strictEqual', 1, optionsParam));\n        assert.equal(true, checkIf(10, 'strictEqual', 10, optionsParam));\n        assert.equal(false, checkIf(10, 'strictEqual', 11, optionsParam));\n    });\n\n    it('should return proper value for the `!=` operator', function() {\n        assert.equal(false, checkIf('a', '!=', 'a', optionsParam));\n        assert.equal(false, checkIf('1', '!=', 1, optionsParam));\n        assert.equal(true, checkIf('a', '!=', 'b', optionsParam));\n        assert.equal(true, checkIf(10, '!=', 11, optionsParam));\n    });\n\n    it('should return proper value for the `different` operator', function() {\n        assert.equal(false, checkIf('a', 'different', 'a', optionsParam));\n        assert.equal(false, checkIf('1', 'different', 1, optionsParam));\n        assert.equal(true, checkIf('a', 'different', 'b', optionsParam));\n        assert.equal(true, checkIf(10, 'different', 11, optionsParam));\n    });\n\n    it('should return proper value for the `!==` operator', function() {\n        assert.equal(false, checkIf('a', '!==', 'a', optionsParam));\n        assert.equal(true, checkIf('1', '!==', 1, optionsParam));\n        assert.equal(false, checkIf(10, '!==', 10, optionsParam));\n        assert.equal(true, checkIf(10, '!==', 11, optionsParam));\n    });\n\n    it('should return proper value for the `strictDifferent` operator', function() {\n        assert.equal(false, checkIf('a', 'strictDifferent', 'a', optionsParam));\n        assert.equal(true, checkIf('1', 'strictDifferent', 1, optionsParam));\n        assert.equal(false, checkIf(10, 'strictDifferent', 10, optionsParam));\n        assert.equal(true, checkIf(10, 'strictDifferent', 11, optionsParam));\n    });\n\n    it('should return proper values for the `&&` operator', function() {\n        assert.equal(true, checkIf(true, '&&', true, optionsParam));\n        assert.equal(false, checkIf(false, '&&', true, optionsParam));\n        assert.equal(false, checkIf(true, '&&', false, optionsParam));\n        assert.equal(false, checkIf(false, '&&', false, optionsParam));\n    });\n\n    it('should return proper values for the `and` operator', function() {\n        assert.equal(true, checkIf(true, 'and', true, optionsParam));\n        assert.equal(false, checkIf(false, 'and', true, optionsParam));\n        assert.equal(false, checkIf(true, 'and', false, optionsParam));\n        assert.equal(false, checkIf(false, 'and', false, optionsParam));\n    });\n\n    it('should return proper values for the `||` operator', function() {\n        assert.equal(true, checkIf(true, '||', true, optionsParam));\n        assert.equal(true, checkIf(false, '||', true, optionsParam));\n        assert.equal(true, checkIf(true, '||', false, optionsParam));\n        assert.equal(false, checkIf(false, '||', false, optionsParam));\n    });\n\n    it('should return proper values for the `or` operator', function() {\n        assert.equal(true, checkIf(true, 'or', true, optionsParam));\n        assert.equal(true, checkIf(false, 'or', true, optionsParam));\n        assert.equal(true, checkIf(true, 'or', false, optionsParam));\n        assert.equal(false, checkIf(false, 'or', false, optionsParam));\n    });\n\n    it('should return proper values for the `<` operator', function() {\n        assert.equal(true, checkIf(10, '<', 11, optionsParam));\n        assert.equal(false, checkIf(10, '<', -11, optionsParam));\n    });\n\n    it('should return proper values for the `lesser` operator', function() {\n        assert.equal(true, checkIf(10, 'lesser', 11, optionsParam));\n        assert.equal(false, checkIf(10, 'lesser', -11, optionsParam));\n    });\n\n    it('should return proper values for the `>` operator', function() {\n        assert.equal(false, checkIf(10, '>', 11, optionsParam));\n        assert.equal(true, checkIf(10, '>', -11, optionsParam));\n    });\n\n    it('should return proper values for the `greater` operator', function() {\n        assert.equal(false, checkIf(10, 'greater', 11, optionsParam));\n        assert.equal(true, checkIf(10, 'greater', -11, optionsParam));\n    });\n\n    it('should return proper values for the `<=` operator', function() {\n        assert.equal(true, checkIf(10, '<=', 10, optionsParam));\n        assert.equal(true, checkIf(10, '<=', 11, optionsParam));\n        assert.equal(false, checkIf(10, '<=', -11, optionsParam));\n    });\n\n    it('should return proper values for the `lesserEqual` operator', function() {\n        assert.equal(true, checkIf(10, 'lesserEqual', 10, optionsParam));\n        assert.equal(true, checkIf(10, 'lesserEqual', 11, optionsParam));\n        assert.equal(false, checkIf(10, 'lesserEqual', -11, optionsParam));\n    });\n\n    it('should return proper values for the `>=` operator', function() {\n        assert.equal(false, checkIf(10, '>=', 11, optionsParam));\n        assert.equal(true, checkIf(10, '>=', -11, optionsParam));\n        assert.equal(true, checkIf(10, '>=', 10, optionsParam));\n    });\n\n    it('should return proper values for the `greaterEqual` operator', function() {\n        assert.equal(false, checkIf(10, 'greaterEqual', 11, optionsParam));\n        assert.equal(true, checkIf(10, 'greaterEqual', -11, optionsParam));\n        assert.equal(true, checkIf(10, 'greaterEqual', 10, optionsParam));\n    });\n\n    it('should return proper values for the `contains` operator', function() {\n        assert.equal(false, checkIf('10', 'contains', 11, optionsParam));\n        assert.equal(false, checkIf('10,11', 'contains', 12, optionsParam));\n        assert.equal(true, checkIf('10,11', 'contains', 11, optionsParam));\n        assert.equal(true, checkIf('10,11,12', 'contains', 12, optionsParam));\n        assert.equal(false, checkIf('10', 'contains', '11', optionsParam));\n        assert.equal(false, checkIf('10,11', 'contains', '12', optionsParam));\n        assert.equal(true, checkIf('10,11', 'contains', '11', optionsParam));\n        assert.equal(true, checkIf('10,11,12', 'contains', '12', optionsParam));\n    });\n\n    it('should return proper values for the `notContains` operator', function() {\n        assert.equal(true, checkIf('10', 'notContains', 11, optionsParam));\n        assert.equal(true, checkIf('10,11', 'notContains', 12, optionsParam));\n        assert.equal(false, checkIf('10,11', 'notContains', 11, optionsParam));\n        assert.equal(false, checkIf('10,11,12', 'notContains', 12, optionsParam));\n        assert.equal(true, checkIf('10', 'notContains', '11', optionsParam));\n        assert.equal(true, checkIf('10,11', 'notContains', '12', optionsParam));\n        assert.equal(false, checkIf('10,11', 'notContains', '11', optionsParam));\n        assert.equal(false, checkIf('10,11,12', 'notContains', '12', optionsParam));\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/feed-link.spec.js",
    "content": "const assert = require('assert');\nconst Handlebars = require('handlebars');\nconst feedLink = require('../feed-link.js').__feedLink;\n\ndescribe('Handlebars - feedLink helper', function() {\n    describe('#feedLink - both RSS and JSON', function() {\n        let rendererInstance = {\n            siteConfig: {\n                domain: 'https://example.com',\n                advanced: {\n                    feed: {\n                        enableRss: 1,\n                        enableJson: 1\n                    }\n                }\n            }\n        };\n\n        it('should return proper URL to feed.xml and feed.json files', function() {\n            assert.equal('<link rel=\"alternate\" type=\"application/atom+xml\" href=\"https://example.com/feed.xml\">' + \"\\n\" + '<link rel=\"alternate\" type=\"application/json\" href=\"https://example.com/feed.json\">' + \"\\n\", feedLink.call(rendererInstance).string);\n        });\n    });\n\n    describe('#feedLink - only RSS', function() {\n        let rendererInstance = {\n            siteConfig: {\n                domain: 'https://example.com',\n                advanced: {\n                    feed: {\n                        enableRss: 1,\n                        enableJson: 0\n                    }\n                }\n            }\n        };\n\n        it('should return proper URL to feed.xml file', function() {\n            assert.equal('<link rel=\"alternate\" type=\"application/atom+xml\" href=\"https://example.com/feed.xml\">' + \"\\n\", feedLink.call(rendererInstance).string);\n        });\n    });\n\n    describe('#feedLink - only JSON', function() {\n        let rendererInstance = {\n            siteConfig: {\n                domain: 'https://example.com',\n                advanced: {\n                    feed: {\n                        enableRss: 0,\n                        enableJson: 1\n                    }\n                }\n            }\n        };\n\n        it('should return proper URL to feed.json file', function() {\n            assert.equal('<link rel=\"alternate\" type=\"application/json\" href=\"https://example.com/feed.json\">' + \"\\n\", feedLink.call(rendererInstance).string);\n        });\n    });\n\n    describe('#feedLink - none', function() {\n        let rendererInstance = {\n            siteConfig: {\n                domain: 'https://example.com',\n                advanced: {\n                    feed: {\n                        enableRss: 0,\n                        enableJson: 0\n                    }\n                }\n            }\n        };\n\n        it('should return empty string', function() {\n            assert.equal('', feedLink.call(rendererInstance).string);\n        });\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/font.spec.js",
    "content": "const assert = require('assert');\nconst Handlebars = require('handlebars');\nconst font = require('../font.js');\n\ndescribe('Handlebars - font helper', function() {\n    describe('#font', function() {\n        it('should return proper URL to Google Fonts API', function() {\n            assert.equal('https://fonts.googleapis.com/css?family&#x3D;', font.__font(''));\n            assert.equal('https://fonts.googleapis.com/css?family&#x3D;Open+Sans', font.__font('Open+Sans'));\n        });\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/is-empty.spec.js",
    "content": "const assert = require('assert');\nconst isEmpty = require('../is-empty.js');\n\ndescribe('Handlebars - isEmpty block helper', function() {\n    // Below object emulates Handlebars behaviour\n    let optionsParam = {\n        fn: () => true,\n        inverse: () => false\n    };\n\n    it('should return true for empty array', function() {\n        assert.equal(true, isEmpty([], optionsParam));\n    });\n\n    it('should return false for non-empty array', function() {\n        assert.equal(false, isEmpty([1,2,3], optionsParam));\n    });\n\n    it('should return true for empty object', function() {\n        assert.equal(true, isEmpty({}, optionsParam));\n    });\n\n    it('should return false for non-empty object', function() {\n        assert.equal(false, isEmpty({a:1, b:2}, optionsParam));\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/is-not-empty.spec.js",
    "content": "const assert = require('assert');\nconst isNotEmpty = require('../is-not-empty.js');\n\ndescribe('Handlebars - isNotEmpty block helper', function() {\n    // Below object emulates Handlebars behaviour\n    let optionsParam = {\n        fn: () => true,\n        inverse: () => false\n    };\n\n    it('should return false for empty array', function() {\n        assert.equal(false, isNotEmpty([], optionsParam));\n    });\n\n    it('should return true for non-empty array', function() {\n        assert.equal(true, isNotEmpty([1,2,3], optionsParam));\n    });\n\n    it('should return false for empty object', function() {\n        assert.equal(false, isNotEmpty({}, optionsParam));\n    });\n\n    it('should return true for non-empty object', function() {\n        assert.equal(true, isNotEmpty({a:1, b:2}, optionsParam));\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/jsonify.spec.js",
    "content": "const assert = require('assert');\nconst Handlebars = require('handlebars');\nconst jsonify = require('../jsonify.js');\n\ndescribe('Handlebars - jsonify helper', function() {\n    describe('#jsonify', function() {\n        it('should return proper value if string is a value', function() {\n            assert.equal('\"string\"', jsonify('string'));\n        });\n\n        it('should return proper value if array is a value', function() {\n            assert.equal('[1,2,3]', jsonify([1,2,3]));\n        });\n\n        it('should return proper value if object is a value', function() {\n            assert.equal('{\"a\":1}', jsonify({a: 1}));\n        });\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/specs/translate.spec.js",
    "content": "const assert = require('assert');\nconst Handlebars = require('handlebars');\nconst translate = require('../translate.js');\n\ndescribe('Handlebars - translate helper', function() {\n    describe('#resolveObject', function() {\n        it('should return undefined when there is no arguments', function() {\n            assert.equal(undefined, translate.__resolveObject());\n        });\n\n        it('should return undefined when one of the arguments is empty', function() {\n            assert.equal(undefined, translate.__resolveObject(undefined, false));\n            assert.equal(undefined, translate.__resolveObject(false));\n        });\n\n        it('should return undefined when the path doesn\\'t exist in the passed object', function() {\n            assert.equal(undefined, translate.__resolveObject({}, 'a'));\n        });\n\n        it('should return proper value when the path exist in the passed object', function() {\n            assert.equal(1, translate.__resolveObject({'a': 1}, 'a'));\n        });\n\n        it('should return proper value when the path exist in the passed object (nested)', function() {\n            assert.equal(1, translate.__resolveObject({'a': {'b': {'c': 1}}}, 'a.b.c'));\n        });\n    });\n\n    describe('#translate', function() {\n        it('should return `[MISSING TRANSLATION]` when there is no translations', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: false,\n                    theme: false\n                }\n            });\n\n            assert.equal('[MISSING TRANSLATION]', translator());\n        });\n\n        it('should return proper value when there is translations', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        'key': 'Hello'\n                    },\n                    theme: false\n                }\n            });\n\n            assert.equal('Hello', translator('key'));\n        });\n\n        it('should return proper value when there is no translations in the user\\'s override', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {},\n                    theme: {\n                        'key': 'Hello'\n                    }\n                }\n            });\n\n            assert.equal('Hello', translator('key'));\n        });\n\n        it('should return user value when there is translations in the theme\\'s language file', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        'key': 'Hello'\n                    },\n                    theme: {\n                        'key': 'Hello2'\n                    }\n                }\n            });\n\n            assert.equal('Hello', translator('key'));\n        });\n\n        it('should return user value when there is nested translations object', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        params: {\n                            'key': 'Hello'\n                        }\n                    },\n                    theme: {\n                        'key': 'Hello2'\n                    }\n                }\n            });\n\n            assert.equal('Hello', translator('params.key'));\n        });\n\n        it('should return [WRONG TRANSLATION ARGUMENTS NUMBER] when there is too few arguments', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        'key': 'Hello %s'\n                    },\n                    theme: {\n                        'key': 'Hello2'\n                    }\n                }\n            });\n\n            assert.equal('[WRONG TRANSLATION ARGUMENTS NUMBER]', translator('key', {}));\n        });\n\n        it('should return [WRONG TRANSLATION ARGUMENTS NUMBER] when there is too much arguments', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        'key': 'Hello %s'\n                    },\n                    theme: {\n                        'key': 'Hello2'\n                    }\n                }\n            });\n\n            assert.equal('[WRONG TRANSLATION ARGUMENTS NUMBER]', translator('key', 'John', 'Doe', {}));\n        });\n\n        it('should return properly replaced text when there is text with one replacement', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        'key': 'Hello %s'\n                    },\n                    theme: {\n                        'key': 'Hello2'\n                    }\n                }\n            });\n\n            assert.equal('Hello John', translator('key', 'John', {}));\n        });\n\n        it('should return properly replaced text when there is text with few replacements', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        'key': 'Hello %s %s'\n                    },\n                    theme: {\n                        'key': 'Hello2'\n                    }\n                }\n            });\n\n            assert.equal('Hello John Doe', translator('key', 'John', 'Doe', {}));\n        });\n\n        it('should return properly replaced text when the argument is a number', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        'key': 'Current date is %s %s'\n                    },\n                    theme: {\n                        'key': 'Hello2'\n                    }\n                }\n            });\n\n            assert.equal('Current date is 12 2017', translator('key', 12, 2017, {}));\n        });\n\n        it('should return [NO NUMBER FOR THE PLURAL PHRASE] when there is no argument', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        plural: {\n                            0: 'Zero',\n                            1: 'One',\n                            2: 'Two',\n                            'default': 'More'\n                        }\n                    },\n                    theme: false\n                }\n            });\n\n            assert.equal('[NO NUMBER FOR THE PLURAL PHRASE]', translator('plural', {}));\n        });\n\n        it('should return [THERE IS NO DEFINITION FOR THE DEFAULT VALUE IN THE PLURAL PHRASE] when there is no definition for the default plural value', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        plural: {\n                            0: 'Zero',\n                            1: 'One',\n                            2: 'Two'\n                        }\n                    },\n                    theme: false\n                }\n            });\n\n            assert.equal('[THERE IS NO DEFINITION FOR THE DEFAULT VALUE IN THE PLURAL PHRASE]', translator('plural', 101, {}));\n        });\n\n        it('should return proper translations for a different numbers in plural case', function() {\n            let translator = translate.__translate.bind({\n                translations: {\n                    user: {\n                        plural: {\n                            0: 'Zero',\n                            1: 'One',\n                            2: 'Two',\n                            'default': 'More'\n                        }\n                    },\n                    theme: false\n                }\n            });\n\n            assert.equal('Zero', translator('plural', 0, {}));\n            assert.equal('One', translator('plural', 1, {}));\n            assert.equal('Two', translator('plural', 2, {}));\n            assert.equal('More', translator('plural', 3, {}));\n            assert.equal('More', translator('plural', 10, {}));\n            assert.equal('More', translator('plural', 2048, {}));\n        });\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/handlebars/helpers/translate.js",
    "content": "const Handlebars = require('handlebars');\n\n/**\n * Helper functin used for creating translatable phrases\n *\n * {{ translate 'key.subkey' [string1 string2 ...] }}\n *\n * @returns {string} - translation for a given strings\n */\nfunction translate(key) {\n    let translation = '[MISSING TRANSLATION]';\n\n    /*\n     * this is pointing to the rendererInstance\n     */\n    if(this.translations.user) {\n        let result = resolveObject(this.translations.user, key);\n\n        if(result) {\n            translation = result;\n        } else if(this.translations.theme) {\n            let result = resolveObject(this.translations.theme, key);\n\n            if (result) {\n                translation = result;\n            }\n        }\n    } else if(this.translations.theme) {\n        let result = resolveObject(this.translations.theme, key);\n\n        if(result) {\n            translation = result;\n        }\n    }\n\n    if(typeof translation === 'object') {\n        // Parse complex translation (plurals)\n        let numberToUse = Array.prototype.slice.call(arguments).slice(1);\n        numberToUse.pop();\n\n        if(numberToUse.length != 1 || isNaN(numberToUse[0])) {\n            translation = '[NO NUMBER FOR THE PLURAL PHRASE]';\n        } else {\n            let number = parseInt(numberToUse[0], 10);\n\n            if(translation[number]) {\n                translation = translation[number];\n            } else {\n                if(translation.default) {\n                    translation = translation.default;\n                } else {\n                    translation = '[THERE IS NO DEFINITION FOR THE DEFAULT VALUE IN THE PLURAL PHRASE]';\n                }\n            }\n        }\n    } else if(translation.indexOf('%s') > -1) {\n        // Parse arguments and merge into translation\n        let phrasesToReplace = Array.prototype.slice.call(arguments).slice(1);\n        phrasesToReplace.pop();\n\n        if(translation.match(/%s/g).length !== phrasesToReplace.length) {\n            translation = '[WRONG TRANSLATION ARGUMENTS NUMBER]';\n        } else {\n            let textToTransform = translation;\n\n            for(let i = 0; i < phrasesToReplace.length; i++) {\n                textToTransform = textToTransform.replace('%s', phrasesToReplace[i]);\n            }\n\n            translation = textToTransform;\n        }\n    }\n\n    return new Handlebars.SafeString(translation);\n}\n\n/**\n * Helper function to return a value inside the object for a given object path\n *\n * @param {object} obj - object to traverse\n * @param {string} path - path in the object structure\n *\n * @returns {mixed} - undefined if specific value does not exist inside the object\n *                    or the value under specific object path\n */\nfunction resolveObject(obj, path) {\n    if(!obj || !path) {\n        return undefined;\n    }\n\n    return path.split('.').reduce(function(previous, current) {\n        return previous ? previous[current] : undefined\n    }, obj);\n}\n\n/**\n * Helper for creating translatable phrases\n *\n * {{ translate 'key.subkey' [string1 string2 ...] }}\n */\nfunction translateHelper(rendererInstance, Handlebars) {\n    Handlebars.registerHelper('translate', translate.bind(rendererInstance));\n}\n\nmodule.exports = {\n    translateHelper: translateHelper,\n    __resolveObject: resolveObject,\n    __translate: translate\n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/content.js",
    "content": "/*\n * Class used to help with operations on\n * the URLs and slugs\n */\n\nconst slug = require('./../../../helpers/slug');\nconst path = require('path');\nconst MarkdownToHtml = require('./../text-renderers/markdown');\nconst BlocksToHtml = require('./../text-renderers/blockeditor');\nconst normalizePath = require('normalize-path');\nconst URLHelper = require('./url');\nconst UtilsHelper = require('./../../../helpers/utils');\n\n/**\n * Class used to prepare content in data items\n */\nclass ContentHelper {\n    /**\n     * Prepares post content\n     *\n     * @param postID\n     * @param originalText\n     * @param siteDomain\n     * @param themeConfig\n     * @param renderer\n     * @returns {string}\n     */\n    static prepareContent(postID, originalText, siteDomain, themeConfig, renderer, editor = 'tinymce') {\n        let domain = normalizePath(siteDomain);\n        domain = URLHelper.fixProtocols(domain);\n\n        // Get media URL\n        let domainMediaPath = domain + '/media/posts/' + postID + '/';\n\n        // Detect forced WebP images\n        let useWebp = false;\n\n        if (renderer.siteConfig?.advanced?.forceWebp) {\n            useWebp = true;\n        }\n\n        // Replace domain name constant with real URL to media directory\n        let preparedText = originalText.split('#DOMAIN_NAME#').join(domainMediaPath);\n        preparedText = ContentHelper.parseText(preparedText, editor);\n        preparedText = ContentHelper.setWebpCompatibility(useWebp, preparedText);\n\n        // Remove content for AMP or non-AMP depending from ampMode value\n        preparedText = preparedText.replace(/<publii-amp>[\\s\\S]*?<\\/publii-amp>/gmi, '');\n        preparedText = preparedText.replace(/<publii-non-amp>/gmi, '');\n        preparedText = preparedText.replace(/<\\/publii-non-amp>/gmi, '');\n\n        // Remove TOC plugin ID attributes when TOC does not exist\n        if (preparedText.indexOf('class=\"post__toc') === -1) {\n           preparedText = preparedText.replace(/\\sid=\"mcetoc_[a-z0-9]*?\"/gmi, '');\n        }\n\n        // Reduce download=\"download\" to download\n        preparedText = preparedText.replace(/download=\"download\"/gmi, 'download');\n\n        // Remove the last empty paragraph\n        preparedText = preparedText.replace(/<p>&nbsp;<\\/p>\\s?$/gmi, '');\n\n        // Find all images and add srcset and sizes attributes\n        if (renderer.siteConfig.advanced.responsiveImages) {\n            preparedText = preparedText.replace(/<img[\\s\\S]*?src=\"(.*?)\"[\\s\\S]*?>/gmi, function(matches, url) {\n                if (matches.indexOf('data-responsive=\"false\"') > -1) {\n                    return matches;\n                }\n\n                return ContentHelper._addResponsiveAttributes(matches, url, themeConfig, useWebp, domain);\n            });\n        }\n\n        // Add loading=\"lazy\" attributes to img, video, audio, iframe tags\n        if (renderer.siteConfig.advanced.mediaLazyLoad) {\n            preparedText = preparedText.replace(/<img\\s/gmi, '<img loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<video\\s/gmi, '<video loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<audio\\s/gmi, '<audio loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<iframe\\s/gmi, '<iframe loading=\"lazy\" ');\n            preparedText = preparedText.replace(/<img\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<img$1');\n            preparedText = preparedText.replace(/<video\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<video$1');\n            preparedText = preparedText.replace(/<audio\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<audio$1');\n            preparedText = preparedText.replace(/<iframe\\sloading=\"lazy\"([^>].*?\\sloading=\"[^>].*?>)/gmi, '<iframe$1');\n        }\n\n        if (editor === 'tinymce' || editor === 'markdown') {\n            // Wrap images with classes into <figure>\n            preparedText = preparedText.replace(/(<p.*?>\\s*?)?<img[^>]*?(class=\".*?\").*?>(\\s*?<\\/p>)?/gmi, function(matches, p1, classes) {\n                return '<figure ' + classes + '>' + matches.replace('</p>', '').replace(/<p.*?>/, '').replace(classes, '') + '</figure>';\n            });\n\n            // Fix some specific syntax cases for double figure elements\n            preparedText = preparedText.replace(/<figure contenteditable=\"false\">[\\s]*?<figure class=\"post__image\">([\\s\\S]*?)<\\/figure>[\\s]*?<\\/figure>/gmi, '<figure class=\"post__image\">$1</figure>');\n            preparedText = preparedText.replace(/<figure contenteditable=\"false\">[\\s]*?<figure class=\"post__image\">([\\s\\S]*?)<\\/figure>[\\s]*?<figcaption contentEditable=\"true\">([\\s\\S]*?)<\\/figcaption>[\\s]*?<\\/figure>/gmi, '<figure class=\"post__image\">$1<figcaption>$2</figcaption></figure>');\n        }\n\n        // Remove contenteditable attributes\n        preparedText = preparedText.replace(/contenteditable=\".*?\"/gi, '');\n\n        if (editor === 'tinymce') {\n            // Wrap galleries with classes into div with gallery-wrapper CSS class\n            preparedText = preparedText.replace(/<div class=\"gallery([\\s\\S]*?)\"[\\s\\S]*?<\\/div>?/gmi, function(matches, classes) {\n                return '<div class=\"gallery-wrapper' + classes + '\">' + matches.replace(classes, '') + '</div>';\n            });\n        }\n\n        // Remove double slashes from the gallery URLs (if they appears)\n        preparedText = preparedText.replace(/\\/\\/gallery\\/$/gmi, '/gallery/');\n\n        // Remove paragraphs around <iframe>'s\n        preparedText = preparedText.replace(/\\<p\\>\\<iframe/gmi, '<iframe');\n        preparedText = preparedText.replace(/\\<\\/iframe\\>\\<\\/p\\>/gmi, '</iframe>');\n\n        // Wrap iframes into <div class=\"post__iframe\">\n        preparedText = preparedText.replace(/(?<!<figure[\\s\\S]*?class=\"post__video\">[\\s\\S]*?)(<iframe.*?>[\\s\\S]*?<\\/iframe>)/gmi, function(matches) {\n            if (matches.indexOf('data-responsive=\"false\"') > -1) {\n                return matches;\n            }\n\n            return '<div class=\"post__iframe\">' + matches + '</div>';\n        });\n\n        // Remove CDATA sections inside scripts added by TinyMCE\n        preparedText = preparedText.replace(/\\<script\\>\\/\\/ \\<\\!\\[CDATA\\[/g, '<script>');\n        preparedText = preparedText.replace(/\\/\\/ \\]\\]\\>\\<\\/script\\>/g, '</script>');\n\n        // Add embed consents\n        if (\n            themeConfig.supportedFeatures &&\n            themeConfig.supportedFeatures.embedConsents &&\n            renderer.siteConfig.advanced.gdpr.enabled &&\n            renderer.siteConfig.advanced.gdpr.allowAdvancedConfiguration &&\n            renderer.siteConfig.advanced.gdpr.embedConsents &&\n            renderer.siteConfig.advanced.gdpr.embedConsents.length\n        ) {\n            preparedText = ContentHelper.addEmbedConsents(preparedText, renderer.siteConfig.advanced.gdpr.embedConsents);\n        }\n\n        // Add dnt=1 for Vimeo links\n        if (renderer.siteConfig.advanced.gdpr.vimeoNoTrack) {\n            preparedText = preparedText.replace(/src=\"(http[s]?\\:\\/\\/player\\.vimeo\\.com\\/video\\/.*?)\"/gmi, function (url, matches) {\n                if (matches.indexOf('dnt=') > -1) {\n                    return 'src=\"' + matches + '\"';\n                }\n            \n                if (matches.indexOf('?') > -1) {\n                    return 'src=\"' + matches + '&dnt=1\"';\n                }\n            \n                return 'src=\"' + matches + '?dnt=1\"';\n            });\n\n            preparedText = preparedText.replace(/src='(http[s]?\\:\\/\\/player\\.vimeo\\.com\\/video\\/.*?)'/gmi, function (url, matches) {\n                if (matches.indexOf('dnt=') > -1) {\n                    return 'src=\\'' + matches + '\\'';\n                }\n            \n                if (matches.indexOf('?') > -1) {\n                    return 'src=\\'' + matches + '&dnt=1\\'';\n                }\n            \n                return 'src=\\'' + matches + '?dnt=1\\'';\n            });\n        }\n        \n        // Add youtube-nocookie.com domain for YouTube videos\n        if (renderer.siteConfig.advanced.gdpr.ytNoCookies) {\n            preparedText = preparedText.replace(/src=\"http[s]?\\:\\/\\/www\\.youtube\\.com\\/embed\\//gmi, 'src=\"https://www.youtube-nocookie.com/embed/');\n            preparedText = preparedText.replace(/src='http[s]?\\:\\/\\/www\\.youtube\\.com\\/embed\\//gmi, 'src=\\'https://www.youtube-nocookie.com/embed/');\n        }\n\n        return preparedText;\n    }\n\n    /**\n     * Parse text using a editor-specific parser\n     *\n     * @param {*} inputText\n     * @param {*} editor\n     */\n    static parseText (inputText, editor = 'tinymce') {\n        if (editor === 'tinymce') {\n            return inputText;\n        }\n\n        if (editor === 'markdown') {\n            inputText = ContentHelper.prepareMarkdown(inputText);\n            return MarkdownToHtml.parse(inputText);\n        }\n\n        if (editor === 'blockeditor') {\n            return BlocksToHtml.parse(inputText);\n        }\n    }\n\n    /**\n     * Prepares markdown code to display\n     * @param input\n     */\n    static prepareMarkdown (input) {\n        input = input.replace(/\\-\\-\\-READMORE\\-\\-\\-/gmi, '<hr id=\"read-more\">');\n        return input;\n    }\n\n    /**\n     * Prepares post excerpt\n     *\n     * @param length\n     * @param text\n     * @returns {*}\n     */\n    static prepareExcerpt(length, text) {\n        // Detect readmore\n        let readmoreMatches = text.match(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n\n        if(readmoreMatches && readmoreMatches.length) {\n            text = text.split(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n            text = text[0];\n            return text;\n        }\n\n        length = parseInt(length, 10);\n        text = text.replace(/\\<script\\>\\/\\/ \\<\\!\\[CDATA\\[/g, '<script>');\n        text = text.replace(/\\/\\/ \\]\\]\\>\\<\\/script\\>/g, '</script>');\n        text = text.replace(/\\<div class=\"post__toc\"\\>[\\s\\S]*?\\<\\/div\\>/gmi, ''); // Remove ToC\n        text = text.replace(/<script>[\\s\\S]*?<\\/script>/gmi, ''); // Remove scripts\n        text = text.replace(/\\r?\\n/g, ' '); // Replace newline characters with spaces\n        text = text.replace(/<\\/p>.*?<p/gi, '</p> <p'); // Replace paragraphs spaces into real space\n        text = text.replace(/<br.*?>/gi, ' '); // Replace BR tags with spaces\n        text = text.replace(/<publii-non-amp>[\\s\\S]*?<\\/publii-non-amp>/gmi, ''); // Remove conditional content\n        text = text.replace(/<publii-amp>[\\s\\S]*?<\\/publii-amp>/gmi, ''); // Remove conditional content\n        text = text.replace(/<(?:.|\\s)*?>/g, ''); // Remove HTML tags\n        text = text.replace(/\\s{2,}/g, ' '); // Shrink multiple spaces into one space\n        text = text.split(' '); // Create an array of words\n        text = text.filter(word => !!word);\n        let textLength = text.length;\n        text = text.slice(0, parseInt(length, 10)); // Select first X elements\n        text = text.join(' ');  // Merge the text with spaces and return\n\n        // Add dots at the end if the text was longer than the limit\n        if(textLength > length && text.trim().substr(-1) !== '.') {\n            text += '&hellip;';\n        }\n\n        if (text.trim() === '&hellip;') {\n            return '';\n        }\n\n        return text;\n    }\n\n    /**\n     * Returns srcset for featured image\n     */\n    static getFeaturedImageSrcset(baseUrl, themeConfig, useWebp, type = 'post') {\n        if(!ContentHelper._isImage(baseUrl) || !UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            return false;\n        }\n\n        let dimensions = UtilsHelper.responsiveImagesDimensions(themeConfig, 'featuredImages');\n        let dimensionsData = UtilsHelper.responsiveImagesData(themeConfig, 'featuredImages');\n        let groups = UtilsHelper.responsiveImagesGroups(themeConfig, 'featuredImages');\n\n        if (type === 'tag') {\n            dimensions = UtilsHelper.responsiveImagesDimensions(themeConfig, 'tagImages');\n            dimensionsData = UtilsHelper.responsiveImagesData(themeConfig, 'tagImages');\n            groups = UtilsHelper.responsiveImagesGroups(themeConfig, 'tagImages');\n        } else if (type === 'author') {\n            dimensions = UtilsHelper.responsiveImagesDimensions(themeConfig, 'authorImages');\n            dimensionsData = UtilsHelper.responsiveImagesData(themeConfig, 'authorImages');\n            groups = UtilsHelper.responsiveImagesGroups(themeConfig, 'authorImages');\n        }\n\n        if(!dimensions) {\n            dimensions = UtilsHelper.responsiveImagesDimensions(themeConfig, 'contentImages');\n            dimensionsData = UtilsHelper.responsiveImagesData(themeConfig, 'contentImages');\n            groups = false;\n        }\n\n        if(!dimensions) {\n            return false;\n        }\n\n        let srcset = [];\n\n        if(groups === false) {\n            for(let dimension of dimensions) {\n                let responsiveImage = ContentHelper._getSrcSet(baseUrl, dimension, useWebp);\n                srcset.push(responsiveImage + ' ' + dimensionsData[dimension].width + 'w');\n            }\n\n            return srcset.join(' ,');\n        } else {\n            srcset = {};\n\n            for(let dimension of dimensions) {\n                let groupNames = dimensionsData[dimension].group.split(',');\n\n                for(let groupName of groupNames) {\n                    if (!srcset[groupName]) {\n                        srcset[groupName] = [];\n                    }\n\n                    let responsiveImage = ContentHelper._getSrcSet(baseUrl, dimension, useWebp);\n                    srcset[groupName].push(responsiveImage + ' ' + dimensionsData[dimension].width + 'w');\n                }\n            }\n\n            let srcsetKeys = Object.keys(srcset);\n\n            for(let key of srcsetKeys) {\n                srcset[key] = srcset[key].join(' ,');\n            }\n\n            return srcset;\n        }\n    }\n\n    /**\n     * Returns content of the sizes attribute for featured image\n     */\n    static getFeaturedImageSizes(themeConfig, type = 'post') {\n        if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            return false;\n        }\n\n        if (type === 'tag' && UtilsHelper.responsiveImagesConfigExists(themeConfig, 'tagImages')) {\n            return themeConfig.files.responsiveImages.tagImages.sizes;\n        } else if (type === 'author' && UtilsHelper.responsiveImagesConfigExists(themeConfig, 'authorImages')) {\n            return themeConfig.files.responsiveImages.authorImages.sizes;\n        } else if (type === 'post' && UtilsHelper.responsiveImagesConfigExists(themeConfig, 'featuredImages')) {\n            return themeConfig.files.responsiveImages.featuredImages.sizes;\n        } else if (UtilsHelper.responsiveImagesConfigExists(themeConfig, 'contentImages')) {\n            return themeConfig.files.responsiveImages.contentImages.sizes;\n        }\n\n        return false;\n    }\n\n    /**\n     * Returns image srcset attribute\n     *\n     * @param baseUrl\n     * @param themeConfig\n     * @returns {*}\n     */\n    static getContentImageSrcset(baseUrl, themeConfig, useWebp) {\n        if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            return false;\n        }\n\n        let dimensions = UtilsHelper.responsiveImagesDimensions(themeConfig, 'contentImages');\n        let dimensionsData = UtilsHelper.responsiveImagesData(themeConfig, 'contentImages');\n\n        if(!dimensions) {\n            return false;\n        }\n\n        let srcset = [];\n\n        for(let dimension of dimensions) {\n            let responsiveImage = ContentHelper._getSrcSet(baseUrl, dimension, useWebp);\n            srcset.push(responsiveImage + ' ' + dimensionsData[dimension].width + 'w');\n        }\n\n        return srcset.join(' ,');\n    }\n\n    /**\n     * Returns image sizes attribute\n     *\n     * @param themeConfig\n     * @returns {*}\n     */\n    static getContentImageSizes(themeConfig) {\n        if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            return false;\n        }\n\n        if(UtilsHelper.responsiveImagesConfigExists(themeConfig, 'contentImages')) {\n            return themeConfig.files.responsiveImages.contentImages.sizes;\n        }\n\n        return false;\n    }\n\n    /**\n     * Returns srcset attribute\n     *\n     * @param url\n     * @param dimension\n     * @returns {string}\n     * @private\n     */\n    static _getSrcSet(url, dimension, useWebp) {\n        let filename = url.split('/');\n        filename = filename[filename.length-1];\n        let filenameFile = path.parse(filename).name;\n        let filenameExtension = path.parse(filename).ext;\n\n        if (useWebp && ['.jpg', '.jpeg', '.png'].indexOf(filenameExtension.toLowerCase()) > -1) {\n            filenameExtension = '.webp';\n        }\n\n        let baseUrlWithoutFilename = url.replace(filename, '');\n        let responsiveImage = baseUrlWithoutFilename + 'responsive/' + filenameFile + '-' + dimension + filenameExtension;\n        responsiveImage = responsiveImage.replace(/\\s/g, '%20');\n\n        return responsiveImage;\n    }\n\n    /**\n     * Checks if specified URL is an image\n     *\n     * @param url\n     * @returns {boolean}\n     * @private\n     */\n    static _isImage(url) {\n        if (\n            url.toLowerCase().indexOf('.jpg') === -1 &&\n            url.toLowerCase().indexOf('.jpeg') === -1 &&\n            url.toLowerCase().indexOf('.png') === -1 &&\n            url.toLowerCase().indexOf('.webp') === -1\n        ) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Adds responsive-related image attributes\n     *\n     * @param matches\n     * @param url\n     * @param themeConfig\n     * @param domain\n     * @returns {*}\n     * @private\n     */\n    static _addResponsiveAttributes(matches, url, themeConfig, useWebp, domain) {\n        matches = matches.replace('/>', '');\n        matches = matches.replace('>', '');\n\n        if (\n            url.indexOf('media/posts') === -1 &&\n            url.indexOf('media\\posts') === -1 &&\n            url.indexOf('media/website') === -1 &&\n            url.indexOf('media\\website') === -1\n        ) {\n            return matches + ' data-is-external-image=\"true\">';\n        }\n\n        if(\n            ContentHelper.getContentImageSrcset(url, themeConfig, useWebp) !== false &&\n            ContentHelper._imageIsLocal(url, domain) &&\n            !(\n                url.toLowerCase().indexOf('.jpg') === -1 &&\n                url.toLowerCase().indexOf('.jpeg') === -1 &&\n                url.toLowerCase().indexOf('.png') === -1 &&\n                url.toLowerCase().indexOf('.webp') === -1\n            ) &&\n            url.toLowerCase().indexOf('/gallery/') === -1\n        ) {\n            if(ContentHelper.getContentImageSizes(themeConfig)) {\n                return matches +\n                    ' sizes=\"' + ContentHelper.getContentImageSizes(themeConfig) + '\"' +\n                    ' srcset=\"' + ContentHelper.getContentImageSrcset(url, themeConfig, useWebp) + '\">';\n            } else {\n                return matches +\n                    ' srcset=\"' + ContentHelper.getContentImageSrcset(url, themeConfig, useWebp) + '\">';\n            }\n        } else if (!ContentHelper._imageIsLocal(url, domain)) {\n            return matches + ' data-is-external-image=\"true\">';\n        } else {\n            return matches + '>';\n        }\n    }\n\n    /**\n     * Detect if image is an local image\n     *\n     * @param url - image URL\n     * @param domain - site domain\n     * @returns {bool}\n     * @private\n     */\n    static _imageIsLocal (url, domain) {\n        if (url.toLowerCase().indexOf('http://') > -1 || url.toLowerCase().indexOf('https://') > -1) {\n            if (domain.indexOf('/') === 0 || domain === '') {\n                return false;\n            }\n\n            if (url.indexOf(domain) > -1 || url.toLowerCase().indexOf(domain) > -1) {\n                return true;\n            }\n        } else {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Creates internal linking for a given text\n     *\n     * @param text\n     * @param renderer\n     * @returns {string}\n     */\n    static setInternalLinks(text, renderer) {\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'post');\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'page');\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'tag');\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'tags');\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'author');\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'frontpage');\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'blogpage');\n        text = ContentHelper.prepareInternalLinks(text, renderer, 'file');\n\n        return text;\n    }\n\n    /**\n     * Prepares internal links in the text\n     *\n     * @param text\n     * @param renderer\n     * @param type\n     *\n     * @returns {string} - modified text\n     */\n    static prepareInternalLinks(text, renderer, type) {\n        // Extract URLs\n        let regexp = new RegExp('#INTERNAL_LINK#\\/' + type + '\\/[0-9]{1,}', 'gmi');\n\n        if (type === 'file' || type === 'author') {\n            regexp = new RegExp('#INTERNAL_LINK#\\/' + type + '\\/.*?[\\\"\\']{1,1}', 'gmi');\n        }\n\n        let urls = [...new Set(text.match(regexp))];\n\n        // We need to remove trailing '\"' char from the files matches\n        if (type === 'file' || type === 'author') {\n            urls = urls.map(file => file.replace(/\"$/, ''));\n        }\n\n        // When there is no internal links of given type - return unmodified text\n        if (urls.length === 0) {\n            return text;\n        }\n\n        // Get proper URLs for frontpage\n        if (type === 'frontpage') {\n            let url = '#INTERNAL_LINK#/frontpage/1';\n            let link = renderer.siteConfig.domain;\n\n            if (renderer.previewMode || renderer.siteConfig.advanced.urls.addIndex) {\n                link = link + '/index.html';\n            }\n\n            text = text.split(url).join(link);\n\n            return text;\n        }\n\n        // Get proper URLs for blog page\n        if (type === 'blogpage') {\n            let url = '#INTERNAL_LINK#/blogpage/1';\n            let link = renderer.siteConfig.domain;\n\n            if (renderer.siteConfig.advanced.usePageAsFrontpage && renderer.siteConfig.advanced.urls.postsPrefix) {\n                link = renderer.siteConfig.domain + '/' + renderer.siteConfig.advanced.urls.postsPrefix + '/';\n            }\n\n            if (renderer.previewMode || renderer.siteConfig.advanced.urls.addIndex) {\n                link = link + 'index.html';\n            }\n\n            text = text.split(url).join(link);\n\n            return text;\n        }\n\n        // Get proper URLs for frontpage\n        if (type === 'tags') {\n            let url = '#INTERNAL_LINK#/tags/1';\n            let link = renderer.siteConfig.domain + '/' + renderer.siteConfig.advanced.urls.tagsPrefix + '/';\n\n            if (renderer.siteConfig.advanced.urls.postsPrefix && renderer.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                link = renderer.siteConfig.domain + '/' + renderer.siteConfig.advanced.urls.postsPrefix + '/' + renderer.siteConfig.advanced.urls.tagsPrefix + '/';\n            }\n\n            if (renderer.previewMode || renderer.siteConfig.advanced.urls.addIndex) {\n                link = link + 'index.html';\n            }\n\n            text = text.split(url).join(link);\n\n            return text;\n        }\n\n        // Get proper URLs for the files\n        if (type === 'file') {\n            for (let url of urls) {\n                let link = url.replace('#INTERNAL_LINK#/file/', renderer.siteConfig.domain + '/');\n                text = text.split(url).join(link);\n            }\n\n            return text;\n        }\n\n        // Get proper URLs for authors\n        if (type === 'author') {\n            for (let url of urls) {\n                let authorSlug = url.replace('#INTERNAL_LINK#/author/', '');\n                let authorIDs = Object.keys(renderer.cachedItems.authors);\n\n                for (let authorID of authorIDs) {\n                    if (renderer.cachedItems.authors[authorID].username === authorSlug) {\n                        let link = renderer.cachedItems.authors[authorID].url;\n                        text = text.split(url).join(link);\n                    }\n                }\n            }\n\n            return text;\n        }\n\n        // Get proper URLs for other types of content\n        let ids = urls.map(url => url.replace('#INTERNAL_LINK#/' + type + '/', ''));\n        let links = {};\n\n        for (let id of ids) {\n            let baseUrl = '#INTERNAL_LINK#/' + type + '/' + id;\n            let pluralName = type + 's';\n\n            if (renderer.cachedItems[pluralName][id]) {\n                links[baseUrl] = renderer.cachedItems[pluralName][id].url;\n            } else {\n                console.log('(i) Non-existing link: ' + pluralName + ' (' + id + ')');\n                links[baseUrl] = '#non-existing-' + type + '-with-id-' + id;\n            }\n        }\n\n        // Sort urls by length descending - to avoid issues when shorter URLs are used to replace longer URLs\n        urls.sort((urlA, urlB) => {\n            return urlB.length - urlA.length;\n        });\n\n        // Replace original URLs with proper URLs\n        for(let url of urls) {\n            text = text.split(url).join(links[url]);\n        }\n\n        return text;\n    }\n\n    /**\n     * Add overlays for the embed items \n     * \n     * @param {string} text \n     * @param {Array} embedConsents \n     * \n     * @returns {string} - modified text\n     */\n    static addEmbedConsents (text, embedConsents) {\n        for (let i = 0; i < embedConsents.length; i++) {\n            let embedConsent = embedConsents[i];\n            text = text.replace(/\\<iframe[^\\>]*?src=\"([^\"]*?)\"[^\\>]*?\\>\\<\\/iframe\\>/gmi, function (iframe, url) {\n                if (url.indexOf(embedConsent.rule) === -1) {\n                    return iframe;\n                }\n\n                if (embedConsent.cookieGroup === '-' || embedConsent.cookieGroup === '') {\n                    return iframe;\n                }\n\n                if (iframe.indexOf('data-consent-overlay-added=\"true\"') > -1) {\n                    return iframe;\n                }\n\n                iframe = iframe.replace('src=\"', 'data-consent-src=\"');\n                iframe = iframe.replace('<iframe ', '<iframe data-consent-overlay-added=\"true\" ');\n                iframe = `\n                <div \n                    class=\"pec-wrapper\" \n                    data-consent-group-id=\"${embedConsent.cookieGroup}\">\n                    ${iframe}\n                    <div class=\"pec-overlay is-active\" aria-hidden=\"false\">\n                        <div class=\"pec-overlay-inner\">\n                            <p>${embedConsent.text}</p>\n                            <button \n                                class=\"pec-button\" \n                                onclick=\"window.publiiEmbedConsentGiven('${embedConsent.cookieGroup}'); return false;\">\n                                ${embedConsent.buttonLabel}\n                            </button>\n                        </div>\n                    </div>\n                    <script>window.publiiEmbedConsentCheck('${embedConsent.cookieGroup}');</script>\n                </div>\n                `;\n\n                return iframe;\n            });   \n        }\n\n        return text;\n    }\n\n    /**\n     * Replaces non-WebP images to WebP or WebP images to non-WebP images in gallery thumbnails if necessary\n     * @param {boolean} forceWebp - state of force WebP option\n     * @param {string} text - text to modify\n     * @returns {string} - modified text\n     */\n    static setWebpCompatibility (forceWebp, text) {\n        text = text.replace(/\\<figure class=\"gallery__item\">[\\s\\S]*?<a[\\s\\S]*?href=\"(.*?)\"[\\s\\S]+?>[\\s\\S]*?<img[\\s\\S]*?src=\"(.*?)\"/gmi, (matches, linkUrl, imgUrl) => {\n            if (linkUrl && imgUrl) {\n                if (\n                    forceWebp && \n                    ContentHelper.getImageType(linkUrl) === 'webp-compatible' && \n                    !ContentHelper.isWebpImage(imgUrl)\n                ) {\n                    let imgExtension = ContentHelper.getImageExtension(imgUrl);\n                    let newImgUrl = imgUrl.substr(0, imgUrl.length + (-1 * imgExtension.length)) + '.webp';\n                    matches = matches.replace(imgUrl, newImgUrl);\n                } else if (\n                    !forceWebp && \n                    ContentHelper.getImageType(linkUrl) === 'webp-compatible' && \n                    ContentHelper.isWebpImage(imgUrl)\n                ) {\n                    let imgExtension = ContentHelper.getImageExtension(linkUrl);\n                    let newImgUrl = imgUrl.substr(0, imgUrl.length - 5) + imgExtension;\n                    matches = matches.replace(imgUrl, newImgUrl);\n                }\n            }\n\n            return matches;\n        });\n\n        return text;\n    }\n\n    /**\n     * Checks if given URL is a WebP image\n     * @param {string} url \n     * @returns {boolean}\n     */\n    static isWebpImage (url) {\n        if (url.substr(-5).toLowerCase() === '.webp') {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Checks the given URL image type\n     * @param {string} url \n     * @returns {string} - webp, webp-compatible or other\n     */\n    static getImageType (url) {\n        if (url.substr(-5).toLowerCase() === '.webp') {\n            return 'webp';\n        }\n\n        if (\n            url.substr(-5).toLowerCase() === '.jpeg' ||\n            url.substr(-4).toLowerCase() === '.jpg' ||\n            url.substr(-4).toLowerCase() === '.png'\n        ) {\n            return 'webp-compatible';\n        }\n\n        return 'other';\n    }\n\n    /**\n     * Returns image extension - only for webp, jpg, jpeg, png\n     * @param {string} url \n     * @returns {string|boolean} - extension or false if non-compatible image extension\n     */\n    static getImageExtension (url) {\n        if (url.substr(-5).toLowerCase() === '.webp' || url.substr(-5).toLowerCase() === '.jpeg') {\n            return url.substr(-5);\n        } \n\n        if (url.substr(-4).toLowerCase() === '.jpg' || url.substr(-4).toLowerCase() === '.png') {\n            return url.substr(-4);\n        } \n\n        return false;\n    }\n}\n\nmodule.exports = ContentHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/deleteEmpty.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst systemFiles = [\n    '.DS_Store', \n    'desktop.ini', \n    'Thumbs.db'\n];\n\nfunction isSystemFile (file) {\n    return systemFiles.includes(file);\n}\n\nfunction deleteEmpty (dirPath) {\n    if (!fs.existsSync(dirPath)) {\n        return;\n    }\n\n    try {\n        let files = fs.readdirSync(dirPath);\n\n        for (let file of files) {\n            let fullPath = path.join(dirPath, file);\n            let stat = fs.statSync(fullPath);\n\n            if (stat.isDirectory()) {\n                deleteEmpty(fullPath);\n            } else if (isSystemFile(file)) {\n                fs.unlinkSync(fullPath);\n            }\n        }\n\n        files = fs.readdirSync(dirPath);\n        let isDirectoryEmpty = files.length === 0;\n\n        if (isDirectoryEmpty) {\n            fs.rmdirSync(dirPath);\n            console.log('[OK] Deleted empty directory: ', dirPath);\n        }\n    } catch (err) {\n        console.log('(!) Error during deleting empty directories: ', dirPath , ' | Reason: ', err.message);\n    }\n}\n\nmodule.exports = { \n    deleteEmpty \n};\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/diffCopy.js",
    "content": "const fs = require('fs-extra');\nconst list = require('ls-all');\nconst path = require('path');\n\nclass DiffCopy {\n    static async copy (input, output) {\n        let inputFiles = await DiffCopy.listAllFiles(input);\n        inputFiles = inputFiles.map(item => item.path);\n        DiffCopy.compareExistingFiles(inputFiles, input, output);\n        // We remove output files during DiffCopy.compareExistingFiles, \n        // so we need to create index of the output files after this operation to get the current list of output files\n        let outputFiles = await DiffCopy.listAllFiles(output);\n        outputFiles = outputFiles.map(item => item.path);\n        DiffCopy.inputFilesCopy(inputFiles, outputFiles, input, output);\n        DiffCopy.outputFilesDelete(inputFiles, outputFiles, input, output);\n    }\n\n    static async listAllFiles (dir) {\n        return list([dir], { recurse: true, flatten: true });\n    }\n\n    static compareExistingFiles (inputFiles, inputDir, outputDir) {\n        for (let i = 0; i < inputFiles.length; i++) {\n            let fileToCheck = inputFiles[i].replace(inputDir, outputDir);\n\n            if (!fs.existsSync(fileToCheck)) {\n                continue;\n            }\n\n            let fileStats = fs.statSync(fileToCheck);\n\n            if (fileStats.isDirectory()) {\n                continue;\n            }\n\n            if (DiffCopy.getFileSize(inputFiles[i]) !== DiffCopy.getFileSize(fileToCheck)) {\n                fs.removeSync(fileToCheck);\n                console.log('[DIFF REMOVE DUE SIZE]', fileToCheck);\n            }\n        }\n    }\n\n    static inputFilesCopy (inputFiles, outputFiles, inputDir, outputDir) {\n        let copiedDirectories = [];\n        let relativeOutputFiles = outputFiles.map(file => file.replace(outputDir, ''));\n\n        for (let i = 0; i < inputFiles.length; i++) {\n            if (relativeOutputFiles.indexOf(inputFiles[i].replace(inputDir, '')) !== -1) {\n                continue;\n            }\n\n            let isCopied = false;\n\n            for (let j = 0; j < copiedDirectories.length; j++) {\n                if (inputFiles[i].replace(inputDir, outputDir).indexOf(copiedDirectories[j]) === 0) {\n                    isCopied = true;\n                    break;\n                }\n            }\n\n            if (isCopied) {\n                continue;\n            }\n\n            fs.copySync(\n                inputFiles[i],\n                inputFiles[i].replace(inputDir, outputDir)\n            );\n\n            console.log('[DIFF COPY]', inputFiles[i], ' -> ', inputFiles[i].replace(inputDir, outputDir));\n\n            let inputStats = fs.statSync(inputFiles[i]);\n\n            if (inputStats.isDirectory()) {\n                copiedDirectories.push(inputFiles[i]);\n            }\n        }\n    }\n\n    static outputFilesDelete (inputFiles, outputFiles, inputDir, outputDir) {\n        let removedDirectories = [];\n        let relativeInputFiles = inputFiles.map(file => file.replace(inputDir, ''));\n\n        for (let i = 0; i < outputFiles.length; i++) {\n            if (relativeInputFiles.indexOf(outputFiles[i].replace(outputDir, '')) !== -1) {\n                continue;\n            }\n\n            let isRemoved = false;\n\n            for (let j = 0; j < removedDirectories.length; j++) {\n                if (outputFiles[i].replace(inputDir, outputDir).indexOf(removedDirectories[j]) === 0) {\n                    isRemoved = true;\n                    break;\n                }\n            }\n\n            if (isRemoved) {\n                continue;\n            }\n\n            let outputStats = fs.statSync(outputFiles[i]);\n\n            if (outputStats.isDirectory()) {\n                removedDirectories.push(outputFiles[i]);\n            }\n\n            fs.removeSync(outputFiles[i]);\n\n            console.log('[DIFF REMOVE]', outputFiles[i]);\n        }\n    }\n\n    static removeUnusedItemFolders (postIDs, pageIDs, baseOutputPath) {\n        let allPostFolders = fs.readdirSync(baseOutputPath);\n        postIDs = JSON.parse(JSON.stringify(postIDs));\n        postIDs = postIDs.map(id => (id).toString());\n        pageIDs = JSON.parse(JSON.stringify(pageIDs));\n        pageIDs = pageIDs.map(id => (id).toString());\n\n        for (let i = 0; i < allPostFolders.length; i++) {\n            if (allPostFolders[i] === '.' || allPostFolders[i] === '..' || allPostFolders[i] === 'defaults') {\n                continue;\n            }\n\n            if (\n                postIDs.indexOf((allPostFolders[i]).toString()) === -1 && \n                pageIDs.indexOf((allPostFolders[i]).toString()) === -1\n            ) {\n                fs.removeSync(path.join(baseOutputPath, allPostFolders[i]));\n                console.log('[DIFF REMOVE CATALOG]', path.join(baseOutputPath, allPostFolders[i]));\n            }\n        }\n    }\n\n    static getFileSize(filename) {\n        let stats = fs.statSync(filename);\n        let fileSize = stats['size'];\n        \n        return fileSize;\n    }\n}\n\nmodule.exports = DiffCopy;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/files.js",
    "content": "const fs = require('fs-extra');\nconst list = require('ls-all');\nconst path = require('path');\nconst { deleteEmpty } = require('./deleteEmpty.js');\nconst UtilsHelper = require('./../../../helpers/utils');\nconst normalizePath = require('normalize-path');\nconst DiffCopy = require('./diffCopy.js');\nconst PluginsHelpers = require('./../../plugins/plugins-helpers');\n\nclass Files {\n    /**\n     * Copy configuration files\n     *\n     * @param inputDir\n     * @param outputDir\n     */\n    static copyRootFiles(inputDir, outputDir) {\n        let inputPath = path.join(inputDir, 'root-files');\n        let outputPath = path.join(outputDir);\n        \n        fs.copySync(\n            path.join(inputPath),\n            path.join(outputPath),\n            {\n                filter: (src, dest) => {\n                    if (src.substr(-9) === '.DS_Store' || src.substr(-9) === 'Thumbs.db') {\n                        return false;\n                    }\n\n                    return true;\n                }\n            }\n        );\n    }\n\n    /**\n     * Copy all non-ignored assets files\n     *\n     * @param themeDir\n     * @param outputDir\n     * @param themeConfig\n     */\n    static async copyAssetsFiles(themeDir, outputDir, themeConfig) {\n        let assetsPath = path.join(themeDir, themeConfig.files.assetsPath);\n        let overridesPath = path.join(themeDir.replace(/[\\\\\\/]{1,1}$/, '') + '-override', themeConfig.files.assetsPath);\n        let outputPath = path.join(outputDir, themeConfig.files.assetsPath);\n\n        // Create the assets directory\n        fs.ensureDirSync(outputPath);\n\n        // Copy each directory and file from the assets catalog\n        await list([assetsPath], {\n            recurse: true,\n            flatten: true\n        }).then(files => {\n            let dynamicAssetsPath = path.join(assetsPath, 'dynamic');\n            \n            files.filter(item => {\n                let filename = path.parse(item.path).base;\n\n                if (item.path.indexOf(dynamicAssetsPath) > -1) {\n                    return false;\n                }\n\n                return themeConfig.files.ignoreAssets.indexOf(filename) === -1\n            }).forEach(item => {\n                if(item.mode.dir === false) {\n                    let filePath = normalizePath(item.path);\n                    let destinationPath = filePath.replace(\n                        normalizePath(assetsPath),\n                        normalizePath(outputPath)\n                    );\n\n                    fs.copySync(\n                        filePath,\n                        destinationPath\n                    );\n                }\n            });\n        });\n\n        // Check for overrided asset files\n        if (UtilsHelper.dirExists(overridesPath)) {\n            list([overridesPath], {\n                recurse: true,\n                flatten: true\n            }).then(files => {\n                files.filter(item => {\n                    let filename = path.parse(item.path).base;\n                    return themeConfig.files.ignoreAssets.indexOf(filename) === -1\n                }).forEach(item => {\n                    if (item.mode.dir === false) {\n                        let filePath = normalizePath(item.path);\n                        let destinationPath = filePath.replace(\n                            normalizePath(overridesPath),\n                            normalizePath(outputPath)\n                        );\n\n                        fs.copySync(\n                            filePath,\n                            destinationPath\n                        );\n                    }\n                });\n            });\n        }\n    }\n\n    /**\n     * Copy dynamic assets files\n     *\n     * @param themeDir\n     * @param outputDir\n     * @param themeConfig\n     */\n     static copyDynamicAssetsFiles(themeDir, outputDir, themeConfig) {\n        if (!themeConfig.files.useDynamicAssets) {\n            return;\n        }\n\n        let dynamicAssetsPath = path.join(themeDir, themeConfig.files.assetsPath, 'dynamic');\n        let outputPath = path.join(outputDir, themeConfig.files.assetsPath, 'dynamic');\n\n        // Create the dynamic assets directory or clean up it\n        fs.emptyDirSync(outputPath);\n\n        // Create list of files to copy\n        let filesToCopy = [];\n        let filesMappingPath = path.join(themeDir, 'dynamic-assets-mapping.js');\n\n        if (fs.existsSync(filesMappingPath)) {\n            filesToCopy = UtilsHelper.requireWithNoCache(filesMappingPath, themeConfig);\n        }\n\n        // Copy each directory and file from the assets catalog\n        list([dynamicAssetsPath], {\n            recurse: true,\n            flatten: true\n        }).then(files => {\n            files.filter(item => {\n                let filename = normalizePath(item.path.replace(dynamicAssetsPath, ''));\n                return filesToCopy.indexOf(filename) > -1\n            }).forEach(item => {\n                if (item.mode.dir === false) {\n                    let filePath = normalizePath(item.path);\n                    let destinationPath = filePath.replace(\n                        normalizePath(dynamicAssetsPath),\n                        normalizePath(outputPath)\n                    );\n\n                    fs.copySync(\n                        filePath,\n                        destinationPath\n                    );\n                }\n            });\n        });\n    }\n\n    /**\n     * Copy media files from the input dir to the output dir\n     *\n     * @param inputDir\n     * @param outputDir\n     * @param postIDs\n     */\n    static async copyMediaFiles (inputDir, outputDir, postIDs, pageIDs) {\n        let basePathInput = path.join(inputDir, 'media');\n        let basePathOutput = path.join(outputDir, 'media');\n        let dirs = ['website', 'files', 'tags', 'authors', 'posts/defaults'];\n\n        if (postIDs[0] === 0) {\n            postIDs[0] = 'temp';\n        }\n\n        if (pageIDs[0] === 0) {\n            pageIDs[0] = 'temp';\n        }\n\n        for (let i = 0; i < postIDs.length; i++) {\n            dirs.push('posts/' + postIDs[i]);\n        }\n\n        for (let i = 0; i < pageIDs.length; i++) {\n            dirs.push('posts/' + pageIDs[i]);\n        }\n\n        fs.ensureDirSync(path.join(basePathOutput, 'posts'));\n\n        for (let i = 0; i < dirs.length; i++) {\n            if (!UtilsHelper.dirExists(path.join(basePathInput, dirs[i]))) {\n                continue;\n            }\n\n            if (!UtilsHelper.dirExists(path.join(basePathOutput, dirs[i]))) {\n                fs.copySync(\n                    path.join(basePathInput, dirs[i]),\n                    path.join(basePathOutput, dirs[i])\n                );\n            } else {\n                await DiffCopy.copy(\n                    path.join(basePathInput, dirs[i]), \n                    path.join(basePathOutput, dirs[i])\n                );\n            }\n        }\n\n        if (UtilsHelper.dirExists(path.join(basePathOutput, 'tags', 'temp'))) {\n            fs.removeSync(path.join(basePathOutput, 'tags', 'temp'));\n        }\n\n        if (UtilsHelper.dirExists(path.join(basePathOutput, 'authors', 'temp'))) {\n            fs.removeSync(path.join(basePathOutput, 'authors', 'temp'));\n        }\n\n        DiffCopy.removeUnusedItemFolders(postIDs, pageIDs, path.join(basePathOutput, 'posts'));\n    }\n\n    /**\n     * Copy plugin files from the input dir to the output dir\n     *\n     * @param inputDir\n     * @param outputDir\n     */\n     static async copyPluginFiles (inputDir, outputDir, pluginsDir) {\n        let pluginsList = PluginsHelpers.getActivePluginsList(path.join(inputDir, 'config', 'site.plugins.json'));\n        let basePathInput = path.join(inputDir, 'media');\n        let basePathOutput = path.join(outputDir, 'media');\n        \n        // create media dir if not exists\n        if (!UtilsHelper.dirExists(path.join(basePathOutput))) {\n            fs.mkdirSync(path.join(basePathOutput));\n        }\n\n        // if media/plugins dir exists - remove it\n        if (UtilsHelper.dirExists(path.join(basePathOutput, 'plugins'))) {\n            fs.removeSync(path.join(basePathOutput, 'plugins'));\n        }\n\n        // put plugin files if necessary\n        if (pluginsList.length) {\n            fs.mkdirSync(path.join(basePathOutput, 'plugins'));\n        }\n\n        for (let i = 0; i < pluginsList.length; i++) {\n            let pluginName = pluginsList[i];\n            let filesToCopy = PluginsHelpers.getPluginFrontEndFiles(pluginName, pluginsDir);\n            let pluginInputDir = path.join(basePathInput, 'plugins', pluginName);\n            let pluginOutputDir = path.join(basePathOutput, 'plugins', pluginName);\n\n            if (fs.existsSync(pluginInputDir)) {\n                fs.copySync(\n                    pluginInputDir,\n                    pluginOutputDir\n                );\n            }\n\n            if (filesToCopy.length && !fs.existsSync(pluginOutputDir)) {\n                fs.mkdirSync(path.join(basePathOutput, 'plugins', pluginName));\n            }\n\n            for (let j = 0; j < filesToCopy.length; j++) {\n                fs.copySync(\n                    path.join(filesToCopy[j].input),\n                    path.join(basePathOutput, 'plugins', pluginName, filesToCopy[j].output)\n                );\n            }\n        }\n    }\n\n    static async removeEmptyDirectories (outputDir) {\n        let basePathOutput = path.join(outputDir, 'media');\n        deleteEmpty(basePathOutput);\n    }\n}\n\nmodule.exports = Files;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/gdpr.js",
    "content": "const FileHelper = require('./../../../helpers/file.js');\nclass Gdpr {\n    static popupHtmlOutput (configuration, renderer) {\n        let template = FileHelper.readFileSync(__dirname + '/../../../../default-files/gdpr-assets/template.html', 'utf8');\n        let output = Gdpr.parseTemplate(configuration, template, renderer);\n        return output;\n    }\n\n    static prepareCookieGroups (configuration) {\n        let groups = ``;\n\n        for (let i = 0; i < configuration.groups.length; i++) {\n            let description = configuration.groups[i].description;\n\n            if (description.trim() === '') {\n                description = '';\n            } else {\n                description = `<p class=\"pcb__group__txt\">${configuration.groups[i].description}</p>`;\n            }\n            \n            if (configuration.groups[i].id === '-' || configuration.groups[i].id === '') {\n                groups += `<li class=\"pcb__group\">\n                    <details>\n                        <summary class=\"pcb__group__title${description.trim() === '' ? ' no-desc' : ''}\">\n                            ${configuration.groups[i].name}\n                        </summary>\n                        ${description}\n                    </details>\n                    <div class=\"pcb__popup__switch is-checked\">\n                        <input \n                            type=\"checkbox\" \n                            data-group-name=\"\"\n                            id=\"pcb-group-${i}\" \n                            checked>\n                        <label for=\"pcb-group-${i}\">${configuration.groups[i].name}</label>\n                    </div>\n                </li>`;\n                continue;\n            }\n\n            groups += `\n            <li class=\"pcb__group\">\n                <details>\n                    <summary class=\"pcb__group__title${description.trim() === '' ? ' no-desc' : ''}\">\n                        ${configuration.groups[i].name}\n                    </summary>\n                    ${description}\n                </details>\n                <div class=\"pcb__popup__switch\">\n                    <input \n                        type=\"checkbox\"\n                        data-group-name=\"${configuration.groups[i].id}\"\n                        id=\"${configuration.groups[i].id}-cookies\" />\n                    <label for=\"${configuration.groups[i].id}-cookies\">\n                        ${configuration.groups[i].name}\n                    </label>\n                </div>\n            </li>`;\n        }\n\n        return groups;\n    }\n\n    static parseTemplate (configuration, template, renderer) {\n        // Remove unnecessary code fragments from template\n        if (!configuration.popupShowRejectButton || !configuration.allowAdvancedConfiguration) {\n            template = template.replace(/\\{\\{\\#showRejectButton\\}\\}[\\s\\S]*?\\{\\{\\/showRejectButton\\}\\}/gmi, '');\n        } else {\n            template = template.replace(/\\{\\{\\#showRejectButton\\}\\}/gmi, '');\n            template = template.replace(/\\{\\{\\/showRejectButton\\}\\}/gmi, '');\n        }\n\n        if (!configuration.allowAdvancedConfiguration) {\n            template = template.replace(/\\{\\{\\#allowAdvancedConfiguration\\}\\}[\\s\\S]*?\\{\\{\\/allowAdvancedConfiguration\\}\\}/gmi, '');\n        } else {\n            template = template.replace(/\\{\\{\\#allowAdvancedConfiguration\\}\\}/gmi, '');\n            template = template.replace(/\\{\\{\\/allowAdvancedConfiguration\\}\\}/gmi, '');\n        }\n\n        if (!configuration.advancedConfigurationShowDescriptionLink) {\n            template = template.replace(/\\{\\{\\#advancedConfigurationShowDescriptionLink\\}\\}[\\s\\S]*?\\{\\{\\/advancedConfigurationShowDescriptionLink\\}\\}/gmi, '');\n        } else {\n            template = template.replace(/\\{\\{\\#advancedConfigurationShowDescriptionLink\\}\\}/gmi, '');\n            template = template.replace(/\\{\\{\\/advancedConfigurationShowDescriptionLink\\}\\}/gmi, '');\n        }\n\n        if (configuration.behaviour === 'link') {\n            template = template.replace(/\\{\\{\\#showBadge\\}\\}[\\s\\S]*?\\{\\{\\/showBadge\\}\\}/gmi, '');\n        } else {\n            template = template.replace(/\\{\\{\\#showBadge\\}\\}/gmi, '');\n            template = template.replace(/\\{\\{\\/showBadge\\}\\}/gmi, '');\n        }\n\n        // Replace variables\n        let bannerPositionCssClass = '';\n\n        if (['left', 'right', 'bar'].indexOf(configuration.popupPosition) > -1) {\n            bannerPositionCssClass = 'pcb__banner--' + configuration.popupPosition;\n        }        \n\n        let privacyPolicyLink = ``;\n\n        if (Gdpr.getPrivacyPolicyUrl(configuration, renderer)) {\n            privacyPolicyLink = `<a href=\"${Gdpr.getPrivacyPolicyUrl(configuration, renderer)}\">${configuration.privacyPolicyLinkLabel}</a>`;\n        }\n\n        template = template.replace(/\\{\\{behaviour\\}\\}/gmi, configuration.behaviour);\n        template = template.replace(/\\{\\{behaviourLink\\}\\}/gmi, configuration.behaviourLink);\n        template = template.replace(/\\{\\{badgeLabel\\}\\}/gmi, configuration.badgeLabel);\n        template = template.replace(/\\{\\{bannerPositionCssClass\\}\\}/gmi, bannerPositionCssClass);\n        template = template.replace(/\\{\\{popupTitlePrimary\\}\\}/gmi, configuration.popupTitlePrimary);\n        template = template.replace(/\\{\\{popupDesc\\}\\}/gmi, configuration.popupDesc);\n        template = template.replace(/\\{\\{privacyPolicyLink\\}\\}/gmi, privacyPolicyLink);\n        template = template.replace(/\\{\\{advancedConfigurationLinkLabel\\}\\}/gmi, configuration.advancedConfigurationLinkLabel);\n        template = template.replace(/\\{\\{rejectButtonLabel\\}\\}/gmi, configuration.popupRejectButtonLabel);\n        template = template.replace(/\\{\\{saveButtonLabel\\}\\}/gmi, configuration.saveButtonLabel);\n        template = template.replace(/\\{\\{advancedConfigurationTitle\\}\\}/gmi, configuration.advancedConfigurationTitle);\n        template = template.replace(/\\{\\{advancedConfigurationDescription\\}\\}/gmi, configuration.advancedConfigurationDescription);\n        template = template.replace(/\\{\\{cbGroups\\}\\}/gmi, Gdpr.prepareCookieGroups(configuration));\n        template = template.replace(/\\{\\{advancedConfigurationAcceptButtonLabel\\}\\}/gmi, configuration.advancedConfigurationAcceptButtonLabel);\n        template = template.replace(/\\{\\{advancedConfigurationRejectButtonLabel\\}\\}/gmi, configuration.advancedConfigurationRejectButtonLabel);\n        template = template.replace(/\\{\\{advancedConfigurationSaveButtonLabel\\}\\}/gmi, configuration.advancedConfigurationSaveButtonLabel);\n        template = template.replace(/\\{\\{cookieSettingsTTL\\}\\}/gmi, configuration.cookieSettingsTTL);\n        template = template.replace(/\\{\\{cookieSettingsRevision\\}\\}/gmi, configuration.cookieSettingsRevision);\n        template = template.replace(/\\{\\{debugMode\\}\\}/gmi, configuration.debugMode);\n\n        return template;\n    }\n\n    static popupCssOutput () {\n        let output = FileHelper.readFileSync(__dirname + '/../../../../default-files/gdpr-assets/gdpr.css', 'utf8');\n        return output;\n    }\n\n    static popupJsOutput (configuration) {\n        let scriptCode = FileHelper.readFileSync(__dirname + '/../../../../default-files/gdpr-assets/gdpr.js', 'utf8');\n        let consentModeScripts = '';\n\n        if (configuration.gConsentModeEnabled) {\n            consentModeScripts = `\n                window.dataLayer = window.dataLayer || [];\n                function gtag() { dataLayer.push(arguments); }\n\n                window.publiiCBGCM = {\n                    defaultState: ${JSON.stringify(configuration.gConsentModeDefaultState)},\n                    groups: ${JSON.stringify(configuration.gConsentModeGroups)}\n                };\n            `;\n        }\n\n        let output = `\n        <script>\n            ${consentModeScripts}\n            ${scriptCode}\n        </script>`;\n\n        return output;\n    }\n\n    static getPrivacyPolicyUrl (configuration, renderer) {\n        if (!configuration.showPrivacyPolicyLink) {\n            return false;\n        }\n        \n        if (configuration.privacyPolicyLinkType === 'external') {\n            return configuration.privacyPolicyExternalUrl;\n        }\n\n        if (!configuration.privacyPolicyPostId && configuration.privacyPolicyLinkType === 'internal') {\n            return '#not-specified';\n        }\n\n        let result = renderer.cachedItems.posts[configuration.privacyPolicyPostId];\n\n        if (!result) {\n            result = renderer.cachedItems.pages[configuration.privacyPolicyPostId];\n        }\n\n        if (!result) {\n            return '#not-found';\n        }\n\n        return result.url;\n    }\n}\n\nmodule.exports = Gdpr;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/helpers.js",
    "content": "/**\n * Class used to manage common operations in the renderer\n */\n\nclass RendererHelpers {\n    static getRendererOptionValue (optionName, themeConfig) {\n        let optionValue = themeConfig.renderer && themeConfig.renderer[optionName];\n\n        if (themeConfig.customConfig && typeof themeConfig.customConfig[optionName] !== 'undefined') {\n            optionValue = themeConfig.customConfig[optionName];\n        }\n\n        return optionValue;\n    }\n}\n\nmodule.exports = RendererHelpers;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/sitemap.js",
    "content": "/*\n * Note for the future: for big websites most probably the better solution\n * would be replace reading post files in searching of the \"noindex\" phrase\n * by analyzing of the DB records\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst FileHelper = require('./../../../helpers/file.js');\nconst util = require('util');\nconst moment = require('moment');\nconst normalizePath = require('normalize-path');\nconst RendererHelpers = require('./../helpers/helpers.js');\nconst UtilsHelper = require('./../../../helpers/utils');\n\n/**\n * Class used to generate sitemap.xml file\n */\nclass Sitemap {\n    /**\n     * Initializer\n     *\n     * @param directory\n     * @param siteConfig\n     * @param themeConfig\n     */\n    constructor (db, directory, siteConfig, themeConfig) {\n        this.db = db;\n        this.baseDirectory = directory;\n        this.siteConfig = siteConfig;\n        this.themeConfig = themeConfig;\n        this.fileList = [];\n        this.outputXML = '';\n        this.postData = {};\n        this.excludedFiles = [];\n\n        if (this.siteConfig.advanced.sitemapExcludedFiles && this.siteConfig.advanced.sitemapExcludedFiles.trim() !== '') {\n            this.excludedFiles = this.siteConfig.advanced.sitemapExcludedFiles.split(',').map(file => {\n                return '/' + file.trim() + '/';\n            });\n        }\n    }\n\n    /**\n     * Creates sitemap.xml file\n     */\n    async create () {\n        this.getData();\n\n        await this.getFilesList().then(() => {\n            this.renderXML();\n            this.saveXML();\n        });\n    }\n\n    /**\n     * Get post data\n     */\n    getData () {\n        let momentOriginalLocale = moment.locale();\n        moment.locale('en');\n\n        let postDbData = this.db.prepare(`\n            SELECT\n                posts.id,\n                posts.slug,\n                posts.text,\n                posts.modified_at,\n                posts_images.url,\n                posts_images.additional_data,\n                posts_additional_data.value AS core_post_data\n            FROM\n                posts\n            LEFT JOIN\n                posts_images\n                ON\n                posts.featured_image_id = posts_images.id\n            LEFT JOIN\n                posts_additional_data\n                on\n                posts.id = posts_additional_data.post_id\n            WHERE\n                posts_additional_data.key = '_core'\n            ORDER BY\n                posts.id ASC\n        `).all();\n\n        if (postDbData && postDbData.length) { \n            postDbData.forEach(post => {\n                let featuredImageUrl = false;\n                let featuredImageAlt = false;\n                let images = [];\n                \n                if (post.url) {\n                    featuredImageUrl = this.getMediaPath(post) + post.url;\n\n                    try {\n                        let imageData = JSON.parse(post.additional_data);\n                        featuredImageAlt = imageData.alt;\n                    } catch (e) {\n                        console.log('Sitemap: featured image additional JSON data parse error.');\n                    }\n                }\n\n                if (post.text.indexOf('<img') > -1) {\n                    let imgMatches = post.text.match(/\\<img[\\s\\S]*?\\>/gmi);\n\n                    for (let i = 0; i < imgMatches.length; i++) {\n                        let url = imgMatches[i].match(/src=\"(.*?)\"/mi);\n                        let alt = imgMatches[i].match(/alt=\"(.*?)\"/mi);\n\n                        if (url && url[1]) {\n                            url = url[1].replace('#DOMAIN_NAME#', this.getMediaPath(post));\n                        } else {\n                            url = false;\n                        }\n\n                        if (alt && alt[1]) {\n                            alt = alt[1];\n                        } else {\n                            alt = '';\n                        }\n\n                        if (url) {\n                            images.push({\n                                url: url,\n                                alt: alt\n                            });\n                        }\n                    }\n                }\n\n                // detect block editor images\n                if (post.core_post_data.indexOf('\"editor\":\"blockeditor\"') > -1) {\n                    try {\n                        let parsedPostStructure = JSON.parse(post.text);\n\n                        for (let i = 0; i < parsedPostStructure.length; i++) {\n                            let block = parsedPostStructure[i];\n\n                            if (block.type === 'publii-image' && block.content) {\n                                images.push({\n                                    url: block.content.image.replace('#DOMAIN_NAME#', this.getMediaPath(post)),\n                                    alt: block.content.alt\n                                });\n                            } else if (block.type === 'publii-gallery' && block.content && block.content.images) {\n                                for (let j = 0; j < block.content.images.length; j++) {\n                                    let imageData = block.content.images[j];\n\n                                    images.push({\n                                        url: imageData.src.replace('#DOMAIN_NAME#', this.getMediaPath(post)),\n                                        alt: imageData.alt\n                                    });\n                                }\n                            }\n                        }\n                    } catch (e) {\n                        console.log('(!) An error occurred during parsing images in the block editor post');\n                    }\n                }\n\n                // detect markdown editor images\n                if (post.core_post_data.indexOf('\"editor\":\"markdown\"') > -1) {\n                    let imagesRegexp = /!\\[([^\\]]*)\\]\\(([^\\)]*)\\)/gmi;\n                    let foundedImages = [...post.text.matchAll(imagesRegexp)];\n                    \n                    for (let imageMatch of foundedImages) {\n                        images.push({\n                            url: imageMatch[2].replace('#DOMAIN_NAME#', this.getMediaPath(post)).replace(/\\s=\\d+x\\d+$/, ''),\n                            alt: imageMatch[1]\n                        });\n                    }                \n                }\n\n                if (featuredImageUrl) {\n                    images.unshift({\n                        url: featuredImageUrl,\n                        alt: featuredImageAlt\n                    });\n                }\n                \n                this.postData[post.slug] = {\n                    lastMod: moment(post.modified_at).format('YYYY-MM-DDTHH:mm:ssZ'),\n                    images: images\n                };\n            });\n        }\n\n        moment.locale(momentOriginalLocale);\n    }\n\n    /**\n     * Get matches of the regex\n     */\n    getMatches (string, regex, index = 1) {\n        let matches = [];\n        let match;\n        \n        while (match = regex.exec(string)) {\n            matches.push(match[index]);\n        }\n\n        return matches;\n    }\n\n    /**\n     * Retrieves media path for the website\n     */\n    getMediaPath (postObject) {\n        return this.siteConfig.domain + '/' + normalizePath(path.join('media', 'posts', (postObject.id).toString())) + '/';\n    }\n\n    /**\n     * Retrieves list of files to parse\n     */\n    async getFilesList () {\n        let files;\n        let internals = this.getInternalsList();\n\n        if (this.siteConfig.advanced.urls.postsPrefix) {\n            files = fs.readdirSync(path.join(this.baseDirectory, this.siteConfig.advanced.urls.postsPrefix));\n\n            for (let file of files) {\n                // Skip hidden files and internal directories\n                if (file.indexOf('.') === 0 || internals.indexOf(file) > -1) {\n                    continue;\n                }\n    \n                // Detect author pages\n                if (file === this.siteConfig.advanced.urls.authorsPrefix && this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) {\n                    await this.getAuthorsFilesList();\n                    continue;\n                }\n    \n                // Detect homepage pagination\n                if (file === this.siteConfig.advanced.urls.pageName) {\n                    if (!this.siteConfig.advanced.homepageNoIndexPagination && !this.siteConfig.advanced.homepageNoPagination) {\n                        await this.getHomepagePaginationFilesList();\n                    }\n    \n                    continue;\n                }\n    \n                // Detect tag pages - when tags prefix is empty\n                if (file.indexOf('.') === -1 && this.siteConfig.advanced.urls.tagsPrefix === '' && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                    await this.getTagsFilesList(file);\n                    continue;\n                }\n    \n                // Detect tag pages - when tags prefix is not empty\n                if (this.siteConfig.advanced.urls.tagsPrefix !== '' && file === this.siteConfig.advanced.urls.tagsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                    await this.getTagsFilesList(file, this.siteConfig.advanced.urls.tagsPrefix);\n                    continue;\n                }\n    \n                // Detect post/pages files\n                await this.getPostsAndPagesFilesList(file, true);\n            }\n        }\n\n        files = fs.readdirSync(this.baseDirectory);\n        \n        for (let file of files) {\n            // Skip hidden files and internal directories\n            if (file.indexOf('.') === 0 || internals.indexOf(file) > -1) {\n                continue;\n            }\n\n            // Skip posts prefix directory\n            if (file === this.siteConfig.advanced.urls.postsPrefix) {  \n                continue;\n            }\n\n            // Detect author pages\n            if (file === this.siteConfig.advanced.urls.authorsPrefix && !this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) {\n                await this.getAuthorsFilesList();\n                continue;\n            }\n\n            // Detect homepage pagination\n            if (file === this.siteConfig.advanced.urls.pageName && !this.siteConfig.advanced.urls.postsPrefix) {\n                if (!this.siteConfig.advanced.homepageNoIndexPagination && !this.siteConfig.advanced.homepageNoPagination) {\n                    await this.getHomepagePaginationFilesList();\n                }\n\n                continue;\n            }\n\n            // Detect tag pages - when tags prefix is empty\n            if (file.indexOf('.') === -1 && this.siteConfig.advanced.urls.tagsPrefix === '' && !this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                await this.getTagsFilesList(file);\n                continue;\n            }\n\n            // Detect tag pages - when tags prefix is not empty\n            if (this.siteConfig.advanced.urls.tagsPrefix !== '' && file === this.siteConfig.advanced.urls.tagsPrefix && !this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                await this.getTagsFilesList(file, this.siteConfig.advanced.urls.tagsPrefix);\n                continue;\n            }\n\n            // Detect post/pages files with clean URLs disabled\n            if (!this.siteConfig.advanced.urls.cleanUrls && file.indexOf('.html') > 0) {\n                await this.getPostsAndPagesFilesList(file, false, true);\n                continue;\n            }\n\n            // Detect post/pages files\n            if (this.siteConfig.advanced.urls.cleanUrls) {\n                await this.getPostsAndPagesFilesList(file, true, true);\n            }\n        }\n    }\n\n    /**\n     * Prepares list of the items to skip\n     *\n     * @returns {array}\n     */\n    getInternalsList () {\n        let internalsList = [\n            'assets',\n            'media',\n            '.htaccess',\n            '.htpasswd',\n            '_redirects',\n            'robots.txt',\n            'feed.xml',\n            'feed.json',\n            'index.html'\n        ];\n\n        if (RendererHelpers.getRendererOptionValue('create404page', this.themeConfig) && this.siteConfig.advanced.urls.errorPage) {\n            internalsList.push(this.siteConfig.advanced.urls.errorPage);\n        }\n\n        if (RendererHelpers.getRendererOptionValue('createSearchPage', this.themeConfig) && this.siteConfig.advanced.urls.searchPage) {\n            internalsList.push(this.siteConfig.advanced.urls.searchPage);\n        }\n\n        return internalsList;\n    }\n\n    /**\n     * Detects if author pages are no-indexed\n     *\n     * @returns {boolean}\n     */\n    shouldIndexAuthors () {\n        return this.siteConfig.advanced.sitemapAddAuthors;\n    }\n\n    /**\n     * Detects if homepage is no-indexed\n     *\n     * @returns {boolean}\n     */\n    shouldIndexHomepage () {\n        return this.siteConfig.advanced.sitemapAddHomepage;\n    }\n\n    /**\n     * Detects if tag pages are no-indexed\n     *\n     * @returns {boolean}\n     */\n    shouldIndexTags () {\n        return this.siteConfig.advanced.sitemapAddTags;\n    }\n\n    /**\n     * Retrieves list of author pages to index\n     */\n    async getAuthorsFilesList () {\n        const readFile = util.promisify(fs.readFile);\n\n        if (!this.shouldIndexAuthors()) {\n            return;\n        }\n\n        let authorsPathToCheck = path.join(this.baseDirectory, this.siteConfig.advanced.urls.authorsPrefix);\n        let usePostsPrefix = this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix;\n\n        if (usePostsPrefix) {\n            authorsPathToCheck = path.join(this.baseDirectory, this.siteConfig.advanced.urls.postsPrefix, this.siteConfig.advanced.urls.authorsPrefix);\n        }\n\n        let files = fs.readdirSync(authorsPathToCheck);\n\n        for (let file of files) {\n            // Skip files\n            if (file.indexOf('.') > -1) {\n                continue;\n            }\n\n            let authorFileContent = await readFile(path.join(authorsPathToCheck, file, 'index.html'), 'utf8');\n            \n            if (authorFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                if (usePostsPrefix) {\n                    this.fileList.push(this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.authorsPrefix + '/' + file + '/index.html');\n                } else {\n                    this.fileList.push(this.siteConfig.advanced.urls.authorsPrefix + '/' + file + '/index.html');\n                }\n\n                if (this.siteConfig.advanced.authorNoIndexPagination || this.siteConfig.advanced.authorNoPagination) {\n                    continue;\n                }\n\n                let paginationPath = path.join(\n                    authorsPathToCheck,\n                    file,\n                    this.siteConfig.advanced.urls.pageName\n                );\n\n                if (fs.existsSync(paginationPath)) {\n                    let authorFiles = fs.readdirSync(paginationPath);\n\n                    for (let authorFile of authorFiles) {\n                        // Skip files\n                        if (authorFile.indexOf('.') > -1) {\n                            continue;\n                        }\n\n                        // Add all pages of pagination\n                        let pageName = this.siteConfig.advanced.urls.pageName;\n                       \n                        if (usePostsPrefix) {\n                            this.fileList.push(this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.authorsPrefix + '/' + file + '/' + pageName + '/' + authorFile + '/index.html');\n                        } else {\n                            this.fileList.push(this.siteConfig.advanced.urls.authorsPrefix + '/' + file + '/' + pageName + '/' + authorFile + '/index.html');\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Retrieves list of tag pages files to index for given tag slug\n     *\n     * @param tagName\n     * @param prefix\n     */\n    async getTagsFilesList (tagName, prefix = '') {\n        const readFile = util.promisify(fs.readFile);\n        let postsPrefix = this.siteConfig.advanced.urls.postsPrefix;\n        let tagsPrefix = this.siteConfig.advanced.urls.tagsPrefix;\n        let usePostsPrefix = this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix;\n\n        if (!this.shouldIndexTags()) {\n            return Promise.resolve();\n        }\n\n        if (prefix === '') {\n            let tagFileContent;\n            \n            if (usePostsPrefix) {\n                tagFileContent = await readFile(path.join(this.baseDirectory, postsPrefix, tagName, 'index.html'), 'utf8');\n            } else {\n                tagFileContent = await readFile(path.join(this.baseDirectory, tagName, 'index.html'), 'utf8');\n            }\n\n            // Detect if noindex does not exist in the post file\n            if (tagFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                // Add main file to the list\n                this.fileList.push(tagName + '/index.html');\n\n                if (this.siteConfig.advanced.tagNoIndexPagination || this.siteConfig.advanced.tagNoPagination) {\n                    return Promise.resolve();\n                }\n\n                let paginationPath = path.join(this.baseDirectory, tagName, this.siteConfig.advanced.urls.pageName);\n\n                if (usePostsPrefix) {\n                    paginationPath = path.join(this.baseDirectory, postsPrefix, tagName, this.siteConfig.advanced.urls.pageName);\n                }\n\n                if (fs.existsSync(paginationPath)) {\n                    let files = fs.readdirSync(paginationPath);\n\n                    for (let file of files) {\n                        // Skip files\n                        if (file.indexOf('.') > -1) {\n                            continue;\n                        }\n\n                        // Add all pages of pagination\n                        let pageName = this.siteConfig.advanced.urls.pageName;\n\n                        if (usePostsPrefix) {\n                            this.fileList.push(postsPrefix + '/' + tagName + '/' + pageName + '/' + file + '/index.html');\n                        } else {\n                            this.fileList.push(tagName + '/' + pageName + '/' + file + '/index.html');\n                        }\n                    }\n                }\n            }\n\n            return Promise.resolve();\n        }\n\n        let tagsPath = path.join(this.baseDirectory, tagsPrefix);\n\n        if (usePostsPrefix) {\n            tagsPath = path.join(this.baseDirectory, postsPrefix, tagsPrefix);\n        }\n\n        if (UtilsHelper.fileExists(path.join(tagsPath, 'index.html'))) {\n            let tagsFileContent = await readFile(path.join(tagsPath, 'index.html'), 'utf8');\n\n            if (usePostsPrefix && tagsFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                this.fileList.push(postsPrefix + '/' + tagsPrefix + '/index.html');\n            } else if (tagsFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                this.fileList.push(tagsPrefix + '/index.html');\n            }\n        }\n\n        let files = fs.readdirSync(tagsPath);\n\n        for (let file of files) {\n            // Skip files\n            if (file.indexOf('.') > -1) {\n                continue;\n            }\n\n            let tagFileContent = await readFile(path.join(tagsPath, file, 'index.html'), 'utf8');\n\n            // Detect if noindex does not exist in the post file\n            if (tagFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                if (usePostsPrefix) {\n                    this.fileList.push(postsPrefix + '/' + tagsPrefix + '/' + file + '/index.html');\n                } else {\n                    this.fileList.push(tagsPrefix + '/' + file + '/index.html');\n                }\n\n                if (this.siteConfig.advanced.tagNoIndexPagination || this.siteConfig.advanced.tagNoPagination) {\n                    continue;\n                }\n\n                let paginationPath = path.join(\n                    tagsPath,\n                    file,\n                    this.siteConfig.advanced.urls.pageName\n                );\n\n                if (fs.existsSync(paginationPath)) {\n                    let tagsFiles = fs.readdirSync(paginationPath);\n\n                    for (let tagFile of tagsFiles) {\n                        // Skip files\n                        if (tagFile.indexOf('.') > -1) {\n                            continue;\n                        }\n\n                        // Add all pages of pagination\n                        let pageName = this.siteConfig.advanced.urls.pageName;\n\n                        if (usePostsPrefix) {\n                            this.fileList.push(postsPrefix + '/' + tagsPrefix + '/' + file + '/' + pageName + '/' + tagFile + '/index.html');\n                        } else {\n                            this.fileList.push(tagsPrefix + '/' + file + '/' + pageName + '/' + tagFile + '/index.html');\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Retrieves homepage files to index\n     */\n    async getHomepagePaginationFilesList () {\n        if (!this.shouldIndexHomepage()) {\n            return;\n        }\n\n        let postsPrefix = this.siteConfig.advanced.urls.postsPrefix;\n        let files; \n        let filesPath;\n        let homeIndexPath;\n        const readFile = util.promisify(fs.readFile);\n        \n        if (postsPrefix) {\n            filesPath = path.join(this.baseDirectory, postsPrefix, this.siteConfig.advanced.urls.pageName);\n            files = fs.readdirSync(filesPath);\n            homeIndexPath = path.join(this.baseDirectory, postsPrefix);\n        } else {\n            filesPath = path.join(this.baseDirectory, this.siteConfig.advanced.urls.pageName);\n            files = fs.readdirSync(filesPath);\n            homeIndexPath = this.baseDirectory;\n        }\n\n        if (postsPrefix) {\n            let homeIndexFileContent = await readFile(path.join(homeIndexPath, 'index.html'), 'utf8');\n\n            if (homeIndexFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                this.fileList.push(postsPrefix + '/index.html');\n            }\n        }\n\n        for (let file of files) {\n            // Skip files\n            if (file.indexOf('.') > -1) {\n                continue;\n            }\n\n            let homeFileContent = await readFile(path.join(filesPath, file, 'index.html'), 'utf8');\n\n            if (homeFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                let pageName = this.siteConfig.advanced.urls.pageName;\n\n                if (postsPrefix) {\n                    this.fileList.push(postsPrefix + '/' + pageName + '/' + file + '/index.html');\n                } else {\n                    this.fileList.push(pageName + '/' + file + '/index.html');\n                }\n            }\n        }\n    }\n\n    /**\n     * Retrieves single post files to index\n     *\n     * @param file\n     * @param cleanUrlsEnabled\n     */\n    async getPostsAndPagesFilesList (file, cleanUrlsEnabled = false, skipPostsPrefix = false) {\n        const readFile = util.promisify(fs.readFile);\n\n        if (!skipPostsPrefix && this.siteConfig.advanced.urls.postsPrefix) {\n            file = path.join(this.siteConfig.advanced.urls.postsPrefix, file);\n        }\n\n        if (!cleanUrlsEnabled) {\n            // Read post file\n            let postFileContent = await readFile(path.join(this.baseDirectory, file), 'utf8');\n\n            // Detect if noindex does not exist in the post file\n            if (postFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                let fileNameWithoutExt = file.replace('.html', '');\n\n                if (this.postData[fileNameWithoutExt]) {\n                    this.fileList.push({\n                        images: this.postData[fileNameWithoutExt].images,\n                        lastMod: this.postData[fileNameWithoutExt].lastMod,\n                        url: file\n                    });\n                } else {\n                    this.fileList.push(file);\n                }\n            }\n\n            return Promise.resolve();\n        }\n\n        // Read post file\n        let filePath = path.join(this.baseDirectory, file, 'index.html');\n\n        if (fs.existsSync(filePath)) {\n            let postFileContent = await readFile(filePath, 'utf8');\n\n            // Detect if noindex does not exist in the post file\n            if (postFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                if (file === 'index.html') {\n                    this.fileList.push('');\n                } else {\n                    if (this.postData[file]) {\n                        this.fileList.push({\n                            images: this.postData[file].images,\n                            lastMod: this.postData[file].lastMod,\n                            url: file + '/'\n                        });\n                    } else {\n                        this.fileList.push(file + '/');\n                    }\n                }\n            }\n        }\n\n        // Look for subpages\n        let baseDir = path.join(this.baseDirectory, file);\n\n        if (fs.lstatSync(baseDir).isDirectory()) {\n            let subpages = fs.readdirSync(baseDir);\n            this.scanSubpages(subpages, file);\n        }\n\n        return Promise.resolve()\n    }\n\n    // recusive function to scan subpages\n    scanSubpages (subpages, currentPath) {\n        for (let subpage of subpages) {\n            if (subpage.indexOf('.') > -1) {\n                continue;\n            }\n\n            let filePath = path.join(this.baseDirectory, currentPath, subpage, 'index.html');\n\n            if (fs.existsSync(filePath)) {\n                let postFileContent = FileHelper.readFileSync(filePath, 'utf8');\n\n                if (postFileContent.indexOf('name=\"robots\" content=\"noindex') === -1) {\n                    this.fileList.push(currentPath + '/' + subpage + '/');\n                }\n            }\n\n            let pathToScan = path.join(this.baseDirectory, currentPath, subpage);\n            \n            if (!fs.lstatSync(pathToScan).isDirectory()) {\n                continue;\n            }\n\n            let subpagesList = fs.readdirSync(pathToScan);\n\n            if (subpagesList.length) {\n                this.scanSubpages(subpagesList, path.join(currentPath, subpage));\n            }\n        }\n    }\n\n    /**\n     * Returns sitemap XML file\n     */\n    renderXML() {\n        let domain = this.siteConfig.domain + '/';\n        this.outputXML = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>' + \"\\n\";\n        this.outputXML += '<?xml-stylesheet type=\"text/xsl\" href=\"' + domain + 'sitemap.xsl\"?>' + \"\\n\";\n        this.outputXML += '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd\" xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\">' + \"\\n\";\n        this.outputXML += '<url>' + \"\\n\";\n        this.outputXML += '<loc>' + domain + '</loc>' + \"\\n\";\n        this.outputXML += '</url>' + \"\\n\";\n\n        for (let item of this.fileList) {\n            let url = '';\n            let lastMod = false;\n            let images = false;\n\n            if (typeof item === 'string') {\n                url = item;\n            } else {\n                url = item.url;\n                lastMod = item.lastMod;\n                images = item.images;\n            }\n\n            if (this.isExcluded(url)) {\n                continue;\n            }\n\n            this.outputXML += '<url>' + \"\\n\";\n            this.outputXML += '<loc>' + (domain + url.replace(/index\\.html$/, '')).replace(/\\\\/gmi, '/') + '</loc>' + \"\\n\";\n\n            if (lastMod) {\n                this.outputXML += '<lastmod>' + lastMod + '</lastmod>' + \"\\n\";\n            }\n\n            if (images) {\n                for (let i = 0; i < images.length; i++) {\n                    if (!this.siteConfig.advanced.sitemapAddExternalImages && images[i].url.indexOf(domain) === -1) {\n                        continue;\n                    }\n\n                    this.outputXML += '<image:image>' + \"\\n\";\n                    this.outputXML += '<image:loc>' + (images[i].url).replace(/\\\\/gmi, '/') + '</image:loc>' + \"\\n\";\n                    this.outputXML += '<image:title><![CDATA[' + images[i].alt + ']]></image:title>' + \"\\n\";\n                    this.outputXML += '</image:image>' + \"\\n\";\n                }\n            }\n\n            this.outputXML += '</url>' + \"\\n\";\n        }\n\n        this.outputXML += '</urlset>';\n    }\n\n    /**\n     * Save sitemap.xml and sitemap.xsl files\n     */\n    saveXML () {\n        let xslContent = FileHelper.readFileSync(__dirname + '/../../../../default-files/theme-files/sitemap.xsl');\n        let xslFilePath = path.join(this.baseDirectory, 'sitemap.xsl');\n        let sitemapFilePath = path.join(this.baseDirectory, 'sitemap.xml');\n\n        // Check if sitemap.xsl exists - if yes, generate only sitemap\n        if (UtilsHelper.fileExists(xslFilePath)) {\n            fs.writeFileSync(sitemapFilePath, this.outputXML, 'utf8');\n            return;\n        }\n\n        fs.writeFileSync(sitemapFilePath, this.outputXML, 'utf8');\n        fs.writeFileSync(xslFilePath, xslContent, 'utf8');\n    }\n\n    /**\n     * Check if the gived URL should be excluded from the sitemap\n     */\n    isExcluded (url) {\n        url = '/' + url + '/';\n\n        for (let i = 0; i < this.excludedFiles.length; i++) {\n            if (url.indexOf(this.excludedFiles[i]) > -1) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n\nmodule.exports = Sitemap;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/specs/url.spec.js",
    "content": "const assert = require('assert');\nconst URLHelper = require('../url.js');\n\ndescribe('URL helper', function() {\n    describe('#URLHelper.createSlug', function() {\n        it('should create a slug from a given text', function () {\n            assert.equal('lorem-ipsum', URLHelper.createSlug('lorem ipsum'));\n        });\n\n        it('should create a slug without dots', function () {\n            assert.equal('loremipsum', URLHelper.createSlug('lorem.ipsum'));\n        });\n    });\n\n    describe('#URLHelper.createTagPermalink', function() {\n        let urlsConfig = {\n            tagsPrefix: ''\n        };\n\n        let urlsConfigWithPrefix = {\n            tagsPrefix: 'tags'\n        };\n\n        it('should create a slug from a given tag name', function () {\n            assert.equal(\n                'http://example.com/lorem-ipsum/',\n                URLHelper.createTagPermalink('http://example.com', urlsConfig, 'lorem ipsum')\n            );\n        });\n\n        it('should add index.html when it is requried', function () {\n            assert.equal(\n                'http://example.com/lorem-ipsum/index.html',\n                URLHelper.createTagPermalink('http://example.com', urlsConfig, 'lorem ipsum', true)\n            );\n        });\n\n        it('should create a prefixed slug from a given tag name', function () {\n            assert.equal(\n                'http://example.com/tags/lorem-ipsum/',\n                URLHelper.createTagPermalink('http://example.com', urlsConfigWithPrefix, 'lorem ipsum')\n            );\n        });\n\n        it('should add index.html when it is requried for prefixed slug', function () {\n            assert.equal(\n                'http://example.com/tags/lorem-ipsum/index.html',\n                URLHelper.createTagPermalink('http://example.com', urlsConfigWithPrefix, 'lorem ipsum', true)\n            );\n        });\n    });\n\n    describe('#URLHelper.createPaginationPermalink', function() {\n        let themeConfig = {\n            supportedFeatures: {\n                postsPage: false\n            }\n        };\n\n        let siteConfig = {\n            domain: 'http://example.com',\n            advanced: {\n                urls: {\n                    cleanUrls: false,\n                    tagsPrefix: '',\n                    authorsPrefix: 'authors',\n                    pageName: 'page',\n                    errorPage: '404.html',\n                    searchPage: 'search.html'\n                }\n            }\n        };\n\n        it('should create a proper URL for different types of pages', function () {\n            assert.equal(\n                'http://example.com/lorem-ipsum/page/2/',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 2, '', 'lorem-ipsum')\n            );\n\n            assert.equal(\n                'http://example.com/page/2/',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 2, '', false)\n            );\n\n            assert.equal(\n                'http://example.com/authors/lorem-ipsum/page/2/',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 2, 'author', 'lorem-ipsum')\n            );\n        });\n\n        it('should create a proper URL for the first page', function () {\n            assert.equal(\n                'http://example.com/lorem-ipsum/',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 1, '', 'lorem-ipsum')\n            );\n\n            assert.equal(\n                'http://example.com/',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 1, '', false)\n            );\n\n            assert.equal(\n                'http://example.com/authors/lorem-ipsum/',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 1, 'author', 'lorem-ipsum')\n            );\n        });\n\n        it('should create a proper URL with index.html when it is required', function () {\n            assert.equal(\n                'http://example.com/lorem-ipsum/index.html',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 1, '', 'lorem-ipsum', true)\n            );\n\n            assert.equal(\n                'http://example.com/index.html',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 1, '', false, true)\n            );\n\n            assert.equal(\n                'http://example.com/authors/lorem-ipsum/index.html',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 1, 'author', 'lorem-ipsum', true)\n            );\n\n            assert.equal(\n                'http://example.com/lorem-ipsum/page/2/index.html',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 2, '', 'lorem-ipsum', true)\n            );\n\n            assert.equal(\n                'http://example.com/page/2/index.html',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 2, '', false, true)\n            );\n\n            assert.equal(\n                'http://example.com/authors/lorem-ipsum/page/2/index.html',\n                URLHelper.createPaginationPermalink(siteConfig, themeConfig, 2, 'author', 'lorem-ipsum', true)\n            );\n        });\n    });\n\n    describe('#URLHelper.createImageURL', function() {\n        it('should create a proper URL for different types of pages', function () {\n            assert.equal(\n                'http://example.com/media/posts/10/img.jpg',\n                URLHelper.createImageURL('http://example.com', 10, 'img.jpg', 'post')\n            );\n        });\n    });\n\n    describe('#URLHelper.transformAssetURLIntoPath', function() {\n        // inputDir, assetUrl, websiteUrl\n\n        it('should create a proper path for a given media URL', function () {\n            assert.equal('sites/media/posts/10/img.jpg', URLHelper.transformAssetURLIntoPath('sites', 'http://example.com/media/posts/10/img.jpg', 'http://example.com'));\n        });\n\n        it('should remove index.html from the website URL', function () {\n            assert.equal('sites/media/posts/10/img.jpg', URLHelper.transformAssetURLIntoPath('sites', 'http://example.com/media/posts/10/img.jpg', 'http://example.com/index.html'));\n        });\n    });\n\n    describe('#URLHelper.fixProtocols', function() {\n        it('should fix http protocol URLs', function () {\n            assert.equal('http://example.com/index.html', URLHelper.fixProtocols('http:/example.com/index.html'));\n            assert.equal('http://example.com/index.html', URLHelper.fixProtocols('http:///example.com/index.html'));\n        });\n\n        it('should fix https protocol URLs', function () {\n            assert.equal('https://example.com/index.html', URLHelper.fixProtocols('https:/example.com/index.html'));\n            assert.equal('https://example.com/index.html', URLHelper.fixProtocols('https:///example.com/index.html'));\n        });\n\n        it('should fix file protocol URLs', function () {\n            assert.equal('file:///example.com/index.html', URLHelper.fixProtocols('file:/example.com/index.html'));\n            assert.equal('file:///example.com/index.html', URLHelper.fixProtocols('file://example.com/index.html'));\n        });\n    });\n});\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/template.js",
    "content": "// Necessary packages\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./../../../helpers/file.js');\nconst minifyHTML = require('html-minifier').minify;\nconst Utils = require('./../../../helpers/utils.js');\n\n/*\n * Class used to load Theme files as a Handlebar templates\n */\nclass TemplateHelper {\n    /*\n     * Constructor retrieves necessary paths\n     */\n    constructor(themeDir, outputDir, siteConfig) {\n        this.themeDir = themeDir;\n        this.outputDir = outputDir;\n        this.siteConfig = siteConfig;\n        this.themeConfig = this.loadThemeConfig();\n        this.loadedTemplates = {};\n        this.loadedPartialTemplates = {};\n    }\n\n    /*\n     * Loads theme configuration object\n     */\n    loadThemeConfig() {\n        // Load theme's config file\n        let themeConfig = Utils.loadThemeConfig(this.themeDir);\n\n        // Load default config file\n        let defaultConfigPath = path.join(__dirname, '..', '..', '..', '..', 'default-files', 'theme-files', 'config.json');\n        let defaultConfig = JSON.parse(FileHelper.readFileSync(defaultConfigPath, 'utf8'));\n\n        // Return merged config\n        return Utils.mergeObjects(defaultConfig, themeConfig);\n    }\n\n    /*\n     * Loads a specific HBS file\n     */\n    loadTemplate(fileName) {\n        let filePath = path.join(this.themeDir, fileName);\n        let overrideFilePath = Utils.fileIsOverrided(this.themeDir, filePath);\n\n        if (overrideFilePath) {\n            filePath = overrideFilePath;\n        }\n\n        if (Utils.fileExists(filePath)) {\n            if (this.loadedTemplates[filePath]) {\n                return this.loadedTemplates[filePath];\n            }\n\n            this.loadedTemplates[filePath] = FileHelper.readFileSync(filePath, 'utf8');\n            return this.loadedTemplates[filePath];\n        }\n\n        return false;\n    }\n\n    /*\n     * Loads a specific partial HBS file\n     */\n    loadPartialTemplate(fileName) {\n        let filePath = path.join(this.themeDir, this.themeConfig.files.partialsPath, fileName);\n        let overrideFilePath = Utils.fileIsOverrided(this.themeDir, filePath);\n\n        if(overrideFilePath) {\n            filePath = overrideFilePath;\n        }\n\n        if (Utils.fileExists(filePath)) {\n            if (this.loadedPartialTemplates[filePath]) {\n                return this.loadedPartialTemplates[filePath];\n            }\n\n            this.loadedPartialTemplates[filePath] = FileHelper.readFileSync(filePath, 'utf8');\n            return this.loadedPartialTemplates[filePath];\n        }\n\n        return false;\n    }\n\n    /*\n     * Save a compiled Handlebars template under\n     * a specified filename\n     */\n    saveOutputFile(fileName, content) {\n        let filePath = path.join(this.outputDir, fileName);\n\n        if(path.parse(filePath).ext !== '.xml' && path.parse(filePath).ext !== '.json') {\n            content = this.compressHTML(content);\n        }\n\n        fs.ensureDirSync(path.parse(filePath).dir);\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    saveOutputPostFile (postSlug, content) {\n        let suffix = '.html';\n        let baseDir = this.outputDir;\n\n        if (this.siteConfig.advanced.urls.postsPrefix) {\n            baseDir += '/' + this.siteConfig.advanced.urls.postsPrefix;\n        }\n\n        if (this.siteConfig.advanced.urls.cleanUrls) {\n            suffix = '/index.html';\n        }\n\n        let filePath = path.join(baseDir, postSlug + suffix);\n        content = this.compressHTML(content);\n\n        if (this.siteConfig.advanced.urls.cleanUrls) {\n            let dirPath = path.join(baseDir, postSlug);\n            fs.ensureDirSync(dirPath);\n        }\n\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    saveOutputHomePaginationFile(pageNumber, content) {\n        let baseDir = this.outputDir;\n\n        if (this.siteConfig.advanced.urls.postsPrefix) {\n            baseDir += '/' + this.siteConfig.advanced.urls.postsPrefix;\n        }\n\n        let filePath = path.join(baseDir, this.siteConfig.advanced.urls.pageName, pageNumber.toString(), 'index.html');\n        let pageDirPath = path.join(baseDir, this.siteConfig.advanced.urls.pageName);\n        let dirPath = path.join(pageDirPath, pageNumber.toString());\n        content = this.compressHTML(content);\n\n        // Create page directory if not exists\n        fs.ensureDirSync(pageDirPath);\n        // Create dir for specific page\n        fs.ensureDirSync(dirPath);\n        // Create index.html file in the created dir\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    saveOutputPageFile (pageID, pageSlug, content, renderer) {\n        let suffix = '.html';\n        let parentItems = renderer.cachedItems.pagesStructureHierarchy[pageID];\n        \n        if (this.siteConfig.advanced.urls.cleanUrls) {\n            suffix = '/index.html';\n        }\n\n        // If page is set as frontpage - render it in the root directory\n        if (this.siteConfig.advanced.usePageAsFrontpage && this.siteConfig.advanced.pageAsFrontpage === pageID) {\n            let filePath = path.join(this.outputDir, 'index.html');\n            content = this.compressHTML(content);\n            fs.writeFile(filePath, content, {'flags': 'w'});\n            return;\n        }\n\n        if (parentItems && parentItems.length) {\n            let slugs = [];\n\n            for (let i = 0; i < parentItems.length; i++) {\n                if (renderer.cachedItems.pages[parentItems[i]]) {\n                    slugs.push(renderer.cachedItems.pages[parentItems[i]].slug);\n                }\n            }\n\n            slugs.push(pageSlug);\n            pageSlug = slugs.join('/');\n        }\n\n        let filePath = path.join(this.outputDir, pageSlug + suffix);\n        content = this.compressHTML(content);\n\n        if (this.siteConfig.advanced.urls.cleanUrls) {\n            let dirPath = path.join(this.outputDir, pageSlug);\n            fs.ensureDirSync(dirPath);\n        }\n\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    /*\n     * Save a compiled Handlebars template for tags list\n     */\n    saveOutputTagsListFile(content) {\n        let usePostsPrefix = this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix;\n        let postsPrefix = this.siteConfig.advanced.urls.postsPrefix;\n        let baseDir = usePostsPrefix ? path.join(this.outputDir, postsPrefix) : this.outputDir;\n        let tagsPrefix = this.siteConfig.advanced.urls.tagsPrefix;\n        let filePath = path.join(baseDir, tagsPrefix, 'index.html');\n        let dirPath = path.join(baseDir, tagsPrefix);\n        fs.ensureDirSync(dirPath);\n        content = this.compressHTML(content);\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n    \n    /*\n     * Save a compiled Handlebars template under\n     * a specified tag filename\n     */\n    saveOutputTagFile(tagSlug, content, isTagPreview = false) {\n        let usePostsPrefix = this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix;\n        let postsPrefix = this.siteConfig.advanced.urls.postsPrefix;\n        let baseDir = usePostsPrefix ? path.join(this.outputDir, postsPrefix) : this.outputDir;\n        let filePath = path.join(baseDir, tagSlug, 'index.html');\n        let dirPath = path.join(baseDir, tagSlug);\n        let tagsPath = usePostsPrefix ? baseDir : false;\n        let tagsPrefix = this.siteConfig.advanced.urls.tagsPrefix;\n\n        if (isTagPreview) {\n            filePath = path.join(this.outputDir, 'preview.html');\n            content = this.compressHTML(content);\n            fs.writeFile(filePath, content, {'flags': 'w'});\n            return;\n        } else if (tagsPrefix) {\n            filePath = path.join(baseDir, tagsPrefix, tagSlug, 'index.html');\n            dirPath = path.join(baseDir, tagsPrefix, tagSlug);\n            tagsPath = path.join(baseDir, tagsPrefix);\n        }\n\n        if (tagsPath && !Utils.dirExists(tagsPath)) {\n            fs.ensureDirSync(tagsPath);\n        }\n\n        content = this.compressHTML(content);\n        fs.ensureDirSync(dirPath);\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    saveOutputTagPaginationFile(tagSlug, pageNumber, content) {\n        let usePostsPrefix = this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix;\n        let postsPrefix = this.siteConfig.advanced.urls.postsPrefix;\n        let baseDir = usePostsPrefix ? path.join(this.outputDir, postsPrefix) : this.outputDir;\n        let pageName = this.siteConfig.advanced.urls.pageName;\n        let filePath = path.join(baseDir, tagSlug, pageName, pageNumber.toString(), 'index.html');\n        let pageDirPath = path.join(baseDir, tagSlug, pageName);\n        let dirPath = path.join(pageDirPath, pageNumber.toString());\n        let tagsPath = usePostsPrefix ? baseDir : false;\n        let tagsPrefix = this.siteConfig.advanced.urls.tagsPrefix;\n\n        if (tagsPrefix) {\n            filePath = path.join(baseDir, tagsPrefix, tagSlug, pageName, pageNumber.toString(), 'index.html');\n            pageDirPath = path.join(baseDir, tagsPrefix, tagSlug, pageName);\n            dirPath = path.join(pageDirPath, pageNumber.toString());\n            tagsPath = path.join(baseDir, tagsPrefix);\n        }\n\n        if (tagsPath && !Utils.dirExists(tagsPath)) {\n            fs.ensureDirSync(tagsPath);\n        }\n\n        content = this.compressHTML(content);\n\n        // Create page directory if not exists\n        if(!Utils.dirExists(pageDirPath)) {\n            fs.ensureDirSync(pageDirPath);\n        }\n\n        // Create dir for specific page\n        if(!Utils.dirExists(dirPath)) {\n            fs.ensureDirSync(dirPath);\n        }\n\n        // Create index.html file in the created dir\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    /*\n     * Save a compiled Handlebars template under\n     * a specified author filename\n     */\n    saveOutputAuthorFile(authorSlug, content, isAuthorPreview = false) {\n        let usePostsPrefix = this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix;\n        let postsPrefix = this.siteConfig.advanced.urls.postsPrefix;\n        let baseDir = usePostsPrefix ? path.join(this.outputDir, postsPrefix) : this.outputDir;\n        let filePath = path.join(baseDir, this.siteConfig.advanced.urls.authorsPrefix, authorSlug, 'index.html');\n        let dirPath = path.join(baseDir, this.siteConfig.advanced.urls.authorsPrefix, authorSlug);\n        \n        if (isAuthorPreview) {\n            filePath = path.join(this.outputDir, 'preview.html');\n            content = this.compressHTML(content);\n            fs.writeFile(filePath, content, {'flags': 'w'});\n            return;\n        }\n\n        content = this.compressHTML(content);\n        fs.ensureDirSync(dirPath);\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    saveOutputAuthorPaginationFile(authorSlug, pageNumber, content) {\n        let usePostsPrefix = this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix;\n        let postsPrefix = this.siteConfig.advanced.urls.postsPrefix;\n        let baseDir = usePostsPrefix ? path.join(this.outputDir, postsPrefix) : this.outputDir;\n        let filePath = path.join(baseDir, this.siteConfig.advanced.urls.authorsPrefix, authorSlug, this.siteConfig.advanced.urls.pageName, pageNumber.toString(), 'index.html');\n        let pageDirPath = path.join(baseDir, this.siteConfig.advanced.urls.authorsPrefix, authorSlug, this.siteConfig.advanced.urls.pageName);\n        let dirPath = path.join(pageDirPath, pageNumber.toString());\n        content = this.compressHTML(content);\n\n        // Create page directory if not exists\n        fs.ensureDirSync(pageDirPath);\n        // Create dir for specific page\n        fs.ensureDirSync(dirPath);\n        // Create index.html file in the created dir\n        fs.writeFile(filePath, content, {'flags': 'w'});\n    }\n\n    /*\n     * Compress the output HTML if necessary\n     */\n    compressHTML(content) {\n        if(!this.siteConfig.advanced.htmlCompression) {\n            return content;\n        }\n\n        let compressedContent = '';\n\n        try {\n            compressedContent = minifyHTML(content, {\n                collapseWhitespace: true,\n                removeComments: !!this.siteConfig.advanced.htmlCompressionRemoveComments,\n                removeEmptyAttributes: true\n            });\n        } catch(e) {\n            // Return original content without compression and log an error message\n            compressedContent = content;\n            // @LOGTHIS\n            console.log('A HTML parsing error occurred during the compression - uncompressed HTML returned. Error details: ' + JSON.stringify(e));\n        }\n\n        return compressedContent;\n    }\n\n    /*\n     * Loads user partials from the theme\n     */\n    getUserPartials(requiredPartials, optionalPartials) {\n        let userPartials = [];\n        let partialsPath = path.join(this.themeDir, this.themeConfig.files.partialsPath);\n        let overridedPartialsPath = path.join(this.themeDir.replace(/[\\\\\\/]{1,1}$/, '') + '-override', this.themeConfig.files.partialsPath);\n        let filesAndDirs = false;\n        let overridedFilesAndDirs = false;\n\n        if(Utils.dirExists(partialsPath)) {\n            filesAndDirs = this.getHbsFilesRecursively(partialsPath);\n            filesAndDirs = filesAndDirs.map(file => file.replace(partialsPath + '/', '').replace(partialsPath + '\\\\', ''));\n        }\n\n        if(Utils.dirExists(overridedPartialsPath)) {\n            overridedFilesAndDirs = this.getHbsFilesRecursively(overridedPartialsPath);\n            overridedFilesAndDirs = overridedFilesAndDirs.map(file => file.replace(overridedPartialsPath + '/', '').replace(overridedPartialsPath + '\\\\', ''));\n        }\n\n        if (!filesAndDirs.length && !overridedFilesAndDirs.length) {\n            return userPartials;\n        }\n\n        for (let i = 0; i < filesAndDirs.length; i++) {\n            if (filesAndDirs[i].substr(-4) !== '.hbs') {\n                continue;\n            }\n\n            let partialFile = filesAndDirs[i].replace('.hbs', '');\n\n            if(\n                requiredPartials.indexOf(partialFile) === -1 &&\n                optionalPartials.indexOf(partialFile) === -1\n            ) {\n                userPartials.push(partialFile);\n            }\n        }\n\n        for (let i = 0; i < overridedFilesAndDirs.length; i++) {\n            if (overridedFilesAndDirs[i].substr(-4) !== '.hbs') {\n                continue;\n            }\n\n            let partialFile = overridedFilesAndDirs[i].replace('.hbs', '');\n\n            if(\n                requiredPartials.indexOf(partialFile) === -1 &&\n                optionalPartials.indexOf(partialFile) === -1\n            ) {\n                userPartials.push(partialFile);\n            }\n        }\n\n        // Fix for Windows\n        userPartials = userPartials.map(partial => partial.replace(/\\\\/gmi, '/'));\n        return userPartials;\n    }\n\n    getHbsFilesRecursively (dir) {\n        let results = [];\n        let list = fs.readdirSync(dir);\n      \n        list.forEach(file => {\n            let filePath = path.join(dir, file);\n            let stat = fs.statSync(filePath);\n        \n            if (stat.isDirectory()) {\n                results = results.concat(this.getHbsFilesRecursively(filePath));\n            } else {\n                if (path.extname(file) === '.hbs') {\n                    results.push(filePath);\n                }\n            }\n        });\n\n        return results;\n    }\n}\n\nmodule.exports = TemplateHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/url.js",
    "content": "/*\n * Class used to help with operations on\n * the URLs and slugs\n */\n\nconst slug = require('./../../../helpers/slug');\nconst path = require('path');\nconst normalizePath = require('normalize-path');\n\n/**\n * Class used to load Theme files as a Handlebar templates\n */\nclass URLHelper {\n    /**\n     * Creates a slug for a given URL part\n     *\n     * @param string\n     * @returns {*}\n     */\n    static createSlug(string) {\n        // Note: It also transliterate the slug content\n        return slug(string);\n    }\n\n    /**\n     * Creates an URL for a given tag\n     *\n     * @param domain\n     * @param urlsConfig\n     * @param tagName\n     * @returns {string}\n     */\n    static createTagPermalink(domain, urlsConfig, tagName, addIndexHtml = false) {\n        let url = domain + '/' + URLHelper.createSlug(tagName) + '/';\n\n        if(urlsConfig.tagsPrefix) {\n            url = domain + '/' + urlsConfig.tagsPrefix + '/' + URLHelper.createSlug(tagName) + '/';\n\n            if (urlsConfig.postsPrefix && urlsConfig.tagsPrefixAfterPostsPrefix) {\n                url = domain + '/' + urlsConfig.postsPrefix + '/' + urlsConfig.tagsPrefix + '/' + URLHelper.createSlug(tagName) + '/';\n            }\n        }\n\n        if(addIndexHtml) {\n            url += 'index.html';\n        }\n\n        return url;\n    }\n\n    /**\n     * Creates pagination link for a given URL type\n     *\n     * @param siteConfig\n     * @param themeConfig\n     * @param pageNumber\n     * @param pageType\n     * @param pageSlug\n     * @returns {*}\n     */\n    static createPaginationPermalink(siteConfig, themeConfig, pageNumber, pageType, pageSlug, addIndexHtml = false) {\n        let domain = siteConfig.domain;\n        let config = siteConfig.advanced;\n        \n        // When there is no link - skip the operations\n        if(pageNumber === false) {\n            return false;\n        }\n\n        // We need to add tag name for all tag pages\n        let pagePrefix = '';\n\n        if(pageSlug !== false) {\n            pagePrefix = pageSlug + '/';\n        }\n\n        // We need to add the page/X only for page > 1\n        let pageSuffix = '';\n\n        if(pageNumber > 1) {\n            pageSuffix = config.urls.pageName + '/' + pageNumber + '/';\n        }\n\n        let optionalPrefix = '';\n\n        if (pageType === 'home') {\n            if (config.urls.postsPrefix) {\n                optionalPrefix = config.urls.postsPrefix + '/';\n            } \n            \n            if (config.urls.postsPrefix && !config.usePageAsFrontpage && !themeConfig.supportedFeatures.postsPage && pageNumber === 1) {\n                optionalPrefix = '';\n            }\n        }\n\n        if(pageType === 'author') {\n            optionalPrefix = config.urls.authorsPrefix + '/';\n\n            if (config.urls.postsPrefix && config.urls.authorsPrefixAfterPostsPrefix) {\n                optionalPrefix = config.urls.postsPrefix + '/' + config.urls.authorsPrefix + '/';\n            }\n        }\n\n        if(pageType === 'tag' && config.urls.tagsPrefix !== '') {\n            optionalPrefix = config.urls.tagsPrefix + '/';\n\n            if (config.urls.postsPrefix && config.urls.tagsPrefixAfterPostsPrefix) {\n                optionalPrefix = config.urls.postsPrefix + '/' + config.urls.tagsPrefix + '/';\n            }\n        }\n\n        let url = domain + '/' + optionalPrefix + pagePrefix + pageSuffix;\n\n        if(addIndexHtml) {\n            if(url.substr(-1) === '/') {\n                url += 'index.html';\n            } else {\n                url += '/index.html';\n            }\n        }\n\n        return url;\n    }\n\n    /**\n     * Creates an image URL\n     *\n     * @param domain\n     * @param itemID\n     * @param imageURL\n     * @param type\n     * @returns {string}\n     */\n    static createImageURL(domain, itemID, imageURL, type = 'post') {\n        let output = [domain, 'media', type + 's', itemID, imageURL];\n        output = normalizePath(output.join('/'));\n        output = URLHelper.fixProtocols(output);\n\n        return output;\n    }\n\n    /**\n     * Changes given asset URL into file path to this asset\n     *\n     * @param inputDir\n     * @param assetUrl\n     * @param websiteUrl\n     * @returns {string}\n     */\n    static transformAssetURLIntoPath(inputDir, assetUrl, websiteUrl) {\n        websiteUrl = normalizePath(websiteUrl);\n        websiteUrl = websiteUrl.replace('index.html', '');\n        assetUrl = normalizePath(assetUrl);\n        assetUrl = assetUrl.replace(websiteUrl, '');\n        assetUrl = assetUrl.split('/');\n\n        return path.join(inputDir, ...assetUrl);\n    }\n\n    /**\n     * Fixes known problems with protocols in a given URL\n     *\n     * @param input\n     * @returns {*}\n     */\n    static fixProtocols(input) {\n        if (input.substr(0, 8) === './http:/') {\n            input = input.replace('./http:/', 'http:/');\n        }\n\n        if (input.substr(0, 9) === './https:/') {\n            input = input.replace('./https:/', 'https:/');\n        }\n\n        if(input.substr(0,6) === 'http:/' && input.substr(0,7) !== 'http://') {\n            input = input.replace('http:/', 'http://');\n        }\n\n        if(input.substr(0,8) === 'http:///') {\n            input = input.replace('http:///', 'http://');\n        }\n\n        if(input.substr(0,7) === 'https:/' && input.substr(0,8) !== 'https://') {\n            input = input.replace('https:/', 'https://');\n        }\n\n        if(input.substr(0,9) === 'https:///') {\n            input = input.replace('https:///', 'https://');\n        }\n\n        if (input.substr(0, 8) === './file:/') {\n            input = input.replace('./file:/', 'file:/');\n        }\n\n        if(input.substr(0,6) === 'file:/' && input.substr(0,7) !== 'file://') {\n            input = input.replace('file:/', 'file:///');\n        }\n\n        if(input.substr(0,7) === 'file://' && input.substr(0,8) !== 'file:///') {\n            input = input.replace('file://', 'file:///');\n        }\n\n        return input;\n    }\n\n    /**\n     * Adapts URLs of images in the settings to use on the preview or a final website\n     *\n     * @param domain\n     * @param settings\n     * @returns {*}\n     */\n    static prepareSettingsImages(domain, settings) {\n        let groups = Object.keys(settings);\n\n        for (let i = 0; i < groups.length; i++) {\n            let options = Object.keys(settings[groups[i]]);\n\n            for (let j = 0; j < options.length; j++) {\n                if (Array.isArray(settings[groups[i]][options[j]])) {\n                    let items = settings[groups[i]][options[j]];\n\n                    for (let k = 0; k < items.length; k++) {\n                        let item = items[k];\n                        \n                        if (typeof item === 'object') {\n                            let keys = Object.keys(item);\n\n                            for (let l = 0; l < keys.length; l++) {\n                                let key = keys[l];\n\n                                if (typeof item[key] === 'string' && item[key].indexOf('media/website') > -1) {\n                                    item[key] = URLHelper.fixProtocols(normalizePath(domain + '/' + item[key]));\n                                }\n                            }\n                        }\n                    }\n                }\n                \n                if (typeof settings[groups[i]][options[j]] !== \"string\") {\n                    continue;\n                }\n\n                if (settings[groups[i]][options[j]].indexOf('media/website') > -1) {\n                    settings[groups[i]][options[j]] = URLHelper.fixProtocols(normalizePath(domain + '/' + settings[groups[i]][options[j]]));\n                }\n            }\n        }\n\n        return settings;\n    }\n}\n\nmodule.exports = URLHelper;\n"
  },
  {
    "path": "app/back-end/modules/render-html/helpers/view-settings.js",
    "content": "class ViewSettings {\n    static override(viewSettings, defaultViewConfig, itemData, rendererInstance) {\n        let outputConfig = {};\n\n        // Get default config structure\n        let viewType = itemData.type;\n        let viewConfigObject = {}\n\n        if (viewType && (viewType === 'author' || viewType === 'post' || viewType === 'page' || viewType === 'tag')) {\n            let configField = viewType + 'Config';\n            viewConfigObject = JSON.parse(JSON.stringify(rendererInstance.viewConfigStructure[configField]));\n        }\n\n        // Generate default settings structure\n        let defaultViewFields = Object.keys(defaultViewConfig);\n\n        for(let i = 0; i < defaultViewFields.length; i++) {\n            let field = viewConfigObject[defaultViewFields[i]];\n            let defaultField = defaultViewConfig[defaultViewFields[i]];\n\n            if(typeof field !== 'undefined' && (!field.type || (field.type && field.type === 'select'))) {\n                if (\n                    defaultField === 0 ||\n                    defaultField === '0' ||\n                    defaultField.value === 0 ||\n                    defaultField.value === '0'\n                ) {\n                    outputConfig[defaultViewFields[i]] = false;\n                } else if (\n                    defaultField === 1 ||\n                    defaultField === '1' ||\n                    defaultField.value === 1 ||\n                    defaultField.value === '1'\n                ) {\n                    outputConfig[defaultViewFields[i]] = true;\n                } else {\n                    if (typeof defaultField.value !== 'undefined') {\n                        outputConfig[defaultViewFields[i]] = defaultField.value;\n                    } else if (typeof defaultField !== 'object') {\n                        outputConfig[defaultViewFields[i]] = defaultField;\n                    }\n                }\n            } else if (typeof field !== 'undefined' && field.type === 'image') {\n                let dirName = 'posts';\n\n                if (itemData.type === 'tag') {\n                    dirName = 'tags';\n                } else if (itemData.type === 'author') {\n                    dirName = 'authors';\n                }\n\n                if (defaultField) {\n                    let imagePath = '/media/' + dirName + '/defaults/' + defaultField;\n                    outputConfig[defaultViewFields[i]] = rendererInstance.siteConfig.domain + imagePath;\n                } else {\n                    outputConfig[defaultViewFields[i]] = false;\n                }\n            } else {\n                if (defaultField === '0') {\n                    defaultField = 0;\n                }\n\n                if (defaultField === '1') {\n                    defaultField = 1;\n                }\n\n                outputConfig[defaultViewFields[i]] = defaultField;\n            }\n        }\n\n        // Override values with the view settings\n        let viewFields = Object.keys(viewSettings);\n\n        for(let i = 0; i < viewFields.length; i++) {\n            let field = viewSettings[viewFields[i]];\n\n            if (field.type === 'image') {\n                if (field.value) {\n                    let dirName = 'posts';\n\n                    if (itemData.type === 'tag') {\n                        dirName = 'tags';\n                    } else if (itemData.type === 'author') {\n                        dirName = 'authors';\n                    }\n\n                    let imagePath = '/media/' + dirName + '/' + itemData.id + '/' + field.value;\n                    outputConfig[defaultViewFields[i]] = rendererInstance.siteConfig.domain + imagePath;\n                }\n            } else {\n                if(typeof field !== 'undefined' && field.value) {\n                    if(field.value !== \"\") {\n                        outputConfig[viewFields[i]] = field.value;\n                    }\n                } else if(typeof field === 'string' && field !== \"\") {\n                    outputConfig[viewFields[i]] = field;\n                }\n\n                if((field.type && field.type === 'select') || !field.type) {\n                    if(\n                        field === 0 ||\n                        field === '0' ||\n                        field.value === 0 ||\n                        field.value === '0'\n                    ) {\n                        outputConfig[viewFields[i]] = false;\n                    } else if(\n                        field === 1 ||\n                        field === '1' ||\n                        field.value === 1 ||\n                        field.value === '1'\n                    ) {\n                        outputConfig[viewFields[i]] = true;\n                    } else {\n                        if (typeof field.value !== 'undefined' && field.value !== '') {\n                            outputConfig[viewFields[i]] = JSON.stringify(field.value).replace(/\"/g, '');\n                        } else if (typeof field !== 'object' && field !== '') {\n                            outputConfig[viewFields[i]] = JSON.stringify(field).replace(/\"/g, '');\n                        }\n                    }\n                }\n            }\n        }\n\n        // Fix potential issues with deep objects\n        for(let i = 0; i < defaultViewFields.length; i++) {\n            let field = outputConfig[defaultViewFields[i]];\n\n            if(typeof field === 'undefined') {\n                console.log(defaultViewFields[i] + ' NOT EXISTS!');\n                continue;\n            }\n\n            if(field.type && field.value) {\n                outputConfig[defaultViewFields[i]] = field.value;\n            }\n        }\n\n        return outputConfig;\n    }\n}\n\nmodule.exports = ViewSettings;\n"
  },
  {
    "path": "app/back-end/modules/render-html/items/author.js",
    "content": "const path = require('path');\nconst normalizePath = require('normalize-path');\nconst AvatarHelper = require('./../../../helpers/avatar');\nconst URLHelper = require('./../helpers/url');\nconst UtilsHelper = require('./../../../helpers/utils');\nconst slug = require('./../../../helpers/slug');\n\n/**\n * Author item for the renderer\n */\nclass AuthorItem {\n    /**\n     * Constructor\n     *\n     * @param author\n     * @param rendererInstance\n     */\n    constructor(author, rendererInstance) {\n        this.author = author;\n        this.authorID = parseInt(author.id, 10);\n        this.renderer = rendererInstance;\n        this.db = this.renderer.db;\n        this.themeConfig = this.renderer.themeConfig;\n        this.authorData = {};\n\n        this.prepareData();\n        this.storeData();\n    }\n\n    /**\n     * Prepares final author data\n     */\n    prepareData() {\n        let addIndexHtml = this.renderer.previewMode || this.renderer.siteConfig.advanced.urls.addIndex ? 'index.html' : '';\n        let authorConfig = this.author.config ? JSON.parse(this.author.config) : {};\n        let additionalData = this.author.additional_data ? JSON.parse(this.author.additional_data) : {};\n\n        this.authorData = {\n            id: this.authorID,\n            name: this.author.name,\n            username: this.author.username,\n            avatar: '',\n            avatarImage: '',\n            email: '',\n            website: '',\n            description: '',\n            postsNumber: 0,\n            url: '',\n            featuredImage: {},\n            config: authorConfig,\n            additionalData: additionalData,\n            template: authorConfig.template ? authorConfig.template : ''\n        };\n\n        if (this.renderer.cachedItems.featuredImages.authors[this.authorData.id]) {\n            this.authorData.featuredImage = this.renderer.cachedItems.featuredImages.authors[this.authorData.id];\n        }\n\n        try {\n            UtilsHelper.mergeObjects(this.authorData, JSON.parse(this.author.config));\n        } catch(e) {\n            console.log('[WARNING] renderer-context.js: wrong author JSON config data for author with ID: ' + this.authorID);\n        }\n\n        if(typeof this.authorData.avatar === 'string') {\n            if (AvatarHelper.isLocalAvatar(this.authorData.avatar)) {\n                let domain = this.renderer.siteConfig.domain;\n                let avatarUrl = path.join(domain, 'media', 'website', this.authorData.avatar);\n                avatarUrl = normalizePath(avatarUrl);\n                this.authorData.avatar = URLHelper.fixProtocols(avatarUrl);\n\n                if (this.authorData.avatar) {\n                    let avatarPath = path.join(this.renderer.inputDir, 'media', 'website', path.parse(this.authorData.avatar).base);\n                    this.authorData.avatarImage = AvatarHelper.getAvatarData(this.authorData, avatarPath);\n                }\n            } else {\n                this.authorData.avatar = URLHelper.fixProtocols(this.authorData.avatar);\n                this.authorData.avatarImage = AvatarHelper.getAvatarData(this.authorData);\n            }\n        } else {\n            this.authorData.avatar = '';\n            this.authorData.avatarImage = '';\n        }\n\n        this.authorData.postsNumber = this.getPostsNumber();\n        this.authorData.url =   this.renderer.siteConfig.domain + '/' +\n                                this.renderer.siteConfig.advanced.urls.authorsPrefix + '/' +\n                                slug(this.authorData.username) + '/' +\n                                addIndexHtml;\n\n        if (this.renderer.siteConfig.advanced.urls.postsPrefix && this.renderer.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) {\n            this.authorData.url =   this.renderer.siteConfig.domain + '/' +\n                                    this.renderer.siteConfig.advanced.urls.postsPrefix + '/' +\n                                    this.renderer.siteConfig.advanced.urls.authorsPrefix + '/' +\n                                    slug(this.authorData.username) + '/' +\n                                    addIndexHtml;\n        }\n    }\n\n    /**\n     * Stores tag data in the cached items of renderer\n     */\n    storeData() {\n        if (this.renderer.plugins.hasModifiers('authorItemData')) {\n            this.authorData = this.renderer.plugins.runModifiers('authorItemData', this.renderer, this.authorData); \n        }\n\n        // Store tag data without references\n        this.renderer.cachedItems.authors[this.authorID] = JSON.parse(JSON.stringify(this.authorData));\n    }\n\n    /**\n     * Returns number of posts connected with a given tag ID\n     *\n     * @returns {number}\n     */\n    getPostsNumber() {\n        let postsNumber = this.renderer.cachedItems.authorsPostCounts[this.authorID];\n\n        if(postsNumber) {\n            return postsNumber;\n        }\n\n        return 0;\n    }\n\n    /**\n     * Stores author view configuration in cached items\n     * \n     * @param {*} config \n     */\n    setAuthorViewConfig(config) {\n        this.renderer.cachedItems.authors[this.authorID].authorViewConfig = config;\n    }\n}\n\nmodule.exports = AuthorItem;\n"
  },
  {
    "path": "app/back-end/modules/render-html/items/featured-image.js",
    "content": "const path = require('path');\nconst sizeOf = require('image-size');\nconst URLHelper = require('./../helpers/url');\nconst ContentHelper = require('./../helpers/content');\nconst UtilsHelper = require('./../../../helpers/utils');\n\n/**\n * Featured image item for the renderer\n */\nclass FeaturedImageItem {\n    constructor(image, rendererInstance, type = 'featuredImages', cacheItemType = 'post') {\n        this.image = image;\n        this.itemID = parseInt(image.item_id, 10);\n        this.renderer = rendererInstance;\n        this.db = this.renderer.db;\n        this.themeConfig = this.renderer.themeConfig;\n        this.imageData = {};\n        this.imageType = type;\n        this.itemType = 'post';\n        this.cacheItemType = cacheItemType;\n\n        if (type === 'tagImages') {\n            this.itemType = 'tag';\n        } else if (type === 'authorImages') {\n            this.itemType = 'author';\n        }\n\n        this.prepareData();\n        this.storeData();\n    }\n\n    /**\n     * Prepare data of image\n     */\n    prepareData() {\n        if(isNaN(this.itemID)) {\n            this.imageData = false;\n            return;\n        }\n\n        let url = '';\n        let alt = '';\n        let caption = '';\n        let credits = '';\n        let imageDimensions = {\n            width: 0,\n            height: 0\n        };\n\n        let data = JSON.parse(this.image.additional_data);\n        let imagePath = URLHelper.createImageURL(this.renderer.inputDir, this.itemID, this.image.url, this.itemType);\n        let domain = this.renderer.siteConfig.domain;\n\n        url = URLHelper.createImageURL(domain, this.itemID, this.image.url, this.itemType);\n        alt = data.alt;\n        caption = data.caption;\n        credits = data.credits;\n\n        try {\n            if (this.image.url) {\n                imageDimensions = sizeOf(imagePath);\n            } else {\n                this.imageData = false;\n            }\n        } catch(err) {\n            console.log('[WARNING] renderer-context.js: wrong image path - missing dimensions for: ' + imagePath);\n            this.imageData = false;\n            return;\n        }\n\n        if (!this.imageData) {\n            return;\n        }\n\n        let featuredImageSrcSets = '';\n        let useWebp = false;\n\n        if (this.renderer.siteConfig?.advanced?.forceWebp) {\n            useWebp = true;\n        }\n\n        if(!this.isGifOrSvg(url)) {\n            featuredImageSrcSets = ContentHelper.getFeaturedImageSrcset(url, this.themeConfig, useWebp, this.itemType);\n        }\n\n        let featuredImageSizes = false;\n\n        if(featuredImageSrcSets !== false && !this.isGifOrSvg(url)) {\n            featuredImageSizes = ContentHelper.getFeaturedImageSizes(this.themeConfig, this.itemType);\n        }\n\n        if(!featuredImageSrcSets) {\n            featuredImageSrcSets = '';\n        }\n\n        if(!featuredImageSizes) {\n            featuredImageSizes = '';\n        }\n\n        let featuredImageData = {\n            id: this.image.id,\n            url: url,\n            alt: alt,\n            caption: caption,\n            credits: credits,\n            height: imageDimensions.height,\n            width: imageDimensions.width,\n            srcset: featuredImageSrcSets,\n            sizes: featuredImageSizes\n        };\n\n        // Create alternative names for dimensions\n        let dimensions = false;\n\n        if(UtilsHelper.responsiveImagesConfigExists(this.themeConfig)) {\n            dimensions = UtilsHelper.responsiveImagesDimensions(this.themeConfig, this.imageType);\n\n            if (!dimensions) {\n                dimensions = UtilsHelper.responsiveImagesDimensions(this.themeConfig, 'contentImages');\n            }\n\n            if(dimensions !== false && typeof dimensions !== \"boolean\") {\n                for(let dimensionName of dimensions) {\n                    let base = path.parse(url).base;\n                    let filename = path.parse(url).name;\n                    let extension = path.parse(url).ext;\n\n                    if (useWebp) {\n                        extension = '.webp';\n                    }\n\n                    let newFilename = filename + '-' + dimensionName + extension;\n                    let capitalizedDimensionName = dimensionName.charAt(0).toUpperCase() + dimensionName.slice(1);\n\n                    if(!this.isGifOrSvg(url)) {\n                        featuredImageData['url' + capitalizedDimensionName] = url.replace(base, 'responsive/' + newFilename);\n                    } else {\n                        featuredImageData['url' + capitalizedDimensionName] = url;\n                    }\n                }\n            }\n        }\n\n        this.imageData = featuredImageData;\n    }\n\n    /**\n     * Stores image data in the cached items\n     */\n    storeData() {\n        if (this.renderer.plugins.hasModifiers('featuredImageItemData')) {\n            this.imageData = this.renderer.plugins.runModifiers('featuredImageItemData', this.renderer, this.imageData, this.itemType, this.cacheItemType); \n        }\n\n        // Store tag data without references\n        this.renderer.cachedItems.featuredImages[this.cacheItemType + 's'][this.itemID] = JSON.parse(JSON.stringify(this.imageData));\n    }\n\n    /**\n     * Detects if image is a GIF or SVG\n     */\n    isGifOrSvg(url) {\n        if(url.slice(-4) === '.gif' || url.slice(-4) === '.svg') {\n            return true;\n        }\n\n        return false;\n    }\n}\n\nmodule.exports = FeaturedImageItem;\n"
  },
  {
    "path": "app/back-end/modules/render-html/items/page.js",
    "content": "const path = require('path');\nconst ContentHelper = require('./../helpers/content');\n\n/**\n * Page item for the renderer\n */\nclass PageItem {\n    constructor(page, rendererInstance) {\n        this.page = page;\n        this.pageID = parseInt(page.id, 10);\n        this.renderer = rendererInstance;\n        this.db = this.renderer.db;\n        this.themeConfig = this.renderer.themeConfig;\n        this.siteConfig = this.renderer.siteConfig;\n        this.pageData = {};\n        this.metaData = {};\n        this.metaDescription = '';\n        this.subpages = this.renderer.cachedItems.pagesStructure[page.id] || [];\n\n        this.getMetaData();\n        this.prepareData();\n        this.storeData();\n    }\n\n    getMetaData () {\n        let metaDataQuery = this.db.prepare(`SELECT value FROM posts_additional_data WHERE post_id = @pageID AND key = '_core'`);\n        let metaData = metaDataQuery.get({ pageID: this.page.id});\n\n        if (metaData && metaData.value) {\n            this.metaData = JSON.parse(metaData.value);\n        }\n\n        if (!this.metaData.editor) {\n            this.metaData.editor = 'tinymce';\n        }\n\n        if (this.metaData.metaDesc) {\n            this.metaDescription = this.metaData.metaDesc; \n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription;\n        }\n    }\n\n    prepareData() {\n        let preparedText = ContentHelper.prepareContent(this.page.id, this.page.text, this.siteConfig.domain, this.themeConfig, this.renderer, this.metaData.editor);\n        let preparedExcerpt = ContentHelper.prepareExcerpt(this.themeConfig.config.excerptLength, preparedText);\n        let hasCustomExcerpt = false;\n        let readmoreMatches = preparedText.match(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n\n        if (readmoreMatches && readmoreMatches.length) {\n            hasCustomExcerpt = true;\n\n            // Detect if hide of the custom excerpt is enabled\n            if (this.renderer.siteConfig.advanced.pageUseTextWithoutCustomExcerpt) {\n                preparedText = preparedText.split(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n                preparedText = preparedText[1];\n            } else {\n                preparedText = preparedText.replace(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi, '');\n            }\n        }\n\n        this.pageData = {\n            id: this.page.id,\n            title: this.page.title,\n            author: this.renderer.cachedItems.authors[this.page.authors],\n            slug: this.page.slug,\n            text: preparedText,\n            excerpt: preparedExcerpt,\n            createdAt: this.page.created_at,\n            modifiedAt: this.page.modified_at,\n            status: this.page.status,\n            hasGallery: preparedText.indexOf('class=\"gallery') !== -1,\n            template: this.page.template,\n            hasCustomExcerpt: hasCustomExcerpt,\n            editor: this.metaData.editor || 'tinymce',\n            metaDescription: this.metaDescription,\n            subpages: this.subpages\n        };\n\n        if (this.pageData.template === '*') {\n            this.pageData.template = this.themeConfig.defaultTemplates.page;\n        }\n\n        this.pageData.featuredImage = {};\n\n        if (this.renderer.cachedItems.featuredImages.pages[this.pageData.id]) {\n            this.pageData.featuredImage = this.renderer.cachedItems.featuredImages.pages[this.pageData.id];\n        }\n\n        if (this.renderer.plugins.hasModifiers('pageTitle')) {\n            this.pageData.title = this.renderer.plugins.runModifiers('pageTitle', this.renderer, this.pageData.title, { pageData: this.pageData }); \n        }\n\n        if (this.renderer.plugins.hasModifiers('pageText')) {\n            this.pageData.text = this.renderer.plugins.runModifiers('pageText', this.renderer, this.pageData.text, { pageData: this.pageData }); \n        }\n\n        if (this.renderer.plugins.hasModifiers('pageExcerpt')) {\n            this.pageData.excerpt = this.renderer.plugins.runModifiers('pageExcerpt', this.renderer, this.pageData.excerpt, { pageData: this.pageData }); \n        }\n    }\n\n    storeData() {\n        if (this.renderer.plugins.hasModifiers('pageItemData')) {\n            this.pageData = this.renderer.plugins.runModifiers('pageItemData', this.renderer, this.pageData); \n        }\n\n        this.renderer.cachedItems.pages[this.pageID] = JSON.parse(JSON.stringify(this.pageData));\n    }\n\n    setInternalLinks() {\n        let pageText = this.renderer.cachedItems.pages[this.pageID].text;\n        let pageExcerpt = this.renderer.cachedItems.pages[this.pageID].excerpt;\n        this.renderer.cachedItems.pages[this.pageID].text = ContentHelper.setInternalLinks(pageText, this.renderer);\n        this.renderer.cachedItems.pages[this.pageID].excerpt = ContentHelper.setInternalLinks(pageExcerpt, this.renderer);\n    }\n\n    setHierarchyLinks() {\n        let pageURL = this.siteConfig.domain + '/' + this.page.slug + '.html';\n\n        if (this.siteConfig.advanced.urls.cleanUrls) {\n            let parentItems = this.renderer.cachedItems.pagesStructureHierarchy[this.page.id];\n            let pageSlug = this.page.slug;\n\n            if (this.renderer.siteConfig.advanced.urls.cleanUrls && parentItems && parentItems.length) {\n                let slugs = [];\n\n                for (let i = 0; i < parentItems.length; i++) {\n                    if (this.renderer.cachedItems.pages[parentItems[i]]) {\n                        slugs.push(this.renderer.cachedItems.pages[parentItems[i]].slug);\n                    }\n                }\n\n                slugs.push(this.page.slug);\n                pageSlug = slugs.join('/');\n            }\n\n            pageURL = this.siteConfig.domain + '/' + pageSlug + '/';\n\n            if (this.renderer.previewMode || this.renderer.siteConfig.advanced.urls.addIndex) {\n                pageURL += 'index.html';\n            }\n        }\n\n        if (this.siteConfig.advanced.usePageAsFrontpage && this.siteConfig.advanced.pageAsFrontpage === this.page.id) {\n            pageURL = this.siteConfig.domain + '/';\n\n            if (this.renderer.previewMode || this.renderer.siteConfig.advanced.urls.addIndex) {\n                pageURL += 'index.html';\n            }\n        }\n\n        this.renderer.cachedItems.pages[this.pageID].url = pageURL;\n    }\n\n    setPageViewConfig(config) {\n        this.renderer.cachedItems.pages[this.pageID].pageViewConfig = config;\n    }\n}\n\nmodule.exports = PageItem;\n"
  },
  {
    "path": "app/back-end/modules/render-html/items/post.js",
    "content": "const path = require('path');\nconst ContentHelper = require('./../helpers/content');\n\n/**\n * Post item for the renderer\n */\nclass PostItem {\n    constructor(post, rendererInstance) {\n        this.post = post;\n        this.postID = parseInt(post.id, 10);\n        this.renderer = rendererInstance;\n        this.db = this.renderer.db;\n        this.themeConfig = this.renderer.themeConfig;\n        this.siteConfig = this.renderer.siteConfig;\n        this.postData = {};\n        this.metaData = {};\n        this.metaDescription = '';\n\n        this.getMetaData();\n        this.prepareData();\n        this.storeData();\n    }\n\n    getMetaData () {\n        let metaDataQuery = this.db.prepare(`SELECT value FROM posts_additional_data WHERE post_id = @postID AND key = '_core'`);\n        let metaData = metaDataQuery.get({ postID: this.post.id});\n\n        if (metaData && metaData.value) {\n            this.metaData = JSON.parse(metaData.value);\n        }\n\n        if (!this.metaData.editor) {\n            this.metaData.editor = 'tinymce';\n        }\n\n        if (this.metaData.metaDesc) {\n            this.metaDescription = this.metaData.metaDesc; \n        }\n\n        if (this.metaDescription === '') {\n            this.metaDescription = this.siteConfig.advanced.metaDescription;\n        }\n    }\n\n    prepareData() {\n        let postURL = this.siteConfig.domain + '/' + this.post.slug + '.html';\n\n        if (this.siteConfig.advanced.urls.postsPrefix) {\n            postURL = this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.post.slug + '.html';\n        }\n\n        let preparedText = ContentHelper.prepareContent(this.post.id, this.post.text, this.siteConfig.domain, this.themeConfig, this.renderer, this.metaData.editor);\n        let preparedExcerpt = ContentHelper.prepareExcerpt(this.themeConfig.config.excerptLength, preparedText);\n        let hasCustomExcerpt = false;\n        let readmoreMatches = preparedText.match(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n\n        if (readmoreMatches && readmoreMatches.length) {\n            hasCustomExcerpt = true;\n\n            // Detect if hide of the custom excerpt is enabled\n            if (this.renderer.siteConfig.advanced.postUseTextWithoutCustomExcerpt) {\n                preparedText = preparedText.split(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi);\n                preparedText = preparedText[1];\n            } else {\n                preparedText = preparedText.replace(/\\<hr\\s+id=[\"']{1}read-more[\"']{1}[\\s\\S]*?\\/?\\>/gmi, '');\n            }\n        }\n\n        if (this.siteConfig.advanced.urls.cleanUrls) {\n            postURL = this.siteConfig.domain + '/' + this.post.slug + '/';\n\n            if (this.siteConfig.advanced.urls.postsPrefix) {\n                postURL = this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.post.slug + '/';\n            }\n\n            if (this.renderer.previewMode || this.renderer.siteConfig.advanced.urls.addIndex) {\n                postURL += 'index.html';\n            }\n        }\n\n        this.postData = {\n            id: this.post.id,\n            title: this.post.title,\n            author: this.renderer.cachedItems.authors[this.post.authors],\n            slug: this.post.slug,\n            url: postURL,\n            text: preparedText,\n            excerpt: preparedExcerpt,\n            createdAt: this.post.created_at,\n            modifiedAt: this.post.modified_at,\n            status: this.post.status,\n            isFeatured: this.post.status.indexOf('featured') > -1,\n            isHidden: this.post.status.indexOf('hidden') > -1,\n            isExcludedOnHomepage: this.post.status.indexOf('excluded_homepage') > -1,\n            hasGallery: preparedText.indexOf('class=\"gallery') !== -1,\n            template: this.post.template,\n            hasCustomExcerpt: hasCustomExcerpt,\n            editor: this.metaData.editor || 'tinymce',\n            metaDescription: this.metaDescription\n        };\n\n        if (this.postData.template === '*') {\n            this.postData.template = this.themeConfig.defaultTemplates.post;\n        }\n\n        this.postData.featuredImage = {};\n\n        if (this.renderer.cachedItems.featuredImages.posts[this.postData.id]) {\n            this.postData.featuredImage = this.renderer.cachedItems.featuredImages.posts[this.postData.id];\n        }\n\n        if (this.renderer.cachedItems.postTags[this.postID]) {\n            this.postData.tags = this.renderer.cachedItems.postTags[this.postID].map(tagID => this.renderer.cachedItems.tags[tagID]);\n            this.postData.tags = this.postData.tags.filter(tag => !tag.additionalData || tag.additionalData.isHidden !== true);\n            this.postData.tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name));\n            this.postData.hiddenTags = this.renderer.cachedItems.postTags[this.postID].map(tagID => this.renderer.cachedItems.tags[tagID]);\n            this.postData.hiddenTags = this.postData.hiddenTags.filter(tag => tag.additionalData && tag.additionalData.isHidden === true);\n            this.postData.hiddenTags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name));\n\n            if (!this.postData.tags.length) {\n                this.postData.tags = [];\n                this.postData.mainTag = false;\n            } else {\n                if (this.metaData.mainTag && !isNaN(parseInt(this.metaData.mainTag, 10))) {\n                    let mainTagID = parseInt(this.metaData.mainTag, 10);\n\n                    if (this.renderer.cachedItems.tags[mainTagID]) {\n                        if (this.renderer.cachedItems.tags[mainTagID].additionalData.isHidden === true) {\n                            this.postData.mainTag = JSON.parse(JSON.stringify(this.postData.tags[0]));\n                        } else {\n                            this.postData.mainTag = JSON.parse(JSON.stringify(this.renderer.cachedItems.tags[mainTagID]));\n                        }\n                    } else {\n                        this.postData.mainTag = JSON.parse(JSON.stringify(this.postData.tags[0]));\n                    }\n                } else {\n                    this.postData.mainTag = JSON.parse(JSON.stringify(this.postData.tags[0]));\n                }\n            }\n        } else {\n            this.postData.tags = [];\n            this.postData.hiddenTags = [];\n            this.postData.mainTag = false;\n        }\n\n        if (this.renderer.plugins.hasModifiers('postTitle')) {\n            this.postData.title = this.renderer.plugins.runModifiers('postTitle', this.renderer, this.postData.title, { postData: this.postData }); \n        }\n\n        if (this.renderer.plugins.hasModifiers('postText')) {\n            this.postData.text = this.renderer.plugins.runModifiers('postText', this.renderer, this.postData.text, { postData: this.postData }); \n        }\n\n        if (this.renderer.plugins.hasModifiers('postExcerpt')) {\n            this.postData.excerpt = this.renderer.plugins.runModifiers('postExcerpt', this.renderer, this.postData.excerpt, { postData: this.postData }); \n        }\n    }\n\n    storeData() {\n        if (this.renderer.plugins.hasModifiers('postItemData')) {\n            this.postData = this.renderer.plugins.runModifiers('postItemData', this.renderer, this.postData); \n        }\n\n        this.renderer.cachedItems.posts[this.postID] = JSON.parse(JSON.stringify(this.postData));\n    }\n\n    setInternalLinks() {\n        let postText = this.renderer.cachedItems.posts[this.postID].text;\n        let postExcerpt = this.renderer.cachedItems.posts[this.postID].excerpt;\n        this.renderer.cachedItems.posts[this.postID].text = ContentHelper.setInternalLinks(postText, this.renderer);\n        this.renderer.cachedItems.posts[this.postID].excerpt = ContentHelper.setInternalLinks(postExcerpt, this.renderer);\n    }\n\n    setPostViewConfig(config) {\n        this.renderer.cachedItems.posts[this.postID].postViewConfig = config;\n    }\n}\n\nmodule.exports = PostItem;\n"
  },
  {
    "path": "app/back-end/modules/render-html/items/tag.js",
    "content": "const URLHelper = require('./../helpers/url');\n\n/**\n * Tag item for the renderer\n */\nclass TagItem {\n    /**\n     * Constructor\n     *\n     * @param tagData\n     * @param rendererInstance\n     */\n    constructor(tag, rendererInstance, mainTagIDs = []) {\n        this.tag = tag;\n        this.tagID = parseInt(tag.id, 10);\n        this.renderer = rendererInstance;\n        this.db = this.renderer.db;\n        this.themeConfig = this.renderer.themeConfig;\n        this.tagData = {};\n        this.mainTagIDs = mainTagIDs;\n\n        this.prepareData();\n        this.storeData();\n    }\n\n    /**\n     * Prepares final tag data\n     */\n    prepareData() {\n        let addIndexHtml = this.renderer.previewMode || this.renderer.siteConfig.advanced.urls.addIndex;\n        let tagAdditionalData = this.tag.additional_data ? JSON.parse(this.tag.additional_data) : {};\n        let tagURL = URLHelper.createTagPermalink(this.renderer.siteConfig.domain, this.renderer.siteConfig.advanced.urls, this.tag.slug, addIndexHtml);\n\n        if (tagAdditionalData.isHidden === true) {\n            tagURL = '';\n        }\n\n        this.tagData = {\n            id: this.tag.id,\n            name: this.tag.name,\n            slug: this.tag.slug,\n            description: this.tag.description,\n            additionalData: tagAdditionalData,\n            featuredImage: {},\n            postsNumber: this.getPostsNumber(),\n            url: tagURL,\n            template: tagAdditionalData.template ? tagAdditionalData.template : '' \n        };\n\n        if (this.renderer.cachedItems.featuredImages.tags[this.tagData.id]) {\n            this.tagData.featuredImage = this.renderer.cachedItems.featuredImages.tags[this.tagData.id];\n        }\n    }\n\n    /**\n     * Stores tag data in the cached items of renderer\n     */\n    storeData() {\n        if (this.renderer.plugins.hasModifiers('tagItemData')) {\n            this.tagData = this.renderer.plugins.runModifiers('tagItemData', this.renderer, this.tagData); \n        }\n\n        // Store tag data without references\n        this.renderer.cachedItems.tags[this.tagID] = JSON.parse(JSON.stringify(this.tagData));\n\n        if (this.mainTagIDs.indexOf(this.tagID) > -1) {\n            this.renderer.cachedItems.mainTags[this.tagID] = JSON.parse(JSON.stringify(this.tagData));\n        }\n    }\n\n    /**\n     * Returns number of posts connected with a given tag ID\n     *\n     * @returns {number}\n     */\n    getPostsNumber() {\n        let postsNumber = this.renderer.cachedItems.tagsPostCounts[this.tagID];\n\n        if(postsNumber) {\n            return postsNumber;\n        }\n\n        return 0;\n    }\n\n    /**\n     * Stores tag view configuration in cached items\n     * \n     * @param {*} config \n     */\n    setTagViewConfig(config) {\n        this.renderer.cachedItems.tags[this.tagID].tagViewConfig = config;\n    }\n}\n\nmodule.exports = TagItem;\n"
  },
  {
    "path": "app/back-end/modules/render-html/renderer-cache.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst Page = require('./items/page');\nconst Post = require('./items/post');\nconst Author = require('./items/author');\nconst Tag = require('./items/tag');\nconst ContentHelper = require('./helpers/content.js');\nconst FeaturedImage = require('./items/featured-image');\nconst ViewSettingsHelper = require('./helpers/view-settings.js');\nconst RendererHelpers = require('./helpers/helpers.js');\n\nclass RendererCache {\n    /**\n     * Constructor of the instance\n     *\n     * @param rendererInstance\n     */\n    constructor(rendererInstance, themeConfig) {\n        this.renderer = rendererInstance;\n        this.db = this.renderer.db;\n        this.themeConfig = themeConfig;\n    }\n\n    /**\n     * Generate cache for all necessary items\n     */\n    create() {\n        // Set tag-related items\n        this.getFeaturedTagImages();\n        // We need tag posts count before storing tags\n        this.getTagPostCounts();\n        this.getTags();\n        // Set author-related items\n        this.getFeaturedAuthorImages();\n        // We need author posts count before storing tags\n        this.getAuthorPostCounts();\n        this.getAuthors();\n        // Set post/page-related items\n        this.getFeaturedPostImages();\n        this.getFeaturedPageImages();\n        this.getPostTags();\n        // Get pages structure\n        this.getPagesStructure();\n        // At the end we get posts and pages as it uses other cached items\n        let posts = this.getPosts();\n        let pages = this.getPages();\n        // Now we can set internal links\n        this.setInternalLinks(posts, pages);\n    }\n\n    /**\n     * Retrieves tags data\n     */\n    getTags() {\n        console.time('MAINTAGS - QUERY');\n        let mainTagIDs = this.db.prepare(`\n            SELECT DISTINCT \n                json_extract(value, '$.mainTag') AS mainTagID\n            FROM \n                posts_additional_data\n            WHERE \n                key = '_core' AND \n                json_extract(value, '$.mainTag') IS NOT NULL AND \n                json_extract(value, '$.mainTag') != '';\n        `).all();\n\n        mainTagIDs = mainTagIDs.map(mainTag => mainTag.mainTagID);\n        console.timeEnd('MAINTAGS - QUERY');\n\n        console.time('TAGS - QUERY');\n        let tags = this.db.prepare(`\n            SELECT\n                t.id AS id,\n                t.name AS name,\n                t.slug AS slug,\n                t.description AS description,\n                t.additional_data AS additional_data\n            FROM\n                tags AS t\n            ;\n        `).all();\n        console.timeEnd('TAGS - QUERY');\n\n        console.time('TAGS - STORE');\n        // Data are automatically stored in the this.renderer.cachedItems\n        let tagViewConfigObject = JSON.parse(JSON.stringify(this.themeConfig.tagConfig));\n        \n        tags = tags.map(tag => {\n            let tagViewConfig = this.getViewSettings(tagViewConfigObject, tag, {\n                type: 'tag',\n                id: tag.id\n            });\n            let newTag = new Tag(tag, this.renderer, mainTagIDs);\n            newTag.setTagViewConfig(tagViewConfig);\n            return newTag;\n        });\n\n        console.timeEnd('TAGS - STORE');\n    }\n\n    /**\n     *  Retrieves post counts for all tags\n     */\n    getTagPostCounts() {\n        console.time('TAGS POST COUNT - QUERY');\n        let includeFeaturedPosts = '';\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('tagsIncludeFeaturedInPosts', this.themeConfig) === false;\n\n        if (shouldSkipFeaturedPosts) {\n            includeFeaturedPosts = 'AND p.status NOT LIKE \\'%featured%\\'';\n        }\n\n        let tagsPostCount = this.db.prepare(`\n            SELECT\n                pt.tag_id AS id,\n                COUNT(p.id) AS count\n            FROM\n                posts AS p\n            LEFT JOIN\n                posts_tags AS pt\n            ON\n                p.id = pt.post_id\n            WHERE\n                p.status LIKE '%published%' AND\n                p.status NOT LIKE '%hidden%' AND\n                p.status NOT LIKE '%trashed%' AND\n                p.status NOT LIKE '%is-page%' \n                ${includeFeaturedPosts}\n            GROUP BY\n                pt.tag_id;\n        `).all();\n        console.timeEnd('TAGS POST COUNT - QUERY');\n\n        console.time('TAGS POST COUNT - STORE');\n        for(let tagPostCount of tagsPostCount) {\n            this.renderer.cachedItems.tagsPostCounts[tagPostCount.id] = tagPostCount.count;\n        }\n        console.timeEnd('TAGS POST COUNT - STORE');\n    }\n\n    /**\n     * Retrieves authors featured images data\n     */\n    getFeaturedAuthorImages() {\n        console.time('FEATURED AUTHOR IMAGES - QUERY');\n        let authorsData = this.db.prepare(`\n            SELECT\n                a.id AS item_id,\n                a.additional_data AS additional_data\n            FROM\n                authors as a\n            ORDER BY\n                a.id DESC\n        `).all();\n        console.timeEnd('FEATURED AUTHOR IMAGES - QUERY');\n\n        console.time('FEATURED AUTHOR IMAGES - STORE');\n        for (let i = 0; i < authorsData.length; i++) {\n            let additionalData = false;\n            \n            if (authorsData[i].additional_data) {\n                additionalData = JSON.parse(authorsData[i].additional_data);\n            }\n\n            if (additionalData && additionalData.featuredImage) {\n                new FeaturedImage({\n                    id: 0,\n                    item_id: authorsData[i].item_id,\n                    url: additionalData.featuredImage,\n                    additional_data: JSON.stringify({\n                        alt: additionalData.featuredImageAlt,\n                        caption: additionalData.featuredImageCaption,\n                        credits: additionalData.featuredImageCredits\n                    })\n                }, this.renderer, 'authorImages', 'author');\n            }\n        }\n        console.timeEnd('FEATURED AUTHOR IMAGES - STORE');\n    }\n\n    /**\n     * Retrieves authors data\n     */\n    getAuthors() {\n        console.time('AUTHORS - QUERY');\n        let authors = this.db.prepare(`\n            SELECT\n                id,\n                name,\n                username,\n                config,\n                additional_data\n            FROM\n                authors;\n        `).all();\n        console.timeEnd('AUTHORS - QUERY');\n\n        console.time('AUTHORS - STORE');\n        // Data are automatically stored in the this.renderer.cachedItems\n        let authorViewConfigObject = JSON.parse(JSON.stringify(this.themeConfig.authorConfig));\n        \n        authors = authors.map(author => {\n            let authorViewConfig = this.getViewSettings(authorViewConfigObject, author, {\n                type: 'author',\n                id: author.id\n            });\n            let newAuthor = new Author(author, this.renderer);\n            newAuthor.setAuthorViewConfig(authorViewConfig);\n            return newAuthor;\n        });\n\n        console.timeEnd('AUTHORS - STORE');\n    }\n\n    /**\n     *  Retrieves post counts for all authors\n     */\n    getAuthorPostCounts() {\n        console.time('AUTHORS POST COUNT - QUERY');\n        let includeFeaturedPosts = '';\n        let shouldSkipFeaturedPosts = RendererHelpers.getRendererOptionValue('authorsIncludeFeaturedInPosts', this.themeConfig) === false;\n\n        if (shouldSkipFeaturedPosts) {\n            includeFeaturedPosts = 'AND p.status NOT LIKE \\'%featured%\\'';\n        }\n\n        let authorPostCounts = this.db.prepare(`\n            SELECT\n                a.id AS id,\n                COUNT(p.id) AS count\n            FROM\n                posts AS p\n            LEFT JOIN\n                authors AS a\n                ON\n                p.authors = a.id\n            WHERE\n                p.status LIKE '%published%' AND\n                p.status NOT LIKE '%hidden%' AND\n                p.status NOT LIKE '%trashed%' AND\n                p.status NOT LIKE '%is-page%' \n                ${includeFeaturedPosts}\n            GROUP BY\n                a.id\n        `).all();\n        console.timeEnd('AUTHORS POST COUNT - QUERY');\n\n        console.time('AUTHORS POST COUNT - STORE');\n        for(let countData of authorPostCounts) {\n            this.renderer.cachedItems.authorsPostCounts[countData.id] = countData.count;\n        }\n        console.timeEnd('AUTHORS POST COUNT - STORE');\n    }\n\n    /**\n     * Retrieves featured images data for posts\n     */\n    getFeaturedPostImages() {\n        console.time('FEATURED POST IMAGES - QUERY');\n        let featuredImages = this.db.prepare(`\n            SELECT\n                pi.id AS id,\n                pi.post_id AS item_id,\n                pi.url AS url,\n                pi.additional_data AS additional_data\n            FROM\n                posts as p\n            LEFT JOIN\n                posts_images as pi\n                ON\n                p.featured_image_id = pi.id\n            WHERE\n                p.status LIKE '%published%' AND\n                p.status NOT LIKE '%trashed%' AND\n                p.status NOT LIKE '%is-page%'\n            ORDER BY\n                pi.id DESC\n        `).all();\n        console.timeEnd('FEATURED POST IMAGES - QUERY');\n\n        console.time('FEATURED POST IMAGES - STORE');\n        featuredImages.map(image => new FeaturedImage(image, this.renderer, 'featuredImages', 'post'));\n        console.timeEnd('FEATURED POST IMAGES - STORE');\n    }\n\n    /**\n     * Retrieves featured images data for pages\n     */\n    getFeaturedPageImages() {\n        console.time('FEATURED PAGE IMAGES - QUERY');\n        let featuredImages = this.db.prepare(`\n            SELECT\n                pi.id AS id,\n                pi.post_id AS item_id,\n                pi.url AS url,\n                pi.additional_data AS additional_data\n            FROM\n                posts as p\n            LEFT JOIN\n                posts_images as pi\n                ON\n                p.featured_image_id = pi.id\n            WHERE\n                p.status LIKE '%published%' AND\n                p.status NOT LIKE '%trashed%' AND\n                p.status LIKE '%is-page%'\n            ORDER BY\n                pi.id DESC\n        `).all();\n        console.timeEnd('FEATURED PAGE IMAGES - QUERY');\n\n        console.time('FEATURED PAGE IMAGES - STORE');\n        featuredImages.map(image => new FeaturedImage(image, this.renderer, 'featuredImages', 'page'));\n        console.timeEnd('FEATURED PAGE IMAGES - STORE');\n    }\n\n    /**\n     * Retrieves tags featured images data\n     */\n    getFeaturedTagImages() {\n        console.time('FEATURED TAG IMAGES - QUERY');\n        let tagsData = this.db.prepare(`\n            SELECT\n                t.id AS item_id,\n                t.additional_data AS additional_data\n            FROM\n                tags as t\n            ORDER BY\n                t.id DESC\n        `).all();\n        console.timeEnd('FEATURED TAG IMAGES - QUERY');\n\n        console.time('FEATURED TAG IMAGES - STORE');\n        for (let i = 0; i < tagsData.length; i++) {\n            let additionalData = false;\n            \n            if (tagsData[i].additional_data) {\n                additionalData = JSON.parse(tagsData[i].additional_data);\n            }\n\n            if (additionalData && additionalData.featuredImage) {\n                new FeaturedImage({\n                    id: 0,\n                    item_id: tagsData[i].item_id,\n                    url: additionalData.featuredImage,\n                    additional_data: JSON.stringify({\n                        alt: additionalData.featuredImageAlt,\n                        caption: additionalData.featuredImageCaption,\n                        credits: additionalData.featuredImageCredits\n                    })\n                }, this.renderer, 'tagImages', 'tag');\n            }\n        }\n        console.timeEnd('FEATURED TAG IMAGES - STORE');\n    }\n\n    /**\n     * Retrieves post-tags relations\n     */\n    getPostTags() {\n        console.time('POST TAGS - QUERY');\n        // Retrieve post tags\n        let postTags = this.db.prepare(`\n            SELECT\n                pt.post_id AS postID,\n                t.id AS tagID\n            FROM\n                posts_tags AS pt\n            LEFT JOIN\n                tags AS t\n            ON\n                t.id = pt.tag_id\n            ORDER BY\n                t.id ASC\n        `).all();\n        console.timeEnd('POST TAGS - QUERY');\n\n        console.time('POST TAGS - STORE');\n        for(let postTag of postTags) {\n            if(this.renderer.cachedItems.postTags[postTag.postID]) {\n                this.renderer.cachedItems.postTags[postTag.postID].push(postTag.tagID);\n            } else {\n                this.renderer.cachedItems.postTags[postTag.postID] = [postTag.tagID];\n            }\n        }\n\n        console.timeEnd('POST TAGS - STORE');\n    }\n\n    /**\n     * Prepare two hierarchies\n     */\n    getPagesStructure () {\n        // Pages structure\n        let pagesConfigPath = path.join(this.renderer.inputDir, 'config', 'pages.config.json');\n\n        if (fs.existsSync(pagesConfigPath)) {\n            let pagesStructure = JSON.parse(FileHelper.readFileSync(pagesConfigPath));\n            let pagesStructureForHierarchy = JSON.parse(JSON.stringify(pagesStructure));\n            let flatPagesStructure = {};\n            let pagesStack = [...pagesStructure];\n            let hierarchyStructure = {};\n\n            while (pagesStack.length > 0) {\n                let page = pagesStack.pop();\n                \n                if (!flatPagesStructure[page.id]) {\n                    flatPagesStructure[page.id] = [];\n                }\n\n                page.subpages.filter(subpage => !!subpage).forEach(subpage => {\n                    flatPagesStructure[page.id].push(subpage.id);\n                    pagesStack.push(subpage);\n                });\n            }\n\n            let hierarchyTraverse = (node, path = []) => {\n                node.subpages.filter(subpage => !!subpage).forEach(subpage => hierarchyTraverse(subpage, [node.id, ...path]));\n                hierarchyStructure[node.id] = path.reverse();\n            };\n\n            pagesStructureForHierarchy.forEach(page => hierarchyTraverse(page));\n\n            // Flat hierarchy structure of parents when clean URLs are disabled\n            if (!this.renderer.siteConfig.advanced.urls.cleanUrls) {\n                let IDs = Object.keys(hierarchyStructure);\n\n                for (let i = 0; i < IDs.length; i++) {\n                    hierarchyStructure[IDs[i]] = [];\n                }\n            }\n\n            this.renderer.cachedItems.pagesStructure = flatPagesStructure;\n            this.renderer.cachedItems.pagesStructureHierarchy = hierarchyStructure;\n        }\n    }\n\n    /**\n     * Retrieves posts data\n     */\n    getPosts() {\n        console.time('POSTS - QUERY');\n        let posts = this.db.prepare(`\n            SELECT\n                *\n            FROM\n                posts\n            WHERE\n                status NOT LIKE '%trashed%' AND\n                status NOT LIKE '%draft%' AND\n                status NOT LIKE '%is-page%'\n            ORDER BY\n                id ASC;\n        `).all();\n        console.timeEnd('POSTS - QUERY');\n\n        console.time('POSTS - STORE');\n        let postViewConfigObject = JSON.parse(JSON.stringify(this.themeConfig.postConfig));\n        \n        posts = posts.map(post => {\n            let postViewConfig = this.getPostViewSettings(postViewConfigObject, post.id);\n            let newPost = new Post(post, this.renderer);\n            newPost.setPostViewConfig(postViewConfig);\n            return newPost;\n        });\n\n        console.timeEnd('POSTS - STORE');\n        return posts;\n    }\n\n    /**\n     * Retrieves pages data\n     */\n    getPages() {\n        console.time('PAGES - QUERY');\n        let pages = this.db.prepare(`\n            SELECT\n                *\n            FROM\n                posts\n            WHERE\n                status LIKE '%is-page%' AND\n                status NOT LIKE '%trashed%' AND\n                status NOT LIKE '%draft%'\n            ORDER BY\n                id ASC;\n        `).all();\n        console.timeEnd('PAGES - QUERY');\n\n        console.time('PAGES - STORE');\n        let pageViewConfigObject = JSON.parse(JSON.stringify(this.themeConfig.pageConfig));\n        \n        pages = pages.map(page => {\n            let pageViewConfig = this.getPageViewSettings(pageViewConfigObject, page.id);\n            let newPage = new Page(page, this.renderer);\n            newPage.setPageViewConfig(pageViewConfig);\n            return newPage;\n        });\n\n        console.timeEnd('PAGES - STORE');\n        return pages;\n    }\n\n    /**\n     * Retrieve post view settings\n     *\n     * @param defaultPostViewConfig\n     * @param postID\n     *\n     * @returns {object}\n     */\n    getPostViewSettings(defaultPostViewConfig, postID) {\n        let postViewData = false;\n        let postViewSettings = {};\n\n        postViewData = this.db.prepare(`\n            SELECT\n                value\n            FROM\n                posts_additional_data\n            WHERE\n                post_id = @id\n                AND\n                key = 'postViewSettings'\n        `).get({\n            id: postID\n        });\n\n        if (postViewData && postViewData.value) {\n            postViewSettings = JSON.parse(postViewData.value);\n        }\n\n        return ViewSettingsHelper.override(postViewSettings, defaultPostViewConfig, {\n            type: 'post',\n            id: postID\n        }, this.renderer);\n    }\n\n    /**\n     * Retrieve page view settings\n     *\n     * @param defaultPageViewConfig\n     * @param pageID\n     *\n     * @returns {object}\n     */\n    getPageViewSettings(defaultPageViewConfig, pageID) {\n        let pageViewData = false;\n        let pageViewSettings = {};\n\n        pageViewData = this.db.prepare(`\n            SELECT\n                value\n            FROM\n                posts_additional_data\n            WHERE\n                post_id = @id\n                AND\n                key = 'pageViewSettings'\n        `).get({\n            id: pageID\n        });\n\n        if (pageViewData && pageViewData.value) {\n            pageViewSettings = JSON.parse(pageViewData.value);\n        }\n\n        return ViewSettingsHelper.override(pageViewSettings, defaultPageViewConfig, {\n            type: 'page',\n            id: pageID\n        }, this.renderer);\n    }\n\n    /**\n     * Retrieve view settings\n     *\n     * @param defaultViewConfig\n     * @param itemData\n     *\n     * @returns {object}\n     */\n    getViewSettings(defaultViewConfig, itemData, itemConfig) {\n        let viewSettings = {};\n\n        if (itemData && itemData.additional_data) {\n            try {\n                let dataToUse = JSON.parse(itemData.additional_data); \n                \n                if (dataToUse.viewConfig) {\n                    viewSettings = dataToUse.viewConfig;\n                }\n            } catch (e) {\n                viewSettings = {};\n            }\n        }\n\n        return ViewSettingsHelper.override(viewSettings, defaultViewConfig, itemConfig, this.renderer);\n    }\n\n    /**\n     * Set internal links for tags and authors\n     */\n    setInternalLinks (posts, pages) {\n        pages.map(page => {\n            page.setHierarchyLinks();\n        });\n        \n        posts.map(post => {\n            post.setInternalLinks();\n        });\n\n        pages.map(page => {\n            page.setInternalLinks();\n        });\n\n        let authorIDs = Object.keys(this.renderer.cachedItems.authors);\n        let tagIDs = Object.keys(this.renderer.cachedItems.tags);\n\n        for (let i = 0; i < authorIDs.length; i++) {\n            let authorID = authorIDs[i];\n\n            if (typeof this.renderer.cachedItems.authors[authorID].description === 'string') {\n                let description =  this.renderer.cachedItems.authors[authorID].description;\n                this.renderer.cachedItems.authors[authorID].description = ContentHelper.setInternalLinks(description, this.renderer);\n            }\n\n            if (typeof this.renderer.cachedItems.authors[authorID].authorViewConfig === 'object') {\n                let viewConfig = this.renderer.cachedItems.authors[authorID].authorViewConfig;\n                this.renderer.cachedItems.authors[authorID].authorViewConfig = JSON.parse(ContentHelper.setInternalLinks(JSON.stringify(viewConfig), this.renderer));\n            }\n        }\n\n        for (let i = 0; i < tagIDs.length; i++) {\n            let tagID = tagIDs[i];\n\n            if (typeof this.renderer.cachedItems.tags[tagID].description === 'string') {\n                let description =  this.renderer.cachedItems.tags[tagID].description;\n                this.renderer.cachedItems.tags[tagID].description = ContentHelper.setInternalLinks(description, this.renderer);\n            }\n\n            if (typeof this.renderer.cachedItems.tags[tagID].tagViewConfig === 'object') {\n                let viewConfig = this.renderer.cachedItems.tags[tagID].tagViewConfig;\n                this.renderer.cachedItems.tags[tagID].tagViewConfig = JSON.parse(ContentHelper.setInternalLinks(JSON.stringify(viewConfig), this.renderer));\n            }\n        }\n    }\n}\n\nmodule.exports = RendererCache;\n"
  },
  {
    "path": "app/back-end/modules/render-html/renderer-context.js",
    "content": "// Necessary packages\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\nconst slug = require('./../../helpers/slug');\nconst ContentHelper = require('./helpers/content');\nconst URLHelper = require('./helpers/url');\nconst normalizePath = require('normalize-path');\nconst RendererCache = require('./renderer-cache');\nconst RendererHelpers = require('./helpers/helpers.js');\nconst sizeOf = require('image-size');\n\n/*\n * Class used create global context variables\n */\n\nclass RendererContext {\n    constructor(rendererInstance) {\n        this.db = rendererInstance.db;\n        this.siteConfig = rendererInstance.siteConfig;\n        this.themeConfig = rendererInstance.themeConfig;\n        this.inputDir = rendererInstance.inputDir;\n        this.renderer = rendererInstance;\n        this.postsOrdering = 'created_at DESC';\n        this.featuredPostsOrdering = 'created_at DESC';\n        this.hiddenPostsOrdering = 'created_at DESC';\n        this.pluginsConfig = rendererInstance.pluginsConfig;\n\n        if(\n            typeof this.siteConfig.advanced.postsListingOrder === 'string' &&\n            typeof this.siteConfig.advanced.postsListingOrderBy === 'string'\n        ) {\n            this.postsOrdering = this.siteConfig.advanced.postsListingOrderBy + ' ' +\n                                 this.siteConfig.advanced.postsListingOrder;\n        }\n\n        if(\n            typeof this.siteConfig.advanced.featuredPostsListingOrder === 'string' &&\n            typeof this.siteConfig.advanced.featuredPostsListingOrderBy === 'string'\n        ) {\n            this.featuredPostsOrdering = this.siteConfig.advanced.featuredPostsListingOrderBy + ' ' +\n                                         this.siteConfig.advanced.featuredPostsListingOrder;\n        }\n\n        if(\n            typeof this.siteConfig.advanced.hiddenPostsListingOrder === 'string' &&\n            typeof this.siteConfig.advanced.hiddenPostsListingOrderBy === 'string'\n        ) {\n            this.hiddenPostsOrdering = this.siteConfig.advanced.hiddenPostsListingOrderBy + ' ' +\n                                         this.siteConfig.advanced.hiddenPostsListingOrder;\n        }\n    }\n\n    getMenus() {\n        // Retrieve necessary data\n        let tagsData = this.getTagsMenuData();\n        let postsData = this.getPostsMenuData();\n        let pagesData = this.getPagesMenuData();\n\n        // Menu config\n        let menuConfigPath = path.join(this.inputDir, 'config', 'menu.config.json');\n        let menuConfigContent = FileHelper.readFileSync(menuConfigPath);\n        let menuData = JSON.parse(menuConfigContent);\n        let menus = {\n            assigned: {},\n            unassigned: {}\n        };\n\n        for (let i = 0; i < menuData.length; i++) {\n            let positions = menuData[i].position.split(';');\n            let maxLevels = menuData[i].maxLevels;\n\n            if (maxLevels) {\n                maxLevels = maxLevels.split(';').map(level => parseInt(level, 10));\n            } else {\n                maxLevels = Array.from({length: positions.length}, () => -1);\n            }\n\n            if (positions[0] === '') {\n                menuData[i].items = this.prepareMenuItems(menuData[i].items, tagsData, postsData, pagesData);\n                menus.unassigned[slug(menuData[i].name)] = menuData[i];\n            } else {\n                for (let j = 0; j < positions.length; j++) {\n                    let maxLevel = maxLevels[j];\n                    let position = positions[j];\n\n                    if (!maxLevel) {\n                        if (typeof this.themeConfig.menus[position] === 'object' && this.themeConfig.menus[position].maxLevels) {\n                            maxLevel = parseInt(this.themeConfig.menus[position].maxLevels, 10);\n                        } else {\n                            maxLevel = -1;\n                        }\n                    } else {\n                        let themeMaxLevel = -1;\n                        \n                        if (typeof this.themeConfig.menus[position] === 'object' && this.themeConfig.menus[position].maxLevels) {\n                            themeMaxLevel = parseInt(this.themeConfig.menus[position].maxLevels, 10);\n                        }\n\n                        if (themeMaxLevel > -1 && maxLevel > -1 && maxLevel > themeMaxLevel) {\n                            maxLevel = themeMaxLevel;\n                        }\n                    }\n\n                    let positionMenuData = JSON.parse(JSON.stringify(menuData[i]));\n                    positionMenuData.items = this.prepareMenuItems(positionMenuData.items, tagsData, postsData, pagesData, 2, maxLevel + 1);\n                    menus.assigned[position] = positionMenuData;\n                }\n            }\n        }\n\n        if (this.renderer.plugins.hasModifiers('menuStructure')) {\n            menus.assigned = this.renderer.plugins.runModifiers('menuStructure', this.renderer, menus.assigned); \n        }\n\n        if (this.renderer.plugins.hasModifiers('unassignedMenuStructure')) {\n            menus.unassigned = this.renderer.plugins.runModifiers('unassignedMenuStructure', this.renderer, menus.unassigned); \n        }\n\n        return menus;\n    }\n\n    prepareMenuItems(items, tagsData, postsData, pagesData, level = 2, maxLevel = false) {\n        // When max level is exceed - return items\n        if (maxLevel && maxLevel !== -1 && maxLevel < level) {\n            return [];\n        }\n\n        for (let i = 0; i < items.length; i++) {\n            items[i].level = level;\n            items[i].linkID = items[i].link;\n\n            if (items[i].type === 'post') {\n                let foundedPost = postsData.filter(post => post.id == items[i].link);\n\n                if(foundedPost.length && foundedPost[0].status.indexOf('trashed') === -1) {\n                    items[i].link = foundedPost[0].slug;\n                } else {\n                    items[i] = false;\n                }\n            }\n\n            if (items[i].type === 'page') {\n                let foundedPage = pagesData.filter(page => page.id == items[i].link);\n\n                if (foundedPage.length && foundedPage[0].status.indexOf('trashed') === -1) {\n                    items[i].link = foundedPage[0].slug;\n                } else {\n                    items[i] = false;\n                }\n            }\n\n            if (items[i].type === 'tag') {\n                let foundedTag = tagsData.filter(tag => tag.id == items[i].link);\n\n                if (foundedTag.length) {\n                    items[i].link = foundedTag[0].slug;\n                } else {\n                    items[i] = false;\n                }\n            }\n\n            if (items[i].isHidden) {\n                items[i] = false;\n            }\n\n            if (items[i] && !items[i].isHidden && items[i].items.length > 0) {\n                items[i].items = this.prepareMenuItems(items[i].items, tagsData, postsData, pagesData, level + 1, maxLevel);\n            }\n        }\n\n        items = items.filter(item => item !== false);\n\n        return items;\n    }\n\n    getTagsMenuData() {\n        // Retrieve all tags\n        let tags = this.db.prepare(`\n            SELECT\n                t.id AS id,\n                t.slug AS slug\n            FROM\n                tags AS t\n            ORDER BY\n                id ASC\n        `).all();\n\n        return tags;\n    }\n\n    getPostsMenuData () {\n        // Retrieve all tags\n        let posts = this.db.prepare(`\n            SELECT\n                p.id AS id,\n                p.slug AS slug,\n                p.status AS status\n            FROM\n                posts AS p\n            WHERE\n                p.status NOT LIKE '%is-page%'\n            ORDER BY\n                id ASC\n        `).all();\n\n        return posts;\n    }\n\n    getPagesMenuData () {\n        // Retrieve all tags\n        let pages = this.db.prepare(`\n            SELECT\n                p.id AS id,\n                p.slug AS slug,\n                p.status AS status\n            FROM\n                posts AS p\n            WHERE\n                p.status LIKE '%is-page%'\n            ORDER BY\n                id ASC\n        `).all();\n\n        return pages;\n    }\n\n    getAllTags() {\n        // Retrieve post tags\n        let tags = this.db.prepare(`\n            SELECT\n                id\n            FROM\n                tags\n            ORDER BY\n                name ASC\n        `).all();\n\n        tags = tags.map(tag => this.renderer.cachedItems.tags[tag.id]);\n\n        if(!this.siteConfig.advanced.displayEmptyTags) {\n            tags = tags.filter(tag => tag.postsNumber > 0);\n        }\n\n        tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name));\n\n        return tags;\n    }\n\n    getAllMainTags() {\n        // Retrieve post main tags\n        let mainTags = this.db.prepare(`\n            SELECT DISTINCT \n                json_extract(value, '$.mainTag') AS id\n            FROM \n                posts_additional_data\n            WHERE \n                key = '_core' AND \n                json_extract(value, '$.mainTag') IS NOT NULL AND \n                json_extract(value, '$.mainTag') != '';\n        `).all();\n\n        mainTags = mainTags.map(mainTag => {\n            if (this.renderer.cachedItems.tags[mainTag.id]) {\n                return JSON.parse(JSON.stringify(this.renderer.cachedItems.tags[mainTag.id]));\n            }\n\n            return false;\n        }).filter(mainTag => !!mainTag);\n\n        if(!this.siteConfig.advanced.displayEmptyTags) {\n            mainTags = mainTags.filter(mainTag => mainTag.postsNumber > 0);\n        }\n\n        mainTags.sort((mainTagA, mainTagB) => mainTagA.name.localeCompare(mainTagB.name));\n\n        return mainTags;\n    }\n\n    getAuthors() {\n        let authorData = this.db.prepare(`SELECT id FROM authors ORDER BY id ASC;`).all();\n        let authors = [];\n\n        for(let i = 0; i < authorData.length; i++) {\n            authors.push(this.renderer.cachedItems.authors[authorData[i].id]);\n        }\n\n        if(!this.siteConfig.advanced.displayEmptyAuthors) {\n            authors = authors.filter(author => author.postsNumber > 0);\n        }\n\n        authors.sort((authorA, authorB) => authorA.name.localeCompare(authorB.name));\n\n        return authors;\n    }\n\n    setGlobalContext(context, additionalContexts, paginationData, itemSlug, itemConfig, itemContext) {\n        let addIndexHtml = this.renderer.previewMode || this.siteConfig.advanced.urls.addIndex;\n        let fullURL = normalizePath(this.siteConfig.domain);\n        let searchUrl = fullURL + '/' + this.siteConfig.advanced.urls.searchPage;\n        let errorUrl = fullURL + '/' + this.siteConfig.advanced.urls.errorPage;\n        let logoUrl = normalizePath(this.themeConfig.config.logo);\n        let logoSize = false;\n        let assetsUrl = normalizePath(this.siteConfig.domain) + '/' +\n                        normalizePath(this.themeConfig.files.assetsPath);\n        let rootUrl = normalizePath(this.siteConfig.domain) + '/';\n        let mediaFilesUrl = normalizePath(this.siteConfig.domain) + '/media/files/';\n        let postsOrdering = 'desc';\n        \n        if (typeof this.siteConfig.advanced.postsListingOrder === 'string') {\n            postsOrdering = this.siteConfig.advanced.postsListingOrder.toLowerCase();\n        }\n\n        assetsUrl = URLHelper.fixProtocols(assetsUrl);\n        rootUrl = URLHelper.fixProtocols(rootUrl);\n        mediaFilesUrl = URLHelper.fixProtocols(mediaFilesUrl);\n\n        if(addIndexHtml) {\n            fullURL += '/index.html';\n        }\n\n        fullURL = URLHelper.fixProtocols(fullURL);\n        searchUrl = URLHelper.fixProtocols(searchUrl);\n        errorUrl = URLHelper.fixProtocols(errorUrl);\n\n        if (logoUrl !== '') {\n            try {\n                logoSize = sizeOf(path.join(this.inputDir, logoUrl));\n            } catch(e) {\n                logoSize = {\n                    width: '',\n                    height: ''\n                };\n            }\n\n            logoUrl = normalizePath(this.siteConfig.domain) + '/' +\n                      normalizePath(this.themeConfig.config.logo);\n            logoUrl = URLHelper.fixProtocols(logoUrl);\n        }\n\n        let siteNameValue = this.siteConfig.name;\n\n        if(this.siteConfig.displayName) {\n            siteNameValue = this.siteConfig.displayName;\n        }\n\n        if (fullURL.substr(-1) !== '/' && fullURL.substr(-5) !== '.html') {\n            fullURL = fullURL + '/';\n        }\n\n        let contextItems = [context].concat(additionalContexts);\n        contextItems = [...new Set(contextItems)];\n\n        this.context = {\n            context: contextItems,\n            config: URLHelper.prepareSettingsImages(this.siteConfig.domain, {\n                basic: JSON.parse(JSON.stringify(this.themeConfig.config)),\n                site: JSON.parse(JSON.stringify(this.siteConfig.advanced)),\n                custom: JSON.parse(ContentHelper.setInternalLinks(JSON.stringify(this.themeConfig.customConfig), this.renderer))\n            }),\n            website: {\n                url: fullURL,\n                baseUrl: fullURL.replace('/index.html', '/'),\n                searchUrl: searchUrl,\n                errorUrl: errorUrl,\n                tagsUrl: this.getTagsUrl(),\n                pageUrl: this.getPageUrl(context, paginationData, itemSlug, itemContext),\n                name: siteNameValue,\n                logo: logoUrl,\n                logoSize: logoSize,\n                assetsUrl: assetsUrl,\n                rootUrl: rootUrl,\n                mediaFilesUrl: mediaFilesUrl,\n                postsOrdering: postsOrdering,\n                lastUpdate: Date.now(),\n                contentStructure: this.renderer.contentStructure,\n                language: this.siteConfig.language,\n                description: this.siteConfig.description\n            },\n            renderer: {\n                previewMode: this.renderer.previewMode,\n                theme: {\n                    name: this.themeConfig.name,\n                    version: this.themeConfig.version,\n                    author: this.themeConfig.author\n                },\n                createAuthorPages: RendererHelpers.getRendererOptionValue('createAuthorPages', this.themeConfig),\n                createTagPages: RendererHelpers.getRendererOptionValue('createTagPages', this.themeConfig),\n                createSearchPage: RendererHelpers.getRendererOptionValue('createSearchPage', this.themeConfig),\n                create404page: RendererHelpers.getRendererOptionValue('create404page', this.themeConfig),\n                isFirstPage: this.getFirstPageContextData(paginationData),\n                isLastPage: this.getLastPageContextData(paginationData)\n            },\n            pagination: this.getPaginationContextData(paginationData),\n            plugins: this.pluginsConfig,\n            utils: {\n                currentYear: new Date().getFullYear(),\n                buildDate: +new Date()\n            }\n        };\n\n        if (context === 'post' && itemConfig) {\n            this.context.config.post = JSON.parse(ContentHelper.setInternalLinks(JSON.stringify(itemConfig), this.renderer));\n        }\n\n        if (context === 'page' && itemConfig) {\n            this.context.config.page = JSON.parse(ContentHelper.setInternalLinks(JSON.stringify(itemConfig), this.renderer));\n        }\n\n        this.renderer.globalContext = this.context;\n\n        if (itemContext) {\n            if (itemContext.post && itemContext.post.id) {\n                this.renderer.globalContext.itemID = itemContext.post.id;\n            } else if (itemContext.tag && itemContext.tag.id) {\n                this.renderer.globalContext.itemID = itemContext.tag.id;\n            } else if (itemContext.author && itemContext.author.id) {\n                this.renderer.globalContext.itemID = itemContext.author.id;\n            }\n        }\n\n        this.context.headCustomCode = this.getCustomHTMLCode('customHeadCode', itemContext);\n        this.context.bodyCustomCode = this.getCustomHTMLCode('customBodyCode', itemContext);\n        this.context.commentsCustomCode = this.getCustomHTMLCode('customCommentsCode', itemContext);\n        this.context.customSearchInput = this.getCustomHTMLCode('customSearchInput', itemContext);\n        this.context.customSearchContent = this.getCustomHTMLCode('customSearchContent', itemContext);\n        this.context.customSocialSharing = this.getCustomHTMLCode('customSocialSharing', itemContext);\n        this.context.footerCustomCode = this.getCustomHTMLCode('customFooterCode', itemContext);\n        this.context.customHTML = this.getCustomHTMLCodeObject(this.siteConfig.advanced.customHTML, itemContext);\n\n        if (this.renderer.plugins.hasModifiers('globalContext')) {\n            this.context = this.renderer.plugins.runModifiers('globalContext', this.renderer, this.context); \n        }\n    }\n\n    getCustomHTMLCode (optionName, context) {\n        let baseCode = this.siteConfig.advanced[optionName] || '';\n\n        if (this.renderer.plugins.hasInsertions(optionName)) {\n            baseCode += \"\\n\";\n            baseCode += this.renderer.plugins.runInsertions(optionName, this.renderer, context);\n        }\n\n        return baseCode.trim();\n    }\n\n    getCustomHTMLCodeObject (object, context) {\n        if (!object) {\n            return false;\n        }\n\n        object = JSON.parse(JSON.stringify(object));\n\n        let keys = Object.keys(object);\n\n        for (let i = 0; i < keys.length; i++) {\n            let key = keys[i];\n            let code = object[key];\n\n            if (this.renderer.plugins.hasInsertions('customHTML.' + key)) {\n                code += \"\\n\";\n                code += this.renderer.plugins.runInsertions('customHTML.' + key, this.renderer, context);\n            }\n\n            if (this.renderer.plugins.hasModifiers('customHTML.' + key)) {\n                code = this.renderer.plugins.runModifiers('customHTML.' + key, this.renderer, code, context);\n            }\n\n            object[key] = code.trim();\n        }\n\n        return object;\n    }\n\n    getGlobalContext(context, additionalContexts, paginationData, itemSlug, itemConfig, itemContext) {\n        this.setGlobalContext(context, additionalContexts, paginationData, itemSlug, itemConfig, itemContext);\n        return this.context;\n    }\n\n    getContentStructure() {\n        if (!RendererHelpers.getRendererOptionValue('createContentStructure', this.themeConfig)) {\n            return false;\n        }\n\n        let postsOrdering = 'created_at DESC';\n\n        if(\n            typeof this.siteConfig.advanced.postsListingOrder === 'string' &&\n            typeof this.siteConfig.advanced.postsListingOrderBy === 'string'\n        ) {\n            postsOrdering = this.siteConfig.advanced.postsListingOrderBy + ' ' +\n                            this.siteConfig.advanced.postsListingOrder;\n        }\n\n        let posts = this.db.prepare(`\n                SELECT\n                    id\n                FROM\n                    posts\n                WHERE\n                    status LIKE '%published%' AND\n                    status NOT LIKE '%trashed%' AND\n                    status NOT LIKE '%is-page%'\n                ORDER BY\n                    ${postsOrdering}\n        `).all();\n        let pages = this.db.prepare(`\n                SELECT\n                    id\n                FROM\n                    posts\n                WHERE\n                    status LIKE '%published%' AND\n                    status NOT LIKE '%trashed%' AND\n                    status LIKE '%is-page%'\n        `).all();\n        let tags = this.db.prepare(`SELECT id FROM tags`).all();\n        let authors = this.db.prepare(`SELECT id FROM authors`).all();\n\n        posts = posts.map(post => JSON.parse(JSON.stringify(this.renderer.cachedItems.posts[post.id])));\n        pages = pages.map(page => JSON.parse(JSON.stringify(this.renderer.cachedItems.pages[page.id])));\n        tags = tags.map(tag => JSON.parse(JSON.stringify(this.renderer.cachedItems.tags[tag.id])));\n        authors = authors.map(author => JSON.parse(JSON.stringify(this.renderer.cachedItems.authors[author.id])));\n\n        for(let i = 0; i < tags.length; i++) {\n            tags[i].posts = this.getContentStructureTagPosts(tags[i].id);\n        }\n\n        for(let i = 0; i < authors.length; i++) {\n            authors[i].posts = this.getContentStructureAuthorPosts(authors[i].id);\n        }\n\n        let pagesStructure = this.renderer.cachedItems.pagesStructure;\n        let finalContentStructure = {\n            pages,\n            pagesStructure,\n            posts,\n            tags,\n            authors\n        };\n\n        if (this.renderer.plugins.hasModifiers('contentStructure')) {\n            finalContentStructure = this.renderer.plugins.runModifiers('contentStructure', this.renderer, finalContentStructure); \n        }\n\n        return finalContentStructure;\n    }\n\n    getContentStructureTagPosts(tagID) {\n        let postsOrdering = 'created_at DESC';\n\n        if(\n            typeof this.siteConfig.advanced.postsListingOrder === 'string' &&\n            typeof this.siteConfig.advanced.postsListingOrderBy === 'string'\n        ) {\n            postsOrdering = this.siteConfig.advanced.postsListingOrderBy + ' ' +\n                this.siteConfig.advanced.postsListingOrder;\n        }\n\n        let posts = this.db.prepare(`\n                SELECT\n                    posts_tags.post_id AS id\n                FROM\n                    posts_tags\n                LEFT JOIN\n                    posts\n                ON\n                    posts_tags.post_id = posts.id\n                WHERE\n                    posts_tags.tag_id = @tagID AND\n                    posts.status LIKE '%published%' AND\n                    posts.status NOT LIKE '%hidden%' AND\n                    posts.status NOT LIKE '%trashed%'\n                ORDER BY\n                    ${postsOrdering}\n        `).all({\n            tagID: tagID\n        });\n\n        posts = posts.map(post => this.renderer.cachedItems.posts[post.id]);\n        return posts;\n    }\n\n    getContentStructureAuthorPosts(authorID) {\n        let postsOrdering = 'created_at DESC';\n\n        if(\n            typeof this.siteConfig.advanced.postsListingOrder === 'string' &&\n            typeof this.siteConfig.advanced.postsListingOrderBy === 'string'\n        ) {\n            postsOrdering = this.siteConfig.advanced.postsListingOrderBy + ' ' +\n                this.siteConfig.advanced.postsListingOrder;\n        }\n\n        let posts = this.db.prepare(`\n                SELECT\n                    id\n                FROM\n                    posts\n                WHERE\n                    status LIKE '%published%' AND\n                    status NOT LIKE '%hidden%' AND\n                    status NOT LIKE '%trashed%' AND\n                    status NOT LIKE '%is-page%' AND\n                    authors LIKE @authorID\n                ORDER BY\n                    ${postsOrdering}\n        `).all({\n            authorID: authorID.toString()\n        });\n\n        posts = posts.map(post => this.renderer.cachedItems.posts[post.id]);\n        return posts;\n    }\n\n    getCachedItems() {\n        let cache = new RendererCache(this.renderer, this.themeConfig);\n        cache.create();\n    }\n\n    getFeaturedPosts(type) {\n        let postsLimit = 'LIMIT 5';\n\n        if(this.themeConfig.renderer) {\n            if (type === 'homepage' && RendererHelpers.getRendererOptionValue('featuredPostsNumber', this.themeConfig)) {\n                postsLimit = 'LIMIT ' + RendererHelpers.getRendererOptionValue('featuredPostsNumber', this.themeConfig);\n            } else if (type === 'author' && RendererHelpers.getRendererOptionValue('authorsFeaturedPostsNumber', this.themeConfig)) {\n                postsLimit = 'LIMIT ' + RendererHelpers.getRendererOptionValue('authorsFeaturedPostsNumber', this.themeConfig);\n            } else if (type === 'tag' && RendererHelpers.getRendererOptionValue('tagsFeaturedPostsNumber', this.themeConfig)) {\n                postsLimit = 'LIMIT ' + RendererHelpers.getRendererOptionValue('tagsFeaturedPostsNumber', this.themeConfig);\n            }\n\n            if (postsLimit === 'LIMIT -1') {\n                postsLimit = '';\n            }\n        }\n\n        let results = this.db.prepare(`\n            SELECT\n                id\n            FROM\n                posts\n            WHERE\n                status LIKE '%published%' AND\n                status LIKE '%featured%' AND\n                status NOT LIKE '%trashed%' AND\n                status NOT LIKE '%is-page%' AND\n                status NOT LIKE '%hidden%'\n            ORDER BY\n                ${this.featuredPostsOrdering}\n            ${postsLimit}\n        `).all();\n\n        return results;\n    }\n\n    getHiddenPosts() {\n        let results = this.db.prepare(`\n            SELECT\n                id\n            FROM\n                posts\n            WHERE\n                status LIKE '%published%' AND\n                status LIKE '%hidden%' AND\n                status NOT LIKE '%is-page%' AND\n                status NOT LIKE '%trashed%'\n            ORDER BY\n                ${this.hiddenPostsOrdering}\n        `).all();\n\n        return results;\n    }\n\n    getPages() {\n        let results = this.db.prepare(`\n            SELECT\n                id\n            FROM\n                posts\n            WHERE\n                status LIKE '%is-page%' AND\n                status NOT LIKE '%trashed%'\n        `).all();\n\n        return results;\n    }\n\n    getPageUrl (context, paginationData, itemSlug, itemContext) {\n        let pagePart = this.siteConfig.advanced.urls.pageName;\n        let blogBaseUrl = this.siteConfig.domain;\n\n        if (context === 'index' || context === 'blogindex' || context === '404' || context === 'search') {\n            if (!paginationData || paginationData.currentPage === 1) {\n                if (this.siteConfig.advanced.usePageAsFrontpage || context === 'blogindex') {\n                    if (this.siteConfig.advanced.urls.postsPrefix) {\n                        return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/';\n                    }\n\n                    return blogBaseUrl + '/';\n                }\n\n                return this.siteConfig.domain + '/';\n            } else {\n                if (this.siteConfig.advanced.urls.postsPrefix) {\n                    return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + pagePart +  '/' + paginationData.currentPage + '/';\n                }\n\n                return blogBaseUrl + '/' + pagePart +  '/' + paginationData.currentPage + '/';\n            }\n        } else if (context === 'tags') {\n            if (this.siteConfig.advanced.urls.tagsPrefix !== '') {    \n                if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                    return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/'; \n                }\n\n                return blogBaseUrl + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/';\n            } else {\n                return blogBaseUrl + '/';\n            }\n        } else if (context === 'author') {\n            if (!paginationData || paginationData.currentPage === 1) {\n                if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) {\n                    return this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.authorsPrefix + '/' + itemSlug + '/';\n                }\n                \n                return this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.authorsPrefix + '/' + itemSlug + '/';\n            } else {\n                if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) {\n                    return this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.authorsPrefix + '/' + itemSlug + '/' + pagePart + '/' + paginationData.currentPage + '/';\n                }\n\n                return this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.authorsPrefix + '/' + itemSlug + '/' + pagePart + '/' + paginationData.currentPage + '/';\n            }\n        } else if (context === 'tag') {\n            if (!paginationData || paginationData.currentPage === 1) {\n                if (this.siteConfig.advanced.urls.tagsPrefix !== '') {  \n                    if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                        return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/' + itemSlug + '/';\n                    }\n\n                    return blogBaseUrl + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/' + itemSlug + '/';\n                } else {\n                    if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                        return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + itemSlug + '/';    \n                    }\n\n                    return blogBaseUrl + '/' + itemSlug + '/';\n                }\n            } else {\n                if (this.siteConfig.advanced.urls.tagsPrefix !== '') {\n                    if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                        return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/' + itemSlug + '/' + pagePart + '/' + paginationData.currentPage + '/';    \n                    }\n\n                    return blogBaseUrl + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/' + itemSlug + '/' + pagePart + '/' + paginationData.currentPage + '/';\n                } else {\n                    if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                        return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + itemSlug + '/' + pagePart + '/' + paginationData.currentPage + '/';    \n                    }\n\n                    return blogBaseUrl + '/' + itemSlug + '/' + pagePart + '/' + paginationData.currentPage + '/';\n                }\n            }\n        } else if (context === 'post') {\n            if (!this.siteConfig.advanced.urls.cleanUrls) { \n                if (this.siteConfig.advanced.urls.postsPrefix) {\n                    return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + itemSlug + '.html';\n                }\n\n                return blogBaseUrl + '/' + itemSlug + '.html';\n            } else {\n                if (this.siteConfig.advanced.urls.postsPrefix) {\n                    return blogBaseUrl + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + itemSlug + '/';\n                }\n\n                return blogBaseUrl + '/' + itemSlug + '/';\n            }\n        } else if (context === 'page') {\n            if (this.siteConfig.advanced.usePageAsFrontpage && itemContext.page && this.siteConfig.advanced.pageAsFrontpage === itemContext.page.id) {\n                return this.siteConfig.domain + '/';    \n            }\n         \n            if (\n                !this.renderer.cachedItems || \n                !this.renderer.cachedItems.pages || \n                !this.renderer.cachedItems.pages[itemContext.page.id] ||\n                !this.renderer.cachedItems.pages[itemContext.page.id].url\n            ) {\n                return this.siteConfig.domain + '/' + itemSlug + '/';\n            }\n\n            return this.renderer.cachedItems.pages[itemContext.page.id].url;\n        }\n    }\n\n    getPaginationContextData (paginationData) {\n        if (!paginationData) {\n            return false;\n        }\n       \n        return paginationData.pagination;\n    }\n\n    getFirstPageContextData (paginationData) {\n        if (!paginationData) {\n            return true;\n        }\n        \n        return paginationData.isFirstPage;\n    }\n\n    getLastPageContextData (paginationData) {\n        if (!paginationData) {\n            return true;\n        }\n        \n        return paginationData.isLastPage;\n    }\n\n    getTagsUrl () {\n        let tagsUrl = this.siteConfig.domain + '/';\n\n        if (this.siteConfig.advanced.urls.tagsPrefix !== '') {       \n            if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                tagsUrl = this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.postsPrefix + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/';\n            } else {\n                tagsUrl = this.siteConfig.domain + '/' + this.siteConfig.advanced.urls.tagsPrefix + '/';\n            }\n        }\n\n        if (this.renderer.previewMode) {\n            tagsUrl += 'index.html';\n        }\n\n        return tagsUrl;\n    }\n}\n\nmodule.exports = RendererContext;\n"
  },
  {
    "path": "app/back-end/modules/render-html/renderer-plugins.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst FileHelper = require('./../../helpers/file.js');\n\nclass RendererPlugins {\n    constructor (sitePath) {\n        this.sitePath = sitePath;\n        this.insertions = {};\n        this.modifiers = {};\n        this.events = {};\n    }\n\n    /**\n     * Add \n     */\n     _add (type, key, callback, priority, pluginInstance) {\n        if (!this[type][key]) {\n            this[type][key] = [{ priority, callback, pluginInstance }];\n        } else {\n            this[type][key].push({ priority, callback, pluginInstance });\n        }\n\n        this[type][key].sort(this.sortByPriority);\n    }\n\n    addInsertion (place, callback, priority, pluginInstance) {\n        this._add('insertions', place, callback, priority, pluginInstance);\n    }\n\n    addModifier (value, callback, priority, pluginInstance) {\n        this._add('modifiers', value, callback, priority, pluginInstance);\n    }\n\n    addEvent (value, callback, priority, pluginInstance) {\n        this._add('events', value, callback, priority, pluginInstance);\n    }\n\n    /**\n     * Get\n     */\n     _get (type, key) {\n        if (!this[type][key]) {\n            return [];\n        }\n\n        return this[type][key];\n    }\n\n    getInsertions (place) {\n        return this._get('insertions', place);\n    }\n\n    getModifiers (value) {\n        return this._get('modifiers', value);\n    }\n\n    getEvents (value) {\n        return this._get('events', value);\n    }\n\n    /**\n     * Has\n     */\n    _has (type, key) {\n        if (!this[type][key]) {\n            return [];\n        }\n\n        return this[type][key].length > 0;\n    }\n\n    hasInsertions (place) {\n        return this._has('insertions', place);\n    }\n\n    hasModifiers (value) {\n        return this._has('modifiers', value);\n    }\n\n    hasEvents (value) {\n        return this._has('events', value);\n    }\n\n    /**\n     * Remove \n     */\n    _remove (type, key, callback, priority) {\n        if (!this[type][key]) {\n            return;\n        }\n\n        this[type][key] = this[type][key].filter(insertion => insertion.callback !== callback && insertion.priority !== priority);\n    }\n\n    removeInsertion (place, callback, priority) {\n        this._remove('insertions', place, callback, priority);\n    }\n\n    removeModifier (value, callback, priority) {\n        this._remove('modifiers', value, callback, priority);\n    }\n\n    removeEvent (value, callback, priority) {\n        this._remove('events', value, callback, priority);\n    }\n\n    /**\n     * Reset\n     */\n    _reset (type) {\n        this[type] = {}\n    }\n\n    resetInsertions () {\n        this._reset('insertions');\n    }\n\n    resetModifiers () {\n        this._reset('modifiers');\n    }\n\n    resetEvents () {\n        this._reset('events');\n    }\n\n    /**\n     * Run\n     */\n    runInsertions (place, rendererInstance, params = false) {\n        let insertions = this.getInsertions(place);\n        let output = [];\n\n        for (let i = 0; i < insertions.length; i++) {\n            let insertionOutput = insertions[i].callback.bind(insertions[i].pluginInstance, rendererInstance, params)();\n\n            if (insertionOutput) {\n                output.push(insertionOutput);\n            }\n        }\n\n        return output.join(\"\\n\");\n    }\n\n    runModifiers (value, rendererInstance, originalValue, params = false) {\n        let modifiers = this.getModifiers(value);\n        let output = originalValue;\n\n        for (let i = 0; i < modifiers.length; i++) {\n            if (Array.isArray(params)) {\n                output = modifiers[i].callback.bind(modifiers[i].pluginInstance, rendererInstance, output, ...params)();\n            } else {\n                output = modifiers[i].callback.bind(modifiers[i].pluginInstance, rendererInstance, output, params)();\n            }\n        }\n\n        return output;\n    }\n\n    runEvents (value, rendererInstance, params = false) {\n        let events = this.getEvents(value);\n\n        for (let i = 0; i < events.length; i++) {\n            events[i].callback.bind(events[i].pluginInstance, rendererInstance, params)();\n        }\n\n        return true;\n    }\n\n    /**\n     * Helpers \n     */\n    sortByPriority (itemA, itemB) {\n        return itemA.priority - itemB.priority;\n    }\n\n    /**\n     * Read file from input/config/plugins/PLUGIN_NAME/\n     */\n    readFile (fileName, pluginInstance) {\n        let useRootFiles = false;\n        let fileContent;\n        let filePath = false;\n\n        if (fileName.indexOf('[ROOT-FILES]/') === 0) {\n            useRootFiles = true;\n            fileName = fileName.replace('[ROOT-FILES]/', '');\n        }\n\n        fileName = fileName.replace(/[^a-zA-Z0-9\\-\\_\\.\\*\\@\\+]/gmi, '');\n\n        if (useRootFiles) {\n            let rootFilesDir = path.join(this.sitePath, 'input', 'root-files');\n            filePath = path.join(rootFilesDir, fileName);\n        } else {\n            let pluginName = pluginInstance.name.replace(/[^a-zA-Z0-9\\-\\_\\.\\*\\@\\+]/gmi, '');\n            filePath = path.join(this.sitePath, 'input', 'config', 'plugins', pluginName, fileName);\n        }\n        \n        if (filePath && !fs.existsSync(filePath)) {\n            return fileContent;\n        }\n\n        try {\n            if (filePath) {\n                fileContent = FileHelper.readFileSync(filePath).toString();\n            } else {\n                return;\n            }\n        } catch (e) {\n            return;\n        }\n\n        return fileContent;\n    }\n\n    /**\n     * Create file in input/media/PLUGIN_NAME/\n     */\n    createFile (fileName, fileContent, pluginInstance) {\n        let useRootFiles = false;\n        let filePath = false;\n\n        if (fileName.indexOf('[ROOT-FILES]/') === 0) {\n            useRootFiles = true;\n            fileName = fileName.replace('[ROOT-FILES]/', '');\n        }\n\n        fileName = fileName.replace(/[^a-zA-Z0-9\\-\\_\\.\\*\\@\\+]/gmi, '');\n\n        if (useRootFiles) {\n            let rootFilesDir = path.join(this.sitePath, 'input', 'root-files');\n            filePath = path.join(rootFilesDir, fileName);\n        } else {\n            let pluginName = pluginInstance.name.replace(/[^a-zA-Z0-9\\-\\_\\.\\*\\@\\+]/gmi, '');\n            let pluginsDir = path.join(this.sitePath, 'input', 'media', 'plugins');\n            let pluginDir = path.join(pluginsDir, pluginName);\n            filePath = path.join(pluginDir, fileName);\n            \n            if (!fs.existsSync(pluginsDir)) {\n                fs.mkdirSync(pluginsDir, { recursive: true });\n            }\n\n            if (!fs.existsSync(pluginDir)) {\n                fs.mkdirSync(pluginDir, { recursive: true });\n            }\n        }\n\n        try {\n            if (filePath) {\n                fs.writeFileSync(filePath, fileContent);\n            } else {\n                return {\n                    status: 'FILE_NOT_SAVED'\n                };    \n            }\n        } catch (e) {\n            return {\n                status: 'FILE_NOT_SAVED'\n            };\n        }\n\n        return {\n            status: 'FILE_SAVED'\n        };\n    }\n}\n\nmodule.exports = RendererPlugins;\n"
  },
  {
    "path": "app/back-end/modules/render-html/renderer.js",
    "content": "// Necessary packages\nconst fs = require('fs-extra');\nconst FileHelper = require('./../../helpers/file.js');\nconst listAll = require('ls-all');\nconst path = require('path');\nconst Handlebars = require('handlebars');\nconst CleanCSS = require('clean-css');\nconst normalizePath = require('normalize-path');\nconst os = require('os');\n\n// Internal packages\nconst DBUtils = require('./../../helpers/db.utils.js');\nconst Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3');\nconst URLHelper = require('./helpers/url.js');\nconst FilesHelper = require('./helpers/files.js');\nconst ViewSettingsHelper = require('./helpers/view-settings.js');\nconst Themes = require('../../themes.js');\nconst TemplateHelper = require('./helpers/template.js');\nconst Plugins = require('./../../plugins.js');\nconst RendererContext = require('./renderer-context.js');\nconst RendererContextPage = require('./contexts/page.js');\nconst RendererContextPost = require('./contexts/post.js');\nconst RendererContextPagePreview = require('./contexts/page-preview.js');\nconst RendererContextPostPreview = require('./contexts/post-preview.js');\nconst RendererContextTag = require('./contexts/tag.js');\nconst RendererContextTags = require('./contexts/tags.js');\nconst RendererContextAuthor = require('./contexts/author.js');\nconst RendererContextHome = require('./contexts/home.js');\nconst RendererContextFeed = require('./contexts/feed.js');\nconst RendererContext404 = require('./contexts/404.js');\nconst RendererContextSearch = require('./contexts/search.js');\nconst RendererHelpers = require('./helpers/helpers.js');\nconst RendererPlugins = require('./renderer-plugins.js');\nconst themeConfigValidator = require('./validators/theme-config.js');\nconst UtilsHelper = require('./../../helpers/utils');\nconst Sitemap = require('./helpers/sitemap.js');\nconst Gdpr = require('./helpers/gdpr.js');\nconst Git = require('./../deploy/git.js');\n\n// Default config\nconst defaultAstCurrentSiteConfig = require('./../../../config/AST.currentSite.config');\n\n/*\n * Class used to generate HTML output\n * from the site data\n */\n\nclass Renderer {\n    constructor(appDir, sitesDir, siteConfig, itemID = false, postData = false) {\n        this.appDir = appDir;\n        this.sitesDir = sitesDir;\n        this.siteConfig = siteConfig;\n        this.siteName = this.siteConfig.name;\n        this.themeName = this.siteConfig.theme;\n        this.menuContext = '';\n        this.errorLog = [];\n        this.previewMode = false;\n        this.useRelativeUrls = siteConfig.deployment.relativeUrls;\n        let sitePath = path.join(this.sitesDir, this.siteName);\n        this.plugins = new RendererPlugins(sitePath);\n        this.translations = {\n            user: false,\n            theme: false\n        };\n        this.viewConfigStructure = {};\n        this.contentStructure = {};\n        this.commonData = {\n            tags: [],\n            mainTags: [],\n            authors: [],\n            menus: [],\n            pages: [],\n            featuredPosts: {\n                homepage: [],\n                tag: [],\n                author: []\n            },\n            hiddenPosts: []\n        };\n        this.cachedItems = {\n            pages: {},\n            pagesStructure: {},\n            postTags: {},\n            posts: {},\n            tags: {},\n            mainTags: {},\n            tagsPostCounts: {},\n            authors: {},\n            authorsPostCounts: {},\n            featuredImages: {\n                authors: {},\n                pages: {},\n                posts: {},\n                tags: {}\n            }\n        };\n        this.globalContext = null;\n        this.itemID = itemID;\n        this.postData = postData;\n        this.pluginsDir = path.join(this.appDir, 'plugins');\n        this.loadPlugins();\n        this.pluginsConfig = this.getPluginsConfig();\n    }\n\n    /*\n     * Renders the pages\n     */\n    async render(previewMode = false, previewLocation = '', mode = 'full') {\n        this.previewMode = previewMode;\n        this.previewLocation = previewLocation;\n        this.singlePageMode = mode === 'post' || mode === 'page';\n        this.itemType = mode;\n        this.homepageOnlyMode = mode === 'home';\n        this.tagOnlyMode = mode === 'tag';\n        this.authorOnlyMode = mode === 'author';\n        this.setIO();\n        await this.emptyOutputDir();\n        let themeValidationResults = this.themeIsValid();\n\n        if (themeValidationResults === true) {\n            await this.renderSite();\n\n            if (this.errorLog.length === 0) {\n                return true;\n            }\n\n            return this.errorLog;\n        }\n        \n        this.errorLog.push({\n            message: 'An error (1010) occurred during parsing config.json file of the theme.',\n            desc: 'Please check your theme config.json file as it seems to be corrupted.'\n        });\n\n        return this.errorLog;\n    }\n\n    /*\n     * Check if the theme is valid\n     */\n    themeIsValid() {\n        let configFilePath = path.join(this.inputDir, 'themes', this.themeName, 'config.json');\n        let overridedConfigFilePath = UtilsHelper.fileIsOverrided(this.inputDir, this.themeName, configFilePath);\n\n        if(overridedConfigFilePath) {\n            configFilePath = overridedConfigFilePath;\n        }\n\n        let configValidationResult = themeConfigValidator(configFilePath);\n\n        if(configValidationResult !== true) {\n            return 'Theme config.json file is invalid: ' + configValidationResult;\n        }\n\n        return true;\n    }\n\n    /*\n     * Render the page content after removing the old output dir\n     */\n    async renderSite() {\n        try {\n            if (this.singlePageMode) {\n                if (this.itemType === 'post') {\n                    await this.renderPostPreview();\n                } else if (this.itemType === 'page') {\n                    await this.renderPagePreview();\n                }\n            } else if (this.homepageOnlyMode && !this.siteConfig.advanced.usePageAsFrontpage) {\n                await this.renderHomepagePreview();\n            } else if (this.tagOnlyMode) {\n                await this.renderTagPreview();\n            } else if (this.authorOnlyMode) {\n                await this.renderAuthorPreview();\n            } else {\n                await this.renderFullPreview();\n            }\n        } catch (e) {\n            this.errorLog.push({\n                message: 'An error occurred during rendering process:',\n                desc: e.message + \"\\n\\n\" + e.stack\n            });\n        }\n\n        return true;\n    }\n\n    /**\n     * Renders full preview of website\n     */\n    async renderFullPreview() {\n        console.time(\"RENDERING\");\n        this.preparePageToRender();\n        this.triggerEvent('beforeRender');\n        await this.generateWWW();\n        console.timeEnd(\"RENDERING\");\n\n        if (this.siteConfig.deployment.relativeUrls) {\n            console.time(\"RELATIVE URLS\");\n            await this.relativizeUrls();\n            console.timeEnd(\"RELATIVE URLS\");\n        }\n\n        this.sendProgress(100, 'Website files are ready to upload');\n        this.db.close();\n    }\n\n    /**\n     * Prepares website to be rendered\n     */\n    preparePageToRender() {\n        this.loadSiteConfig();\n\n        this.sendProgress(1, 'Loading website config');\n\n        console.time(\"CONFIG\");\n        this.loadSiteTranslations();\n        this.loadDataFromDB();\n        this.loadThemeConfig();\n        this.loadThemeFiles();\n        this.registerHelpers();\n        this.registerThemeHelpers();\n        console.timeEnd(\"CONFIG\");\n\n        this.sendProgress(2, 'Loading website assets');\n        this.loadContentStructure();\n        this.sendProgress(5, 'Loading content structure');\n\n        this.loadCommonData();\n        this.sendProgress(10, 'Preloading common data');\n        this.generatePartials();\n    }\n\n    /**\n     * Creates website content\n     */\n    async generateWWW() {\n        // We must generate CSS before any HTML files to receive correct checksum if version param is used to solve issues with browser cache\n        this.generateCSS();\n        \n        if ((this.homepageOnlyMode && !this.siteConfig.advanced.usePageAsFrontpage) || !this.homepageOnlyMode) {\n            this.sendProgress(11, 'Generating frontpage');\n            this.generateFrontpage();\n            this.sendProgress(20, 'Generating posts');\n            this.generatePosts();\n            this.sendProgress(50, 'Generating pages');\n            this.generatePages();\n\n            if (RendererHelpers.getRendererOptionValue('createTagPages', this.themeConfig)) {\n                this.sendProgress(60, 'Generating tag pages');\n                this.generateTags();\n                this.generateTagsList();\n            }\n\n            if (RendererHelpers.getRendererOptionValue('createAuthorPages', this.themeConfig)) {\n                this.sendProgress(70, 'Generating author pages');\n                this.generateAuthors();\n            }\n\n            this.sendProgress(75, 'Generating other pages');\n\n            if (RendererHelpers.getRendererOptionValue('create404page', this.themeConfig)) {\n                this.generate404s();\n            }\n\n            if (RendererHelpers.getRendererOptionValue('createSearchPage', this.themeConfig)) {\n                this.generateSearch();\n            }\n        } else {\n            this.generatePages();\n        }\n        \n        if (!this.siteConfig.deployment.relativeUrls) {\n            this.generateFeeds();\n        }\n\n        this.sendProgress(80, 'Copying files');\n        await this.copyFiles();\n\n        if (!this.siteConfig.deployment.relativeUrls && !this.siteConfig.advanced.noIndexThisPage) {\n            await this.generateSitemap();\n        }\n\n        this.createRobotsTxt();\n        this.triggerEvent('afterRender');\n        this.sendProgress(90, 'Finishing the render process');\n    }\n\n    preparePreview (type) {\n        this.loadSiteConfig();\n        this.loadSiteTranslations();\n        this.loadDataFromDB();\n        this.loadThemeConfig();\n        this.loadThemeFiles();\n        this.registerHelpers();\n        this.registerThemeHelpers();\n        this.loadContentStructure();\n        this.loadCommonData();\n        this.triggerEvent('beforeRender');\n        this.generatePartials();\n\n        if (type === 'post') {\n            this.generatePost();\n        } else if (type === 'page') {\n            this.generatePage();\n        } else if (type === 'frontpage') {\n            this.generateFrontpage();\n        } else if (type === 'tag') {\n            this.generateTags(this.itemID);\n        } else if (type === 'author') {\n            this.generateAuthors(this.itemID);\n        }\n\n        this.generateCSS();\n    }\n\n    /**\n     * Renders post preview\n     */\n    async renderPostPreview () {\n        this.preparePreview('post');\n\n        await FilesHelper.copyAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        FilesHelper.copyDynamicAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        await FilesHelper.copyMediaFiles(this.inputDir, this.outputDir, [this.itemID], []);\n        FilesHelper.copyPluginFiles(this.inputDir, this.outputDir, this.pluginsDir);\n\n        this.triggerEvent('afterRender');\n    }\n\n    /**\n     * Renders page preview\n     */\n    async renderPagePreview () {\n        this.preparePreview('page');\n\n        await FilesHelper.copyAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        FilesHelper.copyDynamicAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        await FilesHelper.copyMediaFiles(this.inputDir, this.outputDir, [this.itemID], []);\n        FilesHelper.copyPluginFiles(this.inputDir, this.outputDir, this.pluginsDir);\n\n        this.triggerEvent('afterRender');\n    }\n\n    /**\n     * Renders homepage preview\n     */\n    async renderHomepagePreview () {\n        this.preparePreview('frontpage');\n\n        let postIDs = Object.keys(this.cachedItems.posts);\n        let pageIDs = Object.keys(this.cachedItems.pages);\n        await FilesHelper.copyAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        FilesHelper.copyDynamicAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        await FilesHelper.copyMediaFiles(this.inputDir, this.outputDir, postIDs, pageIDs);\n        FilesHelper.copyPluginFiles(this.inputDir, this.outputDir, this.pluginsDir);\n\n        this.triggerEvent('afterRender');\n    }\n\n    /**\n     * Renders tag page preview\n     */\n    async renderTagPreview () {\n        this.preparePreview('tag');\n\n        let postIDs = Object.keys(this.cachedItems.posts);\n        let pageIDs = Object.keys(this.cachedItems.pages);\n        let postIDsToRender = [];\n\n        for (let i = 0; i < postIDs.length; i++) {\n            let postID = postIDs[i];\n            let foundedTags = this.cachedItems.posts[postID].tags.filter(tag => tag.id === this.itemID);\n\n            if (foundedTags.length) {\n                postIDsToRender.push(postIDs[i]);\n            }\n        }\n\n        await FilesHelper.copyAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        FilesHelper.copyDynamicAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        await FilesHelper.copyMediaFiles(this.inputDir, this.outputDir, postIDsToRender, pageIDs);\n        FilesHelper.copyPluginFiles(this.inputDir, this.outputDir, this.pluginsDir);\n\n        this.triggerEvent('afterRender');\n    }\n\n    /**\n     * Renders author page preview\n     */\n    async renderAuthorPreview () {\n        this.preparePreview('author');\n\n        let postIDs = Object.keys(this.cachedItems.posts);\n        let pageIDs = Object.keys(this.cachedItems.pages);\n        let postIDsToRender = [];\n\n        for (let i = 0; i < postIDs.length; i++) {\n            let postID = postIDs[i];\n            let usesCurrentAuthor = this.cachedItems.posts[postID].author.id === this.itemID;\n\n            if (usesCurrentAuthor) {\n                postIDsToRender.push(postIDs[i]);\n            }\n        }\n\n        await FilesHelper.copyAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        FilesHelper.copyDynamicAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        await FilesHelper.copyMediaFiles(this.inputDir, this.outputDir, postIDsToRender, pageIDs);\n        FilesHelper.copyPluginFiles(this.inputDir, this.outputDir, this.pluginsDir);\n\n        this.triggerEvent('afterRender');\n    }\n\n    /**\n     * Send progress to the renderer thread\n     *\n     * @param progress\n     * @param message\n     */\n    sendProgress(progress, message = '') {\n        process.send({\n            type: 'app-rendering-progress',\n            progress: progress,\n            message: message\n        });\n    }\n\n    /*\n     * Make sure the output dir exists and is empty before generating the output files\n     */\n    async emptyOutputDir() {\n        if (UtilsHelper.dirExists(this.outputDir)) {\n            if (this.previewMode === false && this.siteConfig.deployment.protocol === 'git') {\n                let gitClient = new Git(false);\n                await gitClient.prepareToSync(this.siteConfig, this.siteName, this.outputDir, this.sendProgress);\n            }\n\n            let files = fs.readdirSync(this.outputDir);\n            \n            for (let file of files) {\n                if (file === '.git' || file === '.' || file === '..' || file === 'media') {\n                    continue;\n                }\n\n                fs.rmSync(path.join(this.outputDir, file), { recursive: true });\n            }\n        } else {\n            fs.mkdirSync(this.outputDir, { recursive: true });\n\n            if (this.previewMode === false && this.siteConfig.deployment.protocol === 'git') {\n                let gitClient = new Git(false);\n                let result = await gitClient.prepareToSync(this.siteConfig, this.siteName, this.outputDir, this.sendProgress);\n\n                if (result === 'merge-error') {\n                    fs.rmSync(path.join(this.outputDir, '.git'), { recursive: true });\n                    console.log('[i] Git Debug: remove .git folder due merge errors');\n                    await gitClient.prepareToSync(this.siteConfig, this.siteName, this.outputDir, this.sendProgress);\n                }\n            }\n        }\n    }\n\n    /*\n     * Set the directories used as an input and an output\n     */\n    setIO() {\n        let basePath = path.join(this.sitesDir, this.siteName);\n        this.inputDir = path.join(basePath, 'input');\n        this.themeDir = path.join(this.inputDir, 'themes', this.themeName);\n        this.outputDir = path.join(basePath, 'output');\n\n        if(this.previewMode) {\n            this.outputDir = path.join(basePath, 'preview');\n\n            if(this.previewLocation !== '' && UtilsHelper.dirExists(this.previewLocation)) {\n                this.outputDir = this.previewLocation;\n            }\n        }\n    }\n\n    /*\n     * Create built-in helpers\n     */\n    registerHelpers() {\n        const HandlebarsHelpers = require('./handlebars/helpers/_modules.js');\n\n        // Get helpers names\n        let helperNames = Object.keys(HandlebarsHelpers);\n\n        // Register all helpers\n        for(let helperName of helperNames) {\n            if(helperName.substr(-6) !== 'Helper') {\n                Handlebars.registerHelper(\n                    helperName,\n                    HandlebarsHelpers[helperName]\n                );\n            } else {\n                HandlebarsHelpers[helperName](this, Handlebars);\n            }\n        }\n    }\n\n    /*\n     * Create theme custom helpers\n     */\n    registerThemeHelpers() {\n        let helpersFilePath = path.join(this.themeDir, 'helpers.js');\n        let overridedHelpersFilePath = UtilsHelper.fileIsOverrided(this.themeDir, helpersFilePath);\n\n        if(overridedHelpersFilePath) {\n            helpersFilePath = overridedHelpersFilePath;\n        }\n\n        // Check if the helpers.js file exists\n        if(!UtilsHelper.fileExists(helpersFilePath)) {\n            return;\n        }\n\n        // Include the helpers from the helpers.js file\n        let themeHelpers;\n        \n        if (RendererHelpers.getRendererOptionValue('includeHandlebarsInHelpers', this.themeConfig)) {\n            themeHelpers = UtilsHelper.requireWithNoCache(helpersFilePath, Handlebars);\n        } else {\n            themeHelpers = UtilsHelper.requireWithNoCache(helpersFilePath);\n        }\n\n        // Check if the returned value is an object\n        if(themeHelpers.constructor !== Object) {\n            return;\n        }\n\n        // Get helpers names\n        let helperNames = Object.keys(themeHelpers);\n\n        // Register all helpers\n        for(let helperName of helperNames) {\n            Handlebars.registerHelper(helperName, themeHelpers[helperName]);\n        }\n    }\n\n    /*\n     * Load site config\n     */\n    loadSiteConfig() {\n        let defaultSiteConfig = JSON.parse(JSON.stringify(defaultAstCurrentSiteConfig));\n        // Site config\n        let configPath = path.join(this.inputDir, 'config', 'site.config.json');\n        this.siteConfig = JSON.parse(FileHelper.readFileSync(configPath));\n        this.siteConfig = UtilsHelper.mergeObjects(defaultSiteConfig, this.siteConfig);\n\n        if(this.previewMode) {\n            this.siteConfig.domain = 'file:///' + this.outputDir;\n        } else if (this.siteConfig.domain === '/') {\n            this.siteConfig.domain = '';\n        }\n\n        if (!this.previewMode && this.siteConfig.deployment.relativeUrls) {\n            this.siteConfig.domain = '#PUBLII_RELATIVE_URL_BASE#';\n        }\n\n        this.siteConfig.originalDomain = this.siteConfig.domain;\n\n        if(\n            this.siteConfig.advanced &&\n            this.siteConfig.advanced.openGraphImage !== '' &&\n            this.siteConfig.advanced.openGraphImage.indexOf('http://') === -1 &&\n            this.siteConfig.advanced.openGraphImage.indexOf('https://') === -1 &&\n            this.siteConfig.advanced.openGraphImage.indexOf('media/website/') === -1\n        ) {\n            let openGraphImage = path.join(this.siteConfig.domain, 'media', 'website', this.siteConfig.advanced.openGraphImage);\n            openGraphImage = normalizePath(openGraphImage);\n            openGraphImage = URLHelper.fixProtocols(openGraphImage);\n            this.siteConfig.advanced.openGraphImage = openGraphImage;\n        } else {\n            this.siteConfig.advanced.openGraphImage = URLHelper.fixProtocols(this.siteConfig.advanced.openGraphImage);\n        }\n    }\n\n    /*\n     * Load site translations\n     */\n    loadSiteTranslations() {\n        // Path to the custom translations\n        let userTranslationsPath = path.join(\n            this.inputDir,\n            'languages',\n            this.themeName + '.lang.json'\n        );\n\n        // Path to the original translations\n        let themeTranslationsPath = path.join(\n            this.inputDir,\n            'themes',\n            this.themeName,\n            this.themeName + '.lang.json'\n        );\n\n        // Load custom translations\n        if(fs.existsSync(userTranslationsPath)) {\n            this.translations.user = this.parseTranslations(userTranslationsPath);\n        }\n\n        // Load original translations\n        if(fs.existsSync(themeTranslationsPath)) {\n            this.translations.theme = this.parseTranslations(themeTranslationsPath);\n        }\n    }\n\n    /*\n     * Parse site translations\n     */\n    parseTranslations(path) {\n        let translations = false;\n\n        try {\n            translations = JSON.parse(FileHelper.readFileSync(path));\n        } catch(e) {\n            return false;\n        }\n\n        return translations;\n    }\n\n    /*\n     * Load all data from the database\n     */\n    loadDataFromDB() {\n        const dbPath = path.join(this.inputDir, 'db.sqlite');\n        this.db = new DBUtils(new Database(dbPath));\n    }\n\n    /*\n     * Load and parse theme config file\n     */\n    loadThemeConfig() {\n        let themeConfigPath = path.join(this.inputDir, 'config', 'theme.config.json');\n        let tempThemeConfig = Themes.loadThemeConfig(themeConfigPath, this.themeDir);\n        this.themeConfig = JSON.parse(JSON.stringify(tempThemeConfig));\n        \n        let configNames = [\n            'config',\n            'customConfig',\n            'postConfig',\n            'pageConfig',\n            'tagConfig',\n            'authorConfig'\n        ];\n        \n        for (let configName of configNames) {\n            this.themeConfig[configName] = {};\n            this.viewConfigStructure[configName] = {};\n        \n            for (let i = 0; i < tempThemeConfig[configName].length; i++) {\n                let key = tempThemeConfig[configName][i].name;\n                this.themeConfig[configName][key] = tempThemeConfig[configName][i].value;\n                this.viewConfigStructure[configName][key] = { type: tempThemeConfig[configName][i].type };\n            }\n        }\n    }\n\n    /*\n     * Load necessary theme files\n     */\n    loadThemeFiles() {\n        this.templateHelper = new TemplateHelper(this.themeDir, this.outputDir, this.siteConfig);\n    }\n\n    /*\n     * Generate partials\n     */\n    generatePartials() {\n        console.time('PARTIALS GENERATION');\n        let requiredPartials = ['header', 'footer'];\n        let optionalPartials = [\n            'pagination',\n            'menu'\n        ];\n        let userPartials = this.templateHelper.getUserPartials(requiredPartials, optionalPartials);\n        let allPartials = requiredPartials.concat(optionalPartials).concat(userPartials);\n\n        for(let i = 0; i < allPartials.length; i++) {\n            let template = this.templateHelper.loadPartialTemplate(allPartials[i] + '.hbs');\n\n            if (!template && optionalPartials.indexOf(allPartials[i]) > -1) {\n                let optionalPartialPath = path.join(__dirname, '..', '..', '..', 'default-files', 'theme-files', allPartials[i] + '.hbs');\n                template = FileHelper.readFileSync(optionalPartialPath, 'utf8');\n            }\n\n            if(!template) {\n                continue;\n            }\n\n            try {\n                Handlebars.registerPartial(allPartials[i], template);\n            } catch (e) {\n                this.errorLog.push({\n                    message: 'An error (1001) occurred during parsing ' + allPartials[i] + '.hbs partial file.',\n                    desc: e.message + \"\\n\\n\" + e.stack\n                });\n            }\n        }\n\n        console.timeEnd('PARTIALS GENERATION');\n    }\n\n    /*\n     * Generate the main view of the theme\n     */\n    generateFrontpage() {\n        // Load template\n        let inputFile = 'index.hbs';\n        let homeCompiledTemplate = this.compileTemplate(inputFile);\n        let postsCompiledTemplate;\n        let compiledTemplate;\n\n        if (this.themeConfig.supportedFeatures && this.themeConfig.supportedFeatures.postsPage) {\n            postsCompiledTemplate = this.compileTemplate('posts.hbs');\n        } else {\n            postsCompiledTemplate = this.compileTemplate('index.hbs');\n        }\n\n        // Don't render homepage and it's pagination if we use page as frontpage and posts prefix is empty\n        if (this.siteConfig.advanced.usePageAsFrontpage && !this.siteConfig.advanced.urls.postsPrefix) {\n            return false;\n        }\n\n        // Render index site\n        let contextGenerator = new RendererContextHome(this);\n\n        // Detect if we have enough posts to create pagination\n        let totalNumberOfPosts = contextGenerator.getPostsNumber();\n        let postsPerPage = parseInt(this.themeConfig.config.postsPerPage, 10);\n\n        if (isNaN(postsPerPage)) {\n            postsPerPage = 5;\n        }\n\n        let hasBlogPagination = !(totalNumberOfPosts <= postsPerPage || postsPerPage <= 0 || this.siteConfig.advanced.homepageNoPagination);\n        \n        // When we have blog pagination\n        if (hasBlogPagination) {\n            console.time('BLOG PAGINATION');\n            let addIndexHtml = this.previewMode || this.siteConfig.advanced.urls.addIndex;\n\n            // If user set postsPerPage field to -1 - set it for calculations to 999\n            postsPerPage = postsPerPage == -1 ? 999 : postsPerPage;\n\n            for (let offset = 0; offset < totalNumberOfPosts; offset += postsPerPage) {\n                // Add pagination data to the global context\n                let currentPage = 1;\n                let totalPages = 0;\n\n                if (postsPerPage > 0) {\n                    currentPage = parseInt(offset / postsPerPage, 10) + 1;\n                    totalPages = Math.ceil(totalNumberOfPosts / postsPerPage)\n                }\n\n                let nextPage = (currentPage < totalPages) ? currentPage + 1 : false;\n                let previousPage = (currentPage > 1) ? currentPage - 1 : false;\n\n                let pagination = {\n                    context: '',\n                    pages: Array.from({length: totalPages}, (v, k) => k + 1),\n                    totalPosts: totalNumberOfPosts,\n                    totalPages: totalPages,\n                    currentPage: currentPage,\n                    postsPerPage: postsPerPage,\n                    nextPage: nextPage,\n                    previousPage: previousPage,\n                    nextPageUrl: URLHelper.createPaginationPermalink(this.siteConfig, this.themeConfig, nextPage, 'home', false, addIndexHtml),\n                    previousPageUrl: URLHelper.createPaginationPermalink(this.siteConfig, this.themeConfig, previousPage, 'home', false, addIndexHtml)\n                };\n\n                let additionalContexts = [];\n\n                if (offset > 0) {\n                    additionalContexts = ['pagination', 'index-pagination'];\n                }\n\n                // detect frontpage/blog index\n                if (offset === 0) {\n                    if (this.siteConfig.advanced.urls.postsPrefix === '' && !this.siteConfig.advanced.usePageAsFrontpage) {\n                        compiledTemplate = homeCompiledTemplate;\n                        this.menuContext = ['frontpage'];\n                        let context = contextGenerator.getContext(offset, postsPerPage);\n                        additionalContexts.push('homepage');\n                        this.globalContext = this.createGlobalContext('index', additionalContexts, {\n                            pagination,\n                            isFirstPage: currentPage === 1,\n                            isLastPage: currentPage === totalPages,\n                            currentPage\n                        }, false, false, context);\n\n                        let output = this.renderTemplate(compiledTemplate, context, this.globalContext, inputFile);\n\n                        if (this.plugins.hasModifiers('htmlOutput')) {\n                            output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                        }\n                        \n                        this.templateHelper.saveOutputFile('index.html', output);\n                    } else if (this.siteConfig.advanced.urls.postsPrefix !== '') {\n                        compiledTemplate = postsCompiledTemplate;\n                        this.menuContext = ['blogpage'];\n                        let context = contextGenerator.getContext(offset, postsPerPage);\n                        this.globalContext = this.createGlobalContext('blogindex', additionalContexts, {\n                            pagination,\n                            isFirstPage: currentPage === 1,\n                            isLastPage: currentPage === totalPages,\n                            currentPage\n                        }, false, false, context);\n\n                        let output = this.renderTemplate(compiledTemplate, context, this.globalContext, inputFile);\n\n                        if (this.plugins.hasModifiers('htmlOutput')) {\n                            output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                        }\n                        \n                        this.templateHelper.saveOutputFile(path.join(this.siteConfig.advanced.urls.postsPrefix, 'index.html'), output);\n\n                        if (!this.siteConfig.advanced.usePageAsFrontpage) {\n                            compiledTemplate = homeCompiledTemplate;\n\n                            this.menuContext = ['frontpage'];\n                            let context = contextGenerator.getContext(offset, postsPerPage);\n                            additionalContexts.push('homepage');\n                            this.globalContext = this.createGlobalContext('index', additionalContexts, {\n                                pagination,\n                                isFirstPage: currentPage === 1,\n                                isLastPage: currentPage === totalPages,\n                                currentPage\n                            }, false, false, context);\n\n                            let output = this.renderTemplate(compiledTemplate, context, this.globalContext, inputFile);\n\n                            if (this.plugins.hasModifiers('htmlOutput')) {\n                                output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                            }\n                            \n                            this.templateHelper.saveOutputFile('index.html', output);\n                        }\n                    }\n                } else {\n                    this.menuContext = ['frontpage'];\n\n                    if (this.siteConfig.advanced.urls.postsPrefix !== '') {\n                        this.menuContext = ['blogpage'];\n                    }\n                    let context = contextGenerator.getContext(offset, postsPerPage);\n                    this.globalContext = this.createGlobalContext('blogindex', additionalContexts, {\n                        pagination,\n                        isFirstPage: currentPage === 1,\n                        isLastPage: currentPage === totalPages,\n                        currentPage\n                    }, false, false, context);\n\n                    let templateToUse = this.siteConfig.advanced.urls.postsPrefix !== '' ? postsCompiledTemplate : homeCompiledTemplate;\n                    let output = this.renderTemplate(templateToUse, context, this.globalContext, inputFile);\n\n                    if (this.plugins.hasModifiers('htmlOutput')) {\n                        output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                    }\n\n                    // We increase the current page number as we need to start URLs from page/2\n                    this.templateHelper.saveOutputHomePaginationFile(currentPage, output);\n                }\n            }\n            \n            console.timeEnd('BLOG PAGINATION');\n        }\n\n        // When there is no blog pagination\n        if (!hasBlogPagination) {\n            console.time('HOMEPAGE');\n            \n            let output = '';\n\n            if (this.siteConfig.advanced.urls.postsPrefix === '' && !this.siteConfig.advanced.usePageAsFrontpage) {\n                this.menuContext = ['frontpage'];\n                let context = contextGenerator.getContext(0, postsPerPage);\n                this.globalContext = this.createGlobalContext('index', ['homepage'], false, false, false, context);\n\n                output = homeCompiledTemplate(context, {\n                    data: this.globalContext\n                });\n\n                if (this.previewMode) {\n                    output = output.replace(/file:\\/\\/\\/\\//gmi, 'file:///');\n                }\n    \n                if (this.plugins.hasModifiers('htmlOutput')) {\n                    output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                }\n    \n                this.templateHelper.saveOutputFile('index.html', output);\n            } else if (this.siteConfig.advanced.urls.postsPrefix !== '') {\n                this.menuContext = ['blogpage'];\n                let context = contextGenerator.getContext(0, postsPerPage);\n                this.globalContext = this.createGlobalContext('blogindex', [], false, false, false, context);\n\n                output = postsCompiledTemplate(context, {\n                    data: this.globalContext\n                });\n\n                if (this.previewMode) {\n                    output = output.replace(/file:\\/\\/\\/\\//gmi, 'file:///');\n                }\n    \n                if (this.plugins.hasModifiers('htmlOutput')) {\n                    output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                }\n    \n                this.templateHelper.saveOutputFile(path.join(this.siteConfig.advanced.urls.postsPrefix, 'index.html'), output);\n\n                if (!this.siteConfig.advanced.usePageAsFrontpage) {\n                    this.menuContext = ['frontpage'];\n                    let context = contextGenerator.getContext(0, postsPerPage);\n                    this.globalContext = this.createGlobalContext('index', [], false, false, false, context);\n\n                    output = homeCompiledTemplate(context, {\n                        data: this.globalContext\n                    });\n\n                    if (this.previewMode) {\n                        output = output.replace(/file:\\/\\/\\/\\//gmi, 'file:///');\n                    }\n        \n                    if (this.plugins.hasModifiers('htmlOutput')) {\n                        output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                    }\n        \n                    this.templateHelper.saveOutputFile('index.html', output);\n                }\n            }\n\n            console.timeEnd('HOMEPAGE');\n        }\n    }\n\n    /*\n     * Create post sites\n     */\n    generatePosts() {\n        console.time('POSTS');\n        let postIDs = [];\n        let postSlugs = [];\n        let postTemplates = [];\n        let inputFile = 'post.hbs';\n\n        // Get posts\n        let postData = this.db.prepare(`\n            SELECT\n                id,\n                slug,\n                template\n            FROM\n                posts\n            WHERE\n                status LIKE '%published%' AND\n                status NOT LIKE '%is-page%' AND\n                status NOT LIKE '%trashed%'\n            ORDER BY\n                id ASC\n        `).all();\n\n        if (postData && postData.length) { \n            postIDs = postData.map(row => row.id);\n            postSlugs = postData.map(row => row.slug);\n            postTemplates = postData.map(row => {\n                if (row.template === '*') {\n                    return this.themeConfig.defaultTemplates.post\n                }\n\n                return row.template;\n            });\n        } else {\n            postIDs = [];\n            postSlugs = [];\n            postTemplates = [];\n        }\n\n        // Load templates\n        let compiledTemplates = {};\n        compiledTemplates['DEFAULT'] = this.compileTemplate(inputFile);\n\n        if (!compiledTemplates['DEFAULT']) {\n            return false;\n        }\n\n        for (let i = 0; i < postTemplates.length; i++) {\n            let fileSlug = postTemplates[i];\n\n            // When we meet default template - skip the compilation process\n            if (fileSlug === '' || !this.themeConfig.postTemplates[fileSlug]) {\n                continue;\n            }\n\n            compiledTemplates[fileSlug] = this.compileTemplate('post-' + fileSlug + '.hbs');\n\n            if (!compiledTemplates[fileSlug]) {\n                return false;\n            }\n        }\n\n        // Create global context\n        let progressIncrease = 40 / postIDs.length;\n\n        // Render post sites\n        for (let i = 0; i < postIDs.length; i++) {\n            let contextGenerator = new RendererContextPost(this);\n            let context = contextGenerator.getContext(postIDs[i]);\n            let fileSlug = 'DEFAULT';\n            fileSlug = postTemplates[i] === '' ? 'DEFAULT' : postTemplates[i];\n\n            this.menuContext = ['post', postSlugs[i], 'post-' + postIDs[i]];    \n            \n            if (!compiledTemplates[fileSlug]) {\n                fileSlug = 'DEFAULT';\n            }\n\n            inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n            let postViewConfig = this.cachedItems.posts[postIDs[i]].postViewConfig;\n            this.globalContext = this.createGlobalContext('post', [], false, postSlugs[i], postViewConfig, context);\n            let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n            if (this.plugins.hasModifiers('htmlOutput')) {\n                output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n            }\n\n            this.templateHelper.saveOutputPostFile(postSlugs[i], output);\n            this.sendProgress(Math.ceil(20 + (progressIncrease * i)), 'Generating posts (' + (i + 1) + '/' + postIDs.length + ')');\n        }\n        console.timeEnd('POSTS');\n    }\n\n    /*\n     * Create post preview\n     */\n    generatePost() {\n        let postID = this.itemID;\n        let postSlug = 'preview';\n        let postTemplate = this.postData.template;\n        let inputFile = 'post.hbs';\n\n        if (postTemplate === '*') {\n            postTemplate = this.themeConfig.defaultTemplates.post;\n        }\n\n        // Load templates\n        let compiledTemplates = {};\n        compiledTemplates['DEFAULT'] = this.compileTemplate(inputFile);\n\n        if(!compiledTemplates['DEFAULT']) {\n            return false;\n        }\n\n        if(typeof postTemplate === \"string\" && postTemplate !== '') {\n            postTemplate = [postTemplate];\n        }\n\n        for (let i = 0; i < postTemplate.length; i++) {\n            let fileSlug = postTemplate[i];\n\n            // When we meet default template - skip the compilation process\n            if (fileSlug === '' || !this.themeConfig.postTemplates[fileSlug]) {\n                continue;\n            }\n\n            compiledTemplates[fileSlug] = this.compileTemplate('post-' + fileSlug + '.hbs');\n\n            if(!compiledTemplates[fileSlug]) {\n                return false;\n            }\n        }\n\n        // Render post site\n        let contextGenerator = new RendererContextPostPreview(this);\n        let context = contextGenerator.getContext(postID);\n        let fileSlug = 'DEFAULT';\n        fileSlug = postTemplate === '' ? 'DEFAULT' : postTemplate;\n\n        this.menuContext = ['post', postSlug];\n\n        if(!compiledTemplates[fileSlug]) {\n            fileSlug = 'DEFAULT';\n        }\n\n        inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n        let postConfig = this.overrideItemViewSettings(JSON.parse(JSON.stringify(this.themeConfig.postConfig)), postID, 'post', true);\n        this.globalContext = this.createGlobalContext('post', [], false, postSlug, postConfig, context);\n        let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n        if (this.plugins.hasModifiers('htmlOutput')) {\n            output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n        }\n\n        this.templateHelper.saveOutputFile(postSlug + '.html', output);\n    }\n\n    /*\n     * Create page preview\n     */\n    generatePage() {\n        let pageID = this.itemID;\n        let pageSlug = 'preview';\n        let pageTemplate = this.postData.template;\n        let inputFile = 'page.hbs';\n\n        if (pageTemplate === '*') {\n            if (this.themeConfig.defaultTemplates.page) {\n                pageTemplate = this.themeConfig.defaultTemplates.page;\n            } else {\n                pageTemplate = '';\n            }\n        }\n\n        // Load templates\n        let compiledTemplates = {};\n        compiledTemplates['DEFAULT'] = this.compileTemplate(inputFile);\n\n        if(!compiledTemplates['DEFAULT']) {\n            return false;\n        }\n\n        if(typeof pageTemplate === \"string\" && pageTemplate !== '') {\n            pageTemplate = [pageTemplate];\n        }\n\n        for (let i = 0; i < pageTemplate.length; i++) {\n            let fileSlug = pageTemplate[i];\n\n            // When we meet default template - skip the compilation process\n            if (fileSlug === '' || !this.themeConfig.pageTemplates[fileSlug]) {\n                continue;\n            }\n\n            compiledTemplates[fileSlug] = this.compileTemplate('page-' + fileSlug + '.hbs');\n\n            if (!compiledTemplates[fileSlug]) {\n                return false;\n            }\n        }\n\n        // Render page site\n        let contextGenerator = new RendererContextPagePreview(this);\n        let context = contextGenerator.getContext(pageID);\n        let fileSlug = 'DEFAULT';\n        fileSlug = pageTemplate === '' ? 'DEFAULT' : pageTemplate;\n\n        this.menuContext = ['page', pageSlug];\n\n        if(!compiledTemplates[fileSlug]) {\n            fileSlug = 'DEFAULT';\n        }\n\n        inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n        let pageConfig = this.overrideItemViewSettings(JSON.parse(JSON.stringify(this.themeConfig.pageConfig)), pageID, 'page', true);\n        let additionalContexts = [];\n\n        if (this.siteConfig.advanced.usePageAsFrontpage && parseInt(pageID, 10) === parseInt(this.siteConfig.advanced.pageAsFrontpage, 10)) {\n            additionalContexts = ['homepage'];\n        }\n\n        this.globalContext = this.createGlobalContext('page', additionalContexts, false, pageSlug, pageConfig, context);\n        let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n        if (this.plugins.hasModifiers('htmlOutput')) {\n            output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n        }\n\n        this.templateHelper.saveOutputFile(pageSlug + '.html', output);\n    }\n\n    /*\n     * Override post view settings with the settings of the posts\n     */\n    overrideItemViewSettings(defaultViewConfig, itemID, itemType, itemPreview = false) {\n        let itemViewData = false;\n        let itemViewSettings = {};\n\n        if (itemPreview) {\n            itemViewSettings = this.postData[itemType === 'post' ? 'postViewSettings' : 'pageViewSettings'];\n        } else {\n            itemViewData = this.db.prepare(`\n                SELECT\n                    value\n                FROM\n                    posts_additional_data\n                WHERE\n                    post_id = @itemID\n                    AND\n                    key = @itemType\n            `).get({\n                itemID: itemID,\n                itemType: itemType + 'ViewSettings'\n            });\n\n            if (itemViewData && itemViewData.length) {\n                itemViewSettings = JSON.parse(itemViewData.value);\n            }\n        }\n\n        return ViewSettingsHelper.override(itemViewSettings, defaultViewConfig, {\n            type: itemType,\n            id: itemID\n        }, this);\n    }\n\n    /*\n     * Create page sites\n     */\n    generatePages() {\n        if (!this.themeConfig.supportedFeatures || !this.themeConfig.supportedFeatures.pages) {\n            console.log('[i] Pages are not supported by the theme');\n            return;\n        }\n\n        console.time('PAGES');\n        let pageIDs = [];\n        let pageSlugs = [];\n        let pageTemplates = [];\n        let inputFile = 'page.hbs';\n\n        // Get pages\n        let pageData;\n        \n        if (this.homepageOnlyMode && this.siteConfig.advanced.usePageAsFrontpage && this.siteConfig.advanced.pageAsFrontpage) {\n            pageData = this.db.prepare(`\n                SELECT\n                    id,\n                    slug,\n                    template\n                FROM\n                    posts\n                WHERE\n                    status LIKE '%published%' AND\n                    status LIKE '%is-page%' AND\n                    status NOT LIKE '%trashed%' AND\n                    id = ${parseInt(this.siteConfig.advanced.pageAsFrontpage, 10)}\n                ORDER BY\n                    id ASC\n            `).all();\n        } else {\n            pageData = this.db.prepare(`\n                SELECT\n                    id,\n                    slug,\n                    template\n                FROM\n                    posts\n                WHERE\n                    status LIKE '%published%' AND\n                    status LIKE '%is-page%' AND\n                    status NOT LIKE '%trashed%'\n                ORDER BY\n                    id ASC\n            `).all();\n        }\n\n        if (pageData && pageData.length) { \n            pageIDs = pageData.map(row => row.id);\n            pageSlugs = pageData.map(row => row.slug);\n            pageTemplates = pageData.map(row => {\n                if (row.template === '*') {\n                    return this.themeConfig.defaultTemplates.page\n                }\n\n                return row.template;\n            });\n        } else {\n            pageIDs = [];\n            pageSlugs = [];\n            pageTemplates = [];\n        }\n\n        // Load templates\n        let compiledTemplates = {};\n        compiledTemplates['DEFAULT'] = this.compileTemplate(inputFile);\n\n        if (!compiledTemplates['DEFAULT']) {\n            return false;\n        }\n\n        for (let i = 0; i < pageTemplates.length; i++) {\n            let fileSlug = pageTemplates[i];\n\n            // When we meet default template - skip the compilation process\n            if (fileSlug === '' || !this.themeConfig.pageTemplates[fileSlug]) {\n                continue;\n            }\n\n            compiledTemplates[fileSlug] = this.compileTemplate('page-' + fileSlug + '.hbs');\n\n            if (!compiledTemplates[fileSlug]) {\n                return false;\n            }\n        }\n\n        // Create global context\n        let progressIncrease = 40 / pageIDs.length;\n\n        // Render page sites\n        for (let i = 0; i < pageIDs.length; i++) {\n            let contextGenerator = new RendererContextPage(this);\n            let context = contextGenerator.getContext(pageIDs[i]);\n            let fileSlug = 'DEFAULT';\n            fileSlug = pageTemplates[i] === '' ? 'DEFAULT' : pageTemplates[i];\n\n            this.menuContext = ['page', pageSlugs[i], 'page-' + pageIDs[i]];    \n            \n            if (!compiledTemplates[fileSlug]) {\n                fileSlug = 'DEFAULT';\n            }\n\n            inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n            let pageViewConfig = this.cachedItems.pages[pageIDs[i]].pageViewConfig;\n            let additionalContexts = [];\n\n            if (this.siteConfig.advanced.usePageAsFrontpage && parseInt(pageIDs[i], 10) === parseInt(this.siteConfig.advanced.pageAsFrontpage, 10)) {\n                additionalContexts = ['homepage'];\n            }\n\n            this.globalContext = this.createGlobalContext('page', additionalContexts, false, pageSlugs[i], pageViewConfig, context);\n            let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n            if (this.plugins.hasModifiers('htmlOutput')) {\n                output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n            }\n\n            this.templateHelper.saveOutputPageFile(pageIDs[i], pageSlugs[i], output, this);\n            this.sendProgress(Math.ceil(20 + (progressIncrease * i)), 'Generating pages (' + (i + 1) + '/' + pageIDs.length + ')');\n        }\n        console.timeEnd('PAGES');\n    }\n\n    /*\n     * Generate tag pages\n     */\n    generateTagsList() {\n        // Check if we should render tags list\n        if (\n            this.siteConfig.advanced.urls.tagsPrefix === '' ||\n            !this.themeConfig.supportedFeatures ||\n            !this.themeConfig.supportedFeatures.tagsList ||\n            !RendererHelpers.getRendererOptionValue('createTagsList', this.themeConfig)\n        ) {\n            return false;\n        }\n        \n        console.time('TAGS-LIST');\n        let inputFile = 'tags.hbs';\n        \n        // Load template\n        let compiledTemplate = this.compileTemplate(inputFile);\n\n        if (!compiledTemplate) {\n            return false;\n        }\n\n        // Render tags list\n        let contextGenerator = new RendererContextTags(this);\n        let context = contextGenerator.getContext();\n        this.menuContext = ['tags'];\n        this.globalContext = this.createGlobalContext('tags', [], false, false, false, context);\n        let output = this.renderTemplate(compiledTemplate, context, this.globalContext, inputFile);\n\n        if (this.plugins.hasModifiers('htmlOutput')) {\n            output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n        }\n\n        this.templateHelper.saveOutputTagsListFile(output);\n        console.timeEnd('TAGS-LIST');\n    }\n\n    /*\n     * Generate tag pages\n     */\n    generateTags(tagID = false) {\n        if (\n            (\n                this.themeConfig.supportedFeatures && \n                this.themeConfig.supportedFeatures.tagPages === false\n            ) || RendererHelpers.getRendererOptionValue('createTagPages', this.themeConfig) === false\n        ) {\n            return false;\n        }\n        \n        console.time('TAGS');\n        // Get tags\n        let inputFile = 'tag.hbs';\n        let queryCondition = '';\n\n        if (tagID !== false) {\n            queryCondition = `\n                WHERE \n                    t.id = ${parseInt(tagID, 10)}\n            `;\n        }\n\n        let tagsData = this.db.prepare(`\n            SELECT\n                t.id AS id\n            FROM\n                tags AS t\n            ${queryCondition}\n            ORDER BY\n                name ASC\n        `).all();\n        tagsData = tagsData.map(tag => this.cachedItems.tags[tag.id]);\n\n        // Skip hidden tags\n        tagsData = tagsData.filter(tagData => {\n            return tagData.additionalData.isHidden !== true;\n        });\n\n        // Remove empty tags - without posts\n        if (!this.siteConfig.advanced.displayEmptyTags && tagID === false) {\n            tagsData = tagsData.filter(tagData => {\n                return tagData.postsNumber > 0;\n            });\n        }\n\n        // Simplify the structure - change arrays into single value\n        let tagIDs = tagsData.map(tagData => tagData.id);\n        let tagSlugs = tagsData.map(tagData => tagData.slug);\n        let tagTemplates = tagsData.map(tagData => {\n            if (!tagData.additionalData) {\n                return 'DEFAULT';\n            }\n\n            if (tagData.additionalData.template) {\n                if (tagData.additionalData.template === '' || !this.themeConfig.tagTemplates[tagData.additionalData.template]) {\n                    return 'DEFAULT';\n                } else {\n                    return tagData.additionalData.template;\n                }\n            }\n\n            return 'DEFAULT';\n        });\n\n        // Load templates\n        let compiledTemplates = {};\n        compiledTemplates['DEFAULT'] = this.compileTemplate(inputFile);\n\n        if (!compiledTemplates['DEFAULT']) {\n            return false;\n        }\n\n        for (let i = 0; i < tagTemplates.length; i++) {\n            let fileSlug = tagTemplates[i];\n\n            // When we meet default template - skip the compilation process\n            if (fileSlug === '' || fileSlug === 'DEFAULT') {\n                continue;\n            }\n\n            compiledTemplates[fileSlug] = this.compileTemplate('tag-' + fileSlug + '.hbs');\n\n            if (!compiledTemplates[fileSlug]) {\n                return false;\n            }\n        }\n\n        let progressIncrease = 10 / tagsData.length;\n\n        // Render tag sites\n        for (let i = 0; i < tagsData.length; i++) {\n            let contextGenerator = new RendererContextTag(this);\n            let fileSlug = 'DEFAULT';\n            fileSlug = tagTemplates[i] === '' ? 'DEFAULT' : tagTemplates[i];\n\n            // Detect if we have enough posts to create pagination\n            let totalNumberOfPosts = this.cachedItems.tags[tagIDs[i]].postsNumber;\n            let postsPerPage = parseInt(this.themeConfig.config.tagsPostsPerPage, 10);\n            let tagSlug = URLHelper.createSlug(tagSlugs[i]);\n\n            if (typeof tagSlugs[i] === 'undefined' || tagSlugs[i] === null) {\n                console.log(`[i] Skipped tag with ID = ${tagIDs[i]} due lack of slug`);\n                continue;\n            }            \n\n            if (isNaN(postsPerPage)) {\n                postsPerPage = 5;\n            }\n\n            // Check for disabled tag pagination, tag posts amount or tag preview mode to avoid pagination files\n            if (totalNumberOfPosts <= postsPerPage || postsPerPage <= 0 || this.siteConfig.advanced.tagNoPagination || tagID !== false) {\n                let context = contextGenerator.getContext(tagIDs[i], 0, postsPerPage);\n\n                this.menuContext = ['tag', tagSlug];\n                \n                if (!compiledTemplates[fileSlug]) {\n                    fileSlug = 'DEFAULT';\n                }\n\n                inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n                let tagViewConfig = this.cachedItems.tags[tagIDs[i]].tagViewConfig;\n                this.globalContext = this.createGlobalContext('tag', [], false, tagSlug, tagViewConfig, context);\n                let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n                if (this.plugins.hasModifiers('htmlOutput')) {\n                    output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                }\n\n                this.templateHelper.saveOutputTagFile(tagSlug, output, tagID !== false);\n            } else {\n                let addIndexHtml = this.previewMode || this.siteConfig.advanced.urls.addIndex;\n\n                // If user set postsPerPage field to -1 - set it for calculations to 999\n                postsPerPage = postsPerPage == -1 ? 999 : postsPerPage;\n\n                for (let offset = 0; offset < totalNumberOfPosts; offset += postsPerPage) {\n                    let context = contextGenerator.getContext(tagIDs[i], offset, postsPerPage);\n\n                    // Add pagination data to the global context\n                    let currentPage = 1;\n                    let totalPages = 0;\n\n                    if (postsPerPage > 0) {\n                        currentPage = parseInt(offset / postsPerPage, 10) + 1;\n                        totalPages = Math.ceil(totalNumberOfPosts / postsPerPage);\n                    }\n\n                    let nextPage = (currentPage < totalPages) ? currentPage + 1 : false;\n                    let previousPage = (currentPage > 1) ? currentPage - 1 : false;\n                    let tagsContextInUrl = tagSlug;\n\n                    if (this.siteConfig.advanced.urls.tagsPrefix !== '') {\n                        tagsContextInUrl = this.siteConfig.advanced.urls.tagsPrefix + '/' + tagSlug;\n                    }\n\n                    if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.tagsPrefixAfterPostsPrefix) {\n                        tagsContextInUrl = this.siteConfig.advanced.urls.postsPrefix + '/' + tagsContextInUrl;\n                    }\n\n                    let pagination = {\n                        context: tagsContextInUrl,\n                        pages: Array.from({length: totalPages}, (v, k) => k + 1),\n                        totalPosts: totalNumberOfPosts,\n                        totalPages: totalPages,\n                        currentPage: currentPage,\n                        postsPerPage: postsPerPage,\n                        nextPage: nextPage,\n                        previousPage: previousPage,\n                        nextPageUrl: URLHelper.createPaginationPermalink(this.siteConfig, this.themeConfig, nextPage, 'tag', tagSlug, addIndexHtml),\n                        previousPageUrl: URLHelper.createPaginationPermalink(this.siteConfig, this.themeConfig, previousPage, 'tag', tagSlug, addIndexHtml)\n                    };\n\n                    let additionalContexts = [];\n\n                    if (offset > 0) {\n                        additionalContexts = ['pagination', 'tag-pagination'];\n                    }\n\n                    this.menuContext = ['tag', tagSlug];\n\n                    if (!compiledTemplates[fileSlug]) {\n                        fileSlug = 'DEFAULT';\n                    }\n\n                    inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n                    let tagViewConfig = this.cachedItems.tags[tagIDs[i]].tagViewConfig;\n                    this.globalContext = this.createGlobalContext('tag', additionalContexts, {\n                        pagination, \n                        isFirstPage: currentPage === 1,\n                        isLastPage: currentPage === totalPages,\n                        currentPage\n                    }, tagSlug, tagViewConfig, context);\n                    let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n                    if (this.plugins.hasModifiers('htmlOutput')) {\n                        output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                    }\n\n                    if (offset === 0) {\n                        this.templateHelper.saveOutputTagFile(tagSlug, output, tagID !== false);\n                    } else if (tagID === false) {\n                        // We increase the current page number as we need to start URLs from tag-slug/page/2\n                        this.templateHelper.saveOutputTagPaginationFile(tagSlug, currentPage, output);\n                    }\n                }\n            }\n\n            this.sendProgress(Math.ceil(60 + (progressIncrease * i)), 'Generating tag pages (' + (i + 1) + '/' + tagIDs.length + ')');\n        }\n\n        console.timeEnd('TAGS');\n    }\n\n    /*\n     * Generate author pages\n     */\n    generateAuthors(authorID = false) {\n        if (\n            (\n                this.themeConfig.supportedFeatures && \n                this.themeConfig.supportedFeatures.authorPages === false\n            ) || RendererHelpers.getRendererOptionValue('createAuthorPages', this.themeConfig) === false\n        ) {\n            return false;\n        }\n\n        console.time('AUTHORS');\n        // Create directory for authors\n        let authorsDirPath = path.join(this.outputDir, this.siteConfig.advanced.urls.authorsPrefix);\n\n        if (this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix && this.siteConfig.advanced.urls.postsPrefix) {\n            authorsDirPath = path.join(\n                this.outputDir, \n                this.siteConfig.advanced.urls.postsPrefix, \n                this.siteConfig.advanced.urls.authorsPrefix\n            );\n        }\n\n        fs.ensureDirSync(authorsDirPath);\n\n        // Get authors\n        let authorsIDs = [];\n        let authorsUsernames = [];\n        let inputFile = 'author.hbs';\n        let authorTemplates = [];\n        let queryCondition = '';\n\n        if (authorID !== false) {\n            queryCondition = `\n                WHERE \n                    a.id = ${parseInt(authorID, 10)}\n            `;\n        }\n        let authorsData = this.db.prepare(`\n            SELECT\n                a.id AS id,\n                a.username AS slug,\n                a.config AS config,\n                a.additional_data AS additional_data,\n                COUNT(p.id) AS posts_number\n            FROM\n                authors AS a\n            LEFT JOIN\n                posts AS p\n            ON\n                CAST(p.authors AS INTEGER) = a.id\n            ${queryCondition}\n            GROUP BY\n                a.id\n            ORDER BY\n                a.username ASC\n        `).all();\n        \n        authorsData = authorsData.map(authorData => {\n            try {\n                authorData.config = JSON.parse(authorData.config);\n            } catch (e) {\n                authorData.config = '';\n                console.log('[WARNING] Wrong author #' + authorData.id + ' config - invalid JSON value');\n            }\n\n            return authorData;\n        });\n\n        // Remove empty authors - without posts\n        if (!this.siteConfig.advanced.displayEmptyAuthors && authorID === false) {\n            authorsData = authorsData.filter(authorData => {\n                return authorData.posts_number > 0;\n            });\n        }\n\n        // Simplify the structure - change arrays into single value\n        authorsIDs = authorsData.map(authorData => authorData.id);\n        authorsUsernames = authorsData.map(authorData => authorData.slug);\n        authorTemplates = authorsData.map(authorData => {\n            if (authorData.config && authorData.config.template) {\n                if (authorData.config.template === '' || !this.themeConfig.authorTemplates[authorData.config.template]) {\n                    return 'DEFAULT';\n                } else {\n                    return authorData.config.template;\n                }\n            }\n\n            return 'DEFAULT';\n        });\n\n\n        // Load templates\n        let compiledTemplates = {};\n        compiledTemplates['DEFAULT'] = this.compileTemplate(inputFile);\n\n        if (!compiledTemplates['DEFAULT']) {\n            return false;\n        }\n\n        for (let i = 0; i < authorTemplates.length; i++) {\n            let fileSlug = authorTemplates[i];\n\n            // When we meet default template - skip the compilation process\n            if (fileSlug === '' || fileSlug === 'DEFAULT') {\n                continue;\n            }\n\n            compiledTemplates[fileSlug] = this.compileTemplate('author-' + fileSlug + '.hbs');\n\n            if (!compiledTemplates[fileSlug]) {\n                return false;\n            }\n        }\n\n        // Render author sites\n        for (let i = 0; i < authorsData.length; i++) {\n            let contextGenerator = new RendererContextAuthor(this);\n            let fileSlug = 'DEFAULT';\n            fileSlug = authorTemplates[i] === '' ? 'DEFAULT' : authorTemplates[i];\n\n            // Detect if we have enough posts to create pagination\n            let totalNumberOfPosts = this.cachedItems.authors[authorsIDs[i]].postsNumber;\n            let postsPerPage = parseInt(this.themeConfig.config.authorsPostsPerPage, 10);\n            let authorUsername = URLHelper.createSlug(authorsUsernames[i]);\n\n            if (isNaN(postsPerPage)) {\n                postsPerPage = 5;\n            }\n\n            if (totalNumberOfPosts <= postsPerPage || postsPerPage <= 0 || this.siteConfig.advanced.authorNoPagination || authorID !== false) {\n                let context = contextGenerator.getContext(authorsIDs[i], 0, postsPerPage);\n                this.menuContext = ['author', authorUsername];\n                \n                if (!compiledTemplates[fileSlug]) {\n                    fileSlug = 'DEFAULT';\n                }\n\n                inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n                let authorViewConfig = this.cachedItems.authors[authorsIDs[i]].authorViewConfig;\n                this.globalContext = this.createGlobalContext('author', [], false, authorUsername, authorViewConfig, context);\n                let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n                if (this.plugins.hasModifiers('htmlOutput')) {\n                    output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                }\n\n                this.templateHelper.saveOutputAuthorFile(authorUsername, output, authorID !== false);\n            } else {\n                let addIndexHtml = this.previewMode || this.siteConfig.advanced.urls.addIndex;\n\n                // If user set postsPerPage field to -1 - set it for calculations to 999\n                postsPerPage = postsPerPage == -1 ? 999 : postsPerPage;\n\n                for (let offset = 0; offset < totalNumberOfPosts; offset += postsPerPage) {\n                    let context = contextGenerator.getContext(authorsIDs[i], offset, postsPerPage);\n\n                    // Add pagination data to the global context\n                    let currentPage = 1;\n                    let totalPages = 0;\n\n                    if (postsPerPage > 0) {\n                        currentPage = parseInt(offset / postsPerPage, 10) + 1;\n                        totalPages = Math.ceil(totalNumberOfPosts / postsPerPage);\n                    }\n\n                    let nextPage = (currentPage < totalPages) ? currentPage + 1 : false;\n                    let previousPage = (currentPage > 1) ? currentPage - 1 : false;\n                    let authorsContextInUrl = this.siteConfig.advanced.urls.authorsPrefix + '/' + authorUsername;\n\n                    if (this.siteConfig.advanced.urls.postsPrefix && this.siteConfig.advanced.urls.authorsPrefixAfterPostsPrefix) {\n                        authorsContextInUrl = this.siteConfig.advanced.urls.postsPrefix + '/' + authorsContextInUrl;\n                    }\n\n                    let pagination = {\n                        context: authorsContextInUrl,\n                        pages: Array.from({length: totalPages}, (v, k) => k + 1),\n                        totalPosts: totalNumberOfPosts,\n                        totalPages: totalPages,\n                        currentPage: currentPage,\n                        postsPerPage: postsPerPage,\n                        nextPage: nextPage,\n                        previousPage: previousPage,\n                        nextPageUrl: URLHelper.createPaginationPermalink(this.siteConfig, this.themeConfig, nextPage, 'author', authorUsername, addIndexHtml),\n                        previousPageUrl: URLHelper.createPaginationPermalink(this.siteConfig, this.themeConfig, previousPage, 'author', authorUsername, addIndexHtml)\n                    };\n\n                    let additionalContexts = [];\n\n                    if (offset > 0) {\n                        additionalContexts = ['pagination', 'author-pagination'];\n                    }\n\n                    this.menuContext = ['author', authorUsername];\n\n                    if (!compiledTemplates[fileSlug]) {\n                        fileSlug = 'DEFAULT';\n                    }\n\n                    inputFile = inputFile.replace('.hbs', '') + (fileSlug === 'DEFAULT' ? '' : '-' + fileSlug) + '.hbs';\n                    let authorViewConfig = this.cachedItems.authors[authorsIDs[i]].authorViewConfig;\n                    this.globalContext = this.createGlobalContext('author', additionalContexts, {\n                        pagination,\n                        isFirstPage: currentPage === 1,\n                        isLastPage: currentPage === totalPages,\n                        currentPage\n                    }, authorUsername, authorViewConfig, context);\n                    let output = this.renderTemplate(compiledTemplates[fileSlug], context, this.globalContext, inputFile);\n\n                    if (this.plugins.hasModifiers('htmlOutput')) {\n                        output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n                    }\n\n                    if (offset === 0) {\n                        this.templateHelper.saveOutputAuthorFile(authorUsername, output, authorID !== false);\n                    } else if (authorID === false) {\n                        // We increase the current page number as we need to start URLs from /authors/author-username/page/2\n                        this.templateHelper.saveOutputAuthorPaginationFile(authorUsername, currentPage, output);\n                    }\n                }\n            }\n        }\n        console.timeEnd('AUTHORS');\n    }\n\n    /*\n     * Generate the 404 error page (if supported in the theme)\n     */\n    generate404s() {\n        if (\n            (\n                this.themeConfig.supportedFeatures && \n                this.themeConfig.supportedFeatures.errorPage === false\n            ) || !RendererHelpers.getRendererOptionValue('create404Page', this.themeConfig) === false\n        ) {\n            return false;\n        }\n\n        console.time(\"404\");\n        // Load template\n        let inputFile = '404.hbs';\n        let template = this.templateHelper.loadTemplate(inputFile);\n        let compiledTemplate = this.compileTemplate(inputFile);\n\n        if (!compiledTemplate) {\n            return false;\n        }\n\n        // Render index site\n        let contextGenerator = new RendererContext404(this);\n        let context = contextGenerator.getContext();\n        this.menuContext = ['404'];\n        this.globalContext = this.createGlobalContext('404', [], false, false, false, context);\n        let output = this.renderTemplate(compiledTemplate, context, this.globalContext, inputFile);\n\n        if (this.plugins.hasModifiers('htmlOutput')) {\n            output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n        }\n\n        this.templateHelper.saveOutputFile(this.siteConfig.advanced.urls.errorPage, output);\n        console.timeEnd(\"404\");\n    }\n\n    /*\n     * Generate the 404 error page (if supported in the theme)\n     */\n    generateSearch() {\n        if (\n            (\n                this.themeConfig.supportedFeatures && \n                this.themeConfig.supportedFeatures.searchPage === false\n            ) || RendererHelpers.getRendererOptionValue('createSearchPage', this.themeConfig) === false\n        ) {\n            return false;\n        }\n\n        console.time(\"SEARCH\");\n        // Load template\n        let inputFile = 'search.hbs';\n        let compiledTemplate = this.compileTemplate('search.hbs');\n\n        if (!compiledTemplate) {\n            return false;\n        }\n\n        // Render index site\n        let contextGenerator = new RendererContextSearch(this);\n        let context = contextGenerator.getContext();\n        this.menuContext = ['search'];\n        this.globalContext = this.createGlobalContext('search', [], false, false, false, context);\n        let output = this.renderTemplate(compiledTemplate, context, this.globalContext, inputFile);\n\n        if (this.plugins.hasModifiers('htmlOutput')) {\n            output = this.plugins.runModifiers('htmlOutput', this, output, [this.globalContext, context]); \n        }\n\n        this.templateHelper.saveOutputFile(this.siteConfig.advanced.urls.searchPage, output);\n        console.timeEnd(\"SEARCH\");\n    }\n\n    /*\n     * Create the override.css file and merge it with main.css file\n     */\n    generateCSS() {\n        console.time('CSS');\n\n        let themeVariablesCSS = this.getThemeVariablesCSS();\n        let mainCSS = this.getMainCSS();\n        let gdprPopupCSS = this.getGdprPopupCSS();\n        let overrideCSS = this.getOverrideCSS();\n        let customCSS = this.getCustomCSS();\n        // Concatenate all CSS codes into one codebase\n        let styleCSS = themeVariablesCSS + mainCSS + gdprPopupCSS + overrideCSS + customCSS;\n        let newFileName = path.join(this.themeDir, this.themeConfig.files.assetsPath, 'css', 'style.css');\n        let overridedNewFileName = UtilsHelper.fileIsOverrided(this.themeDir, newFileName);\n\n        if (overridedNewFileName) {\n            newFileName = overridedNewFileName;\n        }\n\n        // minify CSS if user enabled it\n        if (this.siteConfig.advanced.cssCompression) {\n            styleCSS = new CleanCSS({ compatibility: '*', rebase: false }).minify(styleCSS);\n            console.log('CSS stats: ' + styleCSS.stats.efficiency + ' (' + styleCSS.stats.minifiedSize + '/' + styleCSS.stats.originalSize + ')');\n            styleCSS = styleCSS.styles;\n        }\n\n        fs.writeFileSync(newFileName, styleCSS, {'flags': 'w'});\n        console.timeEnd('CSS');\n    }\n\n    /**\n     * Returns compiled CSS based on theme-variables.js\n     */\n    getThemeVariablesCSS() {\n        let themeVariablesPath = path.join(this.themeDir, 'theme-variables.js');\n        let overridedThemeVariablesPath = UtilsHelper.fileIsOverrided(this.themeDir, themeVariablesPath);\n        \n        if (overridedThemeVariablesPath) {\n            themeVariablesPath = overridedThemeVariablesPath;\n        }\n\n        // check if the theme contains theme-variables.js file\n        if (UtilsHelper.fileExists(themeVariablesPath)) {\n            try {\n                let generateOverride = UtilsHelper.requireWithNoCache(themeVariablesPath);\n                let customConfig = JSON.parse(JSON.stringify(this.themeConfig.customConfig));\n                let pageConfig = JSON.parse(JSON.stringify(this.themeConfig.pageConfig));\n                let postConfig = JSON.parse(JSON.stringify(this.themeConfig.postConfig));\n                let commonConfig = JSON.parse(JSON.stringify(this.themeConfig.config));\n                return generateOverride(customConfig, postConfig, commonConfig, pageConfig);\n            } catch(e) {\n                this.errorLog.push({\n                    message: 'An error (1003) occurred during preparing CSS theme variables.',\n                    desc: e.message + \"\\n\\n\" + e.stack\n                });\n            }\n        }\n\n        return '';\n    }\n\n    /**\n     * Returns the main CSS of the theme\n     */\n    getMainCSS () {\n        let cssPath = path.join(this.themeDir, this.themeConfig.files.assetsPath, 'css', 'main.css');\n        let overridedCssPath = UtilsHelper.fileIsOverrided(this.themeDir, cssPath);\n\n        if (overridedCssPath) {\n            cssPath = overridedCssPath;\n        }\n\n        return FileHelper.readFileSync(cssPath, 'utf8');\n    }\n\n    /**\n     * Creates CSS for GDPR popup\n     */\n    getGdprPopupCSS () {\n        if (this.siteConfig.advanced.gdpr.enabled) {\n            return Gdpr.popupCssOutput();\n        }\n\n        return '';\n    }\n\n    /**\n     * Returns compiled CSS based on visual-override.js\n     */\n    getOverrideCSS () {\n        let overridePath = path.join(this.themeDir, 'visual-override.js');\n        let overridedOverridePath = UtilsHelper.fileIsOverrided(this.themeDir, overridePath);\n\n        if (overridedOverridePath) {\n            overridePath = overridedOverridePath;\n        }\n\n        // check if the theme contains visual-override.js file\n        if (UtilsHelper.fileExists(overridePath)) {\n            try {\n                let generateOverride = UtilsHelper.requireWithNoCache(overridePath);\n                let customConfig = JSON.parse(JSON.stringify(this.themeConfig.customConfig));\n                let pageConfig = JSON.parse(JSON.stringify(this.themeConfig.pageConfig));\n                let postConfig = JSON.parse(JSON.stringify(this.themeConfig.postConfig));\n                let commonConfig = JSON.parse(JSON.stringify(this.themeConfig.config));\n                return generateOverride(customConfig, postConfig, commonConfig, pageConfig);\n            } catch(e) {\n                this.errorLog.push({\n                    message: 'An error (1003) occurred during preparing CSS overrides.',\n                    desc: e.message + \"\\n\\n\" + e.stack\n                });\n            }\n        }\n\n        return '';\n    }\n\n    /**\n     * Returns additional custom CSS code\n     */\n    getCustomCSS () {\n        let customCSSPath = path.join(this.sitesDir, this.siteName, 'input', 'config', 'custom-css.css');\n\n        if (UtilsHelper.fileExists(customCSSPath)) {\n            return FileHelper.readFileSync(customCSSPath, 'utf8');\n        }\n\n        return '';\n    }\n\n    /*\n     * Create feeds\n     */\n    generateFeeds() {\n        if (!this.siteConfig.advanced.feed.enableRss && !this.siteConfig.advanced.feed.enableJson) {\n            return;\n        }\n\n        console.time('FEEDS');\n\n        if (this.siteConfig.advanced.feed.enableRss) {\n            this.generateFeed('xml');\n        }\n\n        if (this.siteConfig.advanced.feed.enableJson) {\n            this.generateFeed('json');\n        }\n\n        console.timeEnd('FEEDS');\n    }\n\n    /*\n     * Create XML/JSON feed file\n     */\n    generateFeed(format = 'xml') {\n        let compiledTemplate = this.compileTemplate('feed-' + format + '.hbs');\n\n        if(!compiledTemplate) {\n            return false;\n        }\n\n        // Render feed view\n        let contextGenerator = new RendererContextFeed(this);\n        let numberOfPosts = this.siteConfig.advanced.feed.numberOfPosts;\n\n        if(typeof numberOfPosts === \"undefined\") {\n            numberOfPosts = 10;\n        }\n\n        let context = contextGenerator.getContext(numberOfPosts);\n        let output = this.renderTemplate(compiledTemplate, context, false, 'feed-' + format + '.hbs');\n\n        if (format === 'xml' && this.plugins.hasModifiers('feedXmlOutput')) {\n            output = this.plugins.runModifiers('feedXmlOutput', this, output, context); \n        }\n\n        if (format === 'json' && this.plugins.hasModifiers('feedJsonOutput')) {\n            output = this.plugins.runModifiers('feedJsonOutput', this, output, context); \n        }\n\n        this.templateHelper.saveOutputFile('feed.' + format, output);\n    }\n\n    async generateSitemap() {\n        if (!this.siteConfig.advanced.sitemapEnabled) {\n            return;\n        }\n\n        console.time(\"SITEMAP\");\n        let sitemapGenerator = new Sitemap(this.db, this.outputDir, this.siteConfig, this.themeConfig);\n        await sitemapGenerator.create();\n        console.timeEnd(\"SITEMAP\");\n    }\n\n    /**\n     * Copy input files to the output directory\n     */\n    async copyFiles() {\n        console.time(\"FILES\");\n        let postIDs = Object.keys(this.cachedItems.posts);\n        let pageIDs = Object.keys(this.cachedItems.pages);\n\n        FilesHelper.copyRootFiles(this.inputDir, this.outputDir);\n        await FilesHelper.copyAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        FilesHelper.copyDynamicAssetsFiles(this.themeDir, this.outputDir, this.themeConfig);\n        await FilesHelper.copyMediaFiles(this.inputDir, this.outputDir, postIDs, pageIDs);\n        await FilesHelper.copyPluginFiles(this.inputDir, this.outputDir, this.pluginsDir);\n        await FilesHelper.removeEmptyDirectories(this.outputDir);\n        console.timeEnd(\"FILES\");\n    }\n\n    loadContentStructure() {\n        console.time(\"CONTENT DATA\");\n        let globalContextGenerator = new RendererContext(this);\n        this.cachedItems = {\n            pages: {},\n            pagesStructure: {},\n            postTags: {},\n            posts: {},\n            tags: {},\n            mainTags: {},\n            tagsPostCounts: {},\n            authors: {},\n            authorsPostCounts: {},\n            featuredImages: {\n                authors: {},\n                pages: {},\n                posts: {},\n                tags: {}\n            }\n        };\n        globalContextGenerator.getCachedItems();\n        this.contentStructure = globalContextGenerator.getContentStructure();\n\n        console.timeEnd(\"CONTENT DATA\");\n    }\n\n    loadCommonData() {\n        console.time(\"COMMON DATA\");\n        let globalContextGenerator = new RendererContext(this);\n        let menusData = globalContextGenerator.getMenus();\n        let menus = menusData.assigned;\n        let unassignedMenus = menusData.unassigned;\n\n        this.commonData = {\n            tags: globalContextGenerator.getAllTags(),\n            mainTags: globalContextGenerator.getAllMainTags(),\n            authors: globalContextGenerator.getAuthors(),\n            pages: globalContextGenerator.getPages(),\n            menus: menus,\n            unassignedMenus: unassignedMenus,\n            featuredPosts: {\n                homepage: globalContextGenerator.getFeaturedPosts('homepage'),\n                tag: globalContextGenerator.getFeaturedPosts('tag'),\n                author: globalContextGenerator.getFeaturedPosts('author')\n            },\n            hiddenPosts: globalContextGenerator.getHiddenPosts()\n        };\n        console.timeEnd(\"COMMON DATA\");\n    }\n\n    createGlobalContext(context, additionalContexts = [], paginationData = false, itemSlug = false, itemConfig = false, itemContext = false) {\n        let globalContextGenerator = new RendererContext(this);\n        return globalContextGenerator.getGlobalContext(context, additionalContexts, paginationData, itemSlug, itemConfig, itemContext);\n    }\n\n    compileTemplate(inputFile) {\n        let compiledTemplate = false;\n        let template = this.templateHelper.loadTemplate(inputFile);\n\n        if((inputFile === 'feed-xml.hbs' || inputFile === 'feed-json.hbs') && !template) {\n            // Load default feed.hbs file if it not exists inside the theme directory\n            let feedPath = path.join(__dirname, '..', '..', '..', 'default-files', 'theme-files', inputFile);\n            template = FileHelper.readFileSync(feedPath, 'utf8');\n        }\n\n        if(!template) {\n            this.errorLog.push({\n                message: 'File ' + inputFile + ' does not exist.',\n                desc: ''\n            });\n        }\n\n        try {\n            compiledTemplate = Handlebars.compile(template);\n        } catch(e) {\n            this.errorLog.push({\n                message: 'An error (1001) occurred during parsing ' + inputFile + ' file.',\n                desc: e.message + \"\\n\\n\" + e.stack\n            });\n\n            return false;\n        }\n\n        return compiledTemplate;\n    }\n\n    renderTemplate(compiledTemplate, context, globalContext, inputFile) {\n        let output = '';\n\n        try {\n            output = compiledTemplate(context, {\n                data: globalContext\n            });\n        } catch(e) {\n            this.errorLog.push({\n                message: 'An error (1002) occurred during parsing ' + inputFile + ' file.',\n                desc: e.message + \"\\n\\n\" + e.stack\n            });\n\n            return '';\n        }\n\n        if (this.previewMode) {\n            output = output.replace(/file:\\/\\/\\/\\//gmi, 'file:///');\n        }\n\n        return output;\n    }\n\n    /**\n     * Create robots.txt file if it is not created by user\n     */\n    createRobotsTxt () {\n        let robotsTxtPath = path.join(this.outputDir, 'robots.txt');\n\n        // Check if robots.txt exists - if yes, do nothing\n        if (UtilsHelper.fileExists(robotsTxtPath)) {\n            return;\n        }\n\n        // Create robots.txt \n        let robotsTxtContent = '';\n\n        if (this.siteConfig.advanced && this.siteConfig.advanced.noIndexThisPage) {\n            robotsTxtContent = `User-agent: *\\nDisallow: /`;\n        } else {\n            if (this.siteConfig.advanced && this.siteConfig.advanced.noIndexForChatGPTUser) {\n                robotsTxtContent += `User-agent: ChatGPT-User\\nDisallow: /\\n`;\n            }\n\n            if (this.siteConfig.advanced && this.siteConfig.advanced.noIndexForChatGPTBot) {\n                robotsTxtContent += `User-agent: GPTBot\\nDisallow: /\\n`;\n            }\n\n            if (this.siteConfig.advanced && this.siteConfig.advanced.noIndexForCommonCrawlBots) {\n                robotsTxtContent += `User-agent: CCBot\\nDisallow: /\\n`;\n            }\n\n            robotsTxtContent += `User-agent: *\\nDisallow:\\n`;\n\n            if (this.siteConfig.advanced.sitemapEnabled && !this.siteConfig.deployment.relativeUrls) {\n                robotsTxtContent += `Sitemap: ${this.siteConfig.originalDomain}/sitemap.xml`;\n            }\n        }\n\n        fs.writeFileSync(robotsTxtPath, robotsTxtContent, 'utf8');\n    }\n\n    /**\n     * Make URLs in the HTML files relative\n     */\n    async relativizeUrls () {\n        let catalog = this.outputDir;\n        let files = await listAll([catalog], { recurse: true, flatten: true });\n        files = files.filter(file => file.path.substr(-5) === '.html' && file.mode.dir === false);\n        files = files.map(file => file.path);\n        files = files.map(file => file.replace(catalog, ''));\n\n        for (let file of files) {\n            this.relativizeUrlsInFile(file, catalog);\n        }\n    }\n\n    /**\n     * Make URLs relative in a given HTML file\n     * \n     * @param {string} file - relative path to file\n     * @param {string} outputDir - output dir\n     */\n    relativizeUrlsInFile (file, outputDir) {\n        let filePath = path.join(outputDir, file);\n        let content = FileHelper.readFileSync(filePath, 'utf8');\n        let depth = file.replace(/\\\\/gmi, '/').split('/').length - 2;\n        let relativeDomain = './' + '../'.repeat(depth);\n\n        if (relativeDomain.length) {\n            relativeDomain = relativeDomain.slice(0, -1);\n        }\n\n        content = content.replace(/#PUBLII_RELATIVE_URL_BASE#/gmi, relativeDomain);\n        fs.writeFileSync(filePath, content, 'utf8');\n    }\n\n    /**\n     * Load plugins\n     */\n    loadPlugins () {\n        let sitePath = path.join(this.sitesDir, this.siteName, 'input', 'config'); \n        let sitePluginsConfigPath = path.join(sitePath, 'site.plugins.json');\n\n        if (!fs.existsSync(sitePluginsConfigPath)) {\n            return;\n        }\n\n        let pluginsConfig = FileHelper.readFileSync(sitePluginsConfigPath);\n\n        try {\n            pluginsConfig = JSON.parse(pluginsConfig);\n        } catch (e) {\n            console.log('(!) Error during loading plugins config for site ', siteName);\n            return;\n        }\n\n        let pluginNames = Object.keys(pluginsConfig);\n\n        for (let i = 0; i < pluginNames.length; i++) {\n            let pluginName = pluginNames[i];\n\n            if (!pluginsConfig[pluginName]) {\n                continue;\n            }\n\n            let pluginPath = path.join(this.appDir, 'plugins', pluginName, 'main.js');\n            let PluginInstance = require(pluginPath);\n            let pluginSavedConfig = this.loadPluginConfig(pluginName, this.siteName);\n            let plugin = new PluginInstance(this.plugins, pluginName, pluginSavedConfig);\n            \n            if (typeof plugin.addInsertions !== 'undefined') {\n                plugin.addInsertions();\n            }\n\n            if (typeof plugin.addModifiers !== 'undefined') {\n                plugin.addModifiers();\n            }\n\n            if (typeof plugin.addEvents !== 'undefined') {\n                plugin.addEvents();\n            }\n        }\n    }\n\n    loadPluginConfig (pluginName, siteName) {\n        let pluginPath = path.join(this.appDir, 'plugins', pluginName, 'plugin.json');\n        let pluginConfigPath = path.join(this.sitesDir, siteName, 'input', 'config', 'plugins', pluginName + '.json');\n        let pluginData = null;\n        let pluginSavedConfig = null;\n        let output = {};\n\n        if (fs.existsSync(pluginPath)) {\n            try {\n                pluginData = FileHelper.readFileSync(pluginPath, 'utf8');\n                pluginData = JSON.parse(pluginData);\n            } catch (e) {\n                pluginData = {};\n            }\n        } else {\n            pluginData =  {};\n        }\n\n        if (fs.existsSync(pluginConfigPath)) {\n            try {\n                pluginSavedConfig = FileHelper.readFileSync(pluginConfigPath, 'utf8');\n                pluginSavedConfig = JSON.parse(pluginSavedConfig);\n            } catch (e) {\n                pluginSavedConfig = {};\n            }\n        } else {\n            pluginSavedConfig = {};\n        }\n\n        let settings = pluginData.config.map(field => {\n            if (field.type !== 'separator') {\n                if (pluginSavedConfig && typeof pluginSavedConfig[field.name] !== 'undefined') {\n                    return {\n                        name: field.name, \n                        value: pluginSavedConfig[field.name]\n                    };\n                }\n\n                return {\n                    name: field.name, \n                    value: field.value\n                };\n            }\n\n            return false;\n        });\n\n        for (let setting of settings) {\n            if (setting) {\n                output[setting.name] = setting.value;\n            }\n        }\n\n        return output;\n    }\n\n    getPluginsConfig () {\n        let pluginsHelper = new Plugins(this.appDir, this.sitesDir);\n        let pluginsConfig = pluginsHelper.loadSitePluginsConfig(this.siteConfig.name);\n        let pluginNames = Object.keys(pluginsConfig);\n        let siteName = this.siteConfig.name;\n        let config = {};\n\n        for (let i = 0; i < pluginNames.length; i++) {\n            let pluginName = pluginNames[i];\n            \n            config[pluginName] = {\n                state: pluginsConfig[pluginName]\n            };\n\n            if (pluginsConfig[pluginName]) {\n                config[pluginName].config = this.loadPluginConfig(pluginName, siteName)\n            };\n        }\n\n        return config;\n    }\n\n    /**\n     * Trigger events during rendering process\n     */\n    triggerEvent (eventName) {\n        if (this.plugins.hasEvents(eventName)) {\n            this.plugins.runEvents(eventName, this); \n        }\n    }\n}\n\nmodule.exports = Renderer;\n"
  },
  {
    "path": "app/back-end/modules/render-html/text-renderers/blockeditor.js",
    "content": "const blocksMapping = require('./../../../../src/components/block-editor/blocks-mapping.js');\n\nclass BlocksToHtml {\n    static parse (inputJson) {\n        let outputText = '';\n\n        if (inputJson.trim() === '') {\n            return outputText;\n        }\n\n        outputText = [];\n        inputJson = JSON.parse(inputJson);\n\n        for (let block of inputJson) {\n            try {\n                let blockParser = blocksMapping[block.type];\n                let blockContent = blockParser(block);\n                outputText.push(blockContent);\n            } catch (err) {\n                console.error(err);\n            }\n        }\n\n        return outputText.join(\"\\n\");\n    }\n}\n\nmodule.exports = BlocksToHtml;\n"
  },
  {
    "path": "app/back-end/modules/render-html/text-renderers/markdown.js",
    "content": "const marked = require('marked');\n\nclass MarkdownToHtml {\n    static parse (inputText) {\n        // Added support for image sizes - based on: https://github.com/markedjs/marked/issues/1279#issuecomment-1000908564\n        let imageSizeLink = /^!?\\[((?:\\[[^\\[\\]]*\\]|\\\\[\\[\\]]?|`[^`]*`|[^\\[\\]\\\\])*?)\\]\\(\\s*(<(?:\\\\[<>]?|[^\\s<>\\\\])*>|(?:\\\\[()]?|\\([^\\s\\x00-\\x1f()\\\\]*\\)|[^\\s\\x00-\\x1f()\\\\])*?(?:\\s+=(?:[\\w%]+)?x(?:[\\w%]+)?)?)(?:\\s+(\"(?:\\\\\"?|[^\"\\\\])*\"|'(?:\\\\'?|[^'\\\\])*'|\\((?:\\\\\\)?|[^)\\\\])*\\)))?\\s*\\)/;\n        marked.Lexer.rules.inline.normal.link = imageSizeLink;\n        marked.Lexer.rules.inline.gfm.link = imageSizeLink;\n        marked.Lexer.rules.inline.breaks.link = imageSizeLink;\n        let overridedRenderer = new marked.Renderer();\n        \n        overridedRenderer.image = function (href, title, text) {\n            if (this.options.baseUrl && !originIndependentUrl.test(href)) {\n                href = resolveUrl(this.options.baseUrl, href);\n            }\n        \n            let size = href.match(/\\s+=([\\w%]+)?x([\\w%]+)?$/);\n            let dimensions = '';\n        \n            if (size) {\n                href = href.substring(0, href.length - size[0].length);\n                dimensions = ` width=\"${size[1]}\" height=\"${size[2]}\"`;\n            }\n\n            if (typeof title === 'string' && title.trim() !== '') {\n                title = '<figcaption>' + title + '</figcaption>';\n            } else {\n                title = '';\n            }\n        \n            return `<figure class=\"post__image\"><img src=\"${href}\" alt=\"${text}\"${dimensions}>${title}</figure>`;\n        };\n\n        // Solve issues with rendering <figure> elements inside paragraphs\n        overridedRenderer.paragraph = function(text) {\n            if (text.startsWith('<figure') && text.endsWith('</figure>')) {\n                return text;\n            } else {\n                return '<p>' + text + '</p>';\n            }\n        };\n        \n        marked.setOptions({\n            smartLists: true,\n            smartypants: true,\n            xhtml: false,\n            renderer: overridedRenderer\n        });\n\n        let outputText = marked.parse(inputText);\n        return outputText;\n    }\n}\n\nmodule.exports = MarkdownToHtml;\n"
  },
  {
    "path": "app/back-end/modules/render-html/validators/theme-config.js",
    "content": "const FileHelper = require('./../../../helpers/file.js');\n\n/**\n * Checks if theme config file meets the requirements\n *\n * @param configPath - path to the file\n * @returns {boolean|string}\n */\nfunction themeConfigValidator(configPath) {\n    let configContent = FileHelper.readFileSync(configPath);\n    let configParsed = false;\n\n    try {\n        configParsed = JSON.parse(configContent);\n    } catch(e) {\n        return 'Invalid JSON structure';\n    }\n\n    if(!configParsed.name) {\n        return 'Missing name field in config.json';\n    }\n\n    if(!configParsed.version) {\n        return 'Missing version field in config.json';\n    }\n\n    if(!configParsed.author) {\n        return 'Missing author field in config.json';\n    }\n\n    return true;\n}\n\nmodule.exports = themeConfigValidator;\n"
  },
  {
    "path": "app/back-end/page.js",
    "content": "/*\n * Page instance\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst slug = require('./helpers/slug');\nconst Model = require('./model.js');\nconst ImageHelper = require('./helpers/image.helper.js');\nconst Pages = require('./pages.js');\nconst Authors = require('./authors.js');\nconst Themes = require('./themes.js');\nconst Utils = require('./helpers/utils.js');\nconst normalizePath = require('normalize-path');\n\nclass Page extends Model {\n    constructor(appInstance, pageData, storeMode = true) {\n        super(appInstance, pageData);\n        this.id = parseInt(pageData.id, 10);\n        this.pagesData = new Pages(appInstance, pageData);\n        this.authorsData = new Authors(appInstance, pageData);\n        this.storeMode = storeMode;\n\n        if (pageData.title) {\n            this.title = pageData.title;\n            this.slug = slug(pageData.slug);\n            this.author = Number(pageData.author).toString();\n            this.status = pageData.status;\n            this.creationDate = pageData.creationDate;\n            this.modificationDate = pageData.modificationDate;\n            this.template = pageData.template;\n            this.additionalData = pageData.additionalData;\n            this.pageViewSettings = pageData.pageViewSettings;\n        }\n\n        // Separated for the case of use the cancel page event\n        this.text = pageData.text ? pageData.text : '';\n        this.featuredImage = pageData.featuredImage ? pageData.featuredImage : '';\n        this.featuredImageFilename = pageData.featuredImageFilename ? pageData.featuredImageFilename : '';\n        this.featuredImageData = pageData.featuredImageData ? pageData.featuredImageData : false;\n    }\n\n    /*\n     * Load page\n     */\n    load() {\n        let results = {\n            \"pages\": [],\n            \"featuredImage\": '',\n            \"mediaPath\": path.join(this.siteDir, 'input', 'media', 'posts'),\n            \"author\": ''\n        };\n        // Get page data\n        let pagesSqlQuery = `SELECT * FROM posts WHERE id = @id`;\n        results.pages = this.db.prepare(pagesSqlQuery).all({ id: this.id });\n\n        // Get author data\n        let authorSqlQuery = `\n            SELECT\n                a.id AS id,\n                a.name AS name\n            FROM\n                authors AS a\n            LEFT JOIN\n                posts AS p\n                ON\n                p.authors = a.id\n            WHERE\n                p.id = @id\n        `;\n\n        results.author = this.db.prepare(authorSqlQuery).all({ id: this.id });\n\n        // Get image data\n        let imageSqlQuery = `\n            SELECT\n                pi.url AS url,\n                pi.additional_data AS additional_data\n            FROM\n                posts AS p\n            LEFT JOIN\n                posts_images AS pi\n                ON\n                    p.featured_image_id = pi.id\n            WHERE\n                p.id = @id;\n        `;\n        results.featuredImage = this.db.prepare(imageSqlQuery).get({ id: this.id });\n\n        // Get the additional data\n        let additionalDataSqlQuery = `\n            SELECT\n                *\n            FROM\n                posts_additional_data\n            WHERE\n                post_id = @id\n                AND\n                key = '_core'\n        `;\n        let additionalDataResult = this.db.prepare(additionalDataSqlQuery).get({ id: this.id });\n\n        if(additionalDataResult && additionalDataResult.value) {\n            results.additionalData = JSON.parse(additionalDataResult.value);\n        } else {\n            results.additionalData = {};\n        }\n        // Get page view settings\n        let pageViewSqlQuery = `\n            SELECT\n                *\n            FROM\n                posts_additional_data\n            WHERE\n                post_id = @id\n                AND\n                key = 'pageViewSettings'\n        `;\n        let pageViewResult = this.db.prepare(pageViewSqlQuery).get({ id: this.id });\n\n        if (pageViewResult && pageViewResult.value) {\n            results.pageViewSettings = JSON.parse(pageViewResult.value);\n            let pageViewSettingsKeys = Object.keys(results.pageViewSettings);\n\n            for (let i = 0; i < pageViewSettingsKeys.length; i++) {\n                if (results.pageViewSettings[pageViewSettingsKeys[i]].type) {\n                    results.pageViewSettings[pageViewSettingsKeys[i]] = results.pageViewSettings[pageViewSettingsKeys[i]].value;\n                }\n            }\n        } else {\n            results.pageViewSettings = {};\n        }\n\n        // Return all results\n        return results;\n    }\n\n    /*\n     * Save page\n     */\n    save() {\n        let sqlQuery = '';\n        this.checkAndPrepareSlug();\n\n        if (this.id === 0) {\n            // Add page data\n            sqlQuery = this.db.prepare(`INSERT INTO posts VALUES(null, @title, @author, @slug, @text, 0, @creationDate, @modificationDate, @status, @template)`);\n            sqlQuery.run({\n                title: this.title,\n                author: this.author,\n                slug: this.slug,\n                text: this.cleanUpContent(this.text),\n                creationDate: this.creationDate,\n                modificationDate: this.modificationDate,\n                status: this.status,\n                template: this.template\n            });\n        } else {\n            // Update page data\n            sqlQuery = this.db.prepare(`UPDATE posts\n                        SET\n                            title = @title,\n                            authors = @author,\n                            slug = @slug,\n                            text = @text,\n                            status = @status,\n                            created_at = @createdAt,\n                            modified_at = @modifiedAt,\n                            template = @template\n                        WHERE\n                            id = @id`);\n            sqlQuery.run({\n                title: this.title,\n                author: this.author,\n                slug: this.slug,\n                text: this.cleanUpContent(this.text),\n                status: this.status,\n                createdAt: this.creationDate,\n                modifiedAt: this.modificationDate,\n                template: this.template,\n                id: this.id\n            });\n        }\n\n        // Get the newly added item ID if necessary\n        if (this.id === 0) {\n            this.id = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n\n            // Move images from the temp directory\n            let tempDirectoryExists = true;\n            let tempImagesDir = path.join(this.siteDir, 'input', 'media', 'posts', 'temp');\n\n            try {\n                fs.statSync(tempImagesDir).isDirectory();\n            } catch (err) {\n                tempDirectoryExists = false;\n            }\n\n            if (tempDirectoryExists) {\n                let finalImagesDir = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString());\n                fs.copySync(tempImagesDir, finalImagesDir);\n                fs.removeSync(tempImagesDir);\n\n                // Update text\n                if(!this.text) {\n                    this.text = '';\n                }\n\n                this.text = this.text.replace(/file:(\\/){1,}/gmi, 'file:///');\n                this.text = this.text.split(normalizePath(tempImagesDir)).join('#DOMAIN_NAME#');\n                this.text = this.text.replace(/file:(\\/){1,}\\#DOMAIN_NAME\\#/gmi, '#DOMAIN_NAME#');\n                sqlQuery = this.db.prepare(`UPDATE posts\n                        SET\n                            text = @text\n                        WHERE\n                            id = @id`);\n                sqlQuery.run({\n                    text: this.text,\n                    id: this.id\n                });\n            }\n        }\n\n        // Save images\n        if(this.featuredImage) {\n            let image = new ImageHelper(this);\n            image.save();\n        } else if(this.id > 0) {\n            let image = new ImageHelper(this);\n            image.delete();\n        }\n\n        // Save additional data\n        this.saveAdditionalData();\n\n        // Save page view settings\n        this.savePageViewSettings();\n\n        // Remove unused images\n        this.checkAndCleanImages();\n\n        // Return new list of the pages\n        return {\n            pageID: this.id,\n            pages: this.pagesData.load(),\n            pagesAuthors: this.pagesData.loadAuthorsXRef(),\n            authors: this.authorsData.load()\n        };\n    }\n\n    /*\n     * Delete page\n     */\n    delete() {\n        this.db.exec(`DELETE FROM posts WHERE id = ${parseInt(this.id, 10)}`);\n        this.db.exec(`DELETE FROM posts_tags WHERE post_id = ${parseInt(this.id, 10)}`);\n        this.db.exec(`DELETE FROM posts_images WHERE post_id = ${parseInt(this.id, 10)}`);\n        this.db.exec(`DELETE FROM posts_additional_data WHERE post_id = ${parseInt(this.id, 10)}`);\n        ImageHelper.deleteImagesDirectory(this.siteDir, 'posts', this.id);\n\n        return true;\n    }\n\n    /*\n     * Duplicate page\n     */\n    duplicate() {\n        // Get the page data\n        let pageToDuplicate = this.db.prepare(`SELECT * FROM posts WHERE id = @id LIMIT 1`).get({ id: this.id });\n        let pageImagesToDuplicate = this.db.prepare(`SELECT * FROM posts_images WHERE post_id = @id`).all({ id: this.id });\n        let pageAdditionalDataToDuplicate = this.db.prepare(`SELECT * FROM posts_additional_data WHERE post_id = @id`).all({ id: this.id });\n\n        // Add duplicate page row\n        if (pageToDuplicate && pageToDuplicate.id) {\n            // Change title (suffix with \" (copy)\")\n            let copySuffix = 1;\n            let modifiedTitle = pageToDuplicate.title;\n            let pageWithTheSameSlug = false;\n            let modifiedSlug = pageToDuplicate.slug;\n            \n            if (modifiedTitle.substr(-7) !== ' (copy)') {\n                modifiedTitle += ' (copy)';\n            }\n\n            if (modifiedSlug.substr(-2).match(/-\\d/)) {\n                modifiedSlug = modifiedSlug.substr(0, modifiedSlug.length - 2);\n            }\n\n            do {\n                copySuffix++;\n                pageWithTheSameSlug = this.db.prepare(`SELECT id FROM posts WHERE slug = @slug LIMIT 1`).get({ slug: modifiedSlug + '-' + copySuffix });\n            } while (pageWithTheSameSlug && pageWithTheSameSlug.id);\n\n            modifiedSlug = modifiedSlug + '-' + copySuffix;\n            let pageStatus = pageToDuplicate.status;\n\n            if (pageStatus.indexOf('draft') === -1) {\n                pageStatus = pageStatus.replace('published', 'draft');\n\n                if (pageStatus.indexOf('draft') === -1) {\n                    pageStatus += ',draft';\n                }\n            }\n\n            let newCopyPageSqlQuery = this.db.prepare(`INSERT INTO posts VALUES(null, @title, @authors, @slug, @text, @featured_image_id, @created_at, @modified_at, @status, @template)`);\n            newCopyPageSqlQuery.run({\n                title: modifiedTitle,\n                authors: pageToDuplicate.authors,\n                slug: modifiedSlug,\n                text: pageToDuplicate.text,\n                featured_image_id: pageToDuplicate.featured_image_id,\n                created_at: Date.now(),\n                modified_at: Date.now(),\n                status: pageStatus,\n                template: pageToDuplicate.template\n            });\n        } else {\n            return false;\n        }\n\n        // Get newly inserted page ID\n        let copiedPageId = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n\n        // Add posts_images row\n        if (pageImagesToDuplicate.length) {\n            let imagesCount = pageImagesToDuplicate.length;\n\n            for (let i = 0; i < imagesCount; i++) {\n                let newCopyPageImagesSqlQuery = this.db.prepare(`INSERT INTO posts_images VALUES(NULL, @copied_page_id, @url, @title, @caption, @additional_data)`);\n                newCopyPageImagesSqlQuery.run({\n                    copied_page_id: copiedPageId,\n                    url: pageImagesToDuplicate[i].url,\n                    title: pageImagesToDuplicate[i].title,\n                    caption: pageImagesToDuplicate[i].caption,\n                    additional_data: pageImagesToDuplicate[i].additional_data\n                });\n            }\n\n            // Get newly inserted page ID\n            let copiedFeaturedImageId = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n            \n            this.db.prepare(`UPDATE posts SET featured_image_id = @featuredImageID WHERE id = @pageID`).run({\n                featuredImageID: copiedFeaturedImageId,\n                pageID: copiedPageId\n            });\n        }\n\n        // Add posts_additional_data\n        if (pageAdditionalDataToDuplicate.length) {\n            let additionalDataCount = pageAdditionalDataToDuplicate.length;\n\n            for (let i = 0; i < additionalDataCount; i++) {\n                let newCopyPageAdditionalDataSqlQuery = this.db.prepare(`INSERT INTO posts_additional_data VALUES(NULL, @copied_page_id, @key, @value)`);\n                newCopyPageAdditionalDataSqlQuery.run({\n                    copied_page_id: copiedPageId,\n                    key: pageAdditionalDataToDuplicate[i].key,\n                    value: pageAdditionalDataToDuplicate[i].value\n                });\n            }\n        }\n\n        // Copy images\n        ImageHelper.copyImagesDirectory(this.siteDir, this.id, copiedPageId);\n\n        return true;\n    }\n\n    /*\n     * Change page status\n     */\n    changeStatus(status, inverse = false) {\n        let selectQuery = this.db.prepare(`SELECT status FROM posts WHERE id = @id`).all({ id: this.id });\n        let currentStatus = selectQuery[0].status.split(',');\n        let resetTemplateIfNeeded = status === 'is-page' ? ', template = \\'*\\'' : '';\n\n        if (!inverse) {\n            if(currentStatus.indexOf(status) === -1) {\n                currentStatus.push(status);\n            }\n        } else {\n            if(currentStatus.indexOf(status) > -1) {\n                currentStatus = currentStatus.filter((pageStatus) => pageStatus !== status);\n            }\n        }\n\n        currentStatus = currentStatus.filter(status => status.trim() !== '');\n\n        let updateQuery = this.db.prepare(`UPDATE\n                                        posts\n                                    SET\n                                        status = @status\n                                        ${resetTemplateIfNeeded}\n                                    WHERE\n                                        id = @id`);\n        updateQuery.run({\n            status: currentStatus.join(','),\n            id: this.id\n        });\n\n        return true;\n    }\n\n    /*\n     * Check and prepare page slug\n     */\n    checkAndPrepareSlug(suffix = 0) {\n        let pageSlug = this.slug;\n        let restrictedSlugs = [];\n\n        if (this.application.sites[this.site].advanced.urls.cleanUrls) {\n            restrictedSlugs = [\n                'assets',\n                'media',\n                this.application.sites[this.site].advanced.urls.authorsPrefix\n            ];\n\n            if (this.application.sites[this.site].advanced.urls.postsPrefix !== '') {\n                restrictedSlugs.push(this.application.sites[this.site].advanced.urls.postsPrefix);\n            }\n\n            if (this.application.sites[this.site].advanced.urls.tagsPrefix !== '') {\n                restrictedSlugs.push(this.application.sites[this.site].advanced.urls.tagsPrefix);\n            }\n        }\n\n        if (suffix > 0) {\n            pageSlug = this.escape(this.slug + '-' + suffix);\n        }\n\n        if (restrictedSlugs.indexOf(pageSlug) > -1) {\n            this.checkAndPrepareSlug(2);\n            return;\n        }\n\n        let result = this.db.prepare(`SELECT slug FROM posts WHERE slug LIKE @slug AND id != @id`).all({\n            slug: pageSlug,\n            id: this.id\n        });\n\n        if (result && result.length) {\n            if(suffix === 0) {\n                suffix = 2;\n            } else {\n                suffix++;\n            }\n\n            this.checkAndPrepareSlug(suffix);\n        } else {\n            this.slug = pageSlug;\n        }\n    }\n\n    /*\n     * Remove unused images\n     */\n    checkAndCleanImages(cancelEvent = false) {\n        let pageDir = this.id;\n\n        if(this.id === 0) {\n            pageDir = 'temp';\n        }\n\n        let imagesDir = path.join(this.siteDir, 'input', 'media', 'posts', (pageDir).toString());\n        let galleryImagesDir = path.join(imagesDir, 'gallery');\n        let pageDirectoryExists = true;\n\n        try {\n            fs.statSync(imagesDir).isDirectory();\n        } catch (err) {\n            pageDirectoryExists = false;\n        }\n\n        if(!pageDirectoryExists) {\n            return;\n        }\n\n        let images = fs.readdirSync(imagesDir);\n        let galleryImages = false;\n\n        if (Utils.dirExists(galleryImagesDir)) {\n            galleryImages = fs.readdirSync(galleryImagesDir);\n        }\n\n        // Get previous text if the page is cancelled and it is a not a new page\n        if (cancelEvent && pageDir !== 'temp') {\n            let textSqlQuery = `\n                    SELECT\n                        text\n                    FROM\n                        posts\n                    WHERE\n                        id = @id\n                `;\n\n            let textResult = this.db.prepare(textSqlQuery).get({ id: pageDir });\n\n            if (textResult && textResult.text) {\n                this.text = textResult.text;\n            }\n        }\n\n        this.cleanImages(images, imagesDir, cancelEvent);\n\n        if (galleryImages) {\n            this.cleanImages(galleryImages, galleryImagesDir, cancelEvent);\n        }\n    }\n\n    /*\n     * Removes images from a given image dir\n     */\n    cleanImages(images, imagesDir, cancelEvent) {\n        let pageDir = this.id;\n        let featuredImage = path.parse(this.featuredImageFilename).base;\n\n        // If page is cancelled - get the previous featured image\n        if (cancelEvent && this.id !== 0) {\n            let featuredImageSqlQuery = `\n                    SELECT\n                        url\n                    FROM\n                        posts_images\n                    WHERE\n                        post_id = @id\n                `;\n\n            let featuredImageResult = this.db.prepare(featuredImageSqlQuery).all({ \n                id: this.id \n            });\n\n            if (featuredImageResult && featuredImageResult.url) {\n                featuredImage = featuredImageResult.url;\n            }\n        }\n\n        if (this.id === 0) {\n            pageDir = 'temp';\n        }\n\n        let imagesInPageViewSettings = [];\n        \n        if (this.pageViewSettings) {\n            imagesInPageViewSettings = Object.values(this.pageViewSettings).filter(item => item.type === \"image\").map(item => item.value);\n        }\n        \n        // Iterate through images\n        for (let i in images) {\n            let imagePath = images[i];\n            let fullPath = path.join(imagesDir, imagePath);\n\n            // Skip dirs and symlinks\n            if(imagePath === '.' || imagePath === '..' || imagePath === 'responsive' || imagePath === 'gallery') {\n                continue;\n            }\n\n            // Remove files which does not exist in the page text, as featured image and pageViewSettings\n            if(\n                (cancelEvent && pageDir === 'temp') ||\n                (\n                    this.text.indexOf(imagePath) === -1 && \n                    imagesInPageViewSettings.indexOf(imagePath) === -1 &&\n                    featuredImage !== imagePath\n                )\n            ) {\n                try {\n                    fs.unlinkSync(fullPath);\n                } catch(e) {\n                    console.error(e);\n                }\n\n                // Remove responsive images\n                this.removeResponsiveImages(fullPath);\n            }\n        }\n    }\n\n    /*\n     * Remove unused responsive images\n     */\n    removeResponsiveImages(originalPath) {\n        let themesHelper = new Themes(this.application, { site: this.site });\n        let currentTheme = themesHelper.currentTheme();\n\n        // If there is no selected theme\n        if (currentTheme === 'not selected') {\n            return;\n        }\n\n        // Load theme config\n        let themeConfig = Utils.loadThemeConfig(path.join(this.siteDir, 'input'), currentTheme);\n\n        // check if responsive images config exists\n        if (Utils.responsiveImagesConfigExists(themeConfig)) {\n            let dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages');\n            let featuredDimensions = Utils.responsiveImagesDimensions(themeConfig, 'featuredImages');\n\n            if (featuredDimensions !== false) {\n                featuredDimensions.forEach(item => {\n                    if (dimensions.indexOf(item) === -1) {\n                        dimensions.push(item);\n                    }\n                });\n            }\n\n            let responsiveImagesDir = path.parse(originalPath).dir;\n            responsiveImagesDir = path.join(responsiveImagesDir, 'responsive');\n\n            if (typeof dimensions === \"boolean\") {\n                return;\n            }\n\n            let forceWebp = !!this.application.sites[this.site]?.advanced?.forceWebp;\n\n            // Remove responsive images of each size\n            for (let dimensionName of dimensions) {\n                let filename = path.parse(originalPath).name;\n                let extension = path.parse(originalPath).ext;\n\n                if (forceWebp && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) {\n                    extension = '.webp'; \n                }\n\n                let responsiveImagePath = path.join(responsiveImagesDir, filename + '-' + dimensionName + extension);\n\n                if (Utils.fileExists(responsiveImagePath)){\n                    fs.unlinkSync(responsiveImagePath);\n                }\n            }\n        }\n    }\n\n    /*\n     * Save additional data\n     */\n    saveAdditionalData() {\n        // Remove old _core additional data\n        this.db.exec(`DELETE FROM posts_additional_data WHERE post_id = ${parseInt(this.id, 10)} AND key = '_core'`);\n\n        // Convert data to JSON string\n        if (typeof this.additionalData !== 'object') {\n            this.additionalData = {};\n        }\n\n        let additionalDataToSave = JSON.stringify(this.additionalData);\n\n        // Store the data\n        let storeSqlQuery = this.db.prepare(`INSERT INTO posts_additional_data VALUES(null, @id, '_core', @data)`);\n        storeSqlQuery.run({\n            id: this.id, \n            data: additionalDataToSave\n        });\n    }\n\n    /*\n     * Save additional data\n     */\n    savePageViewSettings() {\n        // Remove old _core additional data\n        this.db.exec(`DELETE FROM posts_additional_data WHERE post_id = ${parseInt(this.id, 10)} AND key = 'pageViewSettings'`);\n\n        // Convert data to JSON string\n        if(typeof this.pageViewSettings !== 'object') {\n            this.pageViewSettings = {};\n        }\n\n        let dataToSave = JSON.stringify(this.pageViewSettings);\n\n        // Store the data\n        let storeSqlQuery = this.db.prepare(`INSERT INTO posts_additional_data VALUES(null, @id, 'pageViewSettings', @data)`);\n        storeSqlQuery.run({\n            id: this.id, \n            data: dataToSave\n        });\n    }\n\n    /**\n     * Clean up page content from wrong code\n     */\n    cleanUpContent (content) {\n        return content.replace(/\\<figure\\sclass=\\\"post__image\\spost__image\\s/gmi, '<figure class=\"post__image ');\n    }\n}\n\nmodule.exports = Page;\n"
  },
  {
    "path": "app/back-end/pages.js",
    "content": "/*\n * Pages instance\n */\n\nconst Model = require('./model.js');\n\nclass Pages extends Model {\n    constructor(appInstance, pagesData) {\n        super(appInstance, pagesData);\n    }\n\n    /*\n     * Load pages\n     */\n    load () {\n        let sqlQuery = `\n            SELECT \n                p.id AS id, \n                p.title AS title, \n                p.authors AS authors, \n                p.slug AS slug, \n                p.created_at AS created_at, \n                p.modified_at AS modified_at, \n                p.status AS status,\n                pad.value AS additional_data\n            FROM \n                posts AS p\n            LEFT JOIN\n                posts_additional_data AS pad\n                ON\n                pad.post_id = p.id\n            WHERE\n                p.status LIKE '%is-page%' AND\n                (\n                    pad.key = '_core' OR\n                    pad.key IS NULL\n                )\n            ORDER BY \n                id DESC`;\n\n        return this.db.prepare(sqlQuery).all();\n    }\n\n    /*\n     * Load references between pages and authors\n     */\n    loadAuthorsXRef() {\n        let sqlQuery = `\n            SELECT \n                p.id AS pageID,\n                a.id AS authorID,\n                a.name AS authorName\n            FROM \n                posts AS p \n            LEFT JOIN \n                authors AS a \n            ON\n                p.authors = a.id \n            WHERE\n                p.status LIKE '%is-page%'\n            ORDER BY \n                p.id DESC`;\n\n        return this.db.prepare(sqlQuery).all();\n    }\n}\n\nmodule.exports = Pages;\n"
  },
  {
    "path": "app/back-end/plugins.js",
    "content": "/*\n * Plugins instance\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./helpers/file.js');\nconst UtilsHelper = require('./helpers/utils.js');\nconst pluginConfigValidator = require('./helpers/validators/plugin-config.js');\n\nclass Plugins {\n    constructor(appDir, sitesDir) {\n        this.appDir = appDir;\n        this.sitesDir = sitesDir;\n        this.pluginsPath = path.join(this.appDir, 'plugins');\n    }\n\n    /*\n     * Load plugins from a specific path\n     */\n    loadPlugins () {\n        let pathToPlugins = this.pluginsPath;\n        let output = [];\n        let filesAndDirs = fs.readdirSync(pathToPlugins);\n\n        for (let i = 0; i < filesAndDirs.length; i++) {\n            if (filesAndDirs[i][0] === '.' || !UtilsHelper.dirExists(path.join(pathToPlugins, filesAndDirs[i]))) {\n                continue;\n            }\n\n            let configPath = path.join(pathToPlugins, filesAndDirs[i], 'plugin.json');\n\n            // Load only proper plugins\n            if (!fs.existsSync(configPath)) {\n                continue;\n            }\n\n            // Load only properly configured languages\n            if(pluginConfigValidator(configPath) !== true) {\n                continue;\n            }\n\n            let pluginData = FileHelper.readFileSync(configPath, 'utf8');\n            pluginData = JSON.parse(pluginData);\n\n            output.push({\n                scope: pluginData.scope,\n                directory: filesAndDirs[i],\n                name: pluginData.name,\n                version: pluginData.version,\n                author: pluginData.author,\n                minimumPubliiVersion: pluginData.minimumPubliiVersion,\n                assets: pluginData.assets,\n                path: path.join(pathToPlugins, filesAndDirs[i])\n            });\n        }\n\n        return output;\n    }\n\n    /*\n     * Get plugins count\n     */\n    getExistingPluginsDirs () {\n        let pathToPlugins = this.pluginsPath;\n        let output = [];\n        let filesAndDirs = fs.readdirSync(pathToPlugins);\n\n        for (let i = 0; i < filesAndDirs.length; i++) {\n            if (filesAndDirs[i][0] === '.' || !UtilsHelper.dirExists(path.join(pathToPlugins, filesAndDirs[i]))) {\n                continue;\n            }\n\n            output.push(filesAndDirs[i]);\n        }\n\n        return output;\n    }\n\n    /**\n     * Get status of site-specific plugins \n     */\n    getSiteSpecificPluginsState (siteName) {\n        let pluginsConfig = this.loadSitePluginsConfig(siteName);\n        let pluginNames = typeof pluginsConfig === 'object' ? Object.keys(pluginsConfig) : [];\n        let status = {};\n\n        for (let i = 0; i < pluginNames.length; i++) {\n            let pluginName = pluginNames[i];\n            status[pluginName] = !!pluginsConfig[pluginName];\n        }\n\n        return status;\n    }\n\n    /**\n     * Load plugins config for specific site\n     */\n    loadSitePluginsConfig (siteName) {\n        let sitePath = path.join(this.sitesDir, siteName, 'input', 'config'); \n        let sitePluginsConfigPath = path.join(sitePath, 'site.plugins.json');\n\n        if (!fs.existsSync(sitePluginsConfigPath)) {\n            return {};\n        }\n\n        let pluginsConfig = FileHelper.readFileSync(sitePluginsConfigPath);\n\n        try {\n            pluginsConfig = JSON.parse(pluginsConfig);\n            let loadedPlugins = Object.keys(pluginsConfig);\n            let existingPlugins = this.getExistingPluginsDirs();\n\n            if (!UtilsHelper.arraysHaveTheSameContent(existingPlugins, loadedPlugins)) {\n                this.validateLoadedPlugins(siteName, pluginsConfig, existingPlugins);\n            }\n        } catch (e) {\n            console.log('(!) Error during loading plugins config for site ', siteName);\n            console.log(e);\n            return {};\n        }\n\n        return pluginsConfig;\n    }\n\n    /**\n     * Check for non-existing plugins and resave config\n     */\n    validateLoadedPlugins (siteName, configToCheck, existingPlugins) {\n        console.log('(i) Validate site.plugins.json due change of the plugins count');\n        let loadedPlugins = Object.keys(configToCheck);\n\n        for (let i = 0; i < loadedPlugins.length; i++) {\n            let pluginDirectoryName = loadedPlugins[i];\n\n            if (!UtilsHelper.dirExists(path.join(this.pluginsPath, pluginDirectoryName))) {\n                delete configToCheck[pluginDirectoryName];\n                console.log('(i) Removed plugin from site.plugins.json: ', pluginDirectoryName);\n            }\n        }\n\n        for (let i = 0; i < existingPlugins.length; i++) {\n            let existingPluginDirectoryName = existingPlugins[i];\n\n            if (typeof configToCheck[existingPluginDirectoryName] === 'undefined') {\n                configToCheck[existingPluginDirectoryName] = false;\n                console.log('(i) Added disabled plugin to site.plugins.json: ', existingPluginDirectoryName);\n            }\n        }\n\n        this.saveSitePluginsConfig(siteName, configToCheck);\n    }\n\n    /**\n     * Save plugins config \n     */\n    saveSitePluginsConfig (siteName, config) {\n        let sitePath = path.join(this.sitesDir, siteName, 'input', 'config'); \n        let sitePluginsConfigPath = path.join(sitePath, 'site.plugins.json');\n\n        try {\n            fs.writeFileSync(sitePluginsConfigPath, JSON.stringify(config, null, 4));\n        } catch (error) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * Plugin activation\n     */\n    activatePlugin (siteName, pluginName) {\n        let pluginsConfig = this.loadSitePluginsConfig(siteName);\n        pluginsConfig[pluginName] = true;\n        return this.saveSitePluginsConfig(siteName, pluginsConfig);\n    }\n\n    /**\n     * Plugin deactivation\n     */\n    deactivatePlugin (siteName, pluginName) {\n        let pluginsConfig = this.loadSitePluginsConfig(siteName);\n        pluginsConfig[pluginName] = false;\n        return this.saveSitePluginsConfig(siteName, pluginsConfig);\n    }\n\n    /*\n     * Remove specific plugin from the app directory\n     */\n    removePlugin (directory) {\n        fs.removeSync(path.join(this.pluginsPath, directory));\n    }\n\n    getPluginConfig (siteName, pluginName) {\n        let pluginPath = path.join(this.appDir, 'plugins', pluginName, 'plugin.json');\n        let pluginConfigPath = path.join(this.sitesDir, siteName, 'input', 'config', 'plugins', pluginName + '.json');\n        let output = {\n            pluginData: null,\n            pluginConfig: null\n        };\n\n        if (fs.existsSync(pluginPath)) {\n            try {\n                let pluginData = FileHelper.readFileSync(pluginPath, 'utf8');\n                output.pluginData = JSON.parse(pluginData);\n                output.pluginData.path = path.join(this.appDir, 'plugins', pluginName);\n            } catch (e) {\n                return 0;\n            }\n        } else {\n            return pluginPath;\n        }\n\n        if (fs.existsSync(pluginConfigPath)) {\n            try {\n                let pluginConfig = FileHelper.readFileSync(pluginConfigPath, 'utf8');\n                output.pluginConfig = pluginConfig;\n            } catch (e) {\n                output.pluginConfig = {};\n            }\n        }\n\n        return output;\n    }\n\n    savePluginConfig (siteName, pluginName, newConfig) {\n        let pluginConfigPath = path.join(this.sitesDir, siteName, 'input', 'config', 'plugins', pluginName + '.json');\n\n        try {\n            fs.writeFileSync(pluginConfigPath, JSON.stringify(newConfig, null, 4));\n        } catch (e) {\n            return false;\n        }\n\n        this.checkAndCleanImages(siteName, pluginName, newConfig);\n\n        return true;\n    }\n\n    loadPluginConfig (pluginName, siteName) {\n        let pluginPath = path.join(this.appDir, 'plugins', pluginName, 'plugin.json');\n        let pluginConfigPath = path.join(this.sitesDir, siteName, 'input', 'config', 'plugins', pluginName + '.json');\n        let pluginData = null;\n        let pluginSavedConfig = null;\n        let output = {};\n\n        if (fs.existsSync(pluginPath)) {\n            try {\n                pluginData = FileHelper.readFileSync(pluginPath, 'utf8');\n                pluginData = JSON.parse(pluginData);\n            } catch (e) {\n                pluginData = {};\n            }\n        } else {\n            pluginData =  {};\n        }\n\n        if (fs.existsSync(pluginConfigPath)) {\n            try {\n                pluginSavedConfig = FileHelper.readFileSync(pluginConfigPath, 'utf8');\n                pluginSavedConfig = JSON.parse(pluginConfig);\n            } catch (e) {\n                pluginSavedConfig = {};\n            }\n        } else {\n            pluginSavedConfig = {};\n        }\n\n        let settings = pluginData.config.map(field => {\n            if (field.type !== 'separator') {\n                if (pluginSavedConfig && typeof pluginSavedConfig[field.name] !== 'undefined') {\n                    return {\n                        name: field.name, \n                        value: savedConfig[field.name]\n                    };\n                }\n\n                return {\n                    name: field.name, \n                    value: field.value\n                };\n            }\n\n            return false;\n        });\n\n        for (let setting of settings) {\n            if (setting) {\n                output[setting.name] = setting.value;\n            }\n        }\n\n        return output;\n    }\n\n    checkAndCleanImages (siteName, pluginName, newConfig) {\n        let configString = JSON.stringify(newConfig);\n        let pluginImagesPath = path.join(this.sitesDir, siteName, 'input', 'media', 'plugins', pluginName);\n        \n        if (!fs.existsSync(pluginImagesPath)) {\n            return;\n        }\n        \n        let imagesInConfig = [];\n        let imageRegex = /\"([^\"]+\\.(svg|png|jpg|jpeg|gif|webp))\"/gi;\n        let match = null;\n\n        while (match = imageRegex.exec(configString)) {\n            let imageToPush = match[1];\n            imageToPush = imageToPush.replace(/\\\\/g, '/').split('/').pop();\n            imagesInConfig.push(imageToPush);\n        }\n \n        let files = fs.readdirSync(pluginImagesPath);\n        \n        for (let i = 0; i < files.length; i++) {\n            let file = files[i];\n            \n            if (file.match(/\\.(svg|png|jpg|jpeg|gif|webp)$/i) && imagesInConfig.indexOf(file) === -1) {\n                fs.removeSync(path.join(pluginImagesPath, file));\n            }\n        }\n    }\n}\n\nmodule.exports = Plugins;\n"
  },
  {
    "path": "app/back-end/post.js",
    "content": "/*\n * Post instance\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst slug = require('./helpers/slug');\nconst Model = require('./model.js');\nconst ImageHelper = require('./helpers/image.helper.js');\nconst Posts = require('./posts.js');\nconst Tags = require('./tags.js');\nconst Authors = require('./authors.js');\nconst Themes = require('./themes.js');\nconst Utils = require('./helpers/utils.js');\nconst normalizePath = require('normalize-path');\n\nclass Post extends Model {\n    constructor(appInstance, postData, storeMode = true) {\n        super(appInstance, postData);\n        this.id = parseInt(postData.id, 10);\n        this.postsData = new Posts(appInstance, postData);\n        this.tagsData = new Tags(appInstance, postData);\n        this.authorsData = new Authors(appInstance, postData);\n        this.storeMode = storeMode;\n\n        if(postData.title) {\n            this.title = postData.title;\n            this.slug = slug(postData.slug);\n            this.author = Number(postData.author).toString();\n            this.status = postData.status;\n            this.tags = postData.tags;\n            this.creationDate = postData.creationDate;\n            this.modificationDate = postData.modificationDate;\n            this.template = postData.template;\n            this.additionalData = postData.additionalData;\n            this.postViewSettings = postData.postViewSettings;\n        }\n\n        // Separated for the case of use the cancel post event\n        this.text = postData.text ? postData.text : '';\n        this.featuredImage = postData.featuredImage ? postData.featuredImage : '';\n        this.featuredImageFilename = postData.featuredImageFilename ? postData.featuredImageFilename : '';\n        this.featuredImageData = postData.featuredImageData ? postData.featuredImageData : false;\n    }\n\n    /*\n     * Load post\n     */\n    load() {\n        let results = {\n            \"posts\": [],\n            \"tags\": [],\n            \"featuredImage\": '',\n            \"mediaPath\": path.join(this.siteDir, 'input', 'media', 'posts'),\n            \"author\": ''\n        };\n        // Get post data\n        let postsSqlQuery = `SELECT * FROM posts WHERE id = @id`;\n        results.posts = this.db.prepare(postsSqlQuery).all({ id: this.id });\n\n        // Get author data\n        let authorSqlQuery = `\n            SELECT\n                a.id AS id,\n                a.name AS name\n            FROM\n                authors AS a\n            LEFT JOIN\n                posts AS p\n                ON\n                p.authors = a.id\n            WHERE\n                p.id = @id\n        `;\n\n        results.author = this.db.prepare(authorSqlQuery).all({ id: this.id });\n\n        // Get tags data\n        let tagsSqlQuery = `\n            SELECT\n                t.id AS id,\n                t.name AS name\n            FROM\n                tags AS t\n            LEFT JOIN\n                posts_tags AS pt\n                ON\n                    t.id = pt.tag_id\n            LEFT JOIN\n                posts AS p\n                ON\n                    p.id = pt.post_id\n            WHERE\n                p.id = @id\n            ORDER BY\n                t.name ASC;\n        `;\n        results.tags = this.db.prepare(tagsSqlQuery).all({ id: this.id });\n\n        // Get image data\n        let imageSqlQuery = `\n            SELECT\n                pi.url AS url,\n                pi.additional_data AS additional_data\n            FROM\n                posts AS p\n            LEFT JOIN\n                posts_images AS pi\n                ON\n                    p.featured_image_id = pi.id\n            WHERE\n                p.id = @id;\n        `;\n        results.featuredImage = this.db.prepare(imageSqlQuery).get({ id: this.id });\n\n        // Get the additional data\n        let additionalDataSqlQuery = `\n            SELECT\n                *\n            FROM\n                posts_additional_data\n            WHERE\n                post_id = @id\n                AND\n                key = '_core'\n        `;\n        let additionalDataResult = this.db.prepare(additionalDataSqlQuery).get({ id: this.id });\n\n        if(additionalDataResult && additionalDataResult.value) {\n            results.additionalData = JSON.parse(additionalDataResult.value);\n        } else {\n            results.additionalData = {};\n        }\n        // Get post view settings\n        let postViewSqlQuery = `\n            SELECT\n                *\n            FROM\n                posts_additional_data\n            WHERE\n                post_id = @id\n                AND\n                key = 'postViewSettings'\n        `;\n        let postViewResult = this.db.prepare(postViewSqlQuery).get({ id: this.id });\n\n        if(postViewResult && postViewResult.value) {\n            results.postViewSettings = JSON.parse(postViewResult.value);\n            let postViewSettingsKeys = Object.keys(results.postViewSettings);\n\n            for(let i = 0; i < postViewSettingsKeys.length; i++) {\n                if(results.postViewSettings[postViewSettingsKeys[i]].type) {\n                    results.postViewSettings[postViewSettingsKeys[i]] = results.postViewSettings[postViewSettingsKeys[i]].value;\n                }\n            }\n        } else {\n            results.postViewSettings = {};\n        }\n\n        // Return all results\n        return results;\n    }\n\n    /*\n     * Save post\n     */\n    save() {\n        let sqlQuery = '';\n        this.checkAndPrepareSlug();\n\n        if(this.id === 0) {\n            // Add post data\n            sqlQuery = this.db.prepare(`INSERT INTO posts VALUES(null, @title, @author, @slug, @text, 0, @creationDate, @modificationDate, @status, @template)`);\n            sqlQuery.run({\n                title: this.title,\n                author: this.author,\n                slug: this.slug,\n                text: this.cleanUpContent(this.text),\n                creationDate: this.creationDate,\n                modificationDate: this.modificationDate,\n                status: this.status,\n                template: this.template\n            });\n        } else {\n            // Update post data\n            sqlQuery = this.db.prepare(`UPDATE posts\n                        SET\n                            title = @title,\n                            authors = @author,\n                            slug = @slug,\n                            text = @text,\n                            status = @status,\n                            created_at = @createdAt,\n                            modified_at = @modifiedAt,\n                            template = @template\n                        WHERE\n                            id = @id`);\n            sqlQuery.run({\n                title: this.title,\n                author: this.author,\n                slug: this.slug,\n                text: this.cleanUpContent(this.text),\n                status: this.status,\n                createdAt: this.creationDate,\n                modifiedAt: this.modificationDate,\n                template: this.template,\n                id: this.id\n            });\n        }\n\n        // Get the newly added item ID if necessary\n        if(this.id === 0) {\n            this.id = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n\n            // Move images from the temp directory\n            let tempDirectoryExists = true;\n            let tempImagesDir = path.join(this.siteDir, 'input', 'media', 'posts', 'temp');\n            try {\n                fs.statSync(tempImagesDir).isDirectory();\n            } catch (err) {\n                tempDirectoryExists = false;\n            }\n\n            if (tempDirectoryExists) {\n                let finalImagesDir = path.join(this.siteDir, 'input', 'media', 'posts', (this.id).toString());\n                fs.copySync(tempImagesDir, finalImagesDir);\n                fs.removeSync(tempImagesDir);\n                // Update text\n                if(!this.text) {\n                    this.text = '';\n                }\n\n                this.text = this.text.replace(/file:(\\/){1,}/gmi, 'file:///');\n                this.text = this.text.split(normalizePath(tempImagesDir)).join('#DOMAIN_NAME#');\n                this.text = this.text.replace(/file:(\\/){1,}\\#DOMAIN_NAME\\#/gmi, '#DOMAIN_NAME#');\n                sqlQuery = this.db.prepare(`UPDATE posts\n                        SET\n                            text = @text\n                        WHERE\n                            id = @id`);\n                sqlQuery.run({\n                    text: this.text,\n                    id: this.id\n                });\n            }\n        }\n\n        // Save tags\n        this.saveTags();\n\n        // Save images\n        if(this.featuredImage) {\n            let image = new ImageHelper(this);\n            image.save();\n        } else if(this.id > 0) {\n            let image = new ImageHelper(this);\n            image.delete();\n        }\n\n        // Save additional data\n        this.saveAdditionalData();\n\n        // Save post view settings\n        this.savePostViewSettings();\n\n        // Remove unused images\n        this.checkAndCleanImages();\n\n        // Return new list of the posts\n        return {\n            postID: this.id,\n            posts: this.postsData.load(),\n            tags: this.tagsData.load(),\n            postsTags: this.postsData.loadTagsXRef(),\n            postsAuthors: this.postsData.loadAuthorsXRef(),\n            authors: this.authorsData.load()\n        };\n    }\n\n    /*\n     * Delete post\n     */\n    delete() {\n        this.db.exec(`DELETE FROM posts WHERE id = ${parseInt(this.id, 10)}`);\n        this.db.exec(`DELETE FROM posts_tags WHERE post_id = ${parseInt(this.id, 10)}`);\n        this.db.exec(`DELETE FROM posts_images WHERE post_id = ${parseInt(this.id, 10)}`);\n        this.db.exec(`DELETE FROM posts_additional_data WHERE post_id = ${parseInt(this.id, 10)}`);\n        ImageHelper.deleteImagesDirectory(this.siteDir, 'posts', this.id);\n\n        return true;\n    }\n\n    /*\n     * Duplicate post\n     */\n    duplicate() {\n        // Get the post data\n        let postToDuplicate = this.db.prepare(`SELECT * FROM posts WHERE id = @id LIMIT 1`).get({ id: this.id });\n        let postTagsToDuplicate = this.db.prepare(`SELECT * FROM posts_tags WHERE post_id = @id`).all({ id: this.id });\n        let postImagesToDuplicate = this.db.prepare(`SELECT * FROM posts_images WHERE post_id = @id`).all({ id: this.id });\n        let postAdditionalDataToDuplicate = this.db.prepare(`SELECT * FROM posts_additional_data WHERE post_id = @id`).all({ id: this.id });\n\n        // Add duplicate post row\n        if(postToDuplicate && postToDuplicate.id) {\n            // Change title (suffix with \" (copy)\")\n            let copySuffix = 1;\n            let modifiedTitle = postToDuplicate.title;\n            let postWithTheSameSlug = false;\n            let modifiedSlug = postToDuplicate.slug;\n            \n            if (modifiedTitle.substr(-7) !== ' (copy)') {\n                modifiedTitle += ' (copy)';\n            }\n\n            if (modifiedSlug.substr(-2).match(/-\\d/)) {\n                modifiedSlug = modifiedSlug.substr(0, modifiedSlug.length - 2);\n            }\n\n            do {\n                copySuffix++;\n                postWithTheSameSlug = this.db.prepare(`SELECT id FROM posts WHERE slug = @slug LIMIT 1`).get({ slug: modifiedSlug + '-' + copySuffix });\n            } while (postWithTheSameSlug && postWithTheSameSlug.id);\n\n            modifiedSlug = modifiedSlug + '-' + copySuffix;\n            let postStatus = postToDuplicate.status;\n\n            if (postStatus.indexOf('draft') === -1) {\n                postStatus = postStatus.replace('published', 'draft');\n\n                if (postStatus.indexOf('draft') === -1) {\n                    postStatus += ',draft';\n                }\n            }\n\n            let newCopyPostSqlQuery = this.db.prepare(`INSERT INTO posts VALUES(null, @title, @authors, @slug, @text, @featured_image_id, @created_at, @modified_at, @status, @template)`);\n            newCopyPostSqlQuery.run({\n                title: modifiedTitle,\n                authors: postToDuplicate.authors,\n                slug: modifiedSlug,\n                text: postToDuplicate.text,\n                featured_image_id: postToDuplicate.featured_image_id,\n                created_at: Date.now(),\n                modified_at: Date.now(),\n                status: postStatus,\n                template: postToDuplicate.template\n            });\n        } else {\n            return false;\n        }\n\n        // Get newly inserted post ID\n        let copiedPostId = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n\n        // Add tags row\n        if(postTagsToDuplicate.length) {\n            let tagsCount = postTagsToDuplicate.length;\n\n            for(let i = 0; i < tagsCount; i++) {\n                let newCopyPostTagsSqlQuery = this.db.prepare(`INSERT INTO posts_tags VALUES(@tag_id, @copied_post_id)`);\n                newCopyPostTagsSqlQuery.run({\n                    tag_id: postTagsToDuplicate[i].tag_id,\n                    copied_post_id: copiedPostId\n                });\n            }\n        }\n\n        // Add posts_images row\n        if(postImagesToDuplicate.length) {\n            let imagesCount = postImagesToDuplicate.length;\n\n            for(let i = 0; i < imagesCount; i++) {\n                let newCopyPostImagesSqlQuery = this.db.prepare(`INSERT INTO posts_images VALUES(NULL, @copied_post_id, @url, @title, @caption, @additional_data)`);\n                newCopyPostImagesSqlQuery.run({\n                    copied_post_id: copiedPostId,\n                    url: postImagesToDuplicate[i].url,\n                    title: postImagesToDuplicate[i].title,\n                    caption: postImagesToDuplicate[i].caption,\n                    additional_data: postImagesToDuplicate[i].additional_data\n                });\n            }\n\n            // Get newly inserted post ID\n            let copiedFeaturedImageId = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n            \n            this.db.prepare(`UPDATE posts SET featured_image_id = @featuredImageID WHERE id = @postID`).run({\n                featuredImageID: copiedFeaturedImageId,\n                postID: copiedPostId\n            });\n        }\n\n        // Add posts_additional_data\n        if(postAdditionalDataToDuplicate.length) {\n            let additionalDataCount = postAdditionalDataToDuplicate.length;\n\n            for(let i = 0; i < additionalDataCount; i++) {\n                let newCopyPostAdditionalDataSqlQuery = this.db.prepare(`INSERT INTO posts_additional_data VALUES(NULL, @copied_post_id, @key, @value)`);\n                newCopyPostAdditionalDataSqlQuery.run({\n                    copied_post_id: copiedPostId,\n                    key: postAdditionalDataToDuplicate[i].key,\n                    value: postAdditionalDataToDuplicate[i].value\n                });\n            }\n        }\n\n        // Copy images\n        ImageHelper.copyImagesDirectory(this.siteDir, this.id, copiedPostId);\n\n        return true;\n    }\n\n    /*\n     * Change post status\n     */\n    changeStatus(status, inverse = false) {\n        let selectQuery = this.db.prepare(`SELECT status FROM posts WHERE id = @id`).all({ id: this.id });\n        let currentStatus = selectQuery[0].status.split(',');\n        let resetTemplateIfNeeded = status === 'is-page' ? ', template = \\'*\\'' : '';\n\n        if(!inverse) {\n            if(currentStatus.indexOf(status) === -1) {\n                currentStatus.push(status);\n            }\n        } else {\n            if(currentStatus.indexOf(status) > -1) {\n                currentStatus = currentStatus.filter((postStatus) => postStatus !== status);\n            }\n        }\n\n        if (status === 'is-page') {\n            currentStatus = currentStatus.filter(status => ['excluded_homepage', 'featured', 'hidden'].indexOf(status) === -1 && status.trim() !== '');\n            // Remove post tags xrefs when changing status to page\n            let removeQuery = this.db.prepare(`DELETE FROM posts_tags WHERE post_id = @id`);\n            removeQuery.run({ id: this.id });\n        } else {\n            currentStatus = currentStatus.filter(status => status.trim() !== '');\n        }\n        \n        let updateQuery = this.db.prepare(`UPDATE\n                                        posts\n                                    SET\n                                        status = @status\n                                        ${resetTemplateIfNeeded}\n                                    WHERE\n                                        id = @id`);\n        updateQuery.run({\n            status: currentStatus.join(','),\n            id: this.id\n        });\n\n        return true;\n    }\n\n    /*\n     * Save tags\n     */\n    saveTags() {\n        // Remove tags connected previously with an item\n        this.db.exec(`DELETE FROM posts_tags WHERE post_id = ${parseInt(this.id, 10)}`);\n\n        // For case when there is no tags - check it\n        if (this.tags) {\n            // Split tags into an array\n            let tagsToSave = JSON.parse(JSON.stringify(this.tags));\n\n            // Remove empty tags\n            tagsToSave = tagsToSave.filter(function (item) {\n                if (typeof item === 'string') {\n                    return item.replace(/\\s/g, '') !== '';\n                }\n\n                return false;\n            });\n\n            for (let tagName of tagsToSave) {\n                this.saveTag(tagName);\n            }\n        }\n    }\n\n    /*\n     * Save tag\n     */\n    saveTag(tagName) {\n        // Check if the tag exists\n        let result = this.db.prepare(`SELECT id FROM tags WHERE name = @name OR slug = @slug`).all({\n            name: this.escape(tagName),\n            slug: this.escape(slug(tagName))\n        });\n        let tagID = 0;\n        // If not - add the tag\n        if(result && result.length) {\n            tagID = result[0].id;\n        } else {\n            let sqlQuery = this.db.prepare(`INSERT INTO tags VALUES(null, @name, @slug, '', '')`);\n            sqlQuery.run({\n                name: tagName, \n                slug: slug(tagName)\n            });\n            tagID = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n        }\n\n        // Save binding between post and tag\n        let sqlQuery = this.db.prepare(`INSERT INTO posts_tags VALUES(@tagID, @postID)`);\n        sqlQuery.run({\n            tagID: tagID, \n            postID: this.id\n        });\n    }\n\n    /*\n     * Check and prepare post slug\n     */\n    checkAndPrepareSlug(suffix = 0) {\n        let postSlug = this.slug;\n        let restrictedSlugs = [];\n\n        if(this.application.sites[this.site].advanced.urls.cleanUrls) {\n            restrictedSlugs = [\n                'assets',\n                'media',\n                this.application.sites[this.site].advanced.urls.authorsPrefix\n            ];\n\n            if(this.application.sites[this.site].advanced.urls.tagsPrefix !== '') {\n                restrictedSlugs.push(this.application.sites[this.site].advanced.urls.tagsPrefix);\n            }\n        }\n\n        if(suffix > 0) {\n            postSlug = this.escape(this.slug + '-' + suffix);\n        }\n\n        if(restrictedSlugs.indexOf(postSlug) > -1) {\n            this.checkAndPrepareSlug(2);\n            return;\n        }\n\n        let result = this.db.prepare(`SELECT slug FROM posts WHERE slug LIKE @slug AND id != @id`).all({\n            slug: postSlug,\n            id: this.id\n        });\n\n        if(result && result.length) {\n            if(suffix === 0) {\n                suffix = 2;\n            } else {\n                suffix++;\n            }\n\n            this.checkAndPrepareSlug(suffix);\n        } else {\n            this.slug = postSlug;\n        }\n    }\n\n    /*\n     * Remove unused images\n     */\n    checkAndCleanImages(cancelEvent = false) {\n        let postDir = this.id;\n\n        if(this.id === 0) {\n            postDir = 'temp';\n        }\n\n        let imagesDir = path.join(this.siteDir, 'input', 'media', 'posts', (postDir).toString());\n        let galleryImagesDir = path.join(imagesDir, 'gallery');\n        let postDirectoryExists = true;\n\n        try {\n            fs.statSync(imagesDir).isDirectory();\n        } catch (err) {\n            postDirectoryExists = false;\n        }\n\n        if(!postDirectoryExists) {\n            return;\n        }\n\n        let images = fs.readdirSync(imagesDir);\n        let galleryImages = false;\n\n        if(Utils.dirExists(galleryImagesDir)) {\n            galleryImages = fs.readdirSync(galleryImagesDir);\n        }\n\n        // Get previous text if the post is cancelled and it is a not a new post\n        if(cancelEvent && postDir !== 'temp') {\n            let textSqlQuery = `\n                    SELECT\n                        text\n                    FROM\n                        posts\n                    WHERE\n                        id = @id\n                `;\n\n            let textResult = this.db.prepare(textSqlQuery).get({ id: postDir });\n\n            if(textResult && textResult.text) {\n                this.text = textResult.text;\n            }\n        }\n\n        this.cleanImages(images, imagesDir, cancelEvent);\n\n        if(galleryImages) {\n            this.cleanImages(galleryImages, galleryImagesDir, cancelEvent);\n        }\n    }\n\n    /*\n     * Removes images from a given image dir\n     */\n    cleanImages(images, imagesDir, cancelEvent) {\n        let postDir = this.id;\n        let featuredImage = path.parse(this.featuredImageFilename).base;\n\n        // If post is cancelled - get the previous featured image\n        if (cancelEvent && this.id !== 0) {\n            let featuredImageSqlQuery = `\n                    SELECT\n                        url\n                    FROM\n                        posts_images\n                    WHERE\n                        post_id = @id\n                `;\n\n            let featuredImageResult = this.db.prepare(featuredImageSqlQuery).all({ \n                id: this.id \n            });\n\n            if(featuredImageResult && featuredImageResult.url) {\n                featuredImage = featuredImageResult.url;\n            }\n        }\n\n        if(this.id === 0) {\n            postDir = 'temp';\n        }\n\n        let imagesInPostViewSettings = [];\n        \n        if (this.postViewSettings) {\n            imagesInPostViewSettings = Object.values(this.postViewSettings).filter(item => item.type === \"image\").map(item => item.value);\n        }\n        \n        // Iterate through images\n        for (let i in images) {\n            let imagePath = images[i];\n            let fullPath = path.join(imagesDir, imagePath);\n\n            // Skip dirs and symlinks\n            if(imagePath === '.' || imagePath === '..' || imagePath === 'responsive' || imagePath === 'gallery') {\n                continue;\n            }\n\n            // Remove files which does not exist in the post text, as featured image and postViewSettings\n            if(\n                (cancelEvent && postDir === 'temp') ||\n                (\n                    this.text.indexOf(imagePath) === -1 && \n                    imagesInPostViewSettings.indexOf(imagePath) === -1 &&\n                    featuredImage !== imagePath\n                )\n            ) {\n                try {\n                    fs.unlinkSync(fullPath);\n                } catch(e) {\n                    console.error(e);\n                }\n\n                // Remove responsive images\n                this.removeResponsiveImages(fullPath);\n            }\n        }\n    }\n\n    /*\n     * Remove unused responsive images\n     */\n    removeResponsiveImages(originalPath) {\n        let themesHelper = new Themes(this.application, { site: this.site });\n        let currentTheme = themesHelper.currentTheme();\n\n        // If there is no selected theme\n        if(currentTheme === 'not selected') {\n            return;\n        }\n\n        // Load theme config\n        let themeConfig = Utils.loadThemeConfig(path.join(this.siteDir, 'input'), currentTheme);\n\n        // check if responsive images config exists\n        if(Utils.responsiveImagesConfigExists(themeConfig)) {\n            let dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages');\n            let featuredDimensions = Utils.responsiveImagesDimensions(themeConfig, 'featuredImages');\n\n            if(featuredDimensions !== false) {\n                featuredDimensions.forEach(item => {\n                    if (dimensions.indexOf(item) === -1) {\n                        dimensions.push(item);\n                    }\n                });\n            }\n\n            let responsiveImagesDir = path.parse(originalPath).dir;\n            responsiveImagesDir = path.join(responsiveImagesDir, 'responsive');\n\n            if(typeof dimensions === \"boolean\") {\n                return;\n            }\n\n            let forceWebp = !!this.application.sites[this.site]?.advanced?.forceWebp;\n\n            // Remove responsive images of each size\n            for(let dimensionName of dimensions) {\n                let filename = path.parse(originalPath).name;\n                let extension = path.parse(originalPath).ext;\n\n                if (forceWebp && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) {\n                    extension = '.webp'; \n                }\n\n                let responsiveImagePath = path.join(responsiveImagesDir, filename + '-' + dimensionName + extension);\n\n                if(Utils.fileExists(responsiveImagePath)){\n                    fs.unlinkSync(responsiveImagePath);\n                }\n            }\n        }\n    }\n\n    /*\n     * Save additional data\n     */\n    saveAdditionalData() {\n        // Remove old _core additional data\n        this.db.exec(`DELETE FROM posts_additional_data WHERE post_id = ${parseInt(this.id, 10)} AND key = '_core'`);\n\n        // Convert data to JSON string\n        if (typeof this.additionalData !== 'object') {\n            this.additionalData = {};\n        }\n\n        if (typeof this.additionalData.mainTag === 'string' && this.additionalData.mainTag !== '') {\n            let tagSqlQuery = `\n                    SELECT \n                        id \n                    FROM\n                        tags\n                    WHERE\n                        name = @name\n                `;\n\n            let tagResult = this.db.prepare(tagSqlQuery).get({ name: this.additionalData.mainTag });\n\n            if (tagResult && tagResult.id) {\n                this.additionalData.mainTag = parseInt(tagResult.id, 10);\n            }\n        }\n\n        let additionalDataToSave = JSON.stringify(this.additionalData);\n\n        // Store the data\n        let storeSqlQuery = this.db.prepare(`INSERT INTO posts_additional_data VALUES(null, @id, '_core', @data)`);\n        storeSqlQuery.run({\n            id: this.id, \n            data: additionalDataToSave\n        });\n    }\n\n    /*\n     * Save additional data\n     */\n    savePostViewSettings() {\n        // Remove old _core additional data\n        this.db.exec(`DELETE FROM posts_additional_data WHERE post_id = ${parseInt(this.id, 10)} AND key = 'postViewSettings'`);\n\n        // Convert data to JSON string\n        if(typeof this.postViewSettings !== 'object') {\n            this.postViewSettings = {};\n        }\n\n        let dataToSave = JSON.stringify(this.postViewSettings);\n\n        // Store the data\n        let storeSqlQuery = this.db.prepare(`INSERT INTO posts_additional_data VALUES(null, @id, 'postViewSettings', @data)`);\n        storeSqlQuery.run({\n            id: this.id, \n            data: dataToSave\n        });\n    }\n\n    /**\n     * Clean up post content from wrong code\n     */\n    cleanUpContent (content) {\n        return content.replace(/\\<figure\\sclass=\\\"post__image\\spost__image\\s/gmi, '<figure class=\"post__image ');\n    }\n}\n\nmodule.exports = Post;\n"
  },
  {
    "path": "app/back-end/posts.js",
    "content": "/*\n * Posts instance\n */\n\nconst Model = require('./model.js');\n\nclass Posts extends Model {\n    constructor(appInstance, postsData) {\n        super(appInstance, postsData);\n    }\n\n    /*\n     * Load posts\n     */\n    load() {\n        let sqlQuery = `\n            SELECT \n                p.id AS id, \n                p.title AS title, \n                p.authors AS authors, \n                p.slug AS slug, \n                p.created_at AS created_at, \n                p.modified_at AS modified_at, \n                p.status AS status,\n                pad.value AS additional_data\n            FROM \n                posts AS p\n            LEFT JOIN\n                posts_additional_data AS pad\n                ON\n                pad.post_id = p.id\n            WHERE\n                p.status NOT LIKE '%is-page%'\n                AND (\n                    pad.key = '_core' OR\n                    pad.key IS NULL\n                )\n            ORDER BY \n                id DESC`;\n\n        return this.db.prepare(sqlQuery).all();\n    }\n\n    /*\n     * Load references between posts and tags\n     */\n    loadTagsXRef() {\n        let sqlQuery = `\n            SELECT \n                post_id AS postID, \n                tag_id AS tagID\n            FROM \n                posts_tags AS pt\n            LEFT JOIN\n                posts AS p\n            ON\n                pt.post_id = p.id\n            WHERE\n                p.status NOT LIKE '%is-page%'\n            ORDER BY \n                post_id DESC`;\n\n        return this.db.prepare(sqlQuery).all();\n    }\n\n    /*\n     * Load references between posts and authors\n     */\n    loadAuthorsXRef() {\n        let sqlQuery = `\n            SELECT \n                p.id AS postID,\n                a.id AS authorID,\n                a.name AS authorName\n            FROM \n                posts AS p \n            LEFT JOIN \n                authors AS a \n            ON\n                p.authors = a.id \n            WHERE\n                p.status NOT LIKE '%is-page%'\n            ORDER BY \n                p.id DESC`;\n\n        return this.db.prepare(sqlQuery).all();\n    }\n}\n\nmodule.exports = Posts;\n"
  },
  {
    "path": "app/back-end/site.js",
    "content": "/*\n * Site instance\n */\n\nconst fs = require('fs-extra');\nconst os = require('os');\nconst path = require('path');\nconst FileHelper = require('./helpers/file.js');\nconst Database = os.platform() === 'linux' ? require('node-sqlite3-wasm').Database : require('better-sqlite3');\nconst DBUtils = require('./helpers/db.utils.js');\nconst Themes = require('./themes.js');\nconst Image = require('./image.js');\nconst UtilsHelper = require('./helpers/utils');\nconst childProcess = require('child_process');\nconst slug = require('./helpers/slug');\nconst defaultAstCurrentSiteConfig = require('./../config/AST.currentSite.config');\nconst { shell } = require('electron');\nconst CreateFromBackup = require('./../back-end/modules/backup/create-from-backup');\n\nclass Site {\n    constructor(appInstance, config, maintenanceMode = false) {\n        this.application = appInstance;\n        this.name = config.name;\n        this.description = config.description;\n        this.uuid = config.uuid;\n        this.displayName = config.displayName;\n        // In maintenance mode we need only the website name\n        if (!maintenanceMode) {\n            this.logo = {};\n            this.logo.icon = config.logo.icon || 'fa fa-book';\n            this.logo.color = config.logo.color || 1;\n        }\n        this.appDir = this.application.appDir;\n        this.siteDir = path.join(this.application.sitesDir, this.name);\n    }\n\n    /*\n     * Check if the specific site exists\n     */\n    siteExists() {\n        return fs.existsSync(this.siteDir);\n    }\n\n    /*\n     * Create a new site\n     */\n    create(authorName) {\n        if (!this.siteExists()) {\n            this.createDirectories();\n            this.copyDefaultTheme();\n            this.createConfigFiles();\n            let dbResult = this.createDB();\n\n            if (!dbResult) {\n                return 'db-error';\n            }\n\n            this.createAuthor(authorName);\n            return true;\n        }\n        // If site exists\n        console.log('Site called: ' + this.name + ' exists!');\n        return 'duplicate';\n    }\n\n    /*\n     * Create directories\n     */\n    createDirectories() {\n        // Create main dir\n        fs.mkdirSync(this.siteDir);\n        // Create also other dirs\n        fs.mkdirSync(path.join(this.siteDir, 'input'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'config'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'config', 'plugins'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'root-files'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media', 'temp'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media', 'website'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media', 'posts'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media', 'files'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media', 'tags'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media', 'authors'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'media', 'plugins'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'themes'));\n        fs.mkdirSync(path.join(this.siteDir, 'input', 'languages'));\n        fs.mkdirSync(path.join(this.siteDir, 'output'));\n        fs.mkdirSync(path.join(this.siteDir, 'preview'));\n    }\n\n    /*\n     * Copy files of the default theme\n     */\n    copyDefaultTheme () {\n        fs.copySync(\n            path.join(this.application.appDir, 'themes', 'simple'),\n            path.join(this.siteDir, 'input', 'themes', 'simple')\n        );\n    }\n\n    /*\n     * Create config file\n     */\n    createConfigFiles() {\n        let configDir = path.join(this.siteDir, 'input', 'config');\n        let siteConfig = {\n            'uuid': 'uuid-' + (new Date().getTime()) + '-' + (Math.floor(Math.random() * (999999999 - 100000000 + 1)) + 100000000),\n            'name': this.name,\n            'description': '',\n            'displayName': this.displayName,\n            'author': this.author,\n            'logo': this.logo,\n            'theme': 'simple'\n        };\n\n        this.uuid = siteConfig.uuid;\n        fs.writeFileSync(path.join(configDir, 'site.config.json'), JSON.stringify(siteConfig, null, 4));\n        fs.writeFileSync(path.join(configDir, 'menu.config.json'), '[]');\n        fs.writeFileSync(path.join(configDir, 'theme.config.json'), '{}');\n    }\n\n    /*\n     * Create database\n     */\n    createDB() {\n        let dbPath = path.join(this.siteDir, 'input', 'db.sqlite');\n\n        try {\n            let db = new DBUtils(new Database(dbPath));\n            db.exec(FileHelper.readFileSync(this.application.basedir + '/back-end/sql/1.0.0.sql', 'utf8'));\n            db.close();\n        } catch (error) {\n            console.log('DB error', error);\n            return false;\n        }\n\n        return true;\n    }\n\n    /*\n     * Create author\n     */\n    createAuthor(authorName) {\n        let dbPath = path.join(this.siteDir, 'input', 'db.sqlite');\n        let db = new DBUtils(new Database(dbPath));\n        let sqlQuery = db.prepare(`INSERT INTO authors VALUES(1, @name, @slug, '', '{}', '{}')`);\n        sqlQuery.run({\n            name: authorName,\n            slug: slug(authorName).toLowerCase()\n        });\n        db.close();\n    }\n\n    /*\n     * check if regenerate thumbnails is required\n     */\n    regenerateThumbnailsIsRequired(sender) {\n        let themesHelper = new Themes(this.application, { site: this.name });\n        let themeName = themesHelper.currentTheme();\n\n        // If there is no theme selected\n        if(themeName === 'not selected') {\n            sender.send('app-site-regenerate-thumbnails-required-status', {\n                message: false\n            });\n\n            return;\n        }\n\n        // If there is no responsive images configuration\n        let themeConfig = UtilsHelper.loadThemeConfig(path.join(this.siteDir, 'input'), themeName);\n\n        if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            sender.send('app-site-regenerate-thumbnails-required-status', {\n                message: false\n            });\n\n            return;\n        }\n\n        // Remove all old responsive directories\n        let mediaPath = path.join(this.siteDir, 'input', 'media');\n        let catalogs = fs.readdirSync(path.join(mediaPath, 'posts'));\n        let galleryCatalogs = [];\n        catalogs = catalogs.map(catalog => 'posts/' + catalog);\n        catalogs.push('website');\n        catalogs = catalogs.filter((catalog) => !(catalog.indexOf('/.') > -1 || catalog.trim() === '' || !catalog || UtilsHelper.fileExists(path.join(mediaPath, catalog))));\n\n        for(let catalog of catalogs) {\n            if(catalog.indexOf('/.') > -1) {\n                continue;\n            }\n\n            // Add gallery catalogs\n            let galleryFullPath = path.join(mediaPath, catalog, 'gallery');\n\n            if(UtilsHelper.dirExists(galleryFullPath)) {\n                let galleryShortPath = path.join(catalog, 'gallery');\n                galleryCatalogs.push(galleryShortPath);\n            }\n        }\n\n        // Add gallery catalogs\n        catalogs = catalogs.concat(galleryCatalogs);\n\n        // Count images for the process\n        let numberOfImagesToRegenerate = this.getNumberOfImagesToRegenerate(mediaPath, catalogs);\n\n        // If there is no posts - abort\n        if(numberOfImagesToRegenerate === 0) {\n            sender.send('app-site-regenerate-thumbnails-required-status', {\n                message: false\n            });\n        } else {\n            sender.send('app-site-regenerate-thumbnails-required-status', {\n                message: true\n            });\n        }\n    }\n\n    /*\n     * Regenerate thumbnails\n     */\n    regenerateThumbnails(sender) {\n        // Get theme configuration\n        let self = this;\n        let themesHelper = new Themes(this.application, { site: this.name });\n        let themeName = themesHelper.currentTheme();\n        let dbPath = path.join(this.siteDir, 'input', 'db.sqlite');\n        let db = new DBUtils(new Database(dbPath));\n\n        // If there is no theme selected - abort\n        if(themeName === 'not selected') {\n            sender.send('app-site-regenerate-thumbnails-error', {\n                message: {\n                    translation: 'core.site.noThemeSelected'\n                }\n            });\n\n            return;\n        }\n\n        // If there is no responsive images configuration - abort\n        let themeConfig = UtilsHelper.loadThemeConfig(path.join(this.siteDir, 'input'), themeName);\n\n        if(!UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            sender.send('app-site-regenerate-thumbnails-error', {\n                message: {\n                    translation: 'core.site.noConfigurationForResponsiveImages'\n                }\n            });\n\n            return;\n        }\n\n        // Remove all old responsive directories\n        let mediaPath = path.join(this.siteDir, 'input', 'media');\n        let catalogs = fs.readdirSync(path.join(mediaPath, 'posts'));\n        let tagCatalogs = fs.readdirSync(path.join(mediaPath, 'tags'));\n        let authorsCatalogs = fs.readdirSync(path.join(mediaPath, 'authors'));\n        let galleryCatalogs = [];\n        catalogs = catalogs.map(catalog => 'posts/' + catalog);\n        tagCatalogs = tagCatalogs.map(catalog => 'tags/' + catalog);\n        authorsCatalogs = authorsCatalogs.map(catalog => 'authors/' + catalog);\n        catalogs.push('website');\n        catalogs = catalogs.concat(tagCatalogs);\n        catalogs = catalogs.concat(authorsCatalogs);\n        catalogs = catalogs.filter((catalog) => !(catalog.indexOf('/.') > -1 || catalog.trim() === '' || !catalog || UtilsHelper.fileExists(path.join(mediaPath, catalog))));\n\n        for(let catalog of catalogs) {\n            if(catalog.indexOf('/.') > -1) {\n                continue;\n            }\n\n            let fullPath = path.join(mediaPath, catalog, 'responsive');\n\n            // remove the files form dir or create dir if not exists\n            fs.emptyDirSync(fullPath);\n\n            // Add gallery catalogs\n            let galleryFullPath = path.join(mediaPath, catalog, 'gallery');\n\n            if(UtilsHelper.dirExists(galleryFullPath)) {\n                let galleryShortPath = path.join(catalog, 'gallery');\n                galleryCatalogs.push(galleryShortPath);\n\n                // Remove all gallery thumbnails\n                this.removeGalleryThumbnails(galleryFullPath);\n            }\n        }\n\n        // Add gallery catalogs\n        catalogs = catalogs.concat(galleryCatalogs);\n\n        // Count images for the process\n        let numberOfImagesToRegenerate = this.getNumberOfImagesToRegenerate(mediaPath, catalogs);\n\n        // If there is no posts - abort\n        if(numberOfImagesToRegenerate === 0) {\n            sender.send('app-site-regenerate-thumbnails-error', {\n                message: {\n                    translation: 'core.site.noImagesToRegenerate'\n                }\n            });\n\n            return;\n        }\n\n        // Create featured images post reference\n        this.postImagesRef = db.prepare(`SELECT post_id, url FROM posts_images`).all();\n        // Calculate how many images should be created\n        this.numberOfImages = numberOfImagesToRegenerate;\n        this.totalProgress = 0;\n\n        // For each image - create a new thumbnails (detect featured images)\n        let regenerateProcess = childProcess.fork(__dirname + '/workers/thumbnails/regenerate', {\n            stdio: [\n                null,\n                fs.openSync(this.application.app.getPath('logs') + \"/regenerate-process.log\", \"w\"),\n                fs.openSync(this.application.app.getPath('logs') + \"/regenerate-errors.log\", \"w\"),\n                'ipc'\n            ]\n        });\n\n        regenerateProcess.send({\n            type: 'dependencies',\n            context: {\n                application: {\n                    appConfig: self.application.appConfig,\n                    appDir: self.application.appDir,\n                    sitesDir: self.application.sitesDir,\n                    db: self.application.db,\n                },\n                name: self.name,\n                postImagesRef: self.postImagesRef,\n                totalProgress: self.totalProgress,\n                numberOfImages: self.numberOfImages\n            },\n            catalog: catalogs.shift(),\n            mediaPath: mediaPath\n        });\n\n        regenerateProcess.on('message', function(data) {\n            if(data.type === 'empty' && catalogs.length) {\n                regenerateProcess.send({\n                    type: 'next-images',\n                    catalog: catalogs.shift(),\n                    mediaPath: mediaPath\n                });\n            }\n        });\n\n        regenerateProcess.on('message', function(data) {\n            if(data.type === 'progress') {\n                sender.send('app-site-regenerate-thumbnails-progress', {\n                    value: data.value,\n                    files: data.files\n                });\n\n                if(catalogs.length) {\n                    regenerateProcess.send({\n                        type: 'next-images',\n                        catalog: catalogs.shift(),\n                        mediaPath: mediaPath\n                    });\n                }\n\n                return;\n            }\n\n            if (data.type === 'finished') {\n                sender.send('app-site-regenerate-thumbnails-success', true);\n            }\n        });\n\n        db.close();\n\n        return regenerateProcess;\n    }\n\n    /**\n     * Removes all thumbnails from given gallery catalog\n     *\n     * @param galleryCatalog\n     */\n    removeGalleryThumbnails(galleryCatalog) {\n        let images = fs.readdirSync(galleryCatalog);\n\n        for(let image of images) {\n            if(image.indexOf('-thumbnail.') === -1) {\n                continue;\n            }\n\n            let imagePath = path.join(galleryCatalog, image);\n\n            fs.unlinkSync(imagePath);\n        }\n    }\n\n    /*\n     * Get number of images which shoild be regenerated\n     */\n    getNumberOfImagesToRegenerate(mediaPath, catalogs) {\n        let numberOfImages = 0;\n\n        for(let catalog of catalogs) {\n            if(catalog.indexOf('/.') > -1) {\n                continue;\n            }\n\n            let catalogPath = path.join(mediaPath, catalog);\n            let files = fs.readdirSync(catalogPath);\n\n            for(let file of files) {\n                if(file.substr(0, 1) === '.' || file === 'responsive' || file === 'gallery') {\n                    continue;\n                }\n\n                if(catalog.indexOf('gallery') !== -1 && file.indexOf('-thumbnail.') !== -1) {\n                    continue;\n                }\n\n                if(fs.lstatSync(path.join(mediaPath, catalog, file)).isFile()) {\n                    if (path.parse(file).ext !== '') {\n                        numberOfImages++;\n                    }\n                }\n            }\n        }\n\n        return numberOfImages;\n    }\n\n    /*\n     * Delete website\n     */\n    static delete(appInstance, name) {\n        let sitePath = path.join(appInstance.sitesDir, name);\n\n        if (appInstance.db) {\n            try {\n                appInstance.db.close();\n            } catch (e) {\n                console.log('[SITE DELETE] DB already closed');\n            }\n        }\n\n        setTimeout(async () => {\n            await shell.trashItem(sitePath);\n        }, 500);\n    }\n\n    /*\n     * Clone website\n     */\n    static clone(appInstance, catalogName, siteName) {\n        let newCatalogName = slug(siteName).toLowerCase();\n        let sitePath = path.join(appInstance.sitesDir, catalogName);\n        let newCatalogFreeName = Site.findFreeName(newCatalogName, appInstance.sitesDir);\n        let newSitePath = path.join(appInstance.sitesDir, newCatalogFreeName);\n        fs.copySync(sitePath, newSitePath);\n        Site.updateNameAndUUIDInSiteConfig(newSitePath, newCatalogFreeName, siteName);\n        let configFilePath = path.join(newSitePath, 'input', 'config', 'site.config.json');\n        let siteConfig = FileHelper.readFileSync(configFilePath);\n        siteConfig = JSON.parse(siteConfig);\n        appInstance.addSite(newCatalogFreeName, siteConfig);\n\n        return {\n            siteName: siteName,\n            siteCatalog: newCatalogFreeName,\n            siteConfig: siteConfig\n        };\n    }\n\n    /**\n     *\n     * Find first free name\n     *\n     * e.g. XYZ -> XYZ copy\n     * e.g. XYZ copy -> XYZ copy copy\n     *\n     * @param {string} name\n     */\n    static findFreeName (name, basePath) {\n        let baseName = name;\n        let dirPath = path.join(basePath, baseName);\n        let currentName = baseName;\n\n        while (UtilsHelper.dirExists(dirPath)) {\n            currentName = baseName + '-copy';\n            dirPath = path.join(basePath, currentName);\n        }\n\n        return currentName;\n    }\n\n    /**\n     * Update site.config.json to use a new name of the website\n     *\n     * @param {string} sitePath\n     * @param {string} newNameSlug\n     */\n    static updateNameAndUUIDInSiteConfig (sitePath, newSiteSlug, newSiteName) {\n        let configFilePath = path.join(sitePath, 'input', 'config', 'site.config.json');\n        let siteConfig = FileHelper.readFileSync(configFilePath);\n        siteConfig = JSON.parse(siteConfig);\n        siteConfig.name = newSiteSlug;\n        siteConfig.uuid = 'uuid-' + (new Date().getTime()) + '-' + (Math.floor(Math.random() * (999999999 - 100000000 + 1)) + 100000000);\n        siteConfig.displayName = newSiteName;\n        siteConfig.deployment = JSON.parse(JSON.stringify(defaultAstCurrentSiteConfig.deployment));\n        siteConfig = JSON.stringify(siteConfig, null, 4);\n        fs.writeFileSync(configFilePath, siteConfig);\n    }\n\n    /*\n     * Load Custom CSS code\n     */\n    static loadCustomCSS(appInstance, name) {\n        let cssPathNormal = path.join(appInstance.sitesDir, name, 'input', 'config', 'custom-css.css');\n        let cssNormal = false;\n        \n        if (UtilsHelper.fileExists(cssPathNormal)) {\n            cssNormal = FileHelper.readFileSync(cssPathNormal, 'utf8');\n        }\n\n        return {\n            normal: cssNormal\n        };\n    }\n\n    /*\n     * Save Custom CSS code\n     */\n    static saveCustomCSS(appInstance, name, code) {\n        let cssPathNormal = path.join(appInstance.sitesDir, name, 'input', 'config', 'custom-css.css');\n        fs.writeFileSync(cssPathNormal, code.normal, 'utf8');\n    }\n\n    /**\n     * Checks for the files consistency on existing websites\n     *\n     * Adds (if missing):\n     * - input/root-files directory\n     * - input/media/files directory\n     * - input/media/pages directory\n     * - input/media/tags directory\n     * - input/media/authors directory\n     * - input/config/plugins directory\n     * - input/config/site.plugins.json file\n     *\n     * Moves .htaccess, robots.txt and _redirects files to root-files directory\n     *\n     * @param siteName\n     */\n    static checkFilesConsistency(appInstance, siteName) {\n        let siteBasePath = path.join(appInstance.sitesDir, siteName, 'input');\n        let rootFilesPath = path.join(siteBasePath, 'root-files');\n        let tagImagesPath = path.join(siteBasePath, 'media', 'tags');\n        let authorImagesPath = path.join(siteBasePath, 'media', 'authors');\n        let mediaFilesPath = path.join(siteBasePath, 'media', 'files');\n        let pluginsPath = path.join(siteBasePath, 'config', 'plugins');\n        let pluginsConfigPath = path.join(siteBasePath, 'config', 'site.plugins.json');\n\n        if(!UtilsHelper.dirExists(rootFilesPath)) {\n            fs.mkdirSync(rootFilesPath, { recursive: true });\n        }\n\n        if(!UtilsHelper.dirExists(mediaFilesPath)) {\n            fs.mkdirSync(mediaFilesPath, { recursive: true });\n        }\n\n        if(!UtilsHelper.dirExists(tagImagesPath)) {\n            fs.mkdirSync(tagImagesPath, { recursive: true });\n        }\n\n        if(!UtilsHelper.dirExists(authorImagesPath)) {\n            fs.mkdirSync(authorImagesPath, { recursive: true});\n        }\n\n        if(!UtilsHelper.dirExists(pluginsPath)) {\n            fs.mkdirSync(pluginsPath, { recursive: true });\n        }\n\n        // Create site.plugins.json if not exists\n        if (!UtilsHelper.fileExists(pluginsConfigPath)) {\n            fs.writeFileSync(pluginsConfigPath, '{}');\n        }\n\n        // Move files - if exists to new root-files directory\n        let filesToMove = {\n            'robots.txt': path.join(siteBasePath, 'config', 'robots.txt'),\n            '.htaccess': path.join(siteBasePath, 'config', '.htaccess'),\n            '.htpasswd': path.join(siteBasePath, 'config', '.htpasswd'),\n            '_redirects': path.join(siteBasePath, 'config', '_redirects')\n        };\n\n        let fileNames = Object.keys(filesToMove);\n\n        for (let i = 0; i < fileNames.length; i++) {\n            let fileName = fileNames[i];\n\n            if(!UtilsHelper.dirExists(rootFilesPath)) {\n                break;\n            }\n\n            if (UtilsHelper.fileExists(filesToMove[fileName])) {\n                let destinationPath = path.join(siteBasePath, 'root-files', fileName);\n                fs.moveSync(filesToMove[fileName], destinationPath);\n            }\n        }\n    }\n\n    static async checkWebsiteBackup (appInstance, backupPath) {\n        let siteCreator = new CreateFromBackup(appInstance, backupPath);\n        let result = await siteCreator.prepareBackupToRestore()\n        return result;\n    }\n\n    static checkWebsiteCatalogAvailability (appInstance, siteName) {\n        let catalogName = slug(siteName).toLowerCase();\n\n        if (fs.existsSync(path.join(appInstance.sitesDir, catalogName))) {\n            return {\n                catalogExists: true\n            };\n        }\n\n        return {\n            catalogExists: false\n        };\n    }\n\n    static restoreFromBackup (appInstance, siteName) {\n        let catalogName = slug(siteName).toLowerCase();\n        let source = path.join(appInstance.appDir, 'temp', 'backup-to-restore');\n        let destination = path.join(appInstance.sitesDir, catalogName);\n\n        if (catalogName.trim() === '') {\n            return {\n                status: 'error',\n            };\n        }\n\n        if (fs.existsSync(destination)) {\n            fs.removeSync(destination);\n        }\n\n        fs.moveSync(source, destination);\n        Site.updateNameAndUUIDInSiteConfig(destination, catalogName, siteName);\n        \n        return {\n            status: 'success',\n            data: {\n                siteCatalogName: catalogName\n            }\n        };\n    }\n}\n\nmodule.exports = Site;\n"
  },
  {
    "path": "app/back-end/sites.js",
    "content": "/*\n * Class used to manage the Site class instances\n */\n\nclass Sites {\n    constructor() {\n\n    }\n}\n\nmodule.exports = Sites;\n"
  },
  {
    "path": "app/back-end/sql/1.0.0.sql",
    "content": "----\n-- Basic SQL dump\n----\nBEGIN TRANSACTION;\n\nCREATE TABLE 'authors' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'name' TEXT, 'username' TEXT, 'password' TEXT, 'config' TEXT, 'additional_data' TEXT);\nCREATE TABLE 'posts' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'title' TEXT, 'authors' TEXT, 'slug' TEXT, 'text' TEXT, 'featured_image_id' INTEGER, 'created_at' DATETIME, 'modified_at' DATETIME, 'status' TEXT, 'template' TEXT);\nCREATE TABLE 'posts_additional_data' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'post_id' INTEGER, 'key' TEXT, 'value' TEXT);\nCREATE TABLE 'posts_images' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'post_id' INTEGER, 'url' TEXT, 'title' TEXT, 'caption' TEXT, 'additional_data' TEXT);\nCREATE TABLE 'posts_tags' ('tag_id' INTEGER NOT NULL, 'post_id' INTEGER NOT NULL, PRIMARY KEY ('tag_id', 'post_id'));\nCREATE TABLE 'tags' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'name' TEXT, 'slug' TEXT, 'description' TEXT, 'additional_data' TEXT);\n;\nCOMMIT;\n"
  },
  {
    "path": "app/back-end/tag.js",
    "content": "/*\n * Tag instance\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst Model = require('./model.js');\nconst Tags = require('./tags.js');\nconst slug = require('./helpers/slug');\nconst ImageHelper = require('./helpers/image.helper.js');\nconst Themes = require('./themes.js');\nconst Utils = require('./helpers/utils.js');\n\nclass Tag extends Model {\n    constructor(appInstance, tagData, storeMode = true) {\n        super(appInstance, tagData);\n        this.id = parseInt(tagData.id, 10);\n        this.tagsData = new Tags(appInstance, tagData);\n        this.storeMode = storeMode;\n\n        if (tagData.additionalData) {\n            this.additionalData = tagData.additionalData;\n        }\n\n        if (tagData.imageConfigFields) {\n            this.imageConfigFields = tagData.imageConfigFields;\n        }\n\n        if(tagData.name || tagData.name === '') {\n            this.name = (tagData.name).toString();\n            this.slug = tagData.slug;\n            this.description = tagData.description;\n            this.additionalData = tagData.additionalData;\n            this.prepareTagName();\n        }\n    }\n\n    /*\n     * Save tag\n     */\n    save() {\n        if(this.name === '') {\n            return {\n                status: false,\n                message: 'tag-empty-name'\n            };\n        }\n\n        if(this.slug === '' || this.createSlug(this.slug) === '') {\n            this.slug = this.createSlug(this.name);\n        }\n\n        if(!this.isTagNameUnique()) {\n            return {\n                status: false,\n                message: 'tag-duplicate-name'\n            };\n        }\n\n        if(this.isTagRestrictedSlug()) {\n            return {\n                status: false,\n                message: 'tag-restricted-slug'\n            };\n        }\n\n        if(!this.isTagSlugUnique()) {\n            return {\n                status: false,\n                message: 'tag-duplicate-slug'\n            };\n        }\n\n        if(this.id !== 0) {\n            this.checkAndCleanImages();\n            return this.updateTag();\n        }\n\n        this.checkAndCleanImages();\n        return this.addTag();\n    }\n\n    /*\n     * Add new tag\n     */\n    addTag() {\n        let sqlQuery = this.db.prepare(`INSERT INTO tags VALUES(null, @name, @slug, @desc, @data)`);\n        sqlQuery.run({\n            name: this.name,\n            slug: this.createSlug(this.slug),\n            desc: this.description,\n            data: JSON.stringify(this.additionalData)\n        });\n\n        // Get the newly added item ID if necessary\n        if (this.id === 0) {\n            this.id = this.db.prepare('SELECT last_insert_rowid() AS id').get().id;\n\n            // Move images from the temp directory\n            let tempDirectoryExists = true;\n            let tempImagesDir = path.join(this.siteDir, 'input', 'media', 'tags', 'temp');\n           \n            try {\n                fs.statSync(tempImagesDir).isDirectory();\n            } catch (err) {\n                tempDirectoryExists = false;\n            }\n\n            if (tempDirectoryExists) {\n                let finalImagesDir = path.join(this.siteDir, 'input', 'media', 'tags', (this.id).toString());\n                fs.copySync(tempImagesDir, finalImagesDir);\n                fs.removeSync(tempImagesDir);\n            }\n        }\n\n        return {\n            status: true,\n            message: 'tag-added',\n            tagID: this.id,\n            tags: this.tagsData.load()\n        };\n    }\n\n    /*\n     * Update existing tag\n     */\n    updateTag() {\n        let sqlQuery = this.db.prepare(`UPDATE tags\n                        SET\n                            name = @name,\n                            slug = @slug,\n                            description = @desc,\n                            additional_data = @data\n                        WHERE\n                            id = @id`);\n        sqlQuery.run({\n            name: this.name,\n            slug: this.createSlug(this.slug),\n            desc: this.description,\n            data: JSON.stringify(this.additionalData),\n            id: this.id\n        });\n\n        return {\n            status: true,\n            message: 'tag-updated',\n            tags: this.tagsData.load()\n        };\n    }\n\n    /*\n     * Prepare tag name to save\n     */\n    prepareTagName() {\n        if(typeof this.name == 'undefined') {\n            this.name = '';\n        }\n        // Remove leading and ending spaces (trim it)\n        // it will also exclude case when tag name contains only\n        // whitespaces\n        this.name = this.name.replace(/^\\s+/, '').replace(/\\s+$/, '');\n    }\n\n    /*\n     * Check if the tag name is unique\n     */\n    isTagNameUnique() {\n        let query = this.db.prepare('SELECT * FROM tags WHERE name LIKE @name AND id != @id');\n        let queryParams = {\n            name: this.escape(this.name),\n            id: this.id\n        };\n        let foundedTags = query.all(queryParams);\n\n        if (foundedTags.length) {\n            for (const tag of foundedTags) {\n                if (tag.name === this.name) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n\n    isTagSlugUnique() {\n        let query = this.db.prepare('SELECT slug FROM tags WHERE id != @id');\n        let queryParams = {\n            id: this.id\n        };\n        let foundedSlugs = query.all(queryParams);\n\n        if (foundedSlugs.length) {\n            for (const tag of foundedSlugs) {\n                if (this.slug === tag.slug) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n\n    /*\n     * Check if the tag slug is not restricted\n     */\n    isTagRestrictedSlug() {\n        let slug = this.createSlug(this.slug);\n\n        if(this.application.sites[this.site].advanced.urls.tagsPrefix !== '') {\n            return false;\n        }\n\n        let restrictedSlugs = [\n            'assets',\n            this.application.sites[this.site].advanced.urls.authorsPrefix,\n            'media',\n            this.application.sites[this.site].advanced.urls.pageName\n        ];\n\n        return restrictedSlugs.indexOf(slug) > -1;\n    }\n\n    /*\n     * Delete tag\n     */\n    delete() {\n        this.db.exec(`DELETE FROM tags WHERE id = ${parseInt(this.id, 10)}`);\n        this.db.exec(`DELETE FROM posts_tags WHERE tag_id = ${parseInt(this.id, 10)}`);\n        ImageHelper.deleteImagesDirectory(this.siteDir, 'tags', this.id);\n        \n        return {\n            status: true,\n            message: 'tag-deleted'\n        };\n    }\n\n    /*\n     * Create slug for given string\n     */\n    createSlug(string) {\n        return slug(string);\n    }\n\n    /*\n     * Remove unused images\n     */\n    checkAndCleanImages (cancelEvent = false) {\n        let tagDir = this.id;\n\n        if(this.id === 0) {\n            tagDir = 'temp';\n        }\n\n        let imagesDir = path.join(this.siteDir, 'input', 'media', 'tags', (tagDir).toString());\n        let tagDirectoryExists = true;\n\n        try {\n            fs.statSync(imagesDir).isDirectory();\n        } catch (err) {\n            tagDirectoryExists = false;\n        }\n\n        if(!tagDirectoryExists) {\n            return;\n        }\n\n        let images = fs.readdirSync(imagesDir);\n        this.cleanImages(images, imagesDir, cancelEvent);\n    }\n\n    /*\n     * Removes images from a given image dir\n     */\n    cleanImages(images, imagesDir, cancelEvent) {\n        let tagDir = this.id;\n        let featuredImage = '';\n        let viewConfig = {};\n        \n        if (this.additionalData && this.additionalData.featuredImage) {\n            featuredImage = path.parse(this.additionalData.featuredImage).base;\n        }\n\n        if (this.additionalData && this.additionalData.viewConfig) {\n            viewConfig = this.additionalData.viewConfig;\n        }\n\n        // If tag is cancelled - get the previous featured image and option images data\n        if (cancelEvent && this.id !== 0) {\n            let additionalDataQuery = `SELECT additional_data FROM tags WHERE id = @id`;\n            let additionalDataResult = this.db.prepare(additionalDataQuery).all({ id: this.id });\n\n            if (additionalDataResult) {\n                try {\n                    featuredImage = JSON.parse(additionalDataResult[0].additional_data).featuredImage;\n                    viewConfig = JSON.parse(additionalDataResult[0].additional_data).viewConfig;\n                } catch (e) {\n                    console.log('(!) An issue occurred during parsing tag additional data', this.id);\n                }\n            }\n        }\n\n        if (this.id === 0) {\n            tagDir = 'temp';\n        }\n\n        let imagesInViewSettings = [];\n        \n        imagesInViewSettings = Object.keys(viewConfig).filter((fieldName) => {\n            return this.imageConfigFields.indexOf(fieldName) !== -1 && viewConfig[fieldName] !== '';\n        }).map((fieldName) => {\n            return viewConfig[fieldName];\n        });\n\n        // Iterate through images\n        for (let i in images) {\n            let imagePath = images[i];\n            let fullPath = path.join(imagesDir, imagePath);\n\n            // Skip dirs and symlinks\n            if (imagePath === '.' || imagePath === '..' || imagePath === 'responsive') {\n                continue;\n            }\n\n            // Remove files which does not exist as featured image and tagViewSettings\n            if (\n                (cancelEvent && tagDir === 'temp') ||\n                (\n                    imagesInViewSettings.indexOf(imagePath) === -1 &&\n                    featuredImage !== imagePath\n                )\n            ) {\n                try {\n                    fs.unlinkSync(fullPath);\n                } catch(e) {\n                    console.error(e);\n                }\n\n                this.removeResponsiveImages(fullPath);\n            }\n        }\n    }\n\n    /*\n     * Remove unused responsive images\n     */\n    removeResponsiveImages(originalPath) {\n        let themesHelper = new Themes(this.application, { site: this.site });\n        let currentTheme = themesHelper.currentTheme();\n\n        // If there is no selected theme\n        if (currentTheme === 'not selected') {\n            return;\n        }\n\n        // Load theme config\n        let themeConfig = Utils.loadThemeConfig(path.join(this.siteDir, 'input'), currentTheme);\n\n        // check if responsive images config exists\n        if(Utils.responsiveImagesConfigExists(themeConfig)) {\n            let dimensions = Utils.responsiveImagesDimensions(themeConfig, 'contentImages');\n            let featuredDimensions = Utils.responsiveImagesDimensions(themeConfig, 'tagImages');\n\n            if (featuredDimensions !== false) {\n                featuredDimensions.forEach(item => {\n                    if (dimensions.indexOf(item) === -1) {\n                        dimensions.push(item);\n                    }\n                });\n            }\n\n            let responsiveImagesDir = path.parse(originalPath).dir;\n            responsiveImagesDir = path.join(responsiveImagesDir, 'responsive');\n\n            if (typeof dimensions === \"boolean\") {\n                return;\n            }\n\n            let forceWebp = !!this.application.sites[this.site]?.advanced?.forceWebp;\n\n            // Remove responsive images of each size\n            for(let dimensionName of dimensions) {\n                let filename = path.parse(originalPath).name;\n                let extension = path.parse(originalPath).ext;\n\n                if (forceWebp && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) {\n                    extension = '.webp'; \n                }\n\n                let responsiveImagePath = path.join(responsiveImagesDir, filename + '-' + dimensionName + extension);\n\n                if(Utils.fileExists(responsiveImagePath)){\n                    fs.unlinkSync(responsiveImagePath);\n                }\n            }\n        }\n    }\n\n    /*\n     * Change tag visibility status\n     */\n    changeStatus(status, inverse = false) {\n        let selectQuery = this.db.prepare(`SELECT additional_data FROM tags WHERE id = @id`).all({ id: this.id });\n        let additionalData = selectQuery[0].additional_data;\n\n        try {\n            additionalData = JSON.parse(additionalData);\n\n            if (inverse) {\n                additionalData.isHidden = false;\n            } else {\n                additionalData.isHidden = true;\n            }\n        } catch (e) {\n            return false;\n        }\n\n        let updateQuery = this.db.prepare(`UPDATE\n                                        tags\n                                    SET\n                                        additional_data = @updatedData\n                                    WHERE\n                                        id = @id`);\n        updateQuery.run({\n            updatedData: JSON.stringify(additionalData),\n            id: this.id\n        });\n\n        return true;\n    }\n}\n\nmodule.exports = Tag;\n"
  },
  {
    "path": "app/back-end/tags.js",
    "content": "/*\n * Tags instance\n */\n\nconst Model = require('./model.js');\n\nclass Tags extends Model {\n    constructor(appInstance, tagsData) {\n        super(appInstance, tagsData);\n    }\n\n    /*\n     * Load tags\n     */\n    load() {\n        let sqlQuery = `SELECT\n            id,\n            name,\n            slug,\n            description,\n            additional_data AS additionalData\n        FROM\n            tags\n        GROUP BY\n            id\n        ORDER BY\n            id DESC`;\n\n        return this.db.prepare(sqlQuery).all();\n    }\n}\n\nmodule.exports = Tags;\n"
  },
  {
    "path": "app/back-end/themes.js",
    "content": "/*\n * Themes instance\n */\n\nconst fs = require('fs-extra');\nconst path = require('path');\nconst FileHelper = require('./helpers/file.js');\nconst slug = require('./helpers/slug');\nconst UtilsHelper = require('./helpers/utils.js');\nconst themeConfigValidator = require('./modules/render-html/validators/theme-config.js');\nconst normalizePath = require('normalize-path');\nconst Authors = require('./authors.js');\n\nclass Themes {\n    constructor(appInstance, siteData = false) {\n        this.basePath = appInstance.appDir;\n        this.sitesDir = appInstance.sitesDir;\n        this.themesPath = path.join(this.basePath, 'themes');\n        this.sitePath = false;\n        this.siteConfigPath = false;\n        this.siteName = siteData.site;\n        this.appInstance = appInstance;\n\n        if(siteData) {\n            this.sitePath = path.join(this.sitesDir, this.siteName, 'input', 'themes');\n            this.siteInputPath = path.join(this.sitesDir, this.siteName, 'input');\n            this.siteConfigPath = path.join(this.sitesDir, this.siteName, 'input', 'config', 'site.config.json');\n        }\n    }\n\n    /*\n     * Load themes from a specific path\n     */\n    loadThemes(pathToThemes = false) {\n        if(!pathToThemes) {\n            pathToThemes = this.themesPath;\n        }\n\n        let filesAndDirs = fs.readdirSync(pathToThemes);\n        let output = [];\n\n        let themeLocation = 'app';\n\n        if (this.sitePath && pathToThemes === this.sitePath) {\n            themeLocation = 'site';\n        }\n\n        for(let i = 0; i < filesAndDirs.length; i++) {\n            if (filesAndDirs[i][0] === '.' || !UtilsHelper.dirExists(path.join(pathToThemes, filesAndDirs[i]))) {\n                continue;\n            }\n\n            // Exclude overrided themes\n            if(filesAndDirs[i].substr(-9) === '-override') {\n                continue;\n            }\n\n            let configPath = path.join(pathToThemes, filesAndDirs[i], 'config.json');\n\n            // Load only proper themes\n            if (!fs.existsSync(configPath)) {\n                continue;\n            }\n\n            // Load only properly configured themes\n            if(themeConfigValidator(configPath) !== true) {\n                continue;\n            }\n\n            let themeData = FileHelper.readFileSync(configPath, 'utf8');\n            themeData = JSON.parse(themeData);\n\n            output.push({\n                location: themeLocation,\n                directory: filesAndDirs[i],\n                name: themeData.name,\n                version: themeData.version\n            });\n        }\n\n        return output;\n    }\n\n    /*\n     * Load informations about the current theme\n     */\n    currentTheme(returnDir = false) {\n        let siteData = FileHelper.readFileSync(this.siteConfigPath, 'utf8');\n        siteData = JSON.parse(siteData);\n\n        if(!returnDir && siteData.theme) {\n            let themeData = FileHelper.readFileSync(path.join(this.sitePath, siteData.theme, 'config.json'));\n            themeData = JSON.parse(themeData);\n\n            if(themeData.name) {\n                return themeData.name.toLowerCase();\n            }\n        }\n\n        if(returnDir && siteData.theme) {\n            return siteData.theme.toLowerCase();\n        }\n\n        return \"not selected\";\n    }\n\n    /*\n     * Load results and merge them\n     */\n    load() {\n        let applicationThemes = this.loadThemes(this.themesPath);\n        let siteThemes = this.loadThemes(this.sitePath);\n\n        return [...applicationThemes, ...siteThemes];\n    }\n\n    /*\n     * Change theme to the selected one\n     */\n    changeTheme(newTheme, oldTheme) {\n        if(!newTheme) {\n            return '';\n        }\n\n        if(\n            newTheme.indexOf('use-') === -1 &&\n            newTheme.indexOf('install-use-') === -1 &&\n            newTheme.indexOf('uninstall-') === -1\n        ) {\n            return newTheme;\n        }\n\n        // Remove theme settings when the used theme is changed\n        if(\n            (\n                newTheme.indexOf('use-') === 0 &&\n                newTheme.replace('use-', '') !== oldTheme\n            ) || (\n                newTheme.indexOf('install-use-') > -1 &&\n                newTheme.replace('install-use-', '') !== oldTheme\n            )\n        ) {\n            let oldConfigPath = path.join(this.siteInputPath, 'config', 'theme.config.json');\n            let basicConfig = false;\n\n            if (UtilsHelper.fileExists(oldConfigPath)) {\n                try {\n                    let oldConfig = FileHelper.readFileSync(oldConfigPath);\n                    oldConfig = JSON.parse(oldConfig);\n                    basicConfig = { \n                        config: {\n                            logo: oldConfig.config.logo\n                        }\n                    };\n                    basicConfig = JSON.stringify(basicConfig, null, 4);\n                } catch (e) {\n                    console.log('(!) Cannot parse old config');\n                }\n\n                fs.removeSync(oldConfigPath);\n\n                if (basicConfig) {\n                    fs.writeFileSync(oldConfigPath, basicConfig);\n                }\n            } else {\n                fs.removeSync(oldConfigPath);\n            }\n\n            let newThemeName =  newTheme.replace('install-use-', '')\n                                        .replace('uninstall-', '')\n                                        .replace('use-', '');\n            let backupConfigPath = path.join(this.siteInputPath, 'config', 'theme.' + newThemeName + '.config.json');\n\n            if (UtilsHelper.fileExists(backupConfigPath)) {\n                fs.copySync(\n                    path.join(this.siteInputPath, 'config', 'theme.' + newThemeName + '.config.json'),\n                    path.join(this.siteInputPath, 'config', 'theme.config.json')\n                );\n            }\n        }\n\n        // For changing just installed themes - return the theme name\n        if(newTheme.indexOf('use-') === 0) {\n            return newTheme.replace('use-', '');\n        }\n\n        // For uninstalling themes - return empty string or theme name (if the used theme was not removed)\n        if(newTheme.indexOf('uninstall-') === 0) {\n            let themeToRemove = newTheme.replace('uninstall-', '');\n\n            // Remove theme dir\n            let themeDirStat = false;\n\n            try {\n                themeDirStat = fs.statSync(path.join(this.sitePath, themeToRemove));\n            } catch(e) {}\n\n            if(themeDirStat && themeDirStat.isDirectory()) {\n                // If yes - remove it\n                fs.removeSync(path.join(this.sitePath, themeToRemove));\n            }\n\n            // If current theme is different than removed theme - return its name\n            if(oldTheme !== themeToRemove) {\n                return oldTheme;\n            }\n\n            // Otherwise - return empty string as there is currently no used theme\n            return '';\n        }\n\n        // Prepare the theme directory name\n        newTheme = newTheme.replace('install-use-', '');\n        // For installing new themes:\n        // Check if the theme exists\n        let themeDirStat = false;\n\n        try {\n            themeDirStat = fs.statSync(path.join(this.sitePath, newTheme));\n        } catch(e) {}\n\n        if(themeDirStat && themeDirStat.isDirectory()) {\n            // If yes - remove it\n            fs.removeSync(path.join(this.sitePath, newTheme));\n        }\n\n        // Copy theme to the site directory\n        fs.copySync(\n            path.join(this.themesPath, newTheme),\n            path.join(this.sitePath, newTheme)\n        );\n\n        // Return new name\n        return newTheme;\n    }\n\n    /*\n     * Load available post templates\n     */\n    loadPostTemplates() {\n        let postTemplates = [];\n        let siteData = FileHelper.readFileSync(this.siteConfigPath, 'utf8');\n        siteData = JSON.parse(siteData);\n\n        if(siteData.theme) {\n            let themeDir = path.join(this.sitePath, siteData.theme);\n            let themeConfigPath = path.join(this.sitePath, 'input', 'config', 'theme.config.json');\n            let themeData = Themes.loadThemeConfig(themeConfigPath, themeDir);\n\n            if(themeData.postTemplates) {\n                let templateFiles = Object.keys(themeData.postTemplates);\n\n                for(let i = 0; i < templateFiles.length; i++) {\n                    let fileName = templateFiles[i];\n                    postTemplates.push(\n                        [fileName, themeData.postTemplates[fileName]]\n                    );\n                }\n            }\n        }\n\n        return postTemplates;\n    }\n\n    /*\n     * Load available page templates\n     */\n    loadPageTemplates() {\n        let pageTemplates = [];\n        let siteData = FileHelper.readFileSync(this.siteConfigPath, 'utf8');\n        siteData = JSON.parse(siteData);\n\n        if(siteData.theme) {\n            let themeDir = path.join(this.sitePath, siteData.theme);\n            let themeConfigPath = path.join(this.sitePath, 'input', 'config', 'theme.config.json');\n            let themeData = Themes.loadThemeConfig(themeConfigPath, themeDir);\n\n            if(themeData.pageTemplates) {\n                let templateFiles = Object.keys(themeData.pageTemplates);\n\n                for(let i = 0; i < templateFiles.length; i++) {\n                    let fileName = templateFiles[i];\n                    pageTemplates.push(\n                        [fileName, themeData.pageTemplates[fileName]]\n                    );\n                }\n            }\n        }\n\n        return pageTemplates;\n    }\n\n    /*\n     * Load available tag templates\n     */\n    loadTagTemplates() {\n        let tagTemplates = [];\n        let siteData = FileHelper.readFileSync(this.siteConfigPath, 'utf8');\n        siteData = JSON.parse(siteData);\n\n        if(siteData.theme) {\n            let themeDir = path.join(this.sitePath, siteData.theme);\n            let themeConfigPath = path.join(this.sitePath, 'input', 'config', 'theme.config.json');\n            let themeData = Themes.loadThemeConfig(themeConfigPath, themeDir);\n\n            if(themeData.tagTemplates) {\n                let templateFiles = Object.keys(themeData.tagTemplates);\n\n                for(let i = 0; i < templateFiles.length; i++) {\n                    let fileName = templateFiles[i];\n                    tagTemplates.push(\n                        [fileName, themeData.tagTemplates[fileName]]\n                    );\n                }\n            }\n        }\n\n        return tagTemplates;\n    }\n\n    /*\n     * Load available author templates\n     */\n    loadAuthorTemplates() {\n        let authorTemplates = [];\n        let siteData = FileHelper.readFileSync(this.siteConfigPath, 'utf8');\n        siteData = JSON.parse(siteData);\n\n        if(siteData.theme) {\n            let themeDir = path.join(this.sitePath, siteData.theme);\n            let themeConfigPath = path.join(this.sitePath, 'input', 'config', 'theme.config.json');\n            let themeData = Themes.loadThemeConfig(themeConfigPath, themeDir);\n\n            if(themeData.authorTemplates) {\n                let templateFiles = Object.keys(themeData.authorTemplates);\n\n                for(let i = 0; i < templateFiles.length; i++) {\n                    let fileName = templateFiles[i];\n                    authorTemplates.push(\n                        [fileName, themeData.authorTemplates[fileName]]\n                    );\n                }\n            }\n        }\n\n        return authorTemplates;\n    }\n\n    /*\n     * Remove specific theme from the app directory\n     */\n    removeTheme(directory) {\n        fs.removeSync(path.join(this.themesPath, directory));\n    }\n\n    updateThemeConfig(newConfig) {\n        let themeConfigPath = path.join(this.sitesDir, this.siteName, 'input', 'config', 'theme.config.json');\n        let themeBackupConfigPath = path.join(this.sitesDir, this.siteName, 'input', 'config', 'theme.' + this.currentTheme(true) + '.config.json');\n        let themeDefaultConfig = UtilsHelper.loadThemeConfig(path.join(this.sitesDir, this.siteName, 'input'), this.currentTheme(true));\n        let themeConfig = {\n            config: newConfig.config,\n            customConfig: newConfig.customConfig,\n            postConfig: newConfig.postConfig,\n            pageConfig: newConfig.pageConfig,\n            tagConfig: newConfig.tagConfig,\n            authorConfig: newConfig.authorConfig,\n            defaultTemplates: newConfig.defaultTemplates\n        };\n\n        // Check all options for the media fields\n        let groups = ['config', 'customConfig', 'postConfig', 'pageConfig', 'tagConfig', 'authorConfig'];\n\n        for(let i = 0; i < groups.length; i++) {\n            let options = themeDefaultConfig[groups[i]];\n\n            if(!options) {\n                continue;\n            }\n\n            for (let j = 0; j < options.length; j++) {\n                if (\n                    themeDefaultConfig[groups[i]][j] &&\n                    (\n                        themeDefaultConfig[groups[i]][j].type === 'upload' ||\n                        themeDefaultConfig[groups[i]][j].type === 'smallupload'\n                    )\n                ) {\n                    let mediaPathKey = themeDefaultConfig[groups[i]][j].name;\n\n                    if (themeConfig[groups[i]] && themeConfig[groups[i]][mediaPathKey]) {\n                        let mediaPath = themeConfig[groups[i]][mediaPathKey];\n                        themeConfig[groups[i]][mediaPathKey] = this.normalizeThemeImagePath(mediaPath);\n                    }\n                }\n            }\n        }\n\n        let configString = JSON.stringify(themeConfig, null, 4);\n\n        // Save config\n        fs.writeFileSync(themeConfigPath, configString);\n        fs.writeFileSync(themeBackupConfigPath, configString);\n\n        // Remove unused config images\n        this.checkAndCleanImages(configString);\n    }\n\n    /*\n     * Fixes path for the media file\n     */\n    normalizeThemeImagePath(imagePath) {\n        // Save the image if necessary\n        imagePath = normalizePath(imagePath);\n        imagePath = imagePath.replace('file:/', '');\n\n        return imagePath;\n    }\n\n    /*\n     * Load theme config\n     */\n    static loadThemeConfig(themeConfigPath, themeDir) {\n        if(themeDir === 'not selected') {\n            return false;\n        }\n\n        // Load default config\n        let defaultConfigPath = path.join(__dirname, '..', 'default-files', 'theme-files', 'config.json');\n        let defaultThemeConfig = JSON.parse(FileHelper.readFileSync(defaultConfigPath));\n\n        // Load basic theme config\n        let themeLocalConfig = UtilsHelper.loadThemeConfig(themeDir);\n\n        if (!themeLocalConfig.customConfig) {\n            themeLocalConfig.customConfig = [];\n        }\n\n        defaultThemeConfig = UtilsHelper.mergeObjects(defaultThemeConfig, themeLocalConfig);\n\n        // Load theme config overrides\n        if(UtilsHelper.fileExists(themeConfigPath)) {\n            let themeSavedConfig;\n            \n            try {\n                themeSavedConfig = JSON.parse(FileHelper.readFileSync(themeConfigPath));\n            } catch (err) {\n                console.log('(!) The saved theme config is malformed. Loading default theme config instead.');\n                return;\n            }\n\n            let optionGroups = ['config', 'customConfig', 'postConfig', 'pageConfig', 'tagConfig', 'authorConfig'];\n\n            for(let k = 0; k < optionGroups.length; k++) {\n                let group = optionGroups[k];\n\n                if (themeSavedConfig[optionGroups[k]]) {\n                    for (let i = 0; i < defaultThemeConfig[group].length; i++) {\n                        let name = defaultThemeConfig[group][i].name;\n\n                        if (typeof themeSavedConfig[group][name] === 'undefined') {\n                            continue;\n                        }\n\n                        if (themeSavedConfig[group][name] !== '') {\n                            defaultThemeConfig[group][i].value = themeSavedConfig[group][name];\n                        }\n                    }\n                }\n            }\n\n            if (themeSavedConfig.defaultTemplates) {\n                defaultThemeConfig.defaultTemplates = JSON.parse(JSON.stringify(themeSavedConfig.defaultTemplates));\n            }\n        }\n\n        let computedOptionsConfig = Themes.loadComputedOptions(themeDir, defaultThemeConfig);\n        defaultThemeConfig.customConfig = defaultThemeConfig.customConfig.concat(computedOptionsConfig);\n\n        return defaultThemeConfig;\n    }\n\n    /*\n     * Remove unused images\n     */\n    checkAndCleanImages(configString) {\n        let siteData = FileHelper.readFileSync(this.siteConfigPath, 'utf8');\n        siteData = JSON.parse(siteData);\n        let authors = new Authors(this.appInstance, {site: siteData.name});\n        let authorsData = authors.load();\n        let authorAvatars = [];\n        let ogFallbackImage = '';\n\n        if(authorsData && authorsData.length) {\n            for(let i = 0; i < authorsData.length; i++) {\n                let authorConfig = authorsData[i].config;\n                let avatar = '';\n\n                try {\n                    authorConfig = JSON.parse(authorConfig);\n                    avatar = authorConfig.avatar;\n                } catch(e) {\n                    console.log('Getting user avatar in themes.js:');\n                    console.log(e);\n                }\n\n                if(avatar !== '') {\n                    authorAvatars.push(avatar);\n                }\n            }\n        }\n\n        if(siteData && siteData.advanced && siteData.advanced.openGraphImage) {\n            ogFallbackImage = siteData.advanced.openGraphImage;\n        }\n\n        // Parse json before slashes normalization\n        let configObject = JSON.parse(configString);\n        // Make sure that all slashes are in the same direction\n        configString = normalizePath(configString);\n\n        // Get images dir\n        let imagesDir = path.join(this.siteInputPath, 'media', 'website');\n\n        if(!UtilsHelper.dirExists(imagesDir)) {\n            return;\n        }\n\n        let images = fs.readdirSync(imagesDir);\n\n        // Iterate through images\n        for (let i in images) {\n            let imagePath = images[i];\n            let fullPath = path.join(imagesDir, imagePath);\n\n            // Skip dirs and symlinks\n            if(imagePath === '.' || imagePath === '..' || imagePath === 'responsive' || imagePath === 'gallery') {\n                continue;\n            }\n\n            // Remove files which does not exist in the config string and as avatars/OG fallback image\n            if(\n                configString.indexOf('/' + imagePath) === -1 &&\n                configString.indexOf('\"' + imagePath + '\"') === -1 &&\n                authorAvatars.indexOf(imagePath) === -1 &&\n                imagePath !== ogFallbackImage\n            ) {\n                try {\n                    fs.unlinkSync(fullPath);\n                } catch(e) {\n                    console.error(e);\n                }\n\n                // Remove responsive images\n                this.removeResponsiveImages(fullPath);\n            }\n        }\n\n        // clean up unnecessary default images\n        let assetsToCheck = [\n            {\n                dir: 'posts', \n                configType: 'postConfig'\n            },\n            {\n                dir: 'tags', \n                configType: 'tagConfig'\n            },\n            {\n                dir: 'authors', \n                configType: 'authorConfig'\n            }\n        ];\n\n        for (let i = 0; i < assetsToCheck.length; i++) {\n            let dirToCheck = assetsToCheck[i].dir;\n            let configToCheck = assetsToCheck[i].configType;\n            let viewImagesDir = path.join(this.siteInputPath, 'media', dirToCheck, 'defaults');\n\n            if(!UtilsHelper.dirExists(viewImagesDir)) {\n                return;\n            }\n\n            let viewImages = fs.readdirSync(viewImagesDir);\n            let configImages = Object.values(configObject[configToCheck]);\n\n            // Iterate through images\n            for (let i in viewImages) {\n                let imagePath = viewImages[i];\n                let fullPath = path.join(viewImagesDir, imagePath);\n\n                // Skip dirs and symlinks\n                if (imagePath === '.' || imagePath === '..' || imagePath === 'responsive' || imagePath === 'gallery') {\n                    continue;\n                }\n\n                // Remove files which does not exist in the post text\n                if (configImages.indexOf(imagePath) === -1) {\n                    try {\n                        fs.unlinkSync(fullPath);\n                    } catch(e) {\n                        console.error(e);\n                    }\n\n                    // Remove responsive images\n                    this.removeResponsiveImages(fullPath);\n                }\n            }\n        }\n    }\n\n    /*\n     * Remove unused responsive images\n     */\n    removeResponsiveImages(originalPath) {\n        let currentTheme = this.currentTheme();\n        let dimensions = false;\n\n        // If there is no selected theme\n        if(currentTheme === 'not selected') {\n            return;\n        }\n\n        // Load theme config\n        let themeConfig = UtilsHelper.loadThemeConfig(this.siteInputPath, currentTheme);\n        let forceWebp = !!this.appInstance.sites[this.siteName]?.advanced?.forceWebp;\n        \n        // check if responsive images config exists\n        if(UtilsHelper.responsiveImagesConfigExists(themeConfig)) {\n            dimensions = UtilsHelper.responsiveImagesDimensions(themeConfig, 'optionImages');\n\n            if(dimensions === false) {\n                dimensions = UtilsHelper.responsiveImagesDimensions(themeConfig, 'contentImages');\n            }\n\n            if(dimensions === false) {\n                return;\n            }\n\n            let responsiveImagesDir = path.parse(originalPath).dir;\n            responsiveImagesDir = path.join(responsiveImagesDir, 'responsive');\n\n            // create responsive images for each size\n            for(let dimensionName of dimensions) {\n                let filename = path.parse(originalPath).name;\n                let extension = path.parse(originalPath).ext;\n                \n                if (forceWebp && ['.png', '.jpg', '.jpeg'].indexOf(extension.toLowerCase()) > -1) {\n                    extension = '.webp'; \n                }\n\n                let responsiveImagePath = path.join(responsiveImagesDir, filename + '-' + dimensionName + extension);\n\n                if(UtilsHelper.fileExists(responsiveImagePath)) {\n                    fs.unlinkSync(responsiveImagePath);\n                }\n            }\n        }\n    }\n\n    /**\n     * Load option values from computed-options.js file (if exists)\n     */\n    static loadComputedOptions(inputDir, themeConfig) {\n        let computedOptionsConfig = [];\n        let computedOptionsPath = path.join(inputDir, 'computed-options.js');\n        let overridedComputedOptionsPath = UtilsHelper.fileIsOverrided(inputDir, computedOptionsPath);\n\n        if (overridedComputedOptionsPath) {\n            computedOptionsPath = overridedComputedOptionsPath;\n        }\n\n        if (!UtilsHelper.fileExists(computedOptionsPath)) {\n            return computedOptionsConfig;\n        }\n\n        try {\n            computedOptionsConfig = UtilsHelper.requireWithNoCache(computedOptionsPath, themeConfig);\n        } catch(e) {\n            console.log('The theme computed-options.js file is corrupted');\n            return [];\n        }\n\n        return computedOptionsConfig;\n    }\n}\n\nmodule.exports = Themes;\n"
  },
  {
    "path": "app/back-end/vendor/locutus/htmlspecialchars.js",
    "content": "module.exports = function htmlspecialchars (string, quoteStyle, charset, doubleEncode) {\n    //       discuss at: http://locutus.io/php/htmlspecialchars/\n    //      original by: Mirek Slugen\n    //      improved by: Kevin van Zonneveld (http://kvz.io)\n    //      bugfixed by: Nathan\n    //      bugfixed by: Arno\n    //      bugfixed by: Brett Zamir (http://brett-zamir.me)\n    //      bugfixed by: Brett Zamir (http://brett-zamir.me)\n    //       revised by: Kevin van Zonneveld (http://kvz.io)\n    //         input by: Ratheous\n    //         input by: Mailfaker (http://www.weedem.fr/)\n    //         input by: felix\n    // reimplemented by: Brett Zamir (http://brett-zamir.me)\n    //           note 1: charset argument not supported\n    //        example 1: htmlspecialchars(\"<a href='test'>Test</a>\", 'ENT_QUOTES')\n    //        returns 1: '&lt;a href=&#039;test&#039;&gt;Test&lt;/a&gt;'\n    //        example 2: htmlspecialchars(\"ab\\\"c'd\", ['ENT_NOQUOTES', 'ENT_QUOTES'])\n    //        returns 2: 'ab\"c&#039;d'\n    //        example 3: htmlspecialchars('my \"&entity;\" is still here', null, null, false)\n    //        returns 3: 'my &quot;&entity;&quot; is still here'\n\n    var optTemp = 0\n    var i = 0\n    var noquotes = false\n    if (typeof quoteStyle === 'undefined' || quoteStyle === null) {\n        quoteStyle = 2\n    }\n    string = string || ''\n    string = string.toString()\n\n    if (doubleEncode !== false) {\n        // Put this first to avoid double-encoding\n        string = string.replace(/&/g, '&amp;')\n    }\n\n    string = string\n        .replace(/</g, '&lt;')\n        .replace(/>/g, '&gt;')\n\n    var OPTS = {\n        'ENT_NOQUOTES': 0,\n        'ENT_HTML_QUOTE_SINGLE': 1,\n        'ENT_HTML_QUOTE_DOUBLE': 2,\n        'ENT_COMPAT': 2,\n        'ENT_QUOTES': 3,\n        'ENT_IGNORE': 4\n    }\n    if (quoteStyle === 0) {\n        noquotes = true\n    }\n    if (typeof quoteStyle !== 'number') {\n        // Allow for a single string or an array of string flags\n        quoteStyle = [].concat(quoteStyle)\n        for (i = 0; i < quoteStyle.length; i++) {\n            // Resolve string input to bitwise e.g. 'ENT_IGNORE' becomes 4\n            if (OPTS[quoteStyle[i]] === 0) {\n                noquotes = true\n            } else if (OPTS[quoteStyle[i]]) {\n                optTemp = optTemp | OPTS[quoteStyle[i]]\n            }\n        }\n        quoteStyle = optTemp\n    }\n    if (quoteStyle & OPTS.ENT_HTML_QUOTE_SINGLE) {\n        string = string.replace(/'/g, '&#039;')\n    }\n    if (!noquotes) {\n        string = string.replace(/\"/g, '&quot;')\n    }\n\n    return string\n}\n"
  },
  {
    "path": "app/back-end/window-manager.js",
    "content": "class PubliiWindowManager {\n    constructor () {\n        this.initEvents();\n        this.createMainWindow();\n    }\n\n    initEvents () {\n        \n    }\n\n    createMainWindow () {\n\n    }\n\n    createEditorWindow () {\n        \n    }\n\n    getMainWindow () {\n\n    }\n\n    getEditorWindow () {\n\n    }\n\n    removeMainWindow () {\n\n    }\n\n    removeEditorWindow () {\n\n    }\n}\n"
  },
  {
    "path": "app/back-end/workers/backup/create.js",
    "content": "const { parentPort } = require('worker_threads');\nconst Backup = require('./../../modules/backup/backup.js');\n\nparentPort.on('message', function(msg){\n    if(msg.type === 'dependencies') {\n        let siteName = msg.siteName;\n        let backupsDir = msg.backupsDir;\n        let sourceDir = msg.sourceDir;\n        let backupFilename = msg.backupFilename;\n\n        Backup.create(siteName, backupFilename, backupsDir, sourceDir);\n    }\n});\n"
  },
  {
    "path": "app/back-end/workers/backup/restore.js",
    "content": "const { parentPort } = require('worker_threads');\nconst Backup = require('./../../modules/backup/backup.js');\n\nparentPort.on('message', function(msg){\n    if (msg.type === 'dependencies') {\n        let siteName = msg.siteName;\n        let backupName = msg.backupName;\n        let backupsDir = msg.backupsDir;\n        let destinationDir = msg.destinationDir;\n        let tempDir = msg.tempDir;\n\n        Backup.restore(siteName, backupName, backupsDir, destinationDir, tempDir);\n    }\n});\n"
  },
  {
    "path": "app/back-end/workers/deploy/deployment.js",
    "content": "const Deployment = require('./../../modules/deploy/deployment.js');\nlet deploymentInstance = false;\n\nprocess.on('message', function(msg){\n    if(msg.type === 'dependencies') {\n        let appDir = msg.appDir;\n        let sitesDir = msg.sitesDir;\n        let siteConfig = msg.siteConfig;\n        let useFtpAlt = msg.useFtpAlt;\n        deploymentInstance = new Deployment(appDir, sitesDir, siteConfig, useFtpAlt);\n        deploymentInstance.initSession().then(() => true);\n    }\n\n    if (msg.type === 'continue-sync' && deploymentInstance) {\n        deploymentInstance.continueSync([]);\n    }\n\n    if ((msg.type === 'abort' || msg.type === 'cancel-sync') && deploymentInstance) {\n        if(\n            deploymentInstance.siteConfig.deployment.protocol === 'sftp' ||\n            deploymentInstance.siteConfig.deployment.protocol === 'sftp+key'\n        ) {\n            deploymentInstance.client.connection.end();\n        }\n\n        if(\n            deploymentInstance.siteConfig.deployment.protocol === 'ftp' ||\n            deploymentInstance.siteConfig.deployment.protocol === 'ftp+tls'\n        ) {\n            if (deploymentInstance.client.connection.close) {\n                deploymentInstance.client.connection.close();\n            } else {\n                deploymentInstance.client.connection.destroy();\n            }\n        }\n\n        setTimeout(function() {\n            process.kill(process.pid, 'SIGTERM');\n        }, 1000);\n    }\n});\n"
  },
  {
    "path": "app/back-end/workers/import/check.js",
    "content": "const Import = require('./../../modules/import/import.js');\n\nprocess.on('message', function(msg){\n    if(msg.type === 'dependencies') {\n        let appInstance = null;\n        let siteName = msg.siteName;\n        let filePath = msg.filePath;\n        let importer = new Import(appInstance, siteName, filePath);\n        let results = importer.checkFile();\n\n        process.send(results);\n\n        setTimeout(function () {\n            process.exit();\n        }, 1000);\n    }\n});\n"
  },
  {
    "path": "app/back-end/workers/import/import.js",
    "content": "const Import = require('./../../modules/import/import.js');\n\nprocess.on('message', function(msg){\n    if(msg.type === 'dependencies') {\n        let appInstance = msg.appInstance;\n        let siteName = msg.siteName;\n        let filePath = msg.filePath;\n        let importAuthors = msg.importAuthors;\n        let usedTaxonomy = msg.usedTaxonomy;\n        let autop = msg.autop;\n        let postTypes = msg.postTypes;\n\n        let importer = new Import(appInstance, siteName, filePath);\n        importer.importFile(importAuthors, usedTaxonomy, autop, postTypes);\n    }\n});\n"
  },
  {
    "path": "app/back-end/workers/renderer/preview.js",
    "content": "const Renderer = require('./../../modules/render-html/renderer.js');\n\nprocess.on('message', async function(msg){\n    if(msg.type == 'dependencies') {\n        let appDir = msg.appDir;\n        let sitesDir = msg.sitesDir;\n        let siteConfig = msg.siteConfig;\n        let itemID = msg.itemID;\n        let postData = msg.postData;\n        let previewMode = msg.previewMode;\n        let mode = msg.mode || 'full';\n        let previewLocation = msg.previewLocation;\n        let renderer = new Renderer(appDir, sitesDir, siteConfig, itemID, postData);\n        let result;\n\n        try {\n            result = await renderer.render(previewMode, previewLocation, mode);\n        } catch (e) {\n            process.send({\n                type: 'app-rendering-results',\n                result: e\n            });\n        }\n\n        // When process is ready - finish it by sending a proper event\n        process.send({\n            type: 'app-rendering-results',\n            result: result\n        });\n\n        setTimeout(function () {\n            process.exit();\n        }, 1000);\n    }\n\n    if(msg.type === 'abort') {\n        process.exit();\n    }\n});\n"
  },
  {
    "path": "app/back-end/workers/thumbnails/post-images.js",
    "content": "const Image = require('./../../image.js');\nconst normalizePath = require('normalize-path');\nconst sizeOf = require('image-size');\n\nlet result = false;\nlet appInstance = false;\nlet imageData = false;\nlet image = false;\n\nprocess.on('message', function(msg){\n    if(msg.type == 'dependencies') {\n        appInstance = msg.appInstance;\n        imageData = msg.imageData;\n        image = new Image(appInstance, imageData);\n        result = image.save(false);\n    } else if(msg.type == 'start-regenerating') {\n        if(!result.newPath) {\n            // When process is ready - finish it by sending a proper event\n            process.send({\n                type: 'finished',\n                result: result\n            });\n\n            setTimeout(function () {\n                process.exit();\n            }, 1000);\n\n            return;\n        }\n\n        if(!imageData.imageType) {\n            imageData.imageType = 'contentImages';\n        }\n\n        let promises = image.createResponsiveImages(result.newPath, imageData.imageType);\n\n        if(!promises.length) {\n            setTimeout(() => {\n                let thumbnailDimensions = false;\n\n                try {\n                    thumbnailDimensions = sizeOf(result.url);\n                } catch(e) {\n                    thumbnailDimensions = false;\n                }\n\n                process.send({\n                    type: 'finished',\n                    result: {\n                        baseImage: result,\n                        thumbnailPath: result.url,\n                        thumbnailDimensions: thumbnailDimensions\n                    }\n                });\n            }, 250);\n\n            setTimeout(function() {\n                process.exit();\n            }, 1000);\n\n            return;\n        }\n\n        Promise.all(promises).then(res => {\n            setTimeout(() => {\n                let thumbnailDimensions = false;\n\n                try {\n                    thumbnailDimensions = sizeOf(res[0]);\n                } catch(e) {\n                    thumbnailDimensions = false;\n                }\n\n                // When process is ready - finish it by sending a proper event\n                process.send({\n                    type: 'finished',\n                    result: {\n                        baseImage: result,\n                        thumbnailPath: res.map(url => 'file:///' + normalizePath(url)),\n                        thumbnailDimensions: thumbnailDimensions\n                    }\n                });\n            }, 250);\n\n            setTimeout(function() {\n                process.exit();\n            }, 1000);\n        }).catch(err => console.log(err));\n    }\n});\n"
  },
  {
    "path": "app/back-end/workers/thumbnails/regenerate.js",
    "content": "const fs = require('fs-extra');\nconst path = require('path');\nconst Image = require('./../../image.js');\nconst UtilsHelper = require('./../../helpers/utils');\n\nlet context = false;\n\nprocess.on('message', function(msg){\n    let mediaPath = false;\n    let catalog = false;\n\n    if (msg.type === 'dependencies') {\n        context = msg.context;\n        catalog = msg.catalog;\n        mediaPath = msg.mediaPath;\n\n        regenerateImages(mediaPath, catalog);\n    }\n\n    if (msg.type === 'next-images') {\n        catalog = msg.catalog;\n        mediaPath = msg.mediaPath;\n\n        regenerateImages(mediaPath, catalog);\n    }\n\n    if (msg.type === 'abort') {\n        setTimeout(function() {\n            process.exit();\n        }, 1000);\n    }\n});\n\n/**\n * Regenerate images\n *\n * @param mediaPath\n * @param catalog\n */\nfunction regenerateImages(mediaPath, catalog) {\n    let fullPath = path.join(mediaPath, (catalog).toString());\n    let images = fs.readdirSync(fullPath);\n\n    images = images.filter(image => {\n        let fullImagePath = path.join(fullPath, image);\n        return isImage(image, fullImagePath);\n    });\n\n    if(!images.length) {\n        process.send({\n            type: 'empty'\n        });\n\n        return;\n    }\n\n    regenerateImage(images, fullPath, catalog);\n}\n\n/**\n * Regenerate recursively single images\n *\n * @param images\n * @param fullPath\n * @param catalog\n */\nfunction regenerateImage (images, fullPath, catalog) {\n    if (!images.length) {\n        return;\n    }\n\n    let image = images.shift();\n    let fullImagePath = path.join(fullPath, image);\n    let imageHelper = new Image(context.application, {\n        site: context.name,\n        id: catalog,\n        path: fullImagePath\n    });\n\n    let imageType = getImageType(context, image, catalog);\n    let promises = imageHelper.createResponsiveImages(fullImagePath, imageType);\n\n    if (promises[0] === 'NO-RESPONSIVE-IMAGES') {\n        process.send({\n            type: 'progress',\n            value: parseInt((context.totalProgress / context.numberOfImages) * 100, 10),\n            files: [\n                {\n                    translation: 'core.images.responsiveImagesDisabled'\n                }\n            ]\n        });\n\n        context.totalProgress++;\n\n        if (context.totalProgress >= context.numberOfImages) {\n            finishProcess();\n        } else {\n            regenerateImage(images, fullPath, catalog);\n        }\n\n        return;\n    }\n\n    if (!promises.length && context.totalProgress >= context.numberOfImages) {\n        finishProcess();\n        return;\n    }\n\n    if (promises) {\n        Promise.all(promises).then(results => {\n            // Send a +1 signal for the total progress\n            context.totalProgress++;\n            console.log('PROGRESS: ' + context.totalProgress, context.numberOfImages);\n\n            process.send({\n                type: 'progress',\n                value: parseInt((context.totalProgress / context.numberOfImages) * 100),\n                files: results\n            });\n\n            if (context.totalProgress >= context.numberOfImages) {\n                finishProcess();\n                return;\n            }\n\n            regenerateImage(images, fullPath, catalog);\n        }).catch(err => {\n            console.log(err);\n            context.totalProgress++;\n            regenerateImage(images, fullPath, catalog);\n        });\n    } else {\n        context.totalProgress++;\n\n        if (context.totalProgress >= context.numberOfImages) {\n            finishProcess();\n            return;\n        } else {\n            regenerateImage(images, fullPath, catalog);\n        }\n    }\n}\n\n/**\n * Detect if given filename is an image\n *\n * @param image\n * @param fullImagePath\n * @returns {boolean}\n * @private\n */\nfunction isImage(image, fullImagePath) {\n    if(image.substr(0, 1) === '.') {\n        return false;\n    }\n\n    if(image === 'responsive') {\n        return false;\n    }\n\n    if(image === 'gallery') {\n        return false;\n    }\n\n    if(path.parse(image).ext === '') {\n        return false;\n    }\n\n    if(UtilsHelper.dirExists(fullImagePath)) {\n        return false;\n    }\n\n    return true;\n}\n\n/**\n * Detects image type\n *\n * @param context\n * @param image\n * @param catalog\n * @returns {string}\n * @private\n */\nfunction getImageType(context, image, catalog) {\n    let imageType = 'contentImages';\n    let featuredImage = false;\n    let preparedCatalog = catalog.replace('posts/', '');\n\n    if (context.postImagesRef && context.postImagesRef[0]) {\n        featuredImage = context.postImagesRef.filter(xref => xref.post_id == preparedCatalog);\n    }\n\n    if (featuredImage && featuredImage[0] && featuredImage[0].post_id && image === featuredImage[0].url) {\n        console.log('(i) Featured image detected (' + image + ')', preparedCatalog);\n        imageType = 'featuredImages';\n    } else if(catalog === 'website') {\n        console.log('(i) Website image detected (' + image + ')', preparedCatalog);\n        imageType = 'optionImages';\n    } else if(catalog.indexOf('tags') > -1) {\n        console.log('(i) Tag image detected (' + image + ')', preparedCatalog);\n        imageType = 'tagImages';\n    } else if(catalog.indexOf('authors') > -1) {\n        console.log('(i) Author image detected (' + image + ')', preparedCatalog);\n        imageType = 'authorImages';\n    } else if(catalog.substr(-7) === 'gallery') {\n        console.log('(i) Gallery image detected (' + image + ')', preparedCatalog);\n        imageType = 'galleryImages';\n    } else if (imageType === 'contentImages') {\n        console.log('(i) Content image detected (' + image + ')', preparedCatalog);\n    }\n\n    return imageType;\n}\n\n/**\n * Ends the worker process\n *\n * @private\n */\nfunction finishProcess() {\n    console.log('Finish process...');\n\n    process.send({\n        type: 'finished'\n    });\n\n    setTimeout(function() {\n        process.exit();\n    }, 1000);\n}\n"
  },
  {
    "path": "app/build/config.gypi",
    "content": "# Do not edit. File was generated by node-gyp's \"configure\" step\n{\n  \"target_defaults\": {\n    \"cflags\": [],\n    \"default_configuration\": \"Release\",\n    \"defines\": [],\n    \"include_dirs\": [],\n    \"libraries\": []\n  },\n  \"variables\": {\n    \"asan\": 0,\n    \"build_v8_with_gn\": \"false\",\n    \"coverage\": \"false\",\n    \"debug_nghttp2\": \"false\",\n    \"enable_lto\": \"false\",\n    \"enable_pgo_generate\": \"false\",\n    \"enable_pgo_use\": \"false\",\n    \"force_dynamic_crt\": 0,\n    \"gas_version\": \"2.27\",\n    \"host_arch\": \"x64\",\n    \"icu_data_in\": \"../../deps/icu-small/source/data/in/icudt64l.dat\",\n    \"icu_endianness\": \"l\",\n    \"icu_gyp_path\": \"tools/icu/icu-generic.gyp\",\n    \"icu_locales\": \"en,root\",\n    \"icu_path\": \"deps/icu-small\",\n    \"icu_small\": \"true\",\n    \"icu_ver_major\": \"64\",\n    \"llvm_version\": 0,\n    \"node_byteorder\": \"little\",\n    \"node_debug_lib\": \"false\",\n    \"node_enable_d8\": \"false\",\n    \"node_enable_v8_vtunejit\": \"false\",\n    \"node_install_npm\": \"true\",\n    \"node_module_version\": 64,\n    \"node_no_browser_globals\": \"false\",\n    \"node_prefix\": \"/\",\n    \"node_release_urlbase\": \"https://nodejs.org/download/release/\",\n    \"node_shared\": \"false\",\n    \"node_shared_cares\": \"false\",\n    \"node_shared_http_parser\": \"false\",\n    \"node_shared_libuv\": \"false\",\n    \"node_shared_nghttp2\": \"false\",\n    \"node_shared_openssl\": \"false\",\n    \"node_shared_zlib\": \"false\",\n    \"node_tag\": \"\",\n    \"node_target_type\": \"executable\",\n    \"node_use_bundled_v8\": \"true\",\n    \"node_use_dtrace\": \"false\",\n    \"node_use_etw\": \"false\",\n    \"node_use_large_pages\": \"false\",\n    \"node_use_openssl\": \"true\",\n    \"node_use_pch\": \"false\",\n    \"node_use_perfctr\": \"false\",\n    \"node_use_v8_platform\": \"true\",\n    \"node_with_ltcg\": \"false\",\n    \"node_without_node_options\": \"false\",\n    \"openssl_fips\": \"\",\n    \"openssl_no_asm\": 0,\n    \"shlib_suffix\": \"so.64\",\n    \"target_arch\": \"x64\",\n    \"v8_enable_gdbjit\": 0,\n    \"v8_enable_i18n_support\": 1,\n    \"v8_enable_inspector\": 1,\n    \"v8_no_strict_aliasing\": 1,\n    \"v8_optimized_debug\": 0,\n    \"v8_promise_internal_field_count\": 1,\n    \"v8_random_seed\": 0,\n    \"v8_trace_maps\": 0,\n    \"v8_typed_array_max_size_in_heap\": 0,\n    \"v8_use_snapshot\": \"true\",\n    \"want_separate_host_toolset\": 0,\n    \"nodedir\": \"/home/dziudek/.cache/node-gyp/10.16.0\",\n    \"standalone_static_library\": 1,\n    \"g\": \"true\",\n    \"unsafe_perm\": \"true\"\n  }\n}\n"
  },
  {
    "path": "app/config/AST.app.config.js",
    "content": "const AstAppConfig = {\n    alwaysSaveSearchState: false,\n    backupsLocation: \"\",\n    previewLocation: \"\",\n    licenseAccepted: false,\n    openDevToolsInMain: false,\n    openDevToolsInPreview: false,\n    resizeEngine: \"sharp\",\n    sitesLocation: \"\",\n    startScreen: \"\",\n    timeFormat: 12,\n    closeEditorOnSave: true,\n    wideScrollbars: false,\n    showModificationDate: true,\n    showModificationDateAsColumn: false,\n    showPostSlugs: false,\n    showPostTags: true,\n    postsOrdering: 'id DESC',\n    pagesOrdering: ' DESC',\n    tagsOrdering: 'id DESC',\n    authorsOrdering: 'id DESC',\n    appTheme: 'system',\n    language: 'en-gb',\n    languageType: 'default',\n    enableAdvancedPreview: false,\n    editorFontSize: 18,\n    editorFontFamily: 'sans-serif',\n    experimentalFeatureAppAutoBeautifySourceCode: false,\n    experimentalFeatureAppFtpAlt: false,\n    experimentalFileManagerInSidebar: false,\n    uiZoomLevel: 1.0,\n    notificationsStatus: false\n};\n\nmodule.exports = AstAppConfig;\n"
  },
  {
    "path": "app/config/AST.currentSite.config.js",
    "content": "const AstCurrentSiteConfig = {\n    uuid: '',\n    name: '',\n    displayName: '',\n    description: '',\n    synced: false,\n    logo: {\n        color: '',\n        icon: ''\n    },\n    domain: '',\n    language: 'en-gb',\n    spellchecking: false,\n    advanced: {\n        cssCompression: 1,\n        htmlCompression: 1,\n        htmlCompressionRemoveComments: 1,\n        imagesQuality: 60,\n        alphaQuality: 100,\n        forceWebp: false,\n        webpLossless: false,\n        responsiveImages: 1,\n        mediaLazyLoad: 1,\n        versionSuffix: 1,\n        sitemapEnabled: 1,\n        sitemapAddTags: 1,\n        sitemapAddAuthors: 1,\n        sitemapAddHomepage: 1,\n        sitemapAddExternalImages: 0,\n        sitemapExcludedFiles: '',\n        usePageTitleInsteadItemName: false,\n        openGraphEnabled: 1,\n        openGraphImage: '',\n        openGraphAppId: '',\n        twitterCardsEnabled: 1,\n        twitterCardsType: 'summary',\n        twitterUsername: '',\n        metaTitle: '%sitename',\n        metaDescription: '',\n        noIndexThisPage: false,\n        noIndexForChatGPTBot: false,\n        noIndexForChatGPTUser: false,\n        noIndexForCommonCrawlBots: false,\n        homepageNoIndexPagination: false,\n        homepageNoPagination: false,\n        metaRobotsIndex: 'index, follow',\n        homepageMetaTitle: '%sitename',\n        homepageMetaDescription: '',\n        homepageMetaRobotsIndex: 'index, follow',\n        postMetaTitle: '%posttitle - %sitename ',\n        postMetaDescription: '',\n        pageMetaTitle: '%pagetitle - %sitename ',\n        pageMetaDescription: '',\n        pageUseTextWithoutCustomExcerpt: false,\n        postUseTextWithoutCustomExcerpt: false,\n        tagMetaTitle: 'Tag: %tagname - %sitename',\n        tagMetaDescription: '',\n        tagNoIndexPagination: false,\n        tagNoPagination: false,\n        metaRobotsTags: 'noindex, follow',\n        tagsMetaTitle: 'All tags - %sitename',\n        tagsMetaDescription: '',\n        metaRobotsTagsList: 'noindex, follow',\n        authorMetaTitle: 'Author: %authorname - %sitename',\n        authorMetaDescription: '',\n        metaRobotsAuthors: 'noindex, follow',\n        authorNoIndexPagination: false,\n        authorNoPagination: false,\n        displayEmptyAuthors: false,\n        displayEmptyTags: false,\n        errorMetaTitle: 'Error 404 - %sitename',\n        errorMetaDescription: '',\n        metaRobotsError: 'noindex, follow',\n        searchMetaTitle: 'Search - %sitename',\n        searchMetaDescription: '',\n        metaRobotsSearch: 'noindex, follow',\n        postsListingOrderBy: 'created_at',\n        postsListingOrder: 'DESC',\n        featuredPostsListingOrderBy: 'created_at',\n        featuredPostsListingOrder: 'DESC',\n        hiddenPostsListingOrderBy: 'created_at',\n        hiddenPostsListingOrder: 'DESC',\n        usePageAsFrontpage: false,\n        pageAsFrontpage: 0,\n        feed: {\n            title: 'displayName',\n            titleValue: '',\n            showFullText: 1,\n            numberOfPosts: 10,\n            showFeaturedImage: 1,\n            enableRss: 1,\n            enableJson: 1,\n            updatedDateType: 'createdAt',\n            showOnlyFeatured: 0,\n            excludeFeatured: 0\n        },\n        urls: {\n            cleanUrls: false,\n            addIndex: false,\n            postsPrefix: '',\n            tagsPrefix: 'tags',\n            tagsPrefixAfterPostsPrefix: false,\n            authorsPrefix: 'authors',\n            authorsPrefixAfterPostsPrefix: false,\n            pageName: 'page',\n            errorPage: '404.html',\n            searchPage: 'search.html'\n        },\n        customHeadCode: '',\n        customBodyCode: '',\n        customCommentsCode: '',\n        customSearchInput: '',\n        customSearchContent: '',\n        customFooterCode: '',\n        gdpr: {\n            enabled: false,\n            popupTitlePrimary: 'This website uses cookies',\n            popupDesc: 'Select which cookies to opt-in to via the checkboxes below; our website uses cookies to examine site traffic and user activity while on our site, for marketing, and to provide social media functionality.',\n            showPrivacyPolicyLink: true,\n            privacyPolicyLinkLabel: 'More details...',\n            privacyPolicyPostId: 0,\n            privacyPolicyLinkType: 'internal',\n            privacyPolicyExternalUrl: '',\n            groups: [\n                { \n                    'name': 'Required', \n                    'id': '-', \n                    'description': '' \n                }\n            ],\n            embedConsents: [],\n            gConsentModeEnabled: false,\n            gConsentModeDefaultState: {\n                ad_storage: false,\n                ad_personalization: false,\n                ad_user_data: false,\n                analytics_storage: false,\n                personalization_storage: false,\n                functionality_storage: false,\n                security_storage: false\n            },\n            gConsentModeGroups: [],\n            saveButtonLabel: 'Accept all',\n            behaviour: 'badge',\n            badgeLabel: 'Cookie Policy',\n            behaviourLink: '#cookie-settings',\n            popupPosition: 'centered',\n            popupShowRejectButton: false,\n            popupRejectButtonLabel: 'Reject',\n            allowAdvancedConfiguration: true,\n            advancedConfigurationLinkLabel: 'Manage preferences',\n            advancedConfigurationAcceptButtonLabel: 'Accept all',\n            advancedConfigurationRejectButtonLabel: 'Reject all',\n            advancedConfigurationSaveButtonLabel: 'Save settings',\n            advancedConfigurationTitle: 'Cookie settings',\n            advancedConfigurationDescription: 'We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic. By clicking \"Accept All\", you consent to our use of cookies.',\n            advancedConfigurationShowDescriptionLink: true,\n            cookieSettingsRevision: '1',\n            cookieSettingsTTL: '90',\n            debugMode: false,\n            vimeoNoTrack: false,\n            ytNoCookies: false,\n            settingsVersion: ''\n        },\n        relatedPostsOrder: 'default',\n        relatedPostsCriteria: 'titles-and-tags',\n        relatedPostsIncludeAllPosts: true,\n        editors: {\n            wysiwygAdditionalValidElements: '',\n            customElements: '',\n            codemirrorTabSize: 4,\n            codemirrorAutoIndent: true\n        }\n    },\n    deployment: {\n        protocol: '',\n        relativeUrls: false,\n        port: '',\n        server: '',\n        username: '',\n        password: '',\n        askforpassword: false,\n        rejectUnauthorized: true,\n        path: '',\n        passphrase: '',\n        sftpkey: '',\n        s3: {\n            customProvider: false,\n            provider: 'aws',\n            endpoint: '',\n            id: '',\n            key: '',\n            bucket: '',\n            region: '',\n            customRegion: '',\n            prefix: '',\n            acl: 'public-read',\n            htmlCacheControl: 'no-cache, no-store',\n            otherCacheControl: 'public, max-age=2592000'\n        },\n        git: {\n            url: '',\n            branch: '',\n            user: '',\n            password: '',\n            commitAuthor: '',\n            commitEmail: '',\n            commitMessage: 'Publii: update content'\n        },\n        github: {\n            server: 'api.github.com',\n            user: '',\n            repo: '',\n            branch: '',\n            token: '',\n            parallelOperations: 1,\n            apiRateLimiting: 1\n        },\n        gitlab: {\n            server: 'https://gitlab.com/',\n            rejectUnauthorized: true,\n            repo: '',\n            branch: '',\n            token: ''\n        },\n        netlify: {\n            id: '',\n            token: ''\n        },\n        google: {\n            key: '',\n            bucket: '',\n            prefix: ''\n        },\n        manual: {\n            output: 'catalog',\n            outputDirectory: ''\n        }\n    }\n};\n\nmodule.exports = AstCurrentSiteConfig;\n"
  },
  {
    "path": "app/default-files/default-languages/en-gb/config.json",
    "content": "{\n\t\"name\": \"English - default\",\n\t\"version\": \"1.8.1\",\n\t\"author\": \"Publii Team\",\n\t\"publiiSupport\": \"0.47.0\",\n    \"momentLocale\": \"en\",\n    \"wysiwygTranslation\": false\n}\n"
  },
  {
    "path": "app/default-files/default-languages/en-gb/translations.json",
    "content": "{\n    \"author\": {\n        \"addNewAuthor\": \"Add new author\",\n        \"author\": \"Author\",\n        \"authorDataParsingErrorMessage\": \"An error occurred during parsing of author data for ID: \",\n        \"authorHasBeenUpdated\": \"Author has been updated\",\n        \"authorLink\": \"Author link\",\n        \"authorName\": \"Author name\",\n        \"authorNameCannotBeEmptyErrorMessage\": \"Author name cannot be empty. Please enter a name\",\n        \"authorNameInUseErrorMessage\": \"Provided author name is already in use. Please try another name.\",\n        \"authorNameSimilarInUseErrorMessage\": \"Provided author name is in use; author names are not case-sensitive. Please try another name.\",\n        \"authorPage\": \"Author page\",\n        \"authorSlugCannotBeEmpty\": \"Author slug cannot be empty\",\n        \"avatar\": \"Avatar\",\n        \"avatarAndFeaturedImage\": \"Avatar and Featured image\",\n        \"cannotRemoveMainAuthor\": \"The main author cannot be removed.\",\n        \"changePageAuthor\": \"Change page author\",\n        \"changePostAuthor\": \"Change post author\",\n        \"editAuthor\": \"Edit author\",\n        \"eMail\": \"E-mail\",\n        \"filterOrSearchAuthors\": \"Filter or search authors...\",\n        \"gravatarEmailWarningMessage\": \"Enter the email address you used for registering your account on Gravatar.\",\n        \"mainAuthor\": \"Main author\",\n        \"mainAuthorCannotBeRemoved\": \"This is the main author of this website. It cannot be removed.\",\n        \"newAuthorHasBeenCreated\": \"New author has been created\",\n        \"noAuthorsMatchingYourCriteria\": \"There are no authors matching your criteria.\",\n        \"removeAuthorsMessage\": \"Do you really want to remove selected authors?\",\n        \"removeAuthorsSuccessMessage\": \"Selected authors have been removed\",\n        \"selectAuthor\": \"Select author\",\n        \"selectAuthorPage\": \"Select author page\",\n        \"themeDoesNotSupportFeaturedImagesForAuthors\": \"Your theme does not support featured images for authors\",\n        \"whenYouUseGravatarYourSiteVisitorsWillQueryAThirdPartyServer\": \"When you use the Gravatar service, please note, that your site visitors need to query a third-party server to load your avatar image.\",\n        \"toUseThisOptionEnableIndexingAuthorPages\": \"To use this option, first enable indexing of author pages in SEO settings.\",\n        \"url\": \"URL\",\n        \"useGravatarMessage\": \"Use <a href=\\\"https://gravatar.com/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Gravatar </a> to provide your author avatar\",\n        \"useGravatarServiceMessage\": \"In order to use Gravatar service first provide the author's e-mail.\",\n        \"website\": \"Website\"\n    },\n    \"core\": {\n        \"archive\": {\n            \"destinationNotExists\": \"Specified output destination not exists\",\n            \"errorDuringCreatingTAR\": \"An error occurred during TAR archive creation. Please try again.\",\n            \"errorDuringCreatingZIP\": \"An error occurred during ZIP archive creation. Please try again.\"\n        },\n        \"backup\": {\n            \"destinationDirectoryDoesNotExists\": \"Destination directory does not exist\",\n            \"errorDuringFileSaveProcess\": \"An error occurred during the file saving process\",\n            \"errorDuringReadingBackupFile\": \"An error occurred during reading of the backup file\",\n            \"fileDoesNotExists\": \"Backup file does not exist\",\n            \"fileIsCorrupted\": \"The backup file is corrupted - aborting the restore process.\",\n            \"locationDoesNotExists\": \"Backup location does not exist\",\n            \"temporaryDirectoryDoesNotExists\": \"Temporary directory does not exist\"\n        },\n        \"credits\": {\n            \"errorLoadingLicenseMsg\": \"An error occurred during loading of the license. Please try again.\"\n        },\n        \"images\": {\n            \"responsiveImagesDisabled\": \"Responsive images are disabled under site settings - all responsive images have been removed.\"\n        },\n        \"rendering\": {\n            \"renderingProcessCrashed\": \"Rendering process crashed\",\n            \"renderingProcessCrashedMsg\": \"For more information, check the rendering-errors.log and rendering-process.log files under Tools -> Log viewer.\",\n            \"renderingProcessFailed\": \"Rendering process failed\"\n        },\n        \"server\": {\n            \"branchDoesNotExist\": \"Selected branch does not exist.\",\n            \"creatingNewRemoteFilesTree\": \"Creating new remote files tree...\",\n            \"finishingDeploymentProcess\": \"Finishing the deployment process...\",\n            \"getInfoAboutLatestCommit\": \"Get information about the latest commit...\",\n            \"preparingFilesTreeToUpload\": \"Preparing file tree for upload...\",\n            \"repositoryDoesNotExist\": \"Selected repository does not exist.\",\n            \"requestLimitExceededInfo\": \"Your API request limit was exceeded ({remaining} requests left). Please wait till {resetTime} UTC and then try again.\",\n            \"requestTimeout\": \"Request timeout.\",\n            \"retrievingHandlerOfRemoteFilesTree\": \"Retrieving remote file tree handler...\",\n            \"retrievingRemoteFilesTree\": \"Retrieving the remote file tree...\",\n            \"tokenOrServerAddressInvalid\": \"Provided credentials or server/repository address are invalid.\",\n            \"tooManyFilesInfo\": \"Your website contains over 4000 items ({numberOfFiles} files and directories). Currently our Github Pages implementation only supports websites with up to 4000 items.\"\n        },\n        \"site\": {\n            \"noConfigurationForResponsiveImages\": \"There is no configuration data for responsive images\",\n            \"noImagesToRegenerate\": \"There are no images to regenerate\",\n            \"noThemeSelected\": \"No theme selected\"\n        },\n        \"sureYouWantQuit\": \"Are you sure you want to quit? \\nAll unsaved changes will be lost.\",\n        \"wpImport\": {\n            \"authorsProgressInfo\": \"Importing authors ({progress} / {total})\",\n            \"imageDownloadError\": \"An error occurred during downloading of the image: {image}\",\n            \"imagesProgressInfo\": \"Downloading images ({progress} / {total})\",\n            \"pagesProgressInfo\": \"Importing pages ({progress} / {total})\",\n            \"postsProgressInfo\": \"Importing posts ({progress} / {total})\",\n            \"tagsProgressInfo\": \"Importing tags ({progress} / {total})\"\n        }\n    },\n    \"customHTML\": {\n        \"tabs\": {\n            \"head\": \"Head\",\n            \"body\": \"Body\",\n            \"comments\": \"Comments\",\n            \"searchInput\": \"Search input\",\n            \"searchContent\": \"Search content\",\n            \"socialSharing\": \"Social sharing\",\n            \"footer\": \"Footer\",\n            \"beforePost\": \"Before every post\",\n            \"afterPost\": \"After every post\",\n            \"afterPage\": \"After every page\"\n        }\n    },\n    \"date\": {\n        \"apr\": \"Apr\",\n        \"aug\": \"Aug\",\n        \"changePagePublicationDate\": \"Change page publication date\",\n        \"changePostPublicationDate\": \"Change post publication date\",\n        \"dec\": \"Dec\",\n        \"feb\": \"Feb\",\n        \"jan\": \"Jan\",\n        \"jul\": \"Jul\",\n        \"jun\": \"Jun\",\n        \"mar\": \"Mar\",\n        \"may\": \"May\",\n        \"nov\": \"Nov\",\n        \"oct\": \"Oct\",\n        \"sep\": \"Sep\"\n    },\n    \"editor\": {\n        \"addHeading\": \"Add heading\",\n        \"addLabel\": \"Add label\",\n        \"alignTextCenter\": \"Align text center\",\n        \"alignTextLeft\": \"Align text left\",\n        \"alignTextRight\": \"Align text right\",\n        \"blockEditorHelpPanelDesc\": \"You can insert a block either by clicking the <span>+</span> or <span>TAB</span> button on a new line, or typing the following shortcuts or markdown syntax on a new line:\",\n        \"blocks\": {\n            \"code\": {\n                \"idTooltip\": \"This value will be used as the code ID\"\n            },\n            \"cssClassesLabel\": \"CSS class\",\n            \"cssClassesTooltip\": \"These CSS classes will be added in the class attribute of the field\",\n            \"embed\": {\n                \"idTooltip\": \"This value will be used as the embed ID\"\n            },\n            \"gallery\": {\n                \"idTooltip\": \"This value will be used as the gallery ID\"\n            },\n            \"header\": {\n                \"customIdLabel\": \"Use custom ID\",\n                \"customIdTooltip\": \"Enable this option if you want to use your own ID, instead of the auto-generated ID\",\n                \"idTooltip\": \"This value will be used as the header ID\"\n            },\n            \"html\": {\n                \"idTooltip\": \"This value will be used as the HTML ID\"\n            },\n            \"idLabel\": \"ID\",\n            \"image\": {\n                \"idTooltip\": \"This value will be used as the image ID\"\n            },\n            \"list\": {\n                \"idTooltip\": \"This value will be used as the list ID\"\n            },\n            \"paragraph\": {\n                \"idTooltip\": \"This value will be used as the paragraph ID\",\n                \"styleLabel\": \"Paragraph style\",\n                \"styles\": {\n                    \"highlight\": \"Highlight\",\n                    \"info\": \"Info\",\n                    \"success\": \"Success\",\n                    \"warning\": \"Warning\"\n                },\n                \"styleTooltip\": \"Select additional styling for the paragraph block\"\n            },\n            \"quote\": {\n                \"idTooltip\": \"This value will be used as the quote ID\"\n            },\n            \"separator\": {\n                \"idTooltip\": \"This value will be used as the separator ID\"\n            },\n            \"toc\": {\n                \"idTooltip\": \"This value will be used as the ID of the table of contents\"\n            }\n        },\n        \"cantSavePostWithEmptyTitle\": \"You cannot save a post with an empty title.\",\n        \"cantSavePageWithEmptyTitle\": \"You cannot save a page with an empty title.\",\n        \"changesSaved\": \"Changes have been saved\",\n        \"clearFormatting\": \"Clear formatting\",\n        \"clickToConfirm\": \"Click to confirm\",\n        \"code\": \"Code\",\n        \"conversions\": {\n            \"toCode\": \"Code\",\n            \"toHeader\": \"Heading\",\n            \"toHTML\": \"HTML\",\n            \"toList\": \"List\",\n            \"toParagraph\": \"Paragraph\",\n            \"toQuote\": \"Quote\"\n        },\n        \"convertTo\": \"Convert to:\",\n        \"custom\": \"Custom\",\n        \"defaultSelect\": \"- Select -\",\n        \"deleteBlock\": \"Delete block\",\n        \"dot\": \"Dot\",\n        \"dots\": \"Dots\",\n        \"dragAndDropImgToEditor\": \"Drag and drop an image onto the editor\",\n        \"dropToUploadYourPhotoOr\": \"Drop to upload your photo or\",\n        \"dropToUploadYourPhotosOr\": \"Drop to upload your photos or\",\n        \"element\": \"Element\",\n        \"emptyGalleryBlock\": \"Empty gallery block\",\n        \"emptyImageBlock\": \"Empty image block\",\n        \"enterAltText\": \"Enter alternative text\",\n        \"enterCaption\": \"Enter caption\",\n        \"enterUrlOrEmbedCode\": \"Enter URL or embed code...\",\n        \"errorOccurred\": \"An error occurred - please try again.\",\n        \"gallery\": \"Gallery\",\n        \"header\": \"Heading\",\n        \"heading1\": \"Heading 1\",\n        \"heading2\": \"Heading 2\",\n        \"heading3\": \"Heading 3\",\n        \"heading4\": \"Heading 4\",\n        \"heading5\": \"Heading 5\",\n        \"heading6\": \"Heading 6\",\n        \"hideBulkEdit\": \"Hide Bulk Edit\",\n        \"hideHelp\": \"Hide Help\",\n        \"hideStats\": \"Hide Stats\",\n        \"html\": \"HTML\",\n        \"insertGallery\": \"Insert gallery\",\n        \"list\": \"List\",\n        \"markdown\": \"Markdown\",\n        \"markdownHelpPanelDesc\": \"You can easily manage headings, text styles or create other HTML elements by using the following keyboard shortcuts or markdown syntax:\",\n        \"newDraftCreated\": \"New draft has been created\",\n        \"newPageCreated\": \"New page has been created\",\n        \"newPostCreated\": \"New post has been created\",\n        \"nothingFound\": \"No blocks found\",\n        \"paragraph\": \"Paragraph\",\n        \"quote\": \"Quote\",\n        \"quoteAuthor\": \"Quote author\",\n        \"quoteText\": \"Quote text\",\n        \"readMore\": \"Read more\",\n        \"readMoreBlockName\": \"Readmore\",\n        \"selectFiles\": \"Select files\",\n        \"separator\": \"Separator\",\n        \"shortcuts\": \"Shortcuts\",\n        \"sourceCode\": \"Source code\",\n        \"searchForABlock\": \"Search for a block...\",\n        \"startWriting\": \"Start writing...\",\n        \"startWritingOrPressTabToChooseBlock\": \"Start writing or press the TAB key to choose a block.\",\n        \"toc\": \"TOC\",\n        \"tocAutoGenerationInfo\": \"The table of contents is generated automatically based on the headings (H1-H6) of your content.\",\n        \"togglePostStatsPanel\": \"Toggle post statistics panel\",\n        \"unableToSetSpellCheckerForLanguage\": \"(!) Unable to set spellchecker to use the selected language - \",\n        \"useOrderedList\": \"Use ordered list\",\n        \"useUnorderedList\": \"Use unordered list\",\n        \"viewBulkEdit\": \"Bulk Edit\",\n        \"viewHelp\": \"View Help\",\n        \"viewStats\": \"View Stats\",\n        \"wideLine\": \"Wide line\"\n    },\n    \"file\": {\n        \"addNewFile\": \"Add new file\",\n        \"backups\": \"Backups\",\n        \"createBackup\": \"Create backup\",\n        \"createBackupConfirmMsg\": \"Select a name for your backup - filename can contain only alphanumeric characters, dashes and underscores:\",\n        \"createBackupErrorMsg\": \"An error occurred during backup creation. Please try again.\",\n        \"createBackupNameEmptyMsg\": \"Provided filename cannot be empty. Please enter a name.\",\n        \"createBackupNameInUseMsg\": \"Provided filename (<strong>{filename}</strong>) is already in use by an existing backup. Please try again with a different name.\",\n        \"createBackupSuccessMsg\": \"Backup has been created.\",\n        \"createFirstBackupMsg\": \"You don't have any backups, yet. Let's create the first one!\",\n        \"creatingBackup\": \"Creating backup\",\n        \"creationDate\": \"Creation date\",\n        \"deleteBackupsConfirmMsg\": \"Do you really want to remove the selected backups? This action cannot be undone.\",\n        \"deleteBackupsErrorMsg\": \"An error occurred during removal of the selected backups. Please try again.\",\n        \"deleteBackupsSuccessMsg\": \"Selected backups have been removed\",\n        \"dragAndDropBackupFile\": \"Drag and drop backup file here or\",\n        \"dropYourFileHere\": \"Drop your file here\",\n        \"file\": \"File\",\n        \"fileFromFileManager\": \"File from File Manager\",\n        \"fileManager\": \"File manager\",\n        \"filename\": \"Filename\",\n        \"files\": \"Files\",\n        \"fileSemicolon\": \"File:\",\n        \"fileSize\": \"File Size\",\n        \"filterOrSearchFiles\": \"Filter or search files...\",\n        \"mediaFiles\": \"media/files\",\n        \"noBackupsAvailable\": \"No backups available\",\n        \"noFileInMediaFilesDirInfo\": \"There are no files in the media/files directory...\",\n        \"noFileInRootDirInfo\": \"There are no files in the root directory...\",\n        \"noFileMatchingCriteriaInfo\": \"There are no files matching your criteria.\",\n        \"operations\": \"Operations\",\n        \"preparingFilesInOutputDir\": \"Preparing files in the output directory\",\n        \"provideNameForNewFile\": \"Please provide name for a new file\",\n        \"removeFilesConfirmMsg\": \"Do you really want to remove the selected files? This action cannot be undone.\",\n        \"removeFilesSuccessMsg\": \"Selected files have been removed\",\n        \"rename\": \"Rename\",\n        \"renameBackupConfirmLabel\": \"Rename file\",\n        \"renameBackupConfirmMsg\": \"Please specify a new backup filename:\",\n        \"renameBackupErrorMsg\": \"An error occurred while renaming the selected backup file. Please try again.\",\n        \"renameBackupNameEmptyMsg\": \"The backup filename cannot be empty. Please try again.\",\n        \"renameBackupNameInUseMsg\": \"The specified name is in use by another backup file. Please try again.\",\n        \"renameBackupSameNameMsg\": \"The specified name is the same as the old name. Please try again.\",\n        \"renameBackupSuccessMsg\": \"The backup has been successfully renamed.\",\n        \"restore\": \"Restore\",\n        \"restoreBackupConfirmLabel\": \"Restore backup\",\n        \"restoreBackupConfirmMsg\": \"Do you really want to restore the selected backup? Existing files will be overwritten.\",\n        \"restoreBackupErrorMsg\": \"An error occurred while restoring the selected backup file: \",\n        \"restoreBackupSuccessMsg\": \"The website has been successfully restored.\",\n        \"rootDirectory\": \"root directory\",\n        \"selectedFileExistsMsg\": \"The following files cannot be copied as they already exist in the selected directory: \",\n        \"selectedFilenameInUseMsg\": \"The selected filename is in use. Please use a different filename.\",\n        \"selectFile\": \"Select file\",\n        \"selectFileFromFileManager\": \"Select a file from the File Manager\",\n        \"uploadFiles\": \"Upload files\"\n    },\n    \"gdpr\": {\n        \"addGroup\": \"Add group\",\n        \"embedConsents\": {\n            \"addRule\": \"Add rule\",\n            \"groupButtonLabel\": \"Button label\",\n            \"groupCookieGroup\": \"Cookies group\",\n            \"groupRule\": \"URL contains\",\n            \"groupRulePlaceholder\": \"youtube.com\",\n            \"groupTextPlaceholder\": \"Consent text\"\n        },\n        \"groupDescriptionPlaceholder\": \"Enter a cookies group description here\",\n        \"groupID\": \"Group ID\",\n        \"groupName\": \"Group name\",\n        \"state\": \"State\"\n    },\n    \"image\": {\n        \"addImages\": \"Add images\",\n        \"align\": \"Align\",\n        \"centeredImage\": \"Centred image\",\n        \"columns\": \"Columns:\",\n        \"dropFeaturedImageOr\": \"Drop featured image here or\",\n        \"dropToUploadPhotoOr\": \"Drop to upload your photo or\",\n        \"eightColumns\": \"8 columns\",\n        \"enterAltText\": \"Enter alt text\",\n        \"enterCaption\": \"Enter caption\",\n        \"fiveColumns\": \"5 columns\",\n        \"fourColumns\": \"4 columns\",\n        \"fullWidth\": \"Full-width\",\n        \"fullWidthImage\": \"Full-width image\",\n        \"image\": \"Image\",\n        \"imageAlternativeText\": \"Image alternative text\",\n        \"imageCaption\": \"Image caption\",\n        \"images\": \"Images\",\n        \"insertEditGallery\": \"Insert/Edit Gallery\",\n        \"layout\": \"Layout\",\n        \"loadingImage\": \"Loading image...\",\n        \"none\": \"None\",\n        \"oneColumn\": \"1 column\",\n        \"pictures\": \"pictures\",\n        \"removeImage\": \"Remove image\",\n        \"sevenColumns\": \"7 columns\",\n        \"sixColumns\": \"6 columns\",\n        \"threeColumns\": \"3 columns\",\n        \"twoColumns\": \"2 columns\",\n        \"uploading\": \"Uploading\",\n        \"wide\": \"Wide\",\n        \"wideImage\": \"Wide image\",\n        \"yourGalleryIsEmpty\": \"Your gallery is empty\"\n    },\n    \"langs\": {\n        \"addLanguageSuccessMessage\": \"Language has been added\",\n        \"deleteLanguage\": \"Delete language\",\n        \"getMoreLanguages\": \"Get more languages\",\n        \"goToLanguagesManager\": \"Go to the languages manager\",\n        \"installLanguage\": \"Install language\",\n        \"isOutdated\": \"Outdated\",\n        \"isOutdatedTitle\": \"This language supports Publii up to v.{supportedVersion}, while you are using Publii v.{currentVersion} - please update Publii to use this language.\",\n        \"language\": \"Language:\",\n        \"languageChangedMsg\": \"Application language changed\",\n        \"languageChangeError\": \"Language change error occurred - please try again.\",\n        \"languageLoadingError\": \"An error occurred during loading of app language. The default language (english) will be used instead. Please go to the language settings to select another.\",\n        \"languages\": \"Languages\",\n        \"removeLanguageMessage\": \"Do you really want to remove the {languageName} language?\",\n        \"removeLanguageSuccessMessage\": \"The selected language has been removed\",\n        \"updatedLanguageSuccessMessage\": \"Selected language has been updated\",\n        \"uploadLanguageErrorMessage\": \"An error occurred during installation. Please try again.\"\n    },\n    \"link\": {\n        \"addDownloadAttr\": \"Add \\\"download\\\" attribute\",\n        \"addLink\": \"Add link\",\n        \"addNofollow\": \"Add rel=\\\"nofollow\\\"\",\n        \"downloadAttribute\": \"Link \\\"download\\\" attribute\",\n        \"insertEditLink\": \"Insert/Edit link\",\n        \"linkInvalidMsg\": \"Sorry! This link seems to be invalid.\",\n        \"linkRelAttribute\": \"Link \\\"rel\\\" attribute\",\n        \"linkTitleAttribute\": \"Link \\\"title\\\" attribute\",\n        \"linkClassAttribute\": \"CSS class\",\n        \"linkTarget\": \"Link target\",\n        \"openInNewWindow\": \"Open in new window\",\n        \"previewLinkInBrowser\": \"Preview this link in browser\",\n        \"previewOnlyExternalLinksMsg\": \"You can only preview external links in the post editor\",\n        \"removeLink\": \"Remove link\",\n        \"selectLinkType\": \"Select link type\"\n    },\n    \"menu\": {\n        \"addMenuItem\": \"Add menu item\",\n        \"addNewMenu\": \"Add new menu\",\n        \"addNewMenuItem\": \"Add new menu item\",\n        \"addSubmenu\": \"Add submenu\",\n        \"addSubmenuItem\": \"Add submenu item\",\n        \"assignedMenu\": \"Assigned menu\",\n        \"classCSS\": \"CSS class\",\n        \"createNewMenu\": \"Create new menu\",\n        \"deleteThisMenuItem\": \"Delete this menu item\",\n        \"duplicateThisMenuItem\": \"Duplicate this menu item\",\n        \"editMenuItem\": \"Edit menu item\",\n        \"editMenuName\": \"Edit menu name\",\n        \"editThisMenuItem\": \"Edit this menu item\",\n        \"externalLink\": \"External link\",\n        \"externalURL\": \"External URL\",\n        \"hideThisMenuItem\": \"Hide this menu item\",\n        \"insertActions\": \"Insert selected item:\",\n        \"insertAfter\": \"after\",\n        \"insertAsChild\": \"as submenu\",\n        \"insertBefore\": \"before\",\n        \"internalLink\": \"Internal link\",\n        \"items\": \"Items\",\n        \"label\": \"Label\",\n        \"likedItemError\": \"Linked item hasn't been rendered (empty tag/author page), doesn't exist or has been trashed\",\n        \"likedItemIsADraft\": \"Linked item is a draft\",\n        \"menu\": \"Menu\",\n        \"menuItemsRemoveMessage\": \"Do you really want to remove selected menu item?\",\n        \"menuItemIsHidden\": \"This menu item is hidden\",\n        \"menuNameCannotBeEmptyCreateNewMenuErrorMessage\": \"The menu name field cannot be empty. Please enter a menu name.\",\n        \"menuNameCannotBeEmptyErrorMessage\": \"The menu name field cannot be empty. Please enter a new menu name.\",\n        \"menuNameExistsErrorMessage\": \"The specified menu name already exists. Please use a different name.\",\n        \"menuNameHasBeenEdited\": \"Menu name has been edited\",\n        \"menuNameInUseErrorMessage\": \"The specified menu name is in use. Please enter a new menu name.\",\n        \"menusRemoveMessage\": \"Do you really want to remove the selected menus?\",\n        \"menusRemoveSuccessMessage\": \"The selected menus have been removed\",\n        \"moveItem\": \"Move\",\n        \"newMenuCreated\": \"New menu has been created\",\n        \"noMenusAvailable\": \"No menus available\",\n        \"noMenusCreateNewOne\": \"You don't have any menus, yet. Let's create the first one!\",\n        \"noMenusMessage\": \"There are no menu items; create new ones via the \\\"Add menu item\\\" button above.\",\n        \"provideNameForNewMenu\": \"Provide a name for your new menu:\",\n        \"provideNewNameForMenu\": \"Provide a new name for the selected menu:\",\n        \"selectItemType\": \"Select item type\",\n        \"selectLinkTarget\": \"Select link target\",\n        \"showThisMenuItem\": \"Show this menu item\",\n        \"textSeparator\": \"Text separator\",\n        \"type\": \"Type\",\n        \"unassigned\": \"Unassigned\",\n        \"unselectItem\": \"Unselect\",\n        \"updateLabel\": {\n            \"author\": \"Use author name\",\n            \"page\": \"Use page title\",\n            \"post\": \"Use post title\",\n            \"tag\": \"Use tag name\"\n        }\n    },\n    \"menuPositionPopup\": {\n        \"invalidValue\": \"Invalid value: the entered value cannot be 0 and exceed the theme's default maximum limit. Use -1 for no restrictions.\",\n        \"maxLevels\": \"Max level:\",\n        \"themeDefaultValue\": \"Theme default value:\",\n        \"title\": \"Configure menu position\",\n        \"usedBy\": \"Used by other menu:\"\n    },\n    \"notifications\": {\n        \"badgeNew\": \"New\",\n        \"latestVersion\": \"Latest version\",\n        \"currentVersion\": \"Current version\",\n        \"checkUpdates\": \"Check for updates\",\n        \"consentInfo\": \"You are currently receiving update notifications. If you don't want to receive them, you can\",\n        \"consentReject\": \"click here to reject your consent.\",\n        \"consentStateTitle\": \"Enable update notifications\",\n        \"consentStateDescription\": \"Publii can automatically check for new updates and let you know when they are available. For this, a simple connection to the Publii server is made, which includes your device's IP address. This information is used only to provide update notifications and is not stored. You can disable this option anytime in the app settings.\",\n        \"disabled\": \"Disabled\",\n        \"downloadUpdate\": \"Download\",\n        \"giveConsent\": \"Enable notifications\",\n        \"goToNotificationsCenter\": \"Go to Notifications Center\",\n        \"markAsRead\": \"Mark as read\",\n        \"news\": \"News\",\n        \"notifications\": \"Notifications\",\n        \"noUpdatesTitle\": \"No updates available\",\n        \"noUpdatesDescription\": \"You are using the latest version of Publii and extensions.\",\n        \"pluginUpdatesAvailable\": \"Plugin updates available\",\n        \"publiiUpdateAvailable\": \"Publii update is available\",\n        \"rejectConsentConfirm\": \"By withdrawing your consent, you will no longer receive notifications about new versions of Publii, its extensions (themes and plugins), or other important updates.\",\n        \"viewDetails\": \"View details\",\n        \"readMore\": \"Read more\",\n        \"rejectConsent\": \"Not now\",\n        \"themeUpdatesAvailable\": \"Theme updates available\"\n    },\n    \"page\": {\n        \"addNewPage\": \"Add new page\",\n        \"addPageTitle\": \"Add page title\",\n        \"all\": \"All\",\n        \"closeHierarchy\": \"Close edit\",\n        \"changePageDate\": \"Change page date\",\n        \"cleanUrlsDisabled\": \"Activate 'Pretty URLs' in the Site settings to unlock the Edit hierarchy option. This will allow you to easily manage the parent-child relationships between your pages.\",\n        \"editHierarchy\": \"Edit hierarchy\",\n        \"configureThemeBeforeGeneratingPreview\": \"A theme must be selected and configured for this website before a preview of this page may be generated.\",\n        \"convertToPost\": \"Convert to post\",\n        \"currentDefaultTemplate\": \"Current default template\",\n        \"customPageTypes\": \"Custom Page Types\",\n        \"drafts\": \"Drafts\",\n        \"duplicate\": \"Duplicate\",\n        \"duplicatePageErrorMessage\": \"An error occurred during duplication of the selected pages. Please try again.\",\n        \"duplicatePageSuccessMessage\": \"Selected pages have been duplicated\",\n        \"editorBlock\": \"Block editor\",\n        \"editorBlockInfo\": \"A modern and intuitive editor with shortkey and markdown support to make blogging easy, with no need to worry about HTML or other code elements.\",\n        \"editorBlockNotSupportedEditPageInfo\": \"The current theme does not support the block editor used in this page. This page may be rendered incorrectly to the output files.\",\n        \"editorBlockNotSupportedNewPageInfo\": \"The current theme does not support the block editor used in this page. This page may be rendered incorrectly to the output files.\",\n        \"editorBlockUse\": \"Use Block editor\",\n        \"editorMarkdown\": \"Markdown editor\",\n        \"editorMarkdownInfo\": \"This editor supports Markdown syntax as shorthand for producing content quickly; great for extensive, no-frills projects such as documentation.\",\n        \"editorMarkdownUse\": \"Use Markdown editor\",\n        \"editorWYSIWYG\": \"WYSIWYG editor\",\n        \"editorWYSIWYGInfo\": \"This editor provides a familiar word-processing experience, with additional tools for users that want to control every aspect of their page content.\",\n        \"editorWYSIWYGUse\": \"Use WYSIWYG editor\",\n        \"editPageAnyway\": \"Edit page anyway\",\n        \"filterOrSearchPages\": \"Filter or search pages...\",\n        \"insertActions\": \"Insert selected page\",\n        \"insertAfter\": \"after\",\n        \"insertAsChild\": \"as subpage\",\n        \"insertBefore\": \"before\",\n        \"isHomepage\": \"Homepage\",\n        \"markAsDraft\": \"Mark as draft\",\n        \"markAsFeatured\": \"Mark as featured\",\n        \"markAsUnfeatured\": \"Mark as unfeatured\",\n        \"modificationDate\": \"Modification date\",\n        \"moveItem\": \"Move\",\n        \"moveToTrash\": \"Move to trash\",\n        \"noPagesMatchingYourCriteria\": \"There are no pages matching your criteria.\",\n        \"noParentPage\": \"No parent\",\n        \"openEditorAnyway\": \"Open editor anyway\",\n        \"page\": \"Page\",\n        \"pageAuthor\": \"Page author\",\n        \"pageLink\": \"Page link\",\n        \"pageName\": \"Page name:\",\n        \"pages\": \"Pages\",\n        \"pageSettings\": \"Page settings\",\n        \"pageSlug\": \"Page slug\",\n        \"pageSlugLengthWarning\": \"Page slugs longer than 250 characters can lead to the creation of broken files during website rendering.\",\n        \"pageSlugTooLong\": \"Page slug is too long\",\n        \"pageSlugUpdateButton\": \"Update\",\n        \"pageState\": \"Page state\",\n        \"pageStatusChangeSuccessMessage\": \"Status of the selected pages has been changed\",\n        \"pageTemplate\": \"Page template\",\n        \"parentPage\": \"Parent page\",\n        \"publicationDate\": \"Publication date\",\n        \"publish\": \"Publish\",\n        \"published\": \"Published\",\n        \"removePageMessage\": \"Do you really want to remove the selected pages? This cannot be undone.\",\n        \"removePageSuccessMessage\": \"Selected pages have been removed\",\n        \"selectPage\": \"Select page\",\n        \"setAsParent\": \"Set as parent\",\n        \"setAsSubpage\": \"Set as subpage\",\n        \"setCustomPageDate\": \"Set custom post date\",\n        \"status\": \"Status\",\n        \"thisPageIsADraft\": \"This page is a draft\",\n        \"title\": \"Title\",\n        \"trashed\": \"Trashed\",\n        \"unselectItem\": \"Unselect\",\n        \"updatedOn\": \"Updated on\",\n        \"url\": \"URL\"\n    },\n    \"plugins\": {\n        \"addPluginSuccessMessage\": \"Plugin has been added\",\n        \"deletePlugin\": \"Delete plugin\",\n        \"getMorePlugins\": \"Get more plugins\",\n        \"goToPluginsManager\": \"Go to the plugins manager\",\n        \"installPlugin\": \"Install plugin\",\n        \"isIncompatible\": \"Is outdated\",\n        \"isIncompatibleTitle\": \"This plugin supports Publii from v.{supportedVersion}, while you are using Publii v.{currentVersion} - please update Publii to use this plugin.\",\n        \"newVersionAvailable\": \"New version available\",\n        \"plugin\": \"Plugin:\",\n        \"plugins\": \"Plugins\",\n        \"removePluginMessage\": \"Do you really want to remove the {pluginName} plugin?\",\n        \"removePluginSuccessMessage\": \"The selected plugin has been removed\",\n        \"updatedPluginSuccessMessage\": \"Selected plugin has been updated\",\n        \"uploadPluginErrorMessage\": \"An error occurred during installation. Please try again.\"\n    },\n    \"post\": {\n        \"addNewPost\": \"Add new post\",\n        \"addPostTitle\": \"Add post title\",\n        \"all\": \"All\",\n        \"changePostDate\": \"Change post date\",\n        \"characters\": \"Characters\",\n        \"configureThemeBeforeGeneratingPreview\": \"A theme must be selected and configured for this website before a preview of this post may be generated.\",\n        \"convertToPage\": \"Convert to page\",\n        \"currentDefaultTemplate\": \"Current default template\",\n        \"customPostTypes\": \"Custom Post Types\",\n        \"drafts\": \"Drafts\",\n        \"duplicate\": \"Duplicate\",\n        \"duplicatePostErrorMessage\": \"An error occurred during duplication of the selected posts. Please try again.\",\n        \"duplicatePostSuccessMessage\": \"Selected posts have been duplicated\",\n        \"editorBlock\": \"Block editor\",\n        \"editorBlockInfo\": \"A modern and intuitive editor with shortkey and markdown support to make blogging easy, with no need to worry about HTML or other code elements.\",\n        \"editorBlockNotSupportedEditPostInfo\": \"The current theme does not support the block editor used in this post. This post may be rendered incorrectly to the output files.\",\n        \"editorBlockNotSupportedNewPostInfo\": \"The current theme does not support the block editor used in this post. This post may be rendered incorrectly to the output files.\",\n        \"editorBlockUse\": \"Use Block editor\",\n        \"editorMarkdown\": \"Markdown editor\",\n        \"editorMarkdownInfo\": \"This editor supports Markdown syntax as shorthand for producing content quickly; great for extensive, no-frills projects such as documentation.\",\n        \"editorMarkdownUse\": \"Use Markdown editor\",\n        \"editorWYSIWYG\": \"WYSIWYG editor\",\n        \"editorWYSIWYGInfo\": \"This editor provides a familiar word-processing experience, with additional tools for users that want to control every aspect of their page content.\",\n        \"editorWYSIWYGUse\": \"Use WYSIWYG editor\",\n        \"editPostAnyway\": \"Edit post anyway\",\n        \"excluded\": \"Excluded\",\n        \"excludeFromHomepage\": \"Exclude from homepage\",\n        \"featured\": \"Featured\",\n        \"filterOrSearchPosts\": \"Filter or search posts...\",\n        \"hidden\": \"Hidden\",\n        \"hidePost\": \"Hide Post\",\n        \"includeInHomepage\": \"Include in homepage\",\n        \"markAsDraft\": \"Mark as draft\",\n        \"markAsFeatured\": \"Mark as featured\",\n        \"markAsUnfeatured\": \"Mark as unfeatured\",\n        \"min\": \"min\",\n        \"modificationDate\": \"Modification date\",\n        \"moveToTrash\": \"Move to trash\",\n        \"noPostsMatchingYourCriteria\": \"There are no posts matching your criteria.\",\n        \"openEditorAnyway\": \"Open editor anyway\",\n        \"paragraphs\": \"Paragraphs\",\n        \"post\": \"Post\",\n        \"postAuthor\": \"Post author\",\n        \"postLink\": \"Post link\",\n        \"postName\": \"Post name:\",\n        \"postPage\": \"Post page\",\n        \"posts\": \"posts\",\n        \"postSettings\": \"Post settings\",\n        \"postSlug\": \"Post slug\",\n        \"postSlugLengthWarning\": \"Post slugs longer than 250 characters can lead to the creation of broken files during website rendering.\",\n        \"postSlugTooLong\": \"Post slug is too long\",\n        \"postSlugUpdateButton\": \"Update\",\n        \"postState\": \"Post state\",\n        \"postStatusChangeSuccessMessage\": \"Status of the selected posts has been changed\",\n        \"postTemplate\": \"Post template\",\n        \"postWillNotAppearOnHomepageListMsg\": \"Post will not appear on homepage listings\",\n        \"postWillNotAppearOnListMsg\": \"Post will not appear in any generated post lists such as tag or author pages\",\n        \"publicationDate\": \"Publication date\",\n        \"publish\": \"Publish\",\n        \"published\": \"Published\",\n        \"readingTime\": \"Reading Time\",\n        \"removePostMessage\": \"Do you really want to remove the selected posts? This cannot be undone.\",\n        \"removePostSuccessMessage\": \"Selected posts have been removed\",\n        \"selectPostPage\": \"Select post page\",\n        \"sentences\": \"Sentences\",\n        \"setCustomPostDate\": \"Set custom post date\",\n        \"status\": \"Status\",\n        \"thisPostIsADraft\": \"This post is a draft\",\n        \"thisPostIsExcludedFromHomepage\": \"This post is excluded from the homepage\",\n        \"thisPostIsFeatured\": \"This post is featured\",\n        \"thisPostIsHidden\": \"This post is hidden\",\n        \"title\": \"Title\",\n        \"trashed\": \"Trashed\",\n        \"uniqueWords\": \"Unique words\",\n        \"updatedOn\": \"Updated on\",\n        \"url\": \"URL\",\n        \"words\": \"Words\"\n    },\n    \"publii\": {\n        \"aboutPublii\": \"About Publii\",\n        \"currentPubliiVersion\": \"Version\",\n        \"accept\": \"Accept\",\n        \"copyright\": \"Copyright 2026 <a href=\\\"https://tidycustoms.net\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">TidyCustoms</a>. All rights reserved.<br>Publii is designed and maintained by core team and is made possible by the <a href=\\\"https://electronjs.org\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Electron</a> Open Source project and other \",\n        \"creditsIntro\": \"Publii uses the following third-party Open Source Software:\",\n        \"dataCollectionInfo\": \"<strong>We do not collect any </strong>personal data while you use Publii app; we do not store, track, or allow third parties to collect, personally identifiable information about you. \",\n        \"homepage\": \"Homepage\",\n        \"license\": \"License\",\n        \"licensingInformation\": \"Licensing information\",\n        \"openSourceSoftware\": \"Open Source Software\",\n        \"publiiLicenseAgreement\": \"Publii License Agreement.\",\n        \"publiiLicenseAgreementInfo\": \"This software is licensed under GNU GPL version 3.<br>By clicking \\\"Accept\\\" you agree to the\"\n    },\n    \"rendering\": {\n        \"errorDuringPreviewCreatingMsg\": \"An error occurred during preview creation.\",\n        \"rendering\": \"Rendering...\",\n        \"renderingErrorText\": \"An error occurred during the website rendering process.\",\n        \"renderingPleaseWait\": \"Please wait while the rendering process is completed.\",\n        \"selectThemeBeforeCreatingPreviewMsg\": \"You have to select a theme before trying to create a preview of your website. Please go to the website settings section and select a theme.\"\n    },\n    \"repeater\": {\n        \"addItem\": \"Add item\",\n        \"duplicateItem\": \"Duplicate item\",\n        \"emptyState\": \"Click the button to add first element\",\n        \"removeItem\": \"Remove item\"\n    },\n    \"settings\": {\n        \"addGDPRCookieBanner\": \"Enable banner\",\n        \"additionalValidElementsInWYSIWYGEditor\": \"Additional valid elements in the WYSIWYG editor\",\n        \"additionalValidElementsInWYSIWYGEditorInfo\": \"If the WYSIWYG editor automatically removes some tags from your HTML code, you may add them here as allowed elements.<br> For example: <strong>v-select[*],v-dropdown[*]</strong> will allow custom v-select and v-dropdown tags with any attributes.\",\n        \"advancedOptions\": \"Advanced options\",\n        \"advancedPreviewDesc\": \"Advanced preview allows you to render a website without opening your browser\",\n        \"alwaysAddIndexHTMLInURLs\": \"Always add index.html in URLs\",\n        \"alwaysSaveSearchState\": \"Always save search state\",\n        \"anchorLink\": \"Anchor link\",\n        \"appSettings\": \"App settings\",\n        \"appSettingsSavedMsg\": \"App settings have been successfully saved.\",\n        \"appSettingsSaveErrorMsg\": \"An error occurred during saving of the App Settings. Please try again.\",\n        \"appUIZoomLevel\": \"Interface scale\",\n        \"appUIZoomLevelInfo\": \"Adjust the interface scale to enhance your visibility and comfort while managing your content. This option allows you to scale the user interface to your preferred size, ensuring that text, icons, and other UI elements are easily readable and accessible.\",\n        \"ascending\": \"Ascending\",\n        \"authorPage\": \"Author page\",\n        \"authorPages\": \"Author pages\",\n        \"authorPageTitleVariables\": \"The following variables can be used: %authorname, %sitename.\",\n        \"authorPrefix\": \"Author prefix:\",\n        \"authorPrefixInfo\": \"Defines the prefix that appears before the author slug in a URL e.g. <strong>https://example.com/AUTHOR_PREFIX/author-slug</strong>.<br>\",\n        \"authorPrefixInfoExtended\": \"Defines the prefix that appears before the author slug in a URL e.g. <strong>https://example.com/POSTS_PREFIX/AUTHORS_PREFIX/author-slug</strong>.<br>\",\n        \"authorsPrefixAfterPostsPrefix\": \"Put authors prefix after posts prefix\",\n        \"authorsPrefixCannotBeEmpty\": \"Authors prefix cannot be empty.\",\n        \"authorsPrefixCannotEqualTagsPrefix\": \"The authors prefix cannot be the same as  the tags prefix.\",\n        \"backupLocation\": \"Backup location\",\n        \"backupLocationChangedConfirmMsg\": \"The backups storage location has been changed. All new backups will be stored in this directory. If you require access to earlier backups, they may be moved manually to the new directory.\",\n        \"badge\": \"Badge\",\n        \"badgeAndCustomLink\": \"Badge + custom link\",\n        \"badgeLabel\": \"Badge label\",\n        \"bannerPosition\": \"Banner position\",\n        \"basicSettings\": \"Basic settings\",\n        \"byIDAscending\": \"By ID ascending\",\n        \"byIDDescending\": \"By ID descending\",\n        \"cannotAddIndexHTMLInURLsInfo\": \"Enable this option if you cannot enable loading index.html files by default when a folder on your server is opened.\",\n        \"cardTypes\": \"Card types:\",\n        \"changeSitesLocationWithoutCopyingFiles\": \"Change sites location without moving existing sites to the new catalog\",\n        \"closePostEditorOnSave\": \"Close post editor on save\",\n        \"codeEditor\": \"Code editor (CodeMirror)\",\n        \"colorTheme\": \"Colour theme\",\n        \"content\": \"Content\",\n        \"continueSync\": \"Continue and sync\",\n        \"continueSyncNoRemoteFiles\": \"Publii is unable to find remote files list on your server. If you will continue the sync process, ALL generated website files will be uploaded to your server. You can cancel the sync process to check the reason of your issue and then try again.\",\n        \"convertToWebp\": \"Convert to WebP format\",\n        \"convertToWebpInfo\": \"Enable this option if you want to convert all JPG, JPEG and PNG images to WebP format. Once enabled, your image thumbnails will need to be regenerated. Note, this feature works only if you’re using the Sharp library for generating thumbnails.\",\n        \"convertToWebpJimpWarning\": \"This option does not work with the currently used image resizing engine, Jimp. Instead, switch to Sharp in the app's settings.\",\n        \"cookieGroups\": \"Cookie Groups\",\n        \"cookieBanner\": \"Cookie Banner\",\n        \"cookieBasic\": \"Basic\",\n        \"cookieBasicDescription\": \"This section contains the settings responsible for displaying the cookie banner on the your site, which lets visitors know if the website uses cookies.\",\n        \"cookieAdvanced\": \"Advanced\",\n        \"cookieAdvancedDescription\": \"This section contains options for displaying a modal pop-up with advanced cookie settings, which lets visitors manage consents for a specific group of cookies.\",\n        \"createXMLSitemap\": \"Create XML Sitemap\",\n        \"currentTheme\": \"Current theme:\",\n        \"currentThemeHasOverrides\": \"It seems that you override the theme files, and your custom theme modifications might not fully align with the updated version. Updating your theme requires reviewing and merging these files. Click 'OK' to proceed with the update and address any potential issues later, or 'Cancel' to review your customisations first.\",\n        \"customElementsInWYSIWYGEditor\": \"Custom elements available in the WYSIWYG editor\",\n        \"customElementsInWYSIWYGEditorInfo\": \"If the WYSIWYG editor automatically removes some tags from your HTML code, you may add them here as custom elements.<br> For example: <strong>v-select,v-dropdown</strong>\",\n        \"customFeedTitle\": \"Custom feed title\",\n        \"customLanguageCode\": \"Custom language code:\",\n        \"darkMode\": \"Dark mode\",\n        \"defaultAuthorsOrdering\": \"Default authors ordering:\",\n        \"defaultOrderingOnLists\": \"Default lists ordering:\",\n        \"defaultPagesOrdering\": \"Default pages ordering:\",\n        \"defaultPostsOrdering\": \"Default posts ordering:\",\n        \"defaultTagsOrdering\": \"Default tags ordering:\",\n        \"descending\": \"Descending\",\n        \"disableAuthorsPagination\": \"Disable authors pagination\",\n        \"disableAuthorsPaginationIndexing\": \"Disable authors pagination indexing\",\n        \"disableAuthorsPaginationIndexingInfo\": \"When enabled, your authors pagination files will be excluded from the sitemap and will have the <strong>noindex, follow</strong> robots metatags added.\",\n        \"disableAuthorsPaginationInfo\": \"When enabled, your authors pagination won't be generated.\",\n        \"disableHomepagePagination\": \"Disable homepage pagination\",\n        \"disableHomepagePaginationInfo\": \"When enabled, your homepage pagination won't be generated.\",\n        \"disableHomepagePaginationIndexing\": \"Disable homepage pagination indexing\",\n        \"disableTagsPagination\": \"Disable tags pagination\",\n        \"disableTagsPaginationIndexing\": \"Disable tags pagination indexing\",\n        \"disableTagsPaginationIndexingInfo\": \"When enabled, your tags pagination files will be excluded from the sitemap and will have the <strong>noindex, follow</strong> robots metatags added.\",\n        \"disableTagsPaginationInfo\": \"When enabled, your tags pagination won't be generated.\",\n        \"displayEmptyAuthors\": \"Display authors w/o posts\",\n        \"displayEmptyAuthorsInfo\": \"When enabled, authors without posts assigned to them will still have their subpages created and appear on the authors list.\",\n        \"displayEmptyTags\": \"Display tags w/o posts\",\n        \"displayEmptyTagsInfo\": \"When enabled, tags without posts assigned to them will still have their subpages created and appear on the tags list.\",\n        \"dontIndexThisPage\": \"Ask the search engine not to crawl the whole website.\",\n        \"editorFontFamily\": \"Font family\",\n        \"editorFontFamilySansSerif\": \"Sans serif\",\n        \"editorFontFamilySerif\": \"Serif\",\n        \"editorFontSize\": \"Font size (px)\",\n        \"editors\": \"Editors\",\n        \"embedConsents\": \"Embed content consents\",\n        \"embedConsentsDescription\": \"This section allows you to block content loaded in iframes from third-party content providers that may set cookies. By providing the domain of the iframe URL e.g. <strong>youtube.com</strong>, filling out the required elements such as a message, an accept button, and assigning it to an existing cookie group, iframes will be replaced with a placeholder informing visitors that the content is blocked due to their consent settings (i.e. that the visitor has not consented to the types of cookies used by the iframe in the cookie banner).\",\n        \"embedVideos\": \"Embedding Videos\",\n        \"enableAdvancedPreview\": \"Enable advanced preview\",\n        \"enableAutoIndent\": \"Enable auto-indent\",\n        \"enableCSSCompression\": \"Enable CSS compression\",\n        \"enableHTMLCompression\": \"Enable HTML compression\",\n        \"enableJSONFeed\": \"Enable JSON feed\",\n        \"enableMediaLazyLoad\": \"Enable media lazy load\",\n        \"enableMediaLazyLoadInfo\": \"Enable this option if you want to use native lazy loading that lazy loads images, videos and iframes.\",\n        \"enableResponsiveImages\": \"Enable responsive images\",\n        \"enableResponsiveImagesInfo\": \"Enable this option if you want to deliver different sized images at different screen resolutions depending on breakpoints defined through the config.json file in a theme's folder.\",\n        \"enableRSSFeed\": \"Enable RSS feed\",\n        \"enableSharingButtons\": \"Enable sharing buttons\",\n        \"enableSpellchecker\": \"Enable spellchecker\",\n        \"errorPage\": \"Error page:\",\n        \"errorPageFilenameCannotBeEmpty\": \"Error page filename cannot be empty.\",\n        \"errorPageFilenameCannotEqualSearchPageFilename\": \"Error page filename cannot be the same as search page filename.,\",\n        \"errorPageTitleVariables\": \"The following variables can be used: %sitename.\",\n        \"excludedFiles\": \"Excluded files\",\n        \"excludeFeaturedPosts\": \"Exclude featured posts\",\n        \"excludeFilesFromSitemapInfo\": \"Type a comma-separated list of HTML files or folders to exclude from the sitemap.<br>For example: <strong>avoid-this-file.html,avoid-this-catalog-too</strong>.\",\n        \"experimentalFeaturesWarning\": \"We're always looking for ways to improve our app. This section contains experimental features designed to extend Publii's functionality. Since they're experimental we can't guarantee they will work properly; activate them at your own risk.\",\n        \"experimentalFeatureAppAutoBeautifySourceCode\": \"Auto-beautify source code in WYSIWYG editor\",\n        \"experimentalFeatureAppAutoBeautifySourceCodeDesc\": \"This function enable automatic beautify for source code in the WYSIWYG editor\",\n        \"experimentalFeatureAppFtpAlt\": \"Use alternative FTP library for deployment\",\n        \"experimentalFeatureAppFtpAltDesc\": \"Try this, especially if your hosting uses IPv6 for server address\",\n        \"experimentalFileManagerInSidebar\": \"Show File Manager in sidebar\",\n        \"externalImages\": \"External images\",\n        \"externalPage\": \"External page\",\n        \"facebook\": \"Facebook\",\n        \"facebookAppID\": \"Facebook App ID\",\n        \"facebookAppIDInfo\": \"Read how to obtain <a href=\\\"https://developers.facebook.com/docs/apps/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Facebook App ID</a>.\",\n        \"fallbackImage\": \"Fallback image\",\n        \"fallbackLogoImage\": \"Fallback logo image\",\n        \"fallbackLogoImageInfo\": \"The logo must fit in a 60 &times; 600 pixel box.\",\n        \"featuredPostsOrderBy\": \"Featured posts order by:\",\n        \"featuredPostsOrdering\": \"Featured posts ordering:\",\n        \"feedsRelativeUrlsInfo\": \"To view RSS/JSON feed settings please disable the 'Use relative URLs' option in the Server Settings; the feed won't be generated for your website otherwise.\",\n        \"feedTitle\": \"Feed title:\",\n        \"feedUpdatedDateType\": \"Post update date set as:\",\n        \"filesLocation\": \"Files location\",\n        \"footerText\": \"Footer text\",\n        \"frontpage\": \"Homepage\",\n        \"frontpagePageCannotBeEmpty\": \"Homepage selection cannot be empty.\",\n        \"gConsentModeDefaultState\": \"Default state\",\n        \"gConsentModeEnabled\": \"Enable Google Consent Mode\",\n        \"gConsentMode\": {\n            \"addGroup\": \"Add cookies group settings\",\n            \"cookieGroup\": \"Cookies group\",\n            \"description\": \"This section allows you to specify the default state of the Google Consent Mode and specify signals which will be sent when specific cookies group will be accepted by user. Use these options if you need compatibility with Google Consent Mode. If you are not using Google Analytics, Google Ads or similar tools, most probably you do not need to enable this feature.\",\n            \"title\": \"Google Consent Mode\"\n        },\n        \"GDPR\": \"Privacy Settings\",\n        \"gdprBannerPosition\": {\n            \"centered\": \"Centred\",\n            \"left\": \"Left\",\n            \"right\": \"Right\",\n            \"bar\": \"Bar\"\n        },\n        \"gdprAllowAdvancedConfiguration\": \"Enable advanced cookies configuration\",\n        \"gdprAdvancedConfigurationLinkLabel\": \"Advanced configuration button label\",\n        \"gdprAdvancedConfigurationLinkPlaceholder\": \"Manage preferences\",\n        \"gdprAdvancedConfigurationAcceptButtonLabel\": \"Accept button label\",\n        \"gdprAdvancedConfigurationRejectButtonLabel\": \"Reject button label\",\n        \"gdprAdvancedConfigurationSaveButtonLabel\": \"Save button label\",\n        \"gdprAdvancedConfigurationTitle\": \"Pop-up title\",\n        \"gdprAdvancedConfigurationDescription\": \"Pop-up message\",\n        \"gdprAdvancedConfigurationPrivacyLink\": \"Privacy policy link\",\n        \"gdprAdvancedConfigurationPrivacyLinkDescription\": \"Display a link to the privacy policy page in the pop-up message. If the 'Show privacy policy link' option in the Basic section is disabled, the link will also not be displayed in the pop-up.\",\n        \"gdprBehaviourInfo\": \"Remember to place a link with the above anchor in your website navigation or the website content (e.g. <a href=\\\"#cookie-settings\\\">Cookie preferences</a>). Otherwise, your website's visitors might not be able to change their individual cookie preferences.\",\n        \"gdprCookieSettingsRevision\": \"Settings version\",\n        \"gdprCookieSettingsTTL\": \"Store visitors consent settings for X days\",\n        \"gdprDebugMode\": \"Enable debug mode\",\n        \"gdprShowRejectButton\": \"Show reject button\",\n        \"gdprRejectButtonLabel\": \"Reject button label\",\n        \"gdprBannerTitle\": \"Banner title\",\n        \"gdprBannerMessage\": \"Banner message\",\n        \"generateOpenGraphTags\": \"Generate Open Graph tags\",\n        \"generateTwitterCards\": \"Generate Twitter cards\",\n        \"googleAnalyticsTrackingID\": \"Google Analytics Tracking ID\",\n        \"hiddenPostsOrderBy\": \"Hidden posts order by:\",\n        \"hiddenPostsOrdering\": \"Hidden posts ordering:\",\n        \"hideCustomExcerptsOnPostPages\": \"Hide custom excerpts on post pages\",\n        \"hideCustomExcerptsOnPostPagesInfo\": \"When enabled, your post pages won't display text which is placed above the \\\"Read more\\\" element in the post editor.\",\n        \"hideCustomExcerptsOnPagePages\": \"Hide custom excerpts on pages\",\n        \"hideCustomExcerptsOnPagePagesInfo\": \"When enabled, your pages won't display text which is placed above the \\\"Read more\\\" element in the editor.\",\n        \"homepage\": \"Homepage\",\n        \"homepageNoIndexPagination\": \"When enabled, your homepage pagination files will be excluded from the sitemap and will have the <strong>noindex, follow</strong> robots metatags added.\",\n        \"homepagePagination\": \"Homepage pagination\",\n        \"howToPrepareYourThemeForGDPRInfo\": \"Enabling this option will display a cookie banner on your website. Please read the <a href=\\\"https://getpublii.com/docs/gdpr-cookie-banner-configuration.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">GDPR Cookie Banner Configuration</a> article for more information on how to configure the banner correctly. \",\n        \"howYtNoCookiesWorks\": \"When enabled, the YouTube domain for the embed URL e.g. 'www.youtube.com' will be automatically changed to 'www.youtube-nocookie.com', to prevent the embedded player from personalising the YouTube browsing experience of the visitors, and also ensures that no information is used to customise advertising shown to the visitor outside of your site. Learn more about Enhanced Privacy Mode by visiting <a href=\\\"https://support.google.com/youtube/answer/171780?hl=en#zippy=%2Cturn-on-privacy-enhanced-mode\\\">YouTube help page</a>.\",\n        \"howVimeoNoTrackWorks\": \"When enabled, the additional dnt=1 parameter will be added to the Vimeo embed URL (player.vimeo.com/video) to prevent the embedded player from tracking any session data, including all cookies and analytics. Learn more about Vimeo player parameters by visiting <a href=\\\"https://vimeo.zendesk.com/hc/en-us/articles/360001494447-Player-parameters-overview\\\">Vimeo help page</a>.\",\n        \"imageResizeEngineInfo\": \"The Sharp resize engine is much faster than Jimp, but can cause issues with some images. If you are encountering problems when creating or regenerating thumbnails, please try switching to the Jimp resize engine. Should you wish to use WebP images, then you’ll need to use the Sharp resize engine.\",\n        \"imagesResizeEngine\": \"Images resize engine:\",\n        \"imageResizeEngineWarning\": \"Jimp resize engine and the option to convert images to WebP format won't work properly. Make sure that all of your websites has the “Website Speed / Convert to WebP format” disabled, and make sure to regenerate thumbnails for consistency.\",\n        \"indentSize\": \"Indent size (spaces)\",\n        \"internalPage\": \"Internal page\",\n        \"leaveBlankForDefaultBackupsDir\": \"Leave blank to use the default backups directory\",\n        \"leaveBlankForDefaultPreviewDirectory\": \"Leave blank to use the default preview directory\",\n        \"leaveBlankToUseDefaultPageTitle\": \"Leave blank to use the default Page title\",\n        \"leaveBlankToUseDefaultSitesDir\": \"Leave blank to use the default sites directory\",\n        \"lightMode\": \"Light mode\",\n        \"linkedIn\": \"LinkedIn\",\n        \"linkLabel\": \"Link label\",\n        \"loadAtStart\": \"Load at start:\",\n        \"metaDescription\": \"Meta Description:\",\n        \"metaRobots\": \"Meta Robots:\",\n        \"noIndexForChatGPTBot\": \"Block GPTBot bot\",\n        \"noIndexForChatGPTUser\": \"Block ChatGPT-User bot\",\n        \"noIndexForChatGPTBotInfo\": \"When enabled, this option prevents your site content from being indexed by GPTBot bot (<a href=\\\"https://platform.openai.com/docs/gptbot\\\" target=\\\"_blank\\\">used for crawling</a>). Please note that it will only take effect if you do not use your own robots.txt file.\",\n        \"noIndexForChatGPTUserInfo\": \"When enabled, this option prevents your site content from being indexed by ChatGPT-User bot (<a href=\\\"https://platform.openai.com/docs/plugins/bot\\\" target=\\\"_blank\\\">used in ChatGPT plugins</a>). Please note that it will only take effect if you do not use your own robots.txt file.\",\n        \"noIndexForCommonCrawlBots\": \"Block Common Crawl bots\",\n        \"noIndexForCommonCrawlBotsInfo\": \"When enabled, this option prevents your site content from being indexed by Common Crawl bots. Please note that it will only take effect if you do not use your own robots.txt file.\",\n        \"noIndexWebsite\": \"Noindex website\",\n        \"notificationsCenterEnabled\": \"Allow notifications center to download and display notifications data\",\n        \"notSelected\": \"Not selected\",\n        \"numberOfPostsInFeed\": \"Number of posts in feed\",\n        \"openDevtoolsInMainW\": \"Open DevTools automatically in the Main Window\",\n        \"openGraph\": \"Open Graph\",\n        \"openLastUsedWebsite\": \"Open the last used website\",\n        \"openPopupWindowBy\": \"Open banner by\",\n        \"optionsForDevelopers\": \"Options for developers\",\n        \"optionsForEditors\": \"Options for editors\",\n        \"optionsForExperimentalFeatures\": \"Experimental features\",\n        \"ordering\": {\n            \"authorASC\": \"By author (ascending)\",\n            \"authorDESC\": \"By author (descending)\",\n            \"createdASC\": \"By created date (ascending)\",\n            \"createdDESC\": \"By created date (descending)\",\n            \"hierarchical\": \"Hierarchical\",\n            \"idASC\": \"By ID (ascending)\",\n            \"idDESC\": \"By ID (descending)\",\n            \"modifiedASC\": \"By modified date (ascending)\",\n            \"modifiedDESC\": \"By modified date (descending)\",\n            \"nameASC\": \"By name (ascending)\",\n            \"nameDESC\": \"By name (descending)\",\n            \"postsCounterASC\": \"By posts count (ascending)\",\n            \"postsCounterDESC\": \"By posts count (descending)\",\n            \"titleASC\": \"By title (ascending)\",\n            \"titleDESC\": \"By title (descending)\"\n        },\n        \"pageAsFrontpage\": \"Select page\",\n        \"pageTitle\": \"Page Title\",\n        \"pageTitleVariables\": \"The following variables can be used: %pagetitle, %sitename, %authorname\",\n        \"paginationPhrase\": \"Pagination phrase:\",\n        \"paginationPhraseCannotBeEmpty\": \"Pagination phrase cannot be empty.\",\n        \"paginationPhraseInfo\": \"Defines the phrase used before the page number in a URL e.g. <strong>https://example.com/tags/tag-slug/page/2</strong>.<br>\",\n        \"password\": {\n            \"alwaysAskForPassword\": \"Always ask for password\",\n            \"password\": \"Password\",\n            \"passwordFieldCantBeEmpty\": \"The password field cannot be empty.\"\n        },\n        \"pinterest\": \"Pinterest\",\n        \"postCreationDate\": \"Post creation date\",\n        \"postID\": \"Post ID\",\n        \"postModificationDate\": \"Post modification date\",\n        \"postPageTitleVariables\": \"The following variables can be used: %posttitle, %sitename, %authorname.\",\n        \"postsListing\": \"Posts Listing\",\n        \"postsIndex\": \"Posts Index\",\n        \"postsOrderBy\": \"Posts order by:\",\n        \"postsOrdering\": \"Posts ordering:\",\n        \"postPrefixInfo\": \"The prefix entered here will be added before the post slug in the URL. For example, https://example.com/POSTS_PREFIX/post-slug. This option is necessary if you set a <a href=\\\"#\\\" data-internal-link=\\\"SEO\\\">page as the homepage</a> and need post pagination. In this case, post pagination will be available at https://example.com/POSTS_PREFIX/page/X. Additionally, tags will be rendered under https://example.com/POSTS_PREFIX/TAG_PREFIX/tag-slug.\",\n        \"postsPrefix\": \"Posts prefix\",\n        \"postTitle\": \"Post title\",\n        \"previewLocation\": \"Preview location\",\n        \"previewLocationChangedConfirmMsg\": \"The preview storage location has been changed. Note that existing files in this folder will be deleted when a preview of your website is created.\",\n        \"privacyPolicyLinkLabel\": \"Privacy policy link label\",\n        \"privacyPolicyPage\": \"Select page\",\n        \"privacyPolicyPageURL\": \"Enter external URL\",\n        \"privacyPolicyURL\": \"Privacy policy link source\",\n        \"provideFacebookAppID\": \"Please provide the <a href=\\\"https://developers.facebook.com/docs/apps/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Facebook App ID</a>\",\n        \"random\": \"Random\",\n        \"readAboutOurRecommendedServerSettings\": \"Looking to learn more about recommended server configurations? Please refer to the following documentation <a href=\\\"https://getpublii.com/docs/recommended-server-settings.html\\\" target=\\\"_blank\\\">article</a>.\",\n        \"relatedPostsOptions\": \"Related posts options\",\n        \"relatedPostsOptionsInfo\": \"When enabled, related posts will be taken from all tags. When disabled, related posts will only be generated from the same tags as the current post.\",\n        \"relatedPostsOrdering\": \"Related posts ordering:\",\n        \"relatedPostsSelectionMechanism\": \"Related posts selection mechanism:\",\n        \"removeHTMLComments\": \"Remove HTML comments\",\n        \"responsiveImagesQuality\": \"Responsive Images quality\",\n        \"responsiveImagesAlphaQuality\": \"Responsive Images alpha quality (only WebP)\",\n        \"RSSJSONFeed\": \"RSS/JSON Feed\",\n        \"saveButtonLabel\": \"Save button label\",\n        \"saveSearchStateInfo\": \"When enabled, Publii will save the current search results even when creating a new post, allowing you to return to the listing. By default, Publii only saves search results when opening a post to edit.\",\n        \"requiresRestartingApp\": \"Requires a restart of the Publii app\",\n        \"saveSettings\": \"Save Settings\",\n        \"searchPage\": \"Search page:\",\n        \"searchPageFilenameCannotBeEmpty\": \"Search page filename cannot be empty.\",\n        \"searchPageTitleVariables\": \"The following variables can be used: %sitename.\",\n        \"selectedDirInvalid\": \"Selected directory does not exist.\",\n        \"selectPage\": \"Select page\",\n        \"showFeaturedImage\": \"Show Featured image\",\n        \"showFeaturedImageInfo\": \"Display a post's featured image in the feed.\",\n        \"showFullText\": \"Show full text\",\n        \"showFullTextInFeedInfo\": \"Display full text of the post in the feed.\",\n        \"showModificationDate\": \"Show modification date\",\n        \"showModificationDateAsColumn\": \"Show modification date as column\",\n        \"showOnlyFeaturedPosts\": \"Show only featured posts\",\n        \"showPostTagsOnTheListing\": \"Show post tags on the listing\",\n        \"showPostSlugsOnTheListing\": \"Show slugs across all listing views\",\n        \"showPrivacyPolicyLink\": \"Show privacy policy link\",\n        \"sitemap\": \"Sitemap\",\n        \"sitemapLinkInfo\": \"You can find the XML sitemap here: <a href=\\\"{sitemapLink}\\\" class=\\\"sitemap-external-link\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">sitemap.xml</a>\",\n        \"sitemapRelativeUrlsInfo\": \"To view sitemap settings please disable the 'Use relative URLs' option in the Server Settings; the sitemap won't be generated for your website otherwise.\",\n        \"siteSettings\": \"Site settings\",\n        \"siteSettingsSaveDuplicateNameErrorMessage\": \"Provided site name is in use. Please try another site name.\",\n        \"siteSettingsSaveEmptyNameErrorMessage\": \"Site name cannot be empty. Please enter a site name.\",\n        \"siteSettingsSaveNoKeyringErrorMessage\": \"Publii cannot save settings due to a problem with the safe password storage software. Restart the application and try again. If the problem persists, please report it to our team via the community forum.\",\n        \"siteSettingsSaveNoKeyringErrorMessageLinux\": \"Publii cannot save settings as no safe password storage software is installed. Follow the installation instructions for Node Keytar via https://github.com/atom/node-keytar/ and try again. Most likely the libsecret-1-dev and gnome-keyring packages are missing from your system.\",\n        \"siteSettingsSaveSiteNotExistsErrorMessage\": \"Site does not exist. Please restart the application.\",\n        \"siteSettingsSaveSuccessMessage\": \"Site settings have been successfully saved.\",\n        \"sitesLocation\": \"Sites location\",\n        \"sitesLocationChangedConfirmMsg\": \"The sites location has been changed. Existing files in the new destination folder will be deleted before your sites files are moved there; do you want to continue?\",\n        \"spellcheckerDoesNotSupportLanguage\": \"The spellchecker does not support your selected website language. We recommend disabling this feature.\",\n        \"system\": \"System\",\n        \"systemColors\": \"Use system colours\",\n        \"tagPages\": \"Tag pages\",\n        \"tagPageTitleVariables\": \"The following variables can be used: %tagname, %sitename.\",\n        \"tagPrefix\": \"Tag prefix:\",\n        \"tagPrefixInfo\": \"Prefixes entered here will be added before the tag slug in the URL e.g. <strong>https://example.com/TAGS_PREFIX/tag-slug</strong>.<br>This prefix is also used to generate the tags list page (if supported in your theme) e.g. <strong>https://example.com/TAGS_PREFIX/index.html</strong>.\",\n        \"tagPrefixInfoExtended\": \"Prefixes entered here will be added before the tag slug in the URL e.g. <strong>https://example.com/POSTS_PREFIX/TAGS_PREFIX/tag-slug</strong>.<br>This prefix is also used to generate the tags list page (if supported in your theme) e.g. <strong>https://example.com/POSTS_PREFIX/TAGS_PREFIX/index.html</strong>.\",\n        \"tagsListPage\": \"Tags list page\",\n        \"tagsListPageTitleVariables\": \"The following variables can be used: %sitename.\",\n        \"tagsPrefixAfterPostsPrefix\": \"Put tags prefix after posts prefix\",\n        \"tagsPrefixCannotBeEmpty\": \"Tags prefix cannot be empty if pretty URLs are enabled.\",\n        \"themeDoesNotHaveSupportedFeaturesList\": \"<p>Your theme's <strong>config.json</strong> file does not contain a <strong>supportedFeatures</strong> section. Please update or modify your theme to include accurate messaging about features which are not supported by your currently used theme. <a href=\\\"https://getpublii.com/dev/theme-supported-features\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Read more about supported features</a>.</p>\",\n        \"themeDoesNotSupport404ErrorPage\": \"Your theme does not support a 404 Error page.\",\n        \"themeDoesNotSupportAuthorPages\": \"Your theme does not support author pages.\",\n        \"themeDoesNotSupportErrorPages\": \"Your theme does not support error pages\",\n        \"themeDoesNotSupportSearchPages\": \"Your theme does not support search pages\",\n        \"themeDoesNotSupportTagPages\": \"Your theme does not support tag pages.\",\n        \"themeDoesNotSupportTagsListPage\": \"Your theme does not support a Tags list page.\",\n        \"themeDoesNotSupportEmbedConsents\": \"Your theme does not support consents for embed content.\",\n        \"themePrimaryColor\": \"Theme primary colour\",\n        \"timeFormat\": \"Time format:\",\n        \"toViewSitemapEnableIndexingInfo\": \"To view sitemap settings please disable the 'Ask search engines to not index this website' option in the SEO Settings tab; the sitemap won't be generated for your website otherwise.\",\n        \"tumblr\": \"Tumblr\",\n        \"twitter\": \"Twitter\",\n        \"twitterCards\": \"Twitter Cards\",\n        \"twitterUsername\": \"Twitter username\",\n        \"twitterSummaryCard\": \"Summary card\",\n        \"twitterSummaryCardLargeImage\": \"Summary card with large image\",\n        \"urls\": \"URLs\",\n        \"useAsTitlePageTitle\": \"Use as a title for the title page\",\n        \"useAsTitlePageTitleInfo\": \"When this option is enabled, og:title and twitter:title metatags will contain the page title instead of the post title, tag name or author name.\",\n        \"useGlobalConfiguration\": \"Use global configuration\",\n        \"useOnlyTags\": \"Use only tags\",\n        \"useOnlyTitles\": \"Use only titles\",\n        \"usePageAsFrontpage\": \"Set page as homepage\",\n        \"usePageAsFrontpageNotice\": \"When enabled, the selected page will become your website's homepage, accessible at your main web address (the root URL). Note that leaving the <a href=\\\"#\\\" data-internal-link=\\\"URLs\\\">Posts prefix</a> empty will disable posts pagination.\",\n        \"usePrettyURLs\": \"Use pretty URLs\",\n        \"usePrettyURLsInfo\": \"When enabled, your post URLs won't contain the .html suffix e.g. it will change URLs from <strong>https://example.com/post.html</strong> to <strong>https://example.com/post/</strong>. For pages hierarchy won't be visible in the URL if this option is disabled\",\n        \"useSiteGlobalSettings\": \"Use global site settings\",\n        \"useTitlesAndTags\": \"Use titles and tags\",\n        \"useWideScrollbars\": \"Use wider scrollbars\",\n        \"versionParameter\": \"Version parameter\",\n        \"versionParameterInfo\": \"Adds a version parameter in CSS/JS URLs to skip browser cache. This option can cause more files than usual to be synced during deployment.\",\n        \"vimeoNoTrack\": \"Enable 'Do Not Track' mode for Vimeo videos\",\n        \"webpLossless\": \"Enable lossless compression for responsive WebP images\",\n        \"websiteName\": \"Website name\",\n        \"websiteSpeed\": \"Website Speed\",\n        \"whatsApp\": \"WhatsApp\",\n        \"youMustReviewGdprSettings\": \"Make sure to check and review your Privacy Settings, especially the Cookie Banner settings due to some significant changes in Publii v.0.40. <a href=\\\"https://getpublii.com/blog/release-040.html#cookie-banner\\\" target=\\\"_blank\\\">Read more...</a>\",\n        \"youMustSelectFrontpage\": \"You must specify page used as homepage\",\n        \"ytNoCookies\": \"Enable Privacy-Enhanced mode for YouTube videos\"\n    },\n    \"site\": {\n        \"addNewWebsite\": \"Add new website\",\n        \"cloneWebsite\": \"Clone website\",\n        \"cloneWebsiteSuccessMsg\": \"Website has been cloned. Switched to: \",\n        \"createNewWebsite\": \"Create new website\",\n        \"createWebsite\": \"Create website\",\n        \"createYourFirstWebsite\": \"Create your first website\",\n        \"creationInProgress\": \"Creation in progress...\",\n        \"deleteWebsite\": \"Delete website\",\n        \"deleteWebsiteConfirmMsg\": \"Do you really want to remove this website? This action cannot be undone.\",\n        \"deleteWebsiteCSuccessMsg\": \"Website has been removed.\",\n        \"deleteWebsiteSuccessMsg\": \"Website has been removed. Switched to: \",\n        \"duplicateWebsite\": \"Duplicate website\",\n        \"erroOcurredDuringSiteDatabaseCreationInfo\": \"An error occurred during site database creation. Please check your antivirus software and try again. You can also need to remove the invalid website's folder from the Publii sites directory.\",\n        \"installFromBackup\": \"Install from backup\",\n        \"removeWebsite\": \"Remove website\",\n        \"restoreFromBackup\": {\n            \"createWebsite\": \"Create website\",\n            \"invalidBackupContent\": \"The uploaded file is a not valid backup.\",\n            \"invalidSiteData\": \"The uploaded backup is corrupted.\",\n            \"iWantChangeName\": \"Change website name\",\n            \"restoreFailed\": \"An error occurred during creating website from backup. Please try again.\",\n            \"selectSiteName\": \"Please specify a site name\",\n            \"siteExistsWantOverride\": \"Some websites exist within the selected directory name. Would you like to continue and potentially lose all previous website data? The website from the backup will override the existing site with the same name.\",\n            \"siteNameCannotBeEmpty\": \"Website name cannot be empty\",\n            \"unsupportedFormat\": \"Unsupported format - please use TAR backup file.\",\n            \"unpackError\": \"An error occurred during unpacking backup - please try again.\",\n            \"yesPleaseOverride\": \"Yes, override\"\n        },\n        \"selectedBackupFile\": \"Selected backup file: \",\n        \"siteDescription\": \"Site description (internal)\",\n        \"siteLoadingErrorMsg\": \"An error occurred during loading of the selected website. Please check the website files and try again.\",\n        \"siteName\": \"Site name:\",\n        \"siteSettingsSaveSuccessMsg\": \"Site settings have been successfully saved.\",\n        \"siteWithThisNameExists\": \"A site using this name already exists!\",\n        \"specifyNameForWebsiteDuplicate\": \"Please specify the new name for the duplicate website:\",\n        \"websiteAuthorRequired\": \"author name is required and should contain letters\",\n        \"websiteName\": \"Website name\",\n        \"websiteNameAlreadyInUseMsg\": \"The selected name is in use by another website. Please try again.\",\n        \"websiteNameCantBeEmpty\": \"The website name cannot be empty. Please try again.\",\n        \"websiteNameRequired\": \"A website name is required\",\n        \"websiteNotFound\": \"Website not found\"\n    },\n    \"supportedFeatures\": {\n        \"featureNames\": {\n            \"authorImages\": \"author featured images\",\n            \"authorPages\": \"author subpages\",\n            \"blockEditor\": \"block editor\",\n            \"customComments\": \"custom comments\",\n            \"customSearch\": \"custom search\",\n            \"errorPage\": \"error subpage\",\n            \"searchPage\": \"search subpage\",\n            \"tagImages\": \"tag featured images\",\n            \"tagsList\": \"tags list subpage\",\n            \"tagPages\": \"tag subpages\"\n        },\n        \"yourThemeDoNotSupport\": \"Your current theme do not support required feature: {featureName}\"\n    },\n    \"sync\": {\n        \"accessID\": \"Access ID\",\n        \"accessIDFieldCantBeEmpty\": \"The access ID field cannot be empty\",\n        \"acl\": \"ACL\",\n        \"afterInitialSyncSiteWillBeAvailableOnline\": \"After the initial sync, your website will be available online\",\n        \"allFilesUploadedPart1\": \"All files have been successfully uploaded to your server. \",\n        \"allFilesUploadedPart2\": \"Visit your website by clicking the button below.\",\n        \"apiRateLimiting\": \"API rate limiting\",\n        \"apiRateLimitingNote\": \"Disable this option only if you are using Github Enterprise with disabled API rate limiting. Otherwise disabling this option can cause deployment errors.\",\n        \"apiServer\": \"API Server\",\n        \"apiServerNote\": \"Change this value only if you are using your own GitHub instance (Enterprise edition).\",\n        \"authenticationMethod\": \"Authentication method\",\n        \"branch\": \"Branch\",\n        \"branchExampleGitNote\": \"Examples: <strong>main</strong>, <strong>gh-pages</strong> or <strong>docs</strong>.\",\n        \"branchExampleGitHubNote\": \"Examples: <strong>gh-pages</strong>, <strong>docs</strong> or <strong>main</strong>.\",\n        \"branchExampleGitLabeNote\": \"Example: <strong>main</strong>\",\n        \"branchFieldCantBeEmpty\": \"The branch field cannot be empty\",\n        \"bucket\": \"Bucket\",\n        \"bucketFieldCantBeEmpty\": \"The bucket field cannot be empty\",\n        \"certificates\": \"Certificates\",\n        \"changeServerType\": \"Change server type\",\n        \"checkingConnection\": \"Checking connection...\",\n        \"clickToChangeDeploymentMethod\": \"Click to change the currently used deployment method\",\n        \"commitAuthor\": \"Commit author\",\n        \"commitAuthorFieldCantBeEmpty\": \"Commit author field cannot be empty\",\n        \"commitEmail\": \"Commit author e-mail\",\n        \"commitMessage\": \"Commit message\",\n        \"commitMessageFieldCantBeEmpty\": \"Commit message field cannot be empty\",\n        \"configureServer\": \"Configure Server\",\n        \"connectedToServer\": \"Connected to the server\",\n        \"connectingToServer\": \"Connecting to the server...\",\n        \"connectionToServerErrorAdditionalMessage\": \"An error occurred while connecting to the server: \",\n        \"connectionToServerErrorMessage\": \"An error occurred while connecting to the server. Please check your server settings or try again.\",\n        \"connectionToServerErrorText\": \"An error occurred while connecting to the server...\",\n        \"connectToServerCantStoreFilesErrorMsg\": \"Error! Application was able to connect with your server but was unable to store files. Please check file permissions on your server\",\n        \"connectToServerErrorMsg\": \"Error! The application was unable to connect to your server.\",\n        \"connectToServerSuccessMsg\": \"Success! The application was able to connect to your server.\",\n        \"deploymentMethodFilesPubliiMsg\": \"Selected deployment method uses <strong>files.publii.json</strong> file for sync. Consider access protection for this file in your server configuration if required - <a href=\\\"https://getpublii.com/docs/recommended-server-settings.html#sync-file\\\" target=\\\"_blank\\\">read more</a>\",\n        \"deploymentMethodFtpMsg\": \"FTP protocol uses an unencrypted transmission, which means any data sent over it, including your username and password, could be read by anyone who may intercept your transmission. We strongly recommend using FTPS or SFTP protocols if possible.\",\n        \"deploymentMethodGitMsg\": \"For detailed information about how to configure a website using Git, check Publii's online <a href=\\\"https://getpublii.com/docs/host-static-website-git-repository.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">documentation</a>.\",\n        \"deploymentMethodGithubPagesMsg\": \"For detailed information about how to configure a website using Github Pages, check Publii's online <a href=\\\"https://getpublii.com/docs/host-static-website-github-pages.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">documentation</a>.\",\n        \"deploymentMethodGitNote\": \"Please remember, that if you are using custom domain, you must put CNAME file under File Manager in the root files\",\n        \"deploymentMethodGithubPagesNote\": \"This will be your Github repository path, which should use the following format: <strong>YOUR_USERNAME.github.io/YOUR_REPOSITORY_NAME</strong>.<br>If you are using a custom domain name, set this field to just the custom domain name.\",\n        \"deploymentMethodGitlabPagesMsg\": \"For detailed information about how to configure a website using GitLab Pages, check Publii's online <a href=\\\"https://getpublii.com/docs/host-static-website-gitlab-pages.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">documentation</a>.\",\n        \"deploymentMethodGoogleCloudMsg\": \"For detailed information about how to configure a website using Google Cloud, check Publii's online <a href=\\\"https://getpublii.com/docs/make-static-website-google-cloud.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">documentation</a>.\",\n        \"deploymentMethodNetlifyMsg\": \"For detailed information about how to configure a website using Netlify, check Publii's online <a href=\\\"https://getpublii.com/docs/build-a-static-website-with-netlify.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">documentation</a>.\",\n        \"deploymentMethodS3Msg\": \"For detailed information about how to configure a website using S3, check Publii's online <a href=\\\"https://getpublii.com/docs/setup-static-website-hosting-amazon-s3.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">documentation</a>.\",\n        \"deploymentSettingDatHyperIpfsProtocolNote\": \"The \\\"dat://\\\", \\\"hyper://\\\", \\\"dweb://\\\" and the \\\"ipfs://\\\" protocol is useful only if you have plans to use your website on P2P networks. Read more about <a href=\\\"https://datproject.org/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dat://</a>, <a href=\\\"https://hypercore-protocol.org/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">hyper://</a>, <a href=\\\"https://dwebx.org/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dweb://</a> and <a href=\\\"https://ipfs.io/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">IPFS</a>\",\n        \"deploymentSettingDoubleSlashProtocolNote\": \"Note: while using \\\"//\\\" as protocol, some features like Open Graph tags, sharing buttons etc. will not work properly.\",\n        \"deploymentSettingFileProtocolNote\": \"The \\\"file://\\\" protocol is useful only if you are using the manual deployment method for intranet websites.\",\n        \"deploymentSettingRelativeUrlsNote\": \"Note: while using relative URLs, some features like Open Graph tags, sitemaps, RSS feeds, JSON feeds etc. will be disabled.\",\n        \"deprecated\": \"Deprecated\",\n        \"destinationServerNotConfiguredErrorMessage\": \"Your website cannot currently be synced as the destination server has not been configured correctly. <br>Check your server settings to ensure that the correct information has been entered.\",\n        \"destinationServerNotConfiguredErrorText\": \"Make sure the destination server is properly configured.\",\n        \"domainNameNotSetErrorMessage\": \"Your website cannot currently be synced as the settings appear to lack a domain name. <br>Check your server settings to ensure a domain name has been entered.\",\n        \"domainNameNotSetErrorText\": \"Make sure the domain name is set.\",\n        \"duringSyncYouCantChangeFilesLocation\": \"File locations cannot be changed during the sync process.\",\n        \"fieldIsCaseSensitive\": \"This field is case-sensitive.\",\n        \"filesNotSyncedErrorMessage\": \"Please check the hard-upload-errors-log.txt file using the Tools -&gt; Log Viewer tool.\",\n        \"filesNotSyncedErrorText\": \"Some files were not synced properly.\",\n        \"ftp\": \"FTP\",\n        \"ftps\": \"FTPS\",\n        \"ftpWithSSLTLS\": \"FTP with SSL/TLS\",\n        \"generatePreviewFiles\": \"Generate preview files\",\n        \"getWebsiteFiles\": \"Get website files\",\n        \"git\": \"Git Repository\",\n        \"github\": \"GitHub\",\n        \"githubPages\": \"GitHub Pages\",\n        \"githubSyncedPart1\": \"Changes on Github Pages may not be visible for a few minutes after completing deployment, \",\n        \"githubSyncedPart2\": \"so please be patient.\",\n        \"gitlabPages\": \"GitLab Pages\",\n        \"gitlabSyncedPart1\": \"Changes on Gitlab Pages may not be visible for a few minutes after completing deployment, \",\n        \"gitlabSyncedPart2\": \"so please be patient.\",\n        \"gitPassword\": \"Password / Token\",\n        \"gitPasswordNote\": \"If you use 2FA to protect your account, instead of a password, you must use a previously generated access token\",\n        \"googleCloud\": \"Google Cloud\",\n        \"googleCloudPrefixNote\": \"Your website may be placed in a subdirectory. Please avoid using slashes at the beginning of the prefix name (i.e. <strong>/blog/</strong>) - as it will create an additional directory with the empty name. Proper prefix example: <strong>blog</strong>.\",\n        \"goToAppSettings\": \"Go to app settings\",\n        \"goToSettings\": \"Go to settings\",\n        \"htmlCacheControl\": \"Cache-Control header for HTML files\",\n        \"keyFile\": \"Key file\",\n        \"lastRendered\": \"Last rendered\",\n        \"lastSync\": \"Last sync\",\n        \"leaveBlankToUseDefaultOutputDirectory\": \"Leave blank to use the default output directory\",\n        \"manualDeployment\": \"Manual deployment\",\n        \"manualOutputFieldCantBeEmpty\": \"Manual output selection cannot be empty\",\n        \"ManualUpload\": \"Manual upload\",\n        \"netlify\": \"Netlify\",\n        \"netlifyToken\": \"Netlify token\",\n        \"nonCompressedCatalog\": \"Non-compressed catalog\",\n        \"note\": \"Note:\",\n        \"operationsDone\": \"operations done\",\n        \"otherCacheControl\": \"Cache-Control header for other files\",\n        \"outputDirectory\": \"Output directory\",\n        \"outputDirectoryNote\": \"Publii will create catalog/file {siteName}-files in the selected directory. If it exists - it will be replaced with the new files.\",\n        \"outputType\": \"Output type\",\n        \"outputTypeNote\": \"Publii will generate a catalog or ZIP/TAR archive of your website. Then you can upload these files to any destination server manually.\",\n        \"parallelUploads\": \"Parallel uploads\",\n        \"parallelUploadsNote\": \"More parallel operations can lead to upload errors on slow internet connections or encounter an error 403 due to API rate limits.\",\n        \"passphraseForKey\": \"Passphrase for a key\",\n        \"passphraseForKeyNote\": \"Use this field only if your key needs a passphrase\",\n        \"port\": \"Port\",\n        \"portFormatNote\": \"The port field cannot be empty and must be a positive integer between 1 and 65535.\",\n        \"prefix\": \"Prefix\",\n        \"preparationError\": \"Preparation error\",\n        \"preparedToUpload\": \"Prepared to upload\",\n        \"preparingFiles\": \"Preparing files\",\n        \"previewCatalogDoesNotExistInfo\": \"The preview catalog does not exist. Please go to the App Settings and select the correct preview directory first.\",\n        \"previewChanges\": \"Preview your changes\",\n        \"provideAccessData\": \"Provide access data\",\n        \"provideFTPPasswordFor\": \"Please provide the FTP password for \",\n        \"provideFTPPasswordForServer\": \"Please provide the FTP password for the following server: \",\n        \"region\": \"Region\",\n        \"regionFieldCantBeEmpty\": \"Region selection cannot be empty\",\n        \"remotePath\": \"Remote path\",\n        \"remotePathMatchServerOrRootPathNote\": \"Path should match your server path e.g. public_html/, public_html/blog/ or the root path e.g. /home/username/public_html/.\",\n        \"remotePathMatchServerRootPathNote\": \"Path should match your server root path e.g. /public_html/, /public_html/blog/.\",\n        \"repository\": \"Repository\",\n        \"repositoryFieldCantBeEmpty\": \"The repository field cannot be empty\",\n        \"repositoryUrl\": \"Repository URL\",\n        \"repositoryUrlFieldCantBeEmpty\": \"Repository URL cannot be empty\",\n        \"repositoryUrlNote\": \"An URL to your Git repository used to store your website files\",\n        \"requireCertificateForConnection\": \"Require a valid certificate for connection\",\n        \"requireFTPPassAlwaysOnSync\": \"Require the FTP password every time you sync your site\",\n        \"s3CompatibleStorage\": \"S3 compatible storage\",\n        \"s3PrefixNote\": \"Your website may be placed in a subdirectory. Please avoid using slashes at the beginning of the prefix name (i.e. <strong>/blog/</strong>) - as it will create an additional directory with the empty name. Proper prefix example: <strong>blog/</strong>.\",\n        \"s3ProviderEndpoint\": \"S3 Provider endpoint\",\n        \"s3ProviderEndpointNote\": \"The custom endpoint cannot be empty when \\\"Use a custom S3 provider\\\" is set to true\",\n        \"secretKey\": \"Secret key\",\n        \"secretKeyFieldCantBeEmpty\": \"The secret key field cannot be empty\",\n        \"selectServerType\": \"Select server type:\",\n        \"serverFieldCantBeEmpty\": \"The server field cannot be empty.\",\n        \"serverGitLabNote\": \"Change this value only if you are using your own GitLab instance.\",\n        \"serverSettingsSaveErrorMsg\": \"Publii cannot save settings due to a problem with the safe password storage software. Restart the application and try again. If the problem persists, please report it to our team via the community forum.\",\n        \"serverSettingsSaveLinuxErrorMsg\": \"Publii cannot save settings as no safe password storage software is installed. Follow the installation instructions for Node Keytar via https://github.com/atom/node-keytar/ and try again. Most likely the libsecret-1-dev and gnome-keyring packages are missing from your system.\",\n        \"serverSettingsSaveSuccessMsg\": \"Server settings have been successfully saved.\",\n        \"settings\": \"Settings\",\n        \"sftp\": \"SFTP\",\n        \"sftpKeyNote\": \"Please select the key file\",\n        \"sftpWithKey\": \"SFTP (with key)\",\n        \"sftpWithPassword\": \"SFTP (with password)\",\n        \"siteFieldCantBeEmpty\": \"The Site ID field cannot be empty\",\n        \"siteID\": \"Site ID\",\n        \"siteIsInSync\": \"Site is in sync\",\n        \"syncFTPNoPasswordMsg\": \"Your website cannot be synced without the password. Please try again\",\n        \"syncInProgress\": \"Sync in progress\",\n        \"syncInProgressMessage\": \"During sync process you cannot change site name.\",\n        \"syncYourWebsite\": \"Sync your website\",\n        \"tarArchive\": \"TAR archive\",\n        \"testConnection\": \"Test connection\",\n        \"testConnectionNoPasswordMsg\": \"You cannot test the connection without the password. Please try again\",\n        \"token\": \"Token\",\n        \"tokenFieldCantBeEmpty\": \"The token field cannot be empty\",\n        \"uploadingWebsite\": \"Uploading website\",\n        \"useCustomS3Provider\": \"Use a custom S3 provider\",\n        \"useCustomS3ProviderNote\": \"Note: AWS is the default S3 provider. When using an alternative provider, you need to fill the \\\"S3 provider endpoint\\\" field.\",\n        \"useFtps\": \"Use FTP with SSL/TLS\",\n        \"useRelativeURLs\": \"Use relative URLs\",\n        \"username\": \"Username\",\n        \"usernameFieldCantBeEmpty\": \"The username field cannot be empty.\",\n        \"usernameOrganization\": \"Username / Organisation\",\n        \"visitWebsite\": \"Visit website\",\n        \"visitYourWebsite\": \"Visit your website\",\n        \"websiteFilesPreparedInfo\": \"Your website files has been prepared. Use the \\\"Get website files\\\" button below <br>to get your files in order to manually deploy them.\",\n        \"websiteLinkInvalidMsg\": \"Sorry! The website link seems to be invalid.\",\n        \"websiteSynchronization\": \"Website synchronisation\",\n        \"websiteSynchronizationInfo\": \"Any duplicate files or filenames that already exist in the destination location <br>that match the files generated by Publii will be overwritten.\",\n        \"websiteURL\": \"Website URL\",\n        \"youHaventSelectedAnyThemeInfo\": \"You haven't selected any theme. Please go to the Settings and select the theme first.\",\n        \"yourJSONKey\": \"Your JSON key\",\n        \"yourJSONKeyFieldCantBeEmpty\": \"The JSON key file selection cannot be empty\",\n        \"yourPrivateKey\": \"Your private key\",\n        \"yourWebsiteIsInSync\": \"Your website is now in sync\",\n        \"zipArchive\": \"ZIP archive\"\n    },\n    \"tag\": {\n        \"addNewTag\": \"Add new tag\",\n        \"addThisAsNewTag\": \"Add this as new tag\",\n        \"createFirstTag\": \"You don't have any tags, yet. Let's create the first!\",\n        \"editTag\": \"Edit tag\",\n        \"filterOrSearchTags\": \"Filter or search tags...\",\n        \"hideTag\": \"Hide tag\",\n        \"leaveBlankToUseDefaultTagPageURL\": \"Leave blank to use a default tag page URL\",\n        \"mainTag\": \"Main tag\",\n        \"newTagHasBeenCreated\": \"New tag has been created\",\n        \"noMainTagForPostMsg\": \"If the post has tags but no main tag has been set, the first tag alphabetically will be used by default.\",\n        \"noSupportFoFeaturedImagesForTags\": \"Your theme does not support featured images for tags.\",\n        \"noTagsAvailable\": \"No tags available\",\n        \"noTagsMatchingYourCriteria\": \"There are no tags matching your criteria.\",\n        \"removeTagMessage\": \"Do you really want to remove selected tags?\",\n        \"removeTagSuccessMessage\": \"Selected tags have been removed\",\n        \"themeDoesNotSupportForTagPagesInTheme\": \"The \\\"Save & Preview\\\" option is not available as your theme lacks support for tag pages.\",\n        \"selectTag\": \"Select tag\",\n        \"selectTagPage\": \"Select tag page\",\n        \"tag\": \"Tag\",\n        \"tagDataParsingErrorMessage\": \"An error occurred while parsing tag data for ID: \",\n        \"tagHasBeenEdited\": \"Tag has been edited\",\n        \"tagIsNotAllowed\": \"This tag is not allowed\",\n        \"tagLink\": \"Tag link\",\n        \"tagName\": \"Tag name:\",\n        \"tagNameCannotBeEmptyErrorMessage\": \"Tag name cannot be empty. Please enter a tag name.\",\n        \"tagNameInUseErrorMessage\": \"Provided tag name is in use. Please try another tag name.\",\n        \"tagNameNotAllowedErrorMessage\": \"Selected tag name/slug is not allowed.\",\n        \"tagNameSimilarInUseErrorMessage\": \"Provided tag name is in use; tag names are not case-sensitive. Please try another tag name.\",\n        \"tagPage\": \"Tag page\",\n        \"tagsListLink\": \"Tags list link\",\n        \"tagStatusChangeSuccessMessage\": \"Status of the selected tags has been changed\",\n        \"tagWillNotAppearInGeneratedTagLists\": \"Tag will not appear in any generated tag list such as post or tag/s pages.\",\n        \"thisTagIsHidden\": \"This tag is hidden\",\n        \"toUseThisOptionEnableIndexingTagPages\": \"To use this option, first enable indexing of tag pages in SEO settings.\",\n        \"url\": \"URL\"\n    },\n    \"theme\": {\n        \"addThemeSuccessMessage\": \"Theme has been successfully added.\",\n        \"authorOptions\": \"Author options\",\n        \"authorOptionsInfo\": \"The author options section allows you to set global options for what extra information should be included in your authors. Changes made in this section will affect all authors on your site, but you can also override the Theme Settings on the Author Edit screen for each individual author if necessary.\",\n        \"authorsPostsPerPage\": \"Authors posts per page:\",\n        \"authorsPostsPerPageInfo\": \"Use -1 as value if you need to display all authors' posts on the page.\",\n        \"changeAppTheme\": \"Change application theme\",\n        \"customSettings\": \"Custom settings\",\n        \"defaultPageTemplate\": \"Default page template\",\n        \"defaultPostTemplate\": \"Default post template\",\n        \"defaultTemplate\": \"Default template\",\n        \"deleteTheme\": \"Delete theme\",\n        \"dropYourThemeHere\": \"Drop your theme here\",\n        \"excerptLength\": \"Excerpt length:\",\n        \"getMoreThemes\": \"Get more themes\",\n        \"goToThemesManager\": \"Go to the themes manager\",\n        \"installTheme\": \"Install theme\",\n        \"leaveBlankToUseDefault\": \"Leave blank to use the default value\",\n        \"newVersionAvailable\": \"New version available\",\n        \"pageOptions\": \"Page options\",\n        \"pageOptionsInfo\": \"The Page options section allows you to set global options for what extra information should be included in your pages. Changes made in this section will affect all pages on your site, but you can also override the App Settings on the Page Edit screen for each individual page if necessary.\",\n        \"postOptions\": \"Post options\",\n        \"postOptionsInfo\": \"The Post options section allows you to set global options for what extra information should be included in your posts. Changes made in this section will affect all posts on your site, but you can also override the App Settings on the Post Edit screen for each individual post if necessary.\",\n        \"postsPerPage\": \"Posts per page:\",\n        \"postsPerPageInfo\": \"Use -1 as value if you need to display all posts on page.\",\n        \"removeThemeMessage\": \"Do you really want to remove the {themeName} theme?\",\n        \"removeThemeSuccessMessage\": \"Theme has been successfully removed.\",\n        \"resetThemeSettings\": \"Reset theme settings\",\n        \"saveSettingsSuccessMessage\": \"Theme settings has been successfully saved.\",\n        \"selectTheme\": \"Select theme\",\n        \"settingsResetMessage\": \"Do you really want to reset the theme settings?\",\n        \"settingsResetSuccessMessage\": \"Theme settings has been reset.\",\n        \"tagOptions\": \"Tag options\",\n        \"tagOptionsInfo\": \"The Tag options section allows you to set global options for what extra information should be included in your tags. Changes made in this section will affect all tags on your site, but you can also override the Theme Settings on the Tag Edit screen for each individual tag if necessary.\",\n        \"tagsPostsPerPage\": \"Tags posts per page:\",\n        \"tagsPostsPerPageInfo\": \"Use -1 as value if you need to display all tag posts on page.\",\n        \"themes\": \"Themes\",\n        \"themeSettings\": \"Theme Settings\",\n        \"translations\": \"Translations\",\n        \"translationsInfo\": \"If you need to translate theme phrases to languages other than english - please check our documentation regarding the <a href=\\\"https://getpublii.com/dev/translations-api/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Translations API</a><br><br>\",\n        \"updateThemeSuccessMessage\": \"Theme has been successfully updated.\",\n        \"uploadThemeErrorMessage\": \"The uploaded files are incorrect. Please upload a theme directory or theme ZIP file.\",\n        \"websiteLogo\": \"Website logo:\"\n    },\n    \"tools\": {\n        \"css\": {\n            \"customCSS\": \"Custom CSS\",\n            \"customCSSSaveSuccessMsg\": \"Custom CSS has been successfully saved.\",\n            \"normal\": \"Normal\",\n            \"putCustomCSSComment\": \"Put your custom CSS code here\"\n        },\n        \"customHTML\": \"Custom HTML\",\n        \"find\": \"Find:\",\n        \"findAndReplace\": \"Find and replace:\",\n        \"findAndReplaceShortcut\": \"Ctrl + Alt + F\",\n        \"findAndReplaceShortcutMac\": \"Cmd + Alt + F\",\n        \"findShortcut\": \"Ctrl + F\",\n        \"findShortcutMac\": \"Cmd + F\",\n        \"goToTools\": \"Go to tools\",\n        \"logFileEmpty\": \"The log file is empty...\",\n        \"logViewer\": \"Log viewer\",\n        \"pluginActivationError\": \"Cannot activate plugin - please try again\",\n        \"pluginDeactivationError\": \"Cannot deactivate plugin - please try again\",\n        \"selectFileToLoad\": \"Select file to load\",\n        \"thumbnails\": {\n            \"listRegeneratedFiles\": \"List of regenerated files:\",\n            \"processingRegenerateThumbnailsInfo\": \"Pressing the Regenerate thumbnails button will start generating new image sizes as defined by your new theme.\",\n            \"progress\": \"Progress: \",\n            \"regenerateThumbnails\": \"Regenerate thumbnails\",\n            \"regenerateThumbnailsInfo\": \"If you've changed your theme or are having issues with your responsive images, you can regenerate them using the button below. This might take a while if your site has a lot of images, so please be patient.\",\n            \"regenerateThumbnailsNotNecessaryInfo\": \"Currently you have no selected theme for this website. Thumbnails regeneration is not necessary.\",\n            \"regeneratingThumbnails\": \"Regenerating thumbnails...\",\n            \"skipRegeneration\": \"Skip regeneration\",\n            \"themeOrThumbnailsSettingsChanged\": \"Your theme or thumbnail settings have been changed.\",\n            \"thumbnailsCreated\": \"All thumbnails have been created.\",\n            \"thumbnailsRegenerationCancelled\": \"Thumbnails regeneration cancelled.\"\n        },\n        \"wpImport\": {\n            \"addTagsToContentAutomatically\": \"Add <p> and <br> tags to the post content automatically\",\n            \"categories\": \"Categories\",\n            \"checkingWXRFile\": \"Checking the selected WXR file\",\n            \"contentFormatting\": \"Content formatting:\",\n            \"duringWXRAnalyzeWeHaveFound\": \"During WXR analysis we have found\",\n            \"importAuthors\": \"Import authors\",\n            \"importData\": \"Import data\",\n            \"importingData\": \"Importing data\",\n            \"importNote\": \"Posts will be imported in a format compatible with the WYSIWYG editor.\",\n            \"importSelectedTypesOfPosts\": \"Import selected post types:\",\n            \"pages\": \"pages\",\n            \"postAuthors\": \"Post authors\",\n            \"selectWXRFileLabel\": \"Please select WXR file:\",\n            \"selectWXRFilePlaceholder\": \"Select a WXR file to import\",\n            \"usedTaxonomyForPosts\": \"Used taxonomy for posts:\",\n            \"useMainAuthor\": \"Use main author from Publii\",\n            \"wpImporter\": \"WP Importer\",\n            \"wpImportGoToRegenerateMsg\": \"Your WordPress data has been imported. Thumbnails regeneration may be necessary if you have already selected a theme.\"\n        }\n    },\n    \"toolsPlugin\": {\n        \"pluginSettingsLoadError\": \"An error occurred during loading of plugin settings\",\n        \"pluginSettingsSaveSuccess\": \"Plugin settings saved successfully\",\n        \"pluginSettingsSaveError\": \"An error occurred during saving of plugin settings\",\n        \"tabsLabel\": \"Plugin settings\",\n        \"thisPluginHasNoOptions\": \"This plugin has no additional options...\"\n    },\n    \"ui\": {\n        \"aboutPublii\": \"About Publii\",\n        \"alternativeText\": \"Alternative text\",\n        \"appConfiguration\": \"Application configuration\",\n        \"appIsUnableToGetNotifications\": \"Unable to download notifications. Please try again later. Error: \",\n        \"applyChanges\": \"Apply changes\",\n        \"authors\": \"Authors\",\n        \"backToTools\": \"Back to tools\",\n        \"backToPages\": \"Pages\",\n        \"backToPosts\": \"Posts\",\n        \"basicInformation\": \"Basic information\",\n        \"beautifyCode\": \"Beautify code\",\n        \"blogIndexLink\": \"Posts index link\",\n        \"cancel\": \"Cancel\",\n        \"cancelWarningMsg\": \"You will lose all unsaved changes - do you want to continue?\",\n        \"canonicalURL\": \"Canonical URL\",\n        \"caption\": \"Caption\",\n        \"checkDocumentation\": \"Check Publii documentation\",\n        \"close\": \"Close\",\n        \"copyToClipboard\": \"Copy to clipboard\",\n        \"credits\": \"Credits\",\n        \"customLink\": \"Custom link\",\n        \"customTemplate\": \"Custom template\",\n        \"delete\": \"Delete\",\n        \"description\": \"Description\",\n        \"donate\": \"Donate\",\n        \"edit\": \"Edit\",\n        \"enablePrettyURLs\": \"Enable pretty URLs\",\n        \"featuredImage\": \"Featured image\",\n        \"fillAllRequiredFields\": \"Please fill all required fields\",\n        \"frontpageLink\": \"Homepage link\",\n        \"gdprBreakingChangesConfirmMsg\": \"We've made significant changes to the Privacy Settings (formerly GDPR) section. We recommend that you review your settings and save them again.\",\n        \"githubRepository\": \"Github repository\",\n        \"goBack\": \"Go back\",\n        \"goToPluginCustomOptions\": \"Hide additional options\",\n        \"goToPluginStandardOptions\": \"Show additional options\",\n        \"goToSettings\": \"Go to Privacy Settings\",\n        \"help\": \"Help\",\n        \"hide\": \"Hide\",\n        \"hideThisNotification\": \"Hide this notification\",\n        \"id\": \"ID\",\n        \"ifCanonicalUrlIsSetMetaRobotsTagIsIgnored\": \"If canonical URLs are enabled, the meta robots tag is ignored.\",\n        \"indexFollow\": \"index, follow\",\n        \"indexNofollow\": \"index, nofollow\",\n        \"indexFollowNoArchive\": \"index, follow, noarchive\",\n        \"indexNofollowNoArchive\": \"index, nofollow, noarchive\",\n        \"iUnderstand\": \"OK, I understand\",\n        \"lastModified\": \"Last modified\",\n        \"learnMore\": \"Learn More\",\n        \"leaveBlankToUseDefaultPageTitle\": \"Leave blank to use a default page title\",\n        \"linkTarget\": \"Link target\",\n        \"loading\": \"Loading...\",\n        \"menus\": \"Menus\",\n        \"metaDescription\": \"Meta description\",\n        \"metaRobotsIndex\": \"Meta robots index\",\n        \"minimize\": \"Minimize\",\n        \"more\": \"More\",\n        \"moreInformationOnPublii\": \"More information about Publii\",\n        \"moreItems\": \"More items\",\n        \"name\": \"Name\",\n        \"newWindow\": \"New window\",\n        \"noindexFollow\": \"noindex, follow\",\n        \"noindexNofollow\": \"noindex, nofollow\",\n        \"none\": \"None\",\n        \"notAvailableInYourTheme\": \"Not available in your theme\",\n        \"notSet\": \"Not set\",\n        \"of\": \"of\",\n        \"ok\": \"OK\",\n        \"otherOptions\": \"Other options\",\n        \"pages\": \"Pages\",\n        \"pagesSupportNotEnabledMessage\": \"Your current theme does not support pages. Please choose a theme with built-in page support or update your existing theme to include this functionality. Detailed information on implementing page support in your theme can be found by clicking the 'Learn More' button below.\",\n        \"pageTitle\": \"Page Title\",\n        \"pleaseFillAllRequiredFields\": \"Please fill all required fields\",\n        \"posts\": \"Posts\",\n        \"preview\": \"Preview\",\n        \"previewFrontPageOnly\": \"Preview homepage only\",\n        \"previewFullWebsite\": \"Preview full website\",\n        \"publiiOnGithub\": \"View Publii on Github\",\n        \"publishAndClose\": \"Publish and close\",\n        \"publishPost\": \"Publish post\",\n        \"reloadFile\": \"Reload file\",\n        \"renderFrontPageOnly\": \"Render homepage only\",\n        \"renderFullWebsite\": \"Render full website\",\n        \"reportBugInSupportDesk\": \"Report a bug via our forum\",\n        \"reportIssue\": \"Report an issue\",\n        \"sameWindow\": \"The same window\",\n        \"save\": \"Save\",\n        \"saveAndClose\": \"Save and close\",\n        \"saveAndPreview\": \"Save & Preview\",\n        \"saveAndRender\": \"Save & Render\",\n        \"saveAsDraft\": \"Save as draft\",\n        \"saveChanges\": \"Save Changes\",\n        \"saveDraft\": \"Save draft\",\n        \"saveDraftAndClose\": \"Save draft and close\",\n        \"search\": \"Search...\",\n        \"selectOption\": \"Select option\",\n        \"selectWebsite\": \"Select a website\",\n        \"seo\": \"SEO\",\n        \"server\": \"Server\",\n        \"show\": \"Show\",\n        \"slug\": \"Slug\",\n        \"supportPublii\": \"Support Publii and donate today!\",\n        \"tags\": \"Tags\",\n        \"theme\": \"Theme\",\n        \"tools\": \"Tools & Plugins\",\n        \"unhide\": \"Unhide\",\n        \"updateSlug\": \"Update slug\",\n        \"uploadInProgress\": \"Upload in progress...\",\n        \"whatsNew\": \"What's new?\"\n    }\n}\n"
  },
  {
    "path": "app/default-files/default-languages/en-gb/wysiwyg.json",
    "content": "{\n    \"Action\": \"Action\",\n    \"Activity\": \"Activity\",\n    \"Address\": \"Address\",\n    \"Advanced\": \"Advanced\",\n    \"Align\": \"Align\",\n    \"Align center\": \"Align center\",\n    \"Align left\": \"Align left\",\n    \"Align right\": \"Align right\",\n    \"Alignment\": \"Alignment\",\n    \"All\": \"All\",\n    \"Alternative source\": \"Alternative source\",\n    \"Alternative source URL\": \"Alternative source URL\",\n    \"Anchor\": \"Anchor\",\n    \"Anchor...\": \"Anchor...\",\n    \"Anchors\": \"Anchors\",\n    \"Animals and Nature\": \"Animals and Nature\",\n    \"Arrows\": \"Arrows\",\n    \"B\": \"B\",\n    \"Background color\": \"Background colour\",\n    \"Black\": \"Black\",\n    \"Block\": \"Block\",\n    \"Blockquote\": \"Blockquote\",\n    \"Blue\": \"Blue\",\n    \"Body\": \"Body\",\n    \"Bold\": \"Bold\",\n    \"Border\": \"Border\",\n    \"Border color\": \"Border colour\",\n    \"Border style\": \"Border style\",\n    \"Border width\": \"Border width\",\n    \"Bottom\": \"Bottom\",\n    \"Browse for an image\": \"Browse for an image\",\n    \"Bullet list\": \"Bullet list\",\n    \"Cancel\": \"Cancel\",\n    \"Capitalization\": \"Capitalization\",\n    \"Caption\": \"Caption\",\n    \"Cell\": \"Cell\",\n    \"Cell padding\": \"Cell padding\",\n    \"Cell properties\": \"Cell properties\",\n    \"Cell spacing\": \"Cell spacing\",\n    \"Cell type\": \"Cell type\",\n    \"Center\": \"Center\",\n    \"Characters\": \"Characters\",\n    \"Characters (no spaces)\": \"Characters (no spaces)\",\n    \"Circle\": \"Circle\",\n    \"Class\": \"Class\",\n    \"Clear formatting\": \"Clear formatting\",\n    \"Close\": \"Close\",\n    \"Code\": \"Code\",\n    \"Code sample\": \"Code sample\",\n    \"Color\": \"Colour\",\n    \"Color Picker\": \"Colour Picker\",\n    \"Color swatch\": \"Colour swatch\",\n    \"Cols\": \"Cols\",\n    \"Column\": \"Column\",\n    \"Column group\": \"Column group\",\n    \"Constrain proportions\": \"Constrain proportions\",\n    \"Copy\": \"Copy\",\n    \"Copy row\": \"Copy row\",\n    \"Could not find the specified string.\": \"Could not find the specified string.\",\n    \"Could not load emoticons\": \"Could not load emoticons\",\n    \"Count\": \"Count\",\n    \"Currency\": \"Currency\",\n    \"Current window\": \"Current window\",\n    \"Custom color\": \"Custom colour\",\n    \"Custom...\": \"Custom...\",\n    \"Cut\": \"Cut\",\n    \"Cut row\": \"Cut row\",\n    \"Dark Blue\": \"Dark Blue\",\n    \"Dark Gray\": \"Dark Gray\",\n    \"Dark Green\": \"Dark Green\",\n    \"Dark Orange\": \"Dark Orange\",\n    \"Dark Purple\": \"Dark Purple\",\n    \"Dark Red\": \"Dark Red\",\n    \"Dark Turquoise\": \"Dark Turquoise\",\n    \"Dark Yellow\": \"Dark Yellow\",\n    \"Date/time\": \"Date/time\",\n    \"Decrease indent\": \"Decrease indent\",\n    \"Default\": \"Default\",\n    \"Delete column\": \"Delete column\",\n    \"Delete row\": \"Delete row\",\n    \"Delete table\": \"Delete table\",\n    \"Dimensions\": \"Dimensions\",\n    \"Disc\": \"Disc\",\n    \"Div\": \"Div\",\n    \"Document\": \"Document\",\n    \"Document properties\": \"Document properties\",\n    \"Drop an image here\": \"Drop an image here\",\n    \"Edit\": \"Edit\",\n    \"Edit image\": \"Edit image\",\n    \"Embed\": \"Embed\",\n    \"Emoticons\": \"Emoticons\",\n    \"Emoticons...\": \"Emoticons...\",\n    \"Error\": \"Error\",\n    \"Extended Latin\": \"Extended Latin\",\n    \"Failed to upload image: {0}\": \"Failed to upload image: {0}\",\n    \"Find\": \"Find\",\n    \"Find and replace\": \"Find and replace\",\n    \"Find and replace...\": \"Find and replace..\",\n    \"Find whole words only\": \"Find whole words only\",\n    \"Finish\": \"Finish\",\n    \"Flags\": \"Flags\",\n    \"Font\": \"Font\",\n    \"Font Sizes\": \"Font Sizes\",\n    \"Fonts\": \"Fonts\",\n    \"Food and Drink\": \"Food and Drink\",\n    \"Footer\": \"Footer\",\n    \"Format\": \"Format\",\n    \"Format Painter\": \"Format Painter\",\n    \"Formats\": \"Formats\",\n    \"Fullscreen\": \"Fullscreen\",\n    \"G\": \"G\",\n    \"General\": \"General\",\n    \"Gray\": \"Gray\",\n    \"Green\": \"Green\",\n    \"H Align\": \"H Align\",\n    \"Header\": \"Header\",\n    \"Header 1\": \"Header H1\",\n    \"Header 2\": \"Header H2\",\n    \"Header 3\": \"Header H3\",\n    \"Header 4\": \"Header H4\",\n    \"Header 5\": \"Header H5\",\n    \"Header 6\": \"Header H6\",\n    \"Header cell\": \"Header cell\",\n    \"Headers\": \"Headers\",\n    \"Heading 1\": \"Heading H1\",\n    \"Heading 2\": \"Heading H2\",\n    \"Heading 3\": \"Heading H3\",\n    \"Heading 4\": \"Heading H4\",\n    \"Heading 5\": \"Heading H5\",\n    \"Heading 6\": \"Heading H6\",\n    \"Headings\": \"Headings\",\n    \"Height\": \"Height\",\n    \"Help\": \"Help\",\n    \"Horizontal line\": \"Horizontal line\",\n    \"Horizontal space\": \"Horizontal space\",\n    \"Id\": \"Id\",\n    \"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.\": \"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.\",\n    \"Ignore\": \"Ignore\",\n    \"Ignore all\": \"Ignore all\",\n    \"Image\": \"Image\",\n    \"Image description\": \"Image description\",\n    \"Image list\": \"Image list\",\n    \"Image options\": \"Image options\",\n    \"Image title\": \"Image title\",\n    \"Image...\": \"Image...\",\n    \"Increase indent\": \"Increase indent\",\n    \"Inline\": \"Inline\",\n    \"Insert\": \"Insert\",\n    \"Insert column after\": \"Insert column after\",\n    \"Insert column before\": \"Insert column before\",\n    \"Insert date/time\": \"Insert date/time\",\n    \"Insert image\": \"Insert image\",\n    \"Insert link\": \"Insert link\",\n    \"Insert row after\": \"Insert row after\",\n    \"Insert row before\": \"Insert row before\",\n    \"Insert table\": \"Insert table\",\n    \"Insert video\": \"Insert video\",\n    \"Insert/Edit Link\": \"Insert/Edit Link\",\n    \"Insert/edit iframe\": \"Insert/edit iframe\",\n    \"Insert/edit image\": \"Insert/edit image\",\n    \"Insert/edit link\": \"Insert/edit link\",\n    \"Insert/edit media\": \"Insert/edit media\",\n    \"Insert/edit video\": \"Insert/edit video\",\n    \"Italic\": \"Italic\",\n    \"Justify\": \"Justify\",\n    \"Keyboard Navigation\": \"Keyboard Navigation\",\n    \"Language\": \"Language\",\n    \"Left\": \"Left\",\n    \"Left to right\": \"Left to right\",\n    \"Light Blue\": \"Light Blue\",\n    \"Light Gray\": \"Light Gray\",\n    \"Light Green\": \"Light Green\",\n    \"Light Purple\": \"Light Purple\",\n    \"Light Red\": \"Light Red\",\n    \"Light Yellow\": \"Light Yellow\",\n    \"Link\": \"Link\",\n    \"Link list\": \"Link list\",\n    \"Link...\": \"Link...\",\n    \"Loading emoticons...\": \"Loading emoticons...\",\n    \"Lower Alpha\": \"Lower Alpha\",\n    \"Lower Greek\": \"Lower Greek\",\n    \"Lower Roman\": \"Lower Roman\",\n    \"Match case\": \"Match case\",\n    \"Mathematical\": \"Mathematical\",\n    \"Media\": \"Media\",\n    \"Media poster (Image URL)\": \"Media poster (Image URL)\",\n    \"Media...\": \"Media...\",\n    \"Medium Blue\": \"Medium Blue\",\n    \"Medium Gray\": \"Medium Gray\",\n    \"Medium Purple\": \"Medium Purple\",\n    \"Merge cells\": \"Merge cells\",\n    \"Middle\": \"Middle\",\n    \"Midnight Blue\": \"Midnight Blue\",\n    \"More...\": \"More...\",\n    \"Name\": \"Name\",\n    \"Navy Blue\": \"Navy Blue\",\n    \"New window\": \"New window\",\n    \"Next\": \"Next\",\n    \"No\": \"No\",\n    \"No color\": \"No colour\",\n    \"Nonbreaking space\": \"Nonbreaking space\",\n    \"None\": \"None\",\n    \"Numbered list\": \"Numbered list\",\n    \"OR\": \"OR\",\n    \"Objects\": \"Objects\",\n    \"Ok\": \"Ok\",\n    \"Open help dialog\": \"Open help dialog\",\n    \"Open link in...\": \"Open link in...\",\n    \"Orange\": \"Orange\",\n    \"Page break\": \"Page break\",\n    \"Paragraph\": \"Paragraph\",\n    \"Paste\": \"Paste\",\n    \"Paste as text\": \"Paste as text\",\n    \"Paste or type a link\": \"Paste or type a link\",\n    \"Paste row after\": \"Paste row after\",\n    \"Paste row before\": \"Paste row before\",\n    \"Paste your embed code below:\": \"Paste your embed code below:\",\n    \"People\": \"People\",\n    \"Permanent Pen Properties\": \"Permanent Pen Properties\",\n    \"Permanent pen properties...\": \"Permanent pen properties...\",\n    \"Poster\": \"Poster\",\n    \"Powered by {0}\": \"Powered by {0}\",\n    \"Pre\": \"Pre\",\n    \"Preferences\": \"Preferences\",\n    \"Preformatted\": \"Preformatted\",\n    \"Prev\": \"Prev\",\n    \"Preview\": \"Preview\",\n    \"Previous\": \"Previous\",\n    \"Print\": \"Print\",\n    \"Print...\": \"Print...\",\n    \"Purple\": \"Purple\",\n    \"Quotations\": \"Quotations\",\n    \"R\": \"R\",\n    \"Red\": \"Red\",\n    \"Redo\": \"Redo\",\n    \"Remove color\": \"Remove colour\",\n    \"Remove link\": \"Remove link\",\n    \"Replace\": \"Replace\",\n    \"Replace all\": \"Replace all\",\n    \"Replace with\": \"Replace with\",\n    \"Restore last draft\": \"Restore last draft\",\n    \"Rich Text Area. Press ALT-0 for help.\": \"Rich Text Area. Press ALT-0 for help.\",\n    \"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help\": \"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help\",\n    \"Right\": \"Right\",\n    \"Right to left\": \"Right to lef\",\n    \"Row\": \"Row\",\n    \"Row group\": \"Row group\",\n    \"Row properties\": \"Row properties\",\n    \"Row type\": \"Row type\",\n    \"Rows\": \"Rows\",\n    \"Save\": \"Save\",\n    \"Scope\": \"Scope\",\n    \"Search\": \"Search\",\n    \"Select all\": \"Select all\",\n    \"Select...\": \"Select...\",\n    \"Selection\": \"Selection\",\n    \"Shortcut\": \"Shortcut\",\n    \"Show caption\": \"Show caption\",\n    \"Show invisible characters\": \"Show invisible characters\",\n    \"Size\": \"Size\",\n    \"Source\": \"Source\",\n    \"Source code\": \"Source code\",\n    \"Special character\": \"Special character\",\n    \"Special character...\": \"Special character...\",\n    \"Spellcheck\": \"Spellcheck\",\n    \"Split cell\": \"Split cell\",\n    \"Square\": \"Square\",\n    \"Strikethrough\": \"Strikethrough\",\n    \"Style\": \"Style\",\n    \"Subscript\": \"Subscript\",\n    \"Superscript\": \"Superscript\",\n    \"Switch to or from fullscreen mode\": \"Switch to or from fullscreen mode\",\n    \"Symbols\": \"Symbols\",\n    \"System Font\": \"System Font\",\n    \"Table\": \"Table\",\n    \"Table of Contents\": \"Table of Contents\",\n    \"Table properties\": \"Table properties\",\n    \"Target\": \"Target\",\n    \"Text\": \"Text\",\n    \"Text color\": \"Text colour\",\n    \"Text to display\": \"Text to display\",\n    \"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?\": \"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?\",\n    \"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?\": \"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?\",\n    \"Title Case\": \"Title Case\",\n    \"To open the popup, press Shift+Enter\": \"To open the popup, press Shift+Enter\",\n    \"Tools\": \"Tools\",\n    \"Top\": \"Top\",\n    \"Travel and Places\": \"Travel and Places\",\n    \"Turquoise\": \"Turquoise\",\n    \"UPPERCASE\": \"UPPERCASE\",\n    \"Underline\": \"Underline\",\n    \"Undo\": \"Undo\",\n    \"Update\": \"Update\",\n    \"Upload\": \"Upload\",\n    \"Upper Alpha\": \"Upper Alpha\",\n    \"Upper Roman\": \"Upper Roman\",\n    \"Url\": \"Url\",\n    \"User Defined\": \"User Defined\",\n    \"V Align\": \"V Align\",\n    \"Valid\": \"Valid\",\n    \"Version\": \"Version\",\n    \"Vertical space\": \"Vertical space\",\n    \"View\": \"View\",\n    \"Warn\": \"Warn\",\n    \"White\": \"White\",\n    \"Whole words\": \"Whole words\",\n    \"Width\": \"Width\",\n    \"Word count\": \"Word count\",\n    \"Words\": \"Words\",\n    \"Words: {0}\": \"Words: {0}\",\n    \"Yellow\": \"Yellow\",\n    \"Yes\": \"Yes\",\n    \"You have unsaved changes are you sure you want to navigate away?\": \"You have unsaved changes are you sure you want to navigate away?\",\n    \"alignment\": \"alignment\",\n    \"comments\": \"comments\",\n    \"example\": \"example\",\n    \"formatting\": \"formatting\",\n    \"history\": \"history\",\n    \"indentation\": \"indentation\",\n    \"lowercase\": \"lowercase\",\n    \"permanent pen\": \"permanent pen\",\n    \"styles\": \"styles\",\n    \"{0} characters\": \"{0} characters\",\n    \"{0} words\": \"{0} words\"\n}\n"
  },
  {
    "path": "app/default-files/default-languages/pl/config.json",
    "content": "{\n\t\"name\": \"Polish - default\",\n\t\"version\": \"1.8.0\",\n\t\"author\": \"Publii Team\",\n\t\"publiiSupport\": \"0.47.0\",\n    \"momentLocale\": \"pl\",\n    \"wysiwygTranslation\": true\n}\n"
  },
  {
    "path": "app/default-files/default-languages/pl/translations.json",
    "content": "{\n    \"author\": {\n        \"addNewAuthor\": \"Dodaj nowego autora\",\n        \"author\": \"Autor\",\n        \"authorDataParsingErrorMessage\": \"Wystąpił błąd podczas parsowania danych autora dla ID: \",\n        \"authorHasBeenUpdated\": \"Autor został zaktualizoway\",\n        \"authorLink\": \"Link do autora\",\n        \"authorName\": \"Nazwa autora\",\n        \"authorNameCannotBeEmptyErrorMessage\": \"Nazwa autora nie może być pusta. Proszę podać inną nazwę.\",\n        \"authorNameInUseErrorMessage\": \"Podana nazwa autora jest już używana. Proszę podać inną nazwę.\",\n        \"authorNameSimilarInUseErrorMessage\": \"Podobna nazwa autora (bez rozróżniania wielkości liter) jest już używana. Proszę podać inną nazwę.\",\n        \"authorPage\": \"Strona autora\",\n        \"authorSlugCannotBeEmpty\": \"Slug autora nie może być pusty\",\n        \"avatar\": \"Avatar\",\n        \"avatarAndFeaturedImage\": \"Avatar i obraz wyróżniony\",\n        \"cannotRemoveMainAuthor\": \"Nie możesz usunąć głównego autora.\",\n        \"changePageAuthor\": \"Zmień autora strony\",\n        \"changePostAuthor\": \"Zmień autora wpisu\",\n        \"editAuthor\": \"Edytuj autora\",\n        \"eMail\": \"E-mail\",\n        \"filterOrSearchAuthors\": \"Filtruj lub wyszukaj autorów...\",\n        \"gravatarEmailWarningMessage\": \"Podaj adres e-mail którego użyłeś/aś do zarejestrowania konta Gravatar.\",\n        \"mainAuthor\": \"Główny autor\",\n        \"mainAuthorCannotBeRemoved\": \"To jest główny autor na tej stronie. Nie można go usnąć.\",\n        \"newAuthorHasBeenCreated\": \"Nowy autor został utworzony\",\n        \"noAuthorsMatchingYourCriteria\": \"Brak autorów spełniających Twoje kryteria.\",\n        \"removeAuthorsMessage\": \"Na pewno chcesz usunąć wybranych autorów?\",\n        \"removeAuthorsSuccessMessage\": \"Wybrani autorzy zostali usunięci\",\n        \"selectAuthor\": \"Wybierz autora\",\n        \"selectAuthorPage\": \"Wybierz stronę autora\",\n        \"themeDoesNotSupportFeaturedImagesForAuthors\": \"Twój motyw nie wspiera wyróżnionych obrazków dla autorów.\",\n        \"whenYouUseGravatarYourSiteVisitorsWillQueryAThirdPartyServer\": \"Korzystając z opcji Gravatara, pamiętaj, że odwiedzający twoją witrynę potrzebują wysyłać żądanie do serwera firmy trzeciej, aby załadować obraz awatara.\",\n        \"toUseThisOptionEnableIndexingAuthorPages\": \"Aby użyć tej opcji najpierw włącz, w ustawieniach SEO, indeksowanie stron autorów.\",\n        \"url\": \"URL\",\n        \"useGravatarMessage\": \"Użyj <a href=\\\"https://gravatar.com/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Gravatara </a> aby dodać avatar do autora.\",\n        \"useGravatarServiceMessage\": \"Aby skorzystać z usługi Gravatar podaj adres e-mail autora.\",\n        \"website\": \"Strona internetowa\"\n    },\n    \"core\": {\n        \"archive\": {\n            \"destinationNotExists\": \"Wybrana lokalizacja plików wynikowych nie istnieje\",\n            \"errorDuringCreatingTAR\": \"Wystąpił błąd podczas tworzenia archiwum TAR. Spróbuj ponownie.\",\n            \"errorDuringCreatingZIP\": \"Wystąpił błąd podczas tworzenia archiwum ZIP. Spróbuj ponownie.\"\n        },\n        \"backup\": {\n            \"destinationDirectoryDoesNotExists\": \"Katalog docelowy nie istnieje\",\n            \"errorDuringFileSaveProcess\": \"Wystąpił błąd podczas zapisu pliku\",\n            \"errorDuringReadingBackupFile\": \"Wystapił błąd podczas odczytu pliku kopii zapasowej\",\n            \"fileDoesNotExists\": \"Plik kopii zapasowej nie istnieje\",\n            \"fileIsCorrupted\": \"Plik kopii zapasowej jest uszkodzony - przerwano proces odtwarzania.\",\n            \"locationDoesNotExists\": \"Lokalizacja kopii zapasowych nie istnieje\",\n            \"temporaryDirectoryDoesNotExists\": \"Katalog tymczasowy nie istnieje\"\n        },\n        \"credits\": {\n            \"errorLoadingLicenseMsg\": \"Wystąpił błąd podczas ładowania licencji. Proszę spróbować ponownie...\"\n        },\n        \"images\": {\n            \"responsiveImagesDisabled\": \"Responsywne obrazy są wyłączone w ustawieniach witryny – wszystkie responsywne obrazy zostały usunięte.\"\n        },\n        \"rendering\": {\n            \"renderingProcessCrashed\": \"Proces renderowania uległ awarii\",\n            \"renderingProcessCrashedMsg\": \"Sprawdż pliki rendering-errors.log i rendering-process.log pod Narzędzia -> Przeglądarka logów.\",\n            \"renderingProcessFailed\": \"Proces renderowania nie powiódł się\"\n        },\n        \"server\": {\n            \"branchDoesNotExist\": \"Wybrana gałąź nie istnieje.\",\n            \"creatingNewRemoteFilesTree\": \"Tworzenie nowego zdalnego drzewa plików...\",\n            \"finishingDeploymentProcess\": \"Zakończenie procesu wdrażania...\",\n            \"getInfoAboutLatestCommit\": \"Uzyskaj informacje o najnowszym commicie...\",\n            \"preparingFilesTreeToUpload\": \"Przygotowywanie drzewa plików do przesłania...\",\n            \"repositoryDoesNotExist\": \"Wybrane repozytorium nie istnieje.\",\n            \"requestLimitExceededInfo\": \"Przekroczono limit żądań API (pozostało {remaining} żądań). Proszę zaczekać do {resetTime} UTC i spróbować pózniej.\",\n            \"requestTimeout\": \"Przekroczono limit czasu żądania.\",\n            \"retrievingHandlerOfRemoteFilesTree\": \"Pobieranie obsługi zdalnego drzewa plików...\",\n            \"retrievingRemoteFilesTree\": \"Pobieranie zdalnego drzewa plików...\",\n            \"tokenOrServerAddressInvalid\": \"Podane dane autoryzacyjne lub adres serwera/repozytorium są nieprawidłowe.\",\n            \"tooManyFilesInfo\": \"Twoja strona zawiera ponad 4000 elementów ({numberOfFiles} plików and katalogów). Aktualnie nasz implementacja Github Pages wspiera strony zawierające do 4000 elementów\"\n        },\n        \"site\": {\n            \"noConfigurationForResponsiveImages\": \"Nie ma konfiguracji dla responsywnych obrazów\",\n            \"noImagesToRegenerate\": \"Brak obrazów do regeneracji\",\n            \"noThemeSelected\": \"Nie wybrano motywu\"\n        },\n        \"sureYouWantQuit\": \"Na pewno chcesz wyjść z programu? \\nWszystkie niezapisane zmiany zostaną utracone.\",\n        \"wpImport\": {\n            \"authorsProgressInfo\": \"Importowanie autorów ({progress} / {total})\",\n            \"imageDownloadError\": \"Wystąpił błąd podczas pobierania obrazu: {image}\",\n            \"imagesProgressInfo\": \"Pobieranie obrazów ({progress} / {total})\",\n            \"postsProgressInfo\": \"Importowanie wpisów ({progress} / {total})\",\n            \"tagsProgressInfo\": \"Importowanie tagów ({progress} / {total})\"\n        }\n    },\n    \"customHTML\": {\n        \"tabs\": {\n            \"head\": \"Sekcja head\",\n            \"body\": \"Treść strony\",\n            \"comments\": \"Komentarze\",\n            \"searchInput\": \"Pole wyszukiwarki\",\n            \"searchContent\": \"Wyniki wyszukiwania\",\n            \"socialSharing\": \"Udostępnianie Społecznościowe\",\n            \"footer\": \"Stopka strony\",\n            \"beforePost\": \"Przed każdym wpisem\",\n            \"afterPost\": \"Po każdym wpisie\",\n            \"afterPage\": \"Po każdej podstronie\"\n        }\n    },\n    \"date\": {\n        \"apr\": \"Kwi\",\n        \"aug\": \"Sier\",\n        \"changePagePublicationDate\": \"Zmień datę publikacji strony\",\n        \"changePostPublicationDate\": \"Zmień datę publikacji wpisu\",\n        \"dec\": \"Gru\",\n        \"feb\": \"Lut\",\n        \"jan\": \"sty\",\n        \"jul\": \"Lip\",\n        \"jun\": \"Czer\",\n        \"mar\": \"Marz\",\n        \"may\": \"Maj\",\n        \"nov\": \"List\",\n        \"oct\": \"Paź\",\n        \"sep\": \"Wrz\"\n    },\n    \"editor\": {\n        \"addHeading\": \"Dodaj nagłówek\",\n        \"addLabel\": \"Dodaj etykietę\",\n        \"alignTextCenter\": \"Wyśrodkuj tekst\",\n        \"alignTextLeft\": \"Wyrównaj tekst do lewej\",\n        \"alignTextRight\": \"Wyrównaj tekst do prawej\",\n        \"blockEditorHelpPanelDesc\": \"Możesz wstawić blok, klikając <span>+</span> lub <span>TAB</span> w nowym wierszu lub wpisując następujące skróty lub składnię przecen w nowym wierszu:\",\n        \"blocks\": {\n            \"code\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego kodu\"\n            },\n            \"cssClassesLabel\": \"Klasa CSS\",\n            \"cssClassesTooltip\": \"Ta klasa CSS zostanie dodana jako atrybut klasy tego pola\",\n            \"embed\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego osadzenia\"\n            },\n            \"gallery\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tej galerii\"\n            },\n            \"header\": {\n                \"customIdLabel\": \"Zastosuj własne ID\",\n                \"customIdTooltip\": \"Enable this option if you want to use own ID, instead of the auto-generated one\",\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego nagłówka\"\n            },\n            \"html\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego bloku HTML\"\n            },\n            \"idLabel\": \"ID\",\n            \"image\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego obrazu\"\n            },\n            \"list\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tej listy\"\n            },\n            \"paragraph\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego pragrafu\",\n                \"styleLabel\": \"Styl pragrafu\",\n                \"styles\": {\n                    \"highlight\": \"Wyróżnienie\",\n                    \"info\": \"Informacja\",\n                    \"success\": \"Sukces\",\n                    \"warning\": \"Ostrzeżenie\"\n                },\n                \"styleTooltip\": \"Wybierz dodatkowe stylowanie dla bloku paragrafu\"\n            },\n            \"quote\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego cytatu\"\n            },\n            \"separator\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego separatora\"\n            },\n            \"toc\": {\n                \"idTooltip\": \"Ta wartość zostanie użyta jako ID tego spisu treści\"\n            }\n        },\n        \"cantSavePostWithEmptyTitle\": \"Nie możesz zapisać wpisu z pustym tytułem.\",\n        \"cantSavePageWithEmptyTitle\": \"Nie możesz zapisać strony z pustym tytułem.\",\n        \"changesSaved\": \"Zmiany zostały zapisane\",\n        \"clearFormatting\": \"Wyczyść formatowanie\",\n        \"clickToConfirm\": \"Kliknij, aby potwierdzić\",\n        \"code\": \"Kod\",\n        \"conversions\": {\n            \"toCode\": \"Kod\",\n            \"toHeader\": \"Nagłówek\",\n            \"toHTML\": \"HTML\",\n            \"toList\": \"Lista\",\n            \"toParagraph\": \"Pargraf\",\n            \"toQuote\": \"Cytat\"\n        },\n        \"convertTo\": \"Przekształć w:\",\n        \"custom\": \"Niestandardowy\",\n        \"defaultSelect\": \"- Wybierz -\",\n        \"deleteBlock\": \"Usuń blok\",\n        \"dot\": \"Kropka\",\n        \"dots\": \"Kropki\",\n        \"dragAndDropImgToEditor\": \"Przeciągnij i upuść obraz do edytora\",\n        \"dropToUploadYourPhotoOr\": \"Upuść, aby przesłać swoje zdjęcie lub\",\n        \"dropToUploadYourPhotosOr\": \"Upuść, aby przesłać swoje zdjęcia lub\",\n        \"element\": \"Element\",\n        \"emptyGalleryBlock\": \"Pusty blok galerii\",\n        \"emptyImageBlock\": \"Pusty blok obrazu\",\n        \"enterAltText\": \"Podaj tekst alternatywny\",\n        \"enterCaption\": \"Podaj podpis zdjęcia\",\n        \"enterUrlOrEmbedCode\": \"Podaj URL lub kod do osadzenia...\",\n        \"errorOccurred\": \"Wystąpił błąd - proszę spróbować ponownie.\",\n        \"gallery\": \"Galeria\",\n        \"header\": \"Nagłówek\",\n        \"heading1\": \"Nagłówek 1\",\n        \"heading2\": \"Nagłówek 2\",\n        \"heading3\": \"Nagłówek 3\",\n        \"heading4\": \"Nagłówek 4\",\n        \"heading5\": \"Nagłówek 5\",\n        \"heading6\": \"Nagłówek 6\",\n        \"hideBulkEdit\": \"Ukryj edycję zbiorczą\",\n        \"hideHelp\": \"Ukryj Pomoc\",\n        \"hideStats\": \"Ukryj Statystyki\",\n        \"html\": \"HTML\",\n        \"insertGallery\": \"Wstaw galerię\",\n        \"list\": \"Lista\",\n        \"markdown\": \"Markdown\",\n        \"markdownHelpPanelDesc\": \"Możesz łatwo zarządzać nagłówkami, stylami tekstu lub tworzyć inne elementy HTML, używając następujących skrótów klawiaturowych lub wpisując składnię markdown:\",\n        \"newDraftCreated\": \"Nowy szkic został utworzony\",\n        \"newPageCreated\": \"Nowa strona została utworzona\",\n        \"newPostCreated\": \"Nowy wpis został utworzony\",\n        \"nothingFound\": \"Nie znaleziono bloków\",\n        \"paragraph\": \"Paragraph\",\n        \"quote\": \"Cytat\",\n        \"quoteAuthor\": \"Autor cytatu\",\n        \"quoteText\": \"Tekst cytatu\",\n        \"readMore\": \"Czytaj więcej\",\n        \"readMoreBlockName\": \"Czytaj więcej\",\n        \"selectFiles\": \"Wybierz pliki\",\n        \"separator\": \"Separator\",\n        \"shortcuts\": \"Skróty\",\n        \"sourceCode\": \"Kod źródłowy\",\n        \"searchForABlock\": \"Wyszukaj blok...\",\n        \"startWriting\": \"Zacznij pisać...\",\n        \"startWritingOrPressTabToChooseBlock\": \"Zacznij pisać lub naciśnij klawisz TAB, aby wybrać blok.\",\n        \"toc\": \"Spis treści\",\n        \"tocAutoGenerationInfo\": \"Spis treści jest generowany automatycznie poprzez zbieranie nagłówków (H1-H6) Twoich treści.\",\n        \"togglePostStatsPanel\": \"Przełącz panel statystyk wpisów\",\n        \"unableToSetSpellCheckerForLanguage\": \"(!) Nie można ustawić sprawdzania pisowni dla wybranego języka - \",\n        \"useOrderedList\": \"Użyj listy uporządkowanej\",\n        \"useUnorderedList\": \"Użyj listy nieuporządkowanej\",\n        \"viewBulkEdit\": \"Edycja zbiorcza\",\n        \"viewHelp\": \"Pokaż Pomoc\",\n        \"viewStats\": \"Pokaż Statystyki\",\n        \"wideLine\": \"Szeroka linia\"\n    },\n    \"file\": {\n        \"addNewFile\": \"Dodaj nowy plik\",\n        \"backups\": \"Kopie zapasowe\",\n        \"createBackup\": \"Utwórz kopię zapasową\",\n        \"createBackupConfirmMsg\": \"Wybierz nazwę kopii zapasowej — nazwa pliku może zawierać tylko znaki alfanumeryczne, myślniki i podkreślenia:\",\n        \"createBackupErrorMsg\": \"Wystąpił błąd podczas tworzenia kopii zapasowej. Proszę spóbować ponownie.\",\n        \"createBackupNameEmptyMsg\": \"Podana nazwa pliku nie może być pusta. Proszę spóbować ponownie, podając inną nazwę.\",\n        \"createBackupNameInUseMsg\": \"Podana nazwa pliku (<strong>{filename}</strong>) jest używana przez istniejący plik kopii zapasowej is used by an existing backup. Proszę spóbować ponownie, podając inną nazwę.\",\n        \"createBackupSuccessMsg\": \"Kopia zapasowa została utworzona.\",\n        \"createFirstBackupMsg\": \"Jeszcze nie masz kopii zapasowych. Stwórzmy pierwszą!\",\n        \"creatingBackup\": \"Tworzenie kopii zapasowej\",\n        \"creationDate\": \"Data utworzenia\",\n        \"deleteBackupsConfirmMsg\": \"Czy na pewno chcesz usunąć wybrane kopie zapasowe? Tej czynności nie można cofnąć.\",\n        \"deleteBackupsErrorMsg\": \"Wystąpił błąd podczas usuwania wybranego pliku kopii zapasowej. Proszę spóbować ponownie.\",\n        \"deleteBackupsSuccessMsg\": \"Wybrane kopie zapasowe zostały usunięte\",\n        \"dragAndDropBackupFile\": \"Przeciągnij i upuść plik kopii zapasowej tutaj lub\",\n        \"dropYourFileHere\": \"Upuść plik tutaj\",\n        \"file\": \"Plik\",\n        \"fileFromFileManager\": \"Plik z Menedżera Plików\",\n        \"fileManager\": \"Menedżer plików\",\n        \"filename\": \"Nazwa pliku\",\n        \"files\": \"Pliki\",\n        \"fileSemicolon\": \"Plik:\",\n        \"fileSize\": \"Rozmiar pliku\",\n        \"filterOrSearchFiles\": \"Filtruj lub wyszukaj pliki...\",\n        \"mediaFiles\": \"media/pliki\",\n        \"noBackupsAvailable\": \"Brak dostępnych kopii zapasowych\",\n        \"noFileInMediaFilesDirInfo\": \"W katalogu media/pliki nie ma żadnych plików...\",\n        \"noFileInRootDirInfo\": \"W katalogu głównym nie ma żadnych plików...\",\n        \"noFileMatchingCriteriaInfo\": \"Nie ma plików spełniających Twoje kryteria.\",\n        \"operations\": \"Operacje\",\n        \"preparingFilesInOutputDir\": \"Przygotowywanie plików w katalogu wyjściowym\",\n        \"provideNameForNewFile\": \"Proszę podać nazwę nowego pliku\",\n        \"removeFilesConfirmMsg\": \"Czy na pewno chcesz usunąć wybrane pliki? Nie można tego cofnąć.\",\n        \"removeFilesSuccessMsg\": \"Wybrane pliki zostały usunięte\",\n        \"rename\": \"Zmień nazwę\",\n        \"renameBackupConfirmLabel\": \"Zmień nazwę pliku\",\n        \"renameBackupConfirmMsg\": \"Proszę podać nową nazwę pliku kopii zapasowej:\",\n        \"renameBackupErrorMsg\": \"Wystąpił błąd podczas zmiany nazwy wybranego pliku kopii zapasowej. Proszę spóbować ponownie.\",\n        \"renameBackupNameEmptyMsg\": \"Nazwa pliku kopii zapasowej nie może być pusta. Proszę spóbować ponownie.\",\n        \"renameBackupNameInUseMsg\": \"Podana nazwa jest używana przez inny plik kopii zapasowej. Proszę spóbować ponownie.\",\n        \"renameBackupSameNameMsg\": \"Podana nazwa jest taka sama jak stara nazwa. Proszę spóbować ponownie.\",\n        \"renameBackupSuccessMsg\": \"Nazwa kopii zapasowej została pomyślnie zmieniona.\",\n        \"restore\": \"Przywróć\",\n        \"restoreBackupConfirmLabel\": \"Przywróć kopię zapasową\",\n        \"restoreBackupConfirmMsg\": \"Czy na pewno chcesz przywrócić wybraną kopię zapasową? Istniejące pliki zostaną nadpisane.\",\n        \"restoreBackupErrorMsg\": \"Wystąpił błąd podczas przywracania wybranego pliku kopii zapasowej: \",\n        \"restoreBackupSuccessMsg\": \"Strona została pomyślnie przywrócona.\",\n        \"rootDirectory\": \"katalog główny\",\n        \"selectedFileExistsMsg\": \"Te pliki istnieją w wybranym katalogu, więc nie można ich skopiować: \",\n        \"selectedFilenameInUseMsg\": \"Wybrana nazwa pliku jest w użyciu. Proszę spróbować użyć innej nazwy pliku.\",\n        \"selectFile\": \"Wybierz plik\",\n        \"selectFileFromFileManager\": \"Wybierz plik z Menedżera Plików\",\n        \"uploadFiles\": \"Prześlij pliki\"\n    },\n    \"gdpr\": {\n        \"addGroup\": \"Dodaj grupę\",\n        \"embedConsents\": {\n            \"addRule\": \"Dodaj regułę\",\n            \"groupButtonLabel\": \"Etykieta przycisku\",\n            \"groupCookieGroup\": \"Grupa cookies\",\n            \"groupRule\": \"URL zawiera\",\n            \"groupRulePlaceholder\": \"youtube.com\",\n            \"groupTextPlaceholder\": \"Treść zgody\"\n        },\n        \"groupDescriptionPlaceholder\": \"Dodaj tutaj opis grupy plików cookies\",\n        \"groupID\": \"ID grupy\",\n        \"groupName\": \"Nazwa grupy\",\n        \"state\": \"Stan\"\n    },\n    \"image\": {\n        \"addImages\": \"Dodaj obrazy\",\n        \"align\": \"Wyrównanie\",\n        \"centeredImage\": \"Obraz wyśrodkowany\",\n        \"columns\": \"Kolumny:\",\n        \"dropFeaturedImageOr\": \"Upuść tutaj obrazek wyróżnony lub\",\n        \"dropToUploadPhotoOr\": \"Upuść, aby przesłać swoje zdjęcie lub\",\n        \"eightColumns\": \"8 kolumn\",\n        \"enterAltText\": \"Wpisz tekst alt\",\n        \"enterCaption\": \"Wpisz podpis\",\n        \"fiveColumns\": \"5 kolumn\",\n        \"fourColumns\": \"4 kolumny\",\n        \"fullWidth\": \"Pełna szerokość\",\n        \"fullWidthImage\": \"Obraz pełnej szerokości\",\n        \"image\": \"Obraz\",\n        \"imageAlternativeText\": \"Tekst alternatywny obrazu\",\n        \"imageCaption\": \"Podpis obrazu\",\n        \"images\": \"Obrazy\",\n        \"insertEditGallery\": \"Dodaj/Edytuj Galerię\",\n        \"layout\": \"Układ\",\n        \"loadingImage\": \"Wczytywanie obrazu...\",\n        \"none\": \"Brak\",\n        \"oneColumn\": \"1 kolumna\",\n        \"pictures\": \"obrazów\",\n        \"removeImage\": \"Usuń obraz\",\n        \"sevenColumns\": \"7 kolumn\",\n        \"sixColumns\": \"6 kolumn\",\n        \"threeColumns\": \"3 kolumny\",\n        \"twoColumns\": \"2 kolumny\",\n        \"uploading\": \"Przesyłanie\",\n        \"wide\": \"Szerokie\",\n        \"wideImage\": \"Szeroki obraz\",\n        \"yourGalleryIsEmpty\": \"Twoja galeria jest pusta\"\n    },\n    \"langs\": {\n        \"addLanguageSuccessMessage\": \"Język został dodany\",\n        \"deleteLanguage\": \"Usuń język\",\n        \"getMoreLanguages\": \"Pobierz więcej języków\",\n        \"goToLanguagesManager\": \"Idź do menadżera języków\",\n        \"installLanguage\": \"Zainstaluj język\",\n        \"isOutdated\": \"Nieaktualny\",\n        \"isOutdatedTitle\": \"Ta paczka językowa wspiera Publii do wersji v.{supportedVersion}, podczas gdy używasz Publii w wersji v.{currentVersion} - sprawdź dostępność aktualizacji\",\n        \"language\": \"Język:\",\n        \"languageChangedMsg\": \"Język aplikacji został zmieniony\",\n        \"languageChangeError\": \"Zmiana języka nie powiodła się - proszę spróbować ponownie.\",\n        \"languageLoadingError\": \"Wystapił błąd podczas wczytywania plików językowych. Zostanie użyty domyślny język (angielski). Przejdź do ustawień językowych aby ustawić prawidłowy język.\",\n        \"languages\": \"Języki\",\n        \"removeLanguageMessage\": \"Czy na pewno chcesz usunąć język {languageName}?\",\n        \"removeLanguageSuccessMessage\": \"Usunięto wybrany język\",\n        \"updatedLanguageSuccessMessage\": \"Wybrany język został zaktualizowany\",\n        \"uploadLanguageErrorMessage\": \"Wystapił błąd podczas instalacji. Spróbuj ponownie.\"\n    },\n    \"link\": {\n        \"addDownloadAttr\": \"Dodaj atrybut \\\"download\\\"\",\n        \"addLink\": \"Dodaj link\",\n        \"addNofollow\": \"Dodaj rel=\\\"nofollow\\\"\",\n        \"downloadAttribute\": \"Atrybut \\\"download\\\" linka:\",\n        \"insertEditLink\": \"Wstaw/Edytuj link\",\n        \"linkInvalidMsg\": \"Przepraszamy! Ten link wydaje się być nieprawidłowy.\",\n        \"linkRelAttribute\": \"Atrybut \\\"rel\\\" linka\",\n        \"linkTitleAttribute\": \"Atrybut \\\"title\\\" linka\",\n        \"linkClassAttribute\": \"Klasa CSS\",\n        \"linkTarget\": \"Otwórz w\",\n        \"openInNewWindow\": \"Otwórz w nowym oknie\",\n        \"previewLinkInBrowser\": \"Wyświetl podgląd tego linku w przeglądarce\",\n        \"previewOnlyExternalLinksMsg\": \"W edytorze wpisów możesz wyświetlić podgląd tylko linków zewnętrznych\",\n        \"removeLink\": \"Usuń link\",\n        \"selectLinkType\": \"Wybierz rodzaj linka\"\n    },\n    \"menu\": {\n        \"addMenuItem\": \"Dodaj element menu\",\n        \"addNewMenu\": \"Dodaj nowe menu\",\n        \"addNewMenuItem\": \"Dodaj nowy element menu\",\n        \"addSubmenu\": \"Dodaj podmenu\",\n        \"addSubmenuItem\": \"Dodaj element podmenu\",\n        \"assignedMenu\": \"Przypisane menu\",\n        \"classCSS\": \"Klasa CSS\",\n        \"createNewMenu\": \"Stwórz nowe menu\",\n        \"deleteThisMenuItem\": \"Usuń ten element menu\",\n        \"duplicateThisMenuItem\": \"Duplikuj ten element menu\",\n        \"editMenuItem\": \"Edytuj element menu\",\n        \"editMenuName\": \"Edytuj nazwę menu\",\n        \"editThisMenuItem\": \"Edytuj ten element menu\",\n        \"externalLink\": \"Zewnętrzny link\",\n        \"externalURL\": \"Zewnętrzyny URL\",\n        \"hideThisMenuItem\": \"Ukryj ten element menu\",\n        \"insertActions\": \"Umieść wybrany element:\",\n        \"insertAfter\": \"za\",\n        \"insertAsChild\": \"jako submenu\",\n        \"insertBefore\": \"przed\",\n        \"internalLink\": \"Link wewnętrzny\",\n        \"items\": \"Elementy\",\n        \"label\": \"Etykieta\",\n        \"likedItemError\": \"Podlinkowany element nie jest renderowalny (pusty tag/strona autora), nie istnieje lub został usunięty.\",\n        \"likedItemIsADraft\": \"Podlinkowany element jest kopią roboczą.\",\n        \"menu\": \"Menu\",\n        \"menuItemsRemoveMessage\": \"Na pewno chcesz usunąć wybrane elementy menu?\",\n        \"menuItemIsHidden\": \"Ten element menu jest ukryty\",\n        \"menuNameCannotBeEmptyCreateNewMenuErrorMessage\": \"Pole z nazwą menu nie może być puste.Proszę na nowo utworzyć menu.\",\n        \"menuNameCannotBeEmptyErrorMessage\": \"Pole z nazwą menu nie może być puste.Proszę wprowadzić inną nazwę menu.\",\n        \"menuNameExistsErrorMessage\": \"Menu o podanej nazwie już istnieje. Proszę użyć innej nazwy.\",\n        \"menuNameHasBeenEdited\": \"Nazwa menu została zmieniona\",\n        \"menuNameInUseErrorMessage\": \"Ta nazwa menu już używana. Proszę zmienić nazwę menu.\",\n        \"menusRemoveMessage\": \"Na pewno chcesz usunąć wybrane menun?\",\n        \"menusRemoveSuccessMessage\": \"Wybrane menu zostały usunięte\",\n        \"moveItem\": \"Przenieś\",\n        \"newMenuCreated\": \"Nowe menu zostało utworzone\",\n        \"noMenusAvailable\": \"Brak dostępnych menu\",\n        \"noMenusCreateNewOne\": \"Nie masz jeszcze żadnych menu. Stwórzmy pierwsze!\",\n        \"noMenusMessage\": \"Brak elementów menu; stwórz nowe używając przycisku \\\"Dodaj element menu\\\" znajdującego się powyżej.\",\n        \"provideNameForNewMenu\": \"Podaj nazwę dla Twojego nowego menu:\",\n        \"provideNewNameForMenu\": \"Podaj nową nazwę dla wybranego menu:\",\n        \"selectItemType\": \"Wybierz rodzaj elmentu\",\n        \"selectLinkTarget\": \"Wybierz target linka\",\n        \"showThisMenuItem\": \"Pokaż ten element menu\",\n        \"textSeparator\": \"Separator tekstu\",\n        \"type\": \"Rodzaj\",\n        \"unassigned\": \"Nieprzypisane\",\n        \"unselectItem\": \"Odznacz\",\n        \"updateLabel\": {\n            \"author\": \"Użyj nazwy autora\",\n            \"page\": \"Użyj tytułu strony\",\n            \"post\": \"Użyj tytułu wpisu\",\n            \"tag\": \"Użyj nazwy tagu\"\n        }\n    },\n    \"menuPositionPopup\": {\n        \"invalidValue\": \"Nieprawidłowa wartość: wprowadzona wartość nie może być równa 0 i przekraczać domyślnego maksymalnego limitu motywu. Użycie -1 oznacza brak ograniczeń.\",\n        \"maxLevels\": \"Maksymalny poziom:\",\n        \"themeDefaultValue\": \"Domyślna wartość z motywu:\",\n        \"title\": \"Skonfiguruj pozycję menu\",\n        \"usedBy\": \"Używane przez inne menu:\"\n    },\n    \"notifications\": {\n        \"badgeNew\": \"Nowe\",\n        \"latestVersion\": \"Najnowsza wersja\",\n        \"currentVersion\": \"Zainstalowana wersja\",\n        \"checkUpdates\": \"Sprawdź aktualizacje\",\n        \"consentInfo\": \"Obecnie otrzymujesz powiadomienia o aktualizacjach. Jeśli nie chcesz ich otrzymywać\",\n        \"consentReject\": \"kliknąć tutaj, aby wycofać swoją zgodę.\",\n        \"consentStateTitle\": \"Włącz powiadomienia o aktualizacjach\",\n        \"consentStateDescription\": \"Publii może automatycznie sprawdzać dostępność nowych aktualizacji i powiadomi Cię, gdy będą dostępne. W tym celu nawiązywane jest połączenie z serwerem Publii, które obejmuje adres IP Twojego urządzenia. Informacje te są wykorzystywane wyłącznie do dostarczania powiadomień o aktualizacjach i nie są przechowywane. W każdej chwili możesz wyłączyć tę opcję w ustawieniach aplikacji.\",\n        \"disabled\": \"Wyłączone\",\n        \"downloadUpdate\": \"Pobierz\",\n        \"giveConsent\": \"Włącz powiadomienia\",\n        \"goToNotificationsCenter\": \"Przejdź do centrum powiadomień\",\n        \"markAsRead\": \"Oznacz jako przeczytane\",\n        \"news\": \"Nowości\",\n        \"notifications\": \"Powiadomienia\",\n        \"noUpdatesTitle\": \"Brak dostępnych aktualizacji\",\n        \"noUpdatesDescription\": \"Używasz najnowszej wersji Publii i rozszerzeń.\",\n        \"pluginUpdatesAvailable\": \"Dostępne aktualizacje wtyczek\",\n        \"publiiUpdateAvailable\": \"Dostępna aktualizacja Publii\",\n        \"rejectConsentConfirm\": \"Cofając zgodę na otrzymywanie powiadomień, nie będziesz dłużej otrzymywać informacji o nowych wersjach Publii, rozszerzeń dla Publii (motywy i pluginy) a także ważnych powiadomień dodatkowych.\",\n        \"viewDetails\": \"Pokaż szczegóły\",\n        \"readMore\": \"Czytaj więcej\",\n        \"rejectConsent\": \"Nie teraz\",\n        \"themeUpdatesAvailable\": \"Dostępne aktualizacje motywów\"\n    },\n    \"page\": {\n        \"addNewPage\": \"Dodaj nową stronę\",\n        \"addPageTitle\": \"Dodaj tytuł strony\",\n        \"all\": \"Wszystkie\",\n        \"closeHierarchy\": \"Zakończ edycję\",\n        \"changePageDate\": \"Zmień datę publikacji\",\n        \"cleanUrlsDisabled\": \"Włącz opcję 'Ładne adresy URL' w ustawieniach strony, aby odblokować funkcję edycji hierarchii. Umożliwi to łatwe zarządzanie relacjami między stronami nadrzędnymi i podrzędnymi.\",\n        \"convertToPost\": \"Konwertuj na wpis\",\n        \"editHierarchy\": \"Edytuj hierarchię\",\n        \"configureThemeBeforeGeneratingPreview\": \"Musisz skonfigurować motyw dla tej witryny przed wygenerowaniem podglądu tej strony.\",\n        \"currentDefaultTemplate\": \"Aktualnie domyślny szablon\",\n        \"customPageTypes\": \"Własne rodzaje stron\",\n        \"drafts\": \"Wersje robocze\",\n        \"duplicate\": \"Duplikuj\",\n        \"duplicatePageErrorMessage\": \"Wystąpił błąd podczas duplikowania wybranych stron. Proszę spróbować ponownie.\",\n        \"duplicatePageSuccessMessage\": \"Wybrane strony zostały zduplikowane\",\n        \"editorBlock\": \"Edytor blokowy\",\n        \"editorBlockInfo\": \"Nowoczesny i intuicyjny edytor wspierający skróty klawiszowe i markdown dla ułatwienia tworzenia treści na blogu bez martwienia się o HTML lub inne elementy kodu.\",\n        \"editorBlockNotSupportedEditPageInfo\": \"Aktualny motyw nie wspiera edytora blokowego użytego w tej stronie. Ta strona nie może być poprawnie wyrenderowana do pliku wynikowego.\",\n        \"editorBlockNotSupportedNewPageInfo\": \"Aktualny motyw nie wspiera edytora blokowego, którego chcesz użyć. Ta strona nie może być poprawnie wyrenderowana do pliku wynikowego.\",\n        \"editorBlockUse\": \"Użyj edytora blokowego\",\n        \"editorMarkdown\": \"Edytor Markdown\",\n        \"editorMarkdownInfo\": \"Ten edytor wspiera składnię Markdown jako skrót do szybszego tworzenia treści; doskonale nadaje się do obszernych, prostych projektów, takich jak dokumentacja.\",\n        \"editorMarkdownUse\": \"Użyj edytora Markdown\",\n        \"editorWYSIWYG\": \"Edytor WYSIWYG\",\n        \"editorWYSIWYGInfo\": \"Ten edytor oferuje znajomy sposób przetwarzania tekstu oraz dodatkowe narzędzia dla użytkowników, którzy chcą mieć pod kontrolą każdy aspekt treści swojej strony.\",\n        \"editorWYSIWYGUse\": \"Użyj edytora WYSIWYG\",\n        \"editPageAnyway\": \"Edytuj stronę mimo to\",\n        \"filterOrSearchPages\": \"Filtruj lub wyszukaj strony...\",\n        \"insertActions\": \"Umieść wybrany element:\",\n        \"insertAfter\": \"po\",\n        \"insertAsChild\": \"jako podstronę\",\n        \"insertBefore\": \"przed\",\n        \"isHomepage\": \"Strona główna\",\n        \"markAsDraft\": \"Oznacz jako kopię roboczą\",\n        \"markAsFeatured\": \"Oznacz jako wyróżniony\",\n        \"markAsUnfeatured\": \"Oznacz jako niewyróżniony\",\n        \"modificationDate\": \"Data modyfikacji\",\n        \"moveItem\": \"Przenieś\",\n        \"moveToTrash\": \"Przenieś do kosza\",\n        \"noPagesMatchingYourCriteria\": \"Brak stron spełniających Twoje kryteria.\",\n        \"noParentPage\": \"Brak strony nadrzędnej\",\n        \"openEditorAnyway\": \"Otwórz edytor mimo to\",\n        \"page\": \"Strona\",\n        \"pageAuthor\": \"Autor strony\",\n        \"pageLink\": \"Link do strony\",\n        \"pageName\": \"Nazwa strony:\",\n        \"pages\": \"Strony\",\n        \"pageSettings\": \"Ustawienia stron\",\n        \"pageSlug\": \"Slug strony\",\n        \"pageSlugLengthWarning\": \"Slug strony dłuższy niż 250 znaków może prowadzić do tworzenia uszkodzonych plików podczas renderowania strony internetowej.\",\n        \"pageSlugTooLong\": \"Slug strony jest za długi\",\n        \"pageSlugUpdateButton\": \"Aktualizuj\",\n        \"pageState\": \"Stan strony\",\n        \"pageStatusChangeSuccessMessage\": \"Status wybranych stron został zmieniony\",\n        \"pageTemplate\": \"Szablon strony\",\n        \"parentPage\": \"Strona nadrzędna\",\n        \"publicationDate\": \"Data publikacji\",\n        \"publish\": \"Opublikuj\",\n        \"published\": \"Opublikowano\",\n        \"removePageMessage\": \"Na pewno chcesz usunąć wybrane strony? Tej operacji nie można cofnąć.\",\n        \"removePageSuccessMessage\": \"Wybrane strony zostały usunięte\",\n        \"selectPage\": \"Wybierz stronę\",\n        \"setAsParent\": \"Ustaw jako rodzica\",\n        \"setAsSubpage\": \"Ustaw jako podstronę\",\n        \"setCustomPageDate\": \"Ustaw niestandardową datę publikacji\",\n        \"status\": \"Status\",\n        \"thisPageIsADraft\": \"Ta strona jest kopią roboczą\",\n        \"title\": \"Tytuł\",\n        \"trashed\": \"Wyrzucona do kosza\",\n        \"unselectItem\": \"Odznacz\",\n        \"updatedOn\": \"Zaktualizowano\",\n        \"url\": \"URL\"\n    },\n    \"plugins\": {\n        \"addPluginSuccessMessage\": \"Wtyczka została dodana\",\n        \"deletePlugin\": \"Usuń wtyczkę\",\n        \"getMorePlugins\": \"Znajdź więcej wtyczek\",\n        \"goToPluginsManager\": \"Idź do menadżera wtyczek\",\n        \"installPlugin\": \"Zainstaluj wtyczkę\",\n        \"isIncompatible\": \"Jest niekompatybilna\",\n        \"isIncompatibleTitle\": \"Ta wtyczka wspiera Publii od wersji v.{supportedVersion}, podczas gdy używasz Publii w wersji v.{currentVersion} - sprawdź dostępność aktualizacji Publii\",\n        \"newVersionAvailable\": \"Dostępna jest nowa wersja\",\n        \"plugin\": \"Wtyczka:\",\n        \"plugins\": \"Wtyczki\",\n        \"removePluginMessage\": \"Czy na pewno chcesz usunąć wtyczkę {pluginName}?\",\n        \"removePluginSuccessMessage\": \"Wybrana wtyczka została usunięta\",\n        \"updatedPluginSuccessMessage\": \"Wybrana wtyczka została zaktualizowana\",\n        \"uploadPluginErrorMessage\": \"Wystąpił błąd podczas instalacji. Spróbuj ponownie.\"\n    },\n    \"post\": {\n        \"addNewPost\": \"Dodaj nowy wpis\",\n        \"addPostTitle\": \"Dodaj tytuł wpisu\",\n        \"all\": \"Wszystkie\",\n        \"changePostDate\": \"Zmień datę publikacji\",\n        \"characters\": \"Znaki\",\n        \"configureThemeBeforeGeneratingPreview\": \"Musisz skonfigurować motyw dla tej witryny przed wygenerowaniem podglądu tego wpisu.\",\n        \"convertToPage\": \"Konwertuj na stronę\",\n        \"currentDefaultTemplate\": \"Aktualnie domyślny szablon\",\n        \"customPostTypes\": \"Własne rodzaje wpisów\",\n        \"drafts\": \"Wersje robocze\",\n        \"duplicate\": \"Duplikuj\",\n        \"duplicatePostErrorMessage\": \"Wystąpił błąd podczas duplikowania wybranych wpisów. Proszę spróbować ponownie.\",\n        \"duplicatePostSuccessMessage\": \"Wybrane wpisy zostały zduplikowane\",\n        \"editorBlock\": \"Edytor blokowy\",\n        \"editorBlockInfo\": \"Nowoczesny i intuicyjny edytor wspierający skróty klawiszowe i markdown dla ułatwienia tworzenia treści na blogu bez martwienia się o HTML lub inne elementy kodu.\",\n        \"editorBlockNotSupportedEditPostInfo\": \"Aktualny motyw nie wspiera edytora blokowego użytego w tym wpisie. Ten wpis nie może być poprawnie wyrenderowany do pliku wynikowego.\",\n        \"editorBlockNotSupportedNewPostInfo\": \"Aktualny motyw nie wspiera edytora blokowego, którego chcesz użyć. Ten wpis nie może być poprawnie wyrenderowany do pliku wynikowego.\",\n        \"editorBlockUse\": \"Użyj edytora blokowego\",\n        \"editorMarkdown\": \"Edytor Markdown\",\n        \"editorMarkdownInfo\": \"Ten edytor wspiera składnię Markdown jako skrót do szybszego tworzenia treści; doskonale nadaje się do obszernych, prostych projektów, takich jak dokumentacja.\",\n        \"editorMarkdownUse\": \"Użyj edytora Markdown\",\n        \"editorWYSIWYG\": \"Edytor WYSIWYG\",\n        \"editorWYSIWYGInfo\": \"Ten edytor oferuje znajomy sposób przetwarzania tekstu oraz dodatkowe narzędzia dla użytkowników, którzy chcą mieć pod kontrolą każdy aspekt treści swojej strony.\",\n        \"editorWYSIWYGUse\": \"Użyj edytora WYSIWYG\",\n        \"editPostAnyway\": \"Edytuj wpis mimo to\",\n        \"excluded\": \"Wykluczony\",\n        \"excludeFromHomepage\": \"Wykluczony ze strony głównej\",\n        \"featured\": \"Wyróżniony\",\n        \"filterOrSearchPosts\": \"Filtruj lub wyszukaj wpisy...\",\n        \"hidden\": \"Ukryty\",\n        \"hidePost\": \"Ukryj Wpis\",\n        \"includeInHomepage\": \"Zawrzyj na stronie głównej\",\n        \"markAsDraft\": \"Oznacz jako kopię roboczą\",\n        \"markAsFeatured\": \"Oznacz jako wyróżniony\",\n        \"markAsUnfeatured\": \"Oznacz jako niewyróżniony\",\n        \"min\": \"min\",\n        \"modificationDate\": \"Data modyfikacji\",\n        \"moveToTrash\": \"Przenieś do kosza\",\n        \"noPostsMatchingYourCriteria\": \"Brak wpisów spełniających Twoje kryteria.\",\n        \"openEditorAnyway\": \"Otwórz edytor mimo to\",\n        \"paragraphs\": \"Akapity\",\n        \"post\": \"Wpis\",\n        \"postAuthor\": \"Autor wpisu\",\n        \"postLink\": \"Link do wpisu\",\n        \"postName\": \"Nazwa wpisu:\",\n        \"postPage\": \"Strona wpisu\",\n        \"posts\": \"wpisy\",\n        \"postSettings\": \"Ustawienia wpisów\",\n        \"postSlug\": \"Slug wpisu\",\n        \"postSlugLengthWarning\": \"Slug wpisu dłuższy niż 250 znaków może prowadzić do tworzenia uszkodzonych plików podczas renderowania strony internetowej.\",\n        \"postSlugTooLong\": \"Slug wpisu jest za długi\",\n        \"postSlugUpdateButton\": \"Aktualizuj\",\n        \"postState\": \"Stan wpisu\",\n        \"postStatusChangeSuccessMessage\": \"Status wybranych wpisów został zmieniony\",\n        \"postTemplate\": \"Szablon wpisu\",\n        \"postWillNotAppearOnHomepageListMsg\": \"Wpis nie pojawi się na liście na stronie głównej\",\n        \"postWillNotAppearOnListMsg\": \"Wpis nie pojawi się na żadnych wygenerowanych listach wpisó, takich jak strony ze znacznikami lub autorami\",\n        \"publicationDate\": \"Data publikacji\",\n        \"publish\": \"Opublikuj\",\n        \"published\": \"Opublikowano\",\n        \"readingTime\": \"Czas czytania\",\n        \"removePostMessage\": \"Na pewno chcesz usunąć wybrane wpisy? Tej operacji nie można cofnąć.\",\n        \"removePostSuccessMessage\": \"Wybrane wpisy zostały usunięte\",\n        \"selectPostPage\": \"Wybierz stronę wpisu\",\n        \"sentences\": \"Zdania\",\n        \"setCustomPostDate\": \"Ustaw niestandardową datę publikacji\",\n        \"status\": \"Status\",\n        \"thisPostIsADraft\": \"Ten wpis jest kopią roboczą\",\n        \"thisPostIsExcludedFromHomepage\": \"Ten wpis jest wykluczony ze strony głównej.\",\n        \"thisPostIsFeatured\": \"Ten wpis jest wyróżniony\",\n        \"thisPostIsHidden\": \"Ten wpis jest ukryty\",\n        \"title\": \"Tytuł\",\n        \"trashed\": \"Wyrzucony do kosza\",\n        \"uniqueWords\": \"Unikalne słowa\",\n        \"updatedOn\": \"Zaktualizowano\",\n        \"url\": \"URL\",\n        \"words\": \"Słowa\"\n    },\n    \"publii\": {\n        \"aboutPublii\": \"O Publii\",\n        \"currentPubliiVersion\": \"Wersja\",\n        \"accept\": \"Akceptuj\",\n        \"copyright\": \"Prawa autorskie 2026 <a href=\\\"https://tidycustoms.net\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">TidyCustoms</a>. Wszelkie prawa zastrzeżone.<br>Publii jest zaprojektowane i utrzymywane przez główny zespół i powstało dzięki projektowi Open Source <a href=\\\"https://electronjs.org\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Electron</a> i innemu \",\n        \"creditsIntro\": \"Publii korzysta z następującego oprogramowania Open Source stron trzecich:\",\n        \"dataCollectionInfo\": \"<strong>Nie zbieramy żadnych </strong>danych osobowych podczas korzystania z aplikacji Publii; również nie przechowujemy, nie śledzimy, nie pozwalamy osobom trzecim na zbieranie danych osobowych o Tobie. \",\n        \"homepage\": \"Strona domowa\",\n        \"license\": \"Licencja\",\n        \"licensingInformation\": \"Informacje licencyjne\",\n        \"openSourceSoftware\": \"Oprogramowaniu Open Source\",\n        \"publiiLicenseAgreement\": \"Umowa licencyjna Publii.\",\n        \"publiiLicenseAgreementInfo\": \"To oprogramowanie jest na licencji GNU GPL w wersji 3.<br>Klikając \\\"Akceptuj\\\" godzisz się na\"\n    },\n    \"rendering\": {\n        \"errorDuringPreviewCreatingMsg\": \"Wystąpił błąd podczas tworzenia podglądu.\",\n        \"rendering\": \"Renderowanie...\",\n        \"renderingErrorText\": \"Wystąpił błąd podczas renderowania Twojej witryny.\",\n        \"renderingPleaseWait\": \"Proszę czekać, aż proces renderowania zostanie zakończony.\",\n        \"selectThemeBeforeCreatingPreviewMsg\": \"Musisz wybrać motyw, zanim spróbujesz utworzyć podgląd swojej witryny. Przejdź do ustawień strony i wybierz motyw.\"\n    },\n    \"repeater\": {\n        \"addItem\": \"Dodaj element\",\n        \"duplicateItem\": \"Duplikuj element\",\n        \"emptyState\": \"Kliknij przycisk aby dodać pierwszy element\",\n        \"removeItem\": \"Usuń element\"\n    },\n    \"settings\": {\n        \"addGDPRCookieBanner\": \"Dodaj cookie banner (GDPR)\",\n        \"additionalValidElementsInWYSIWYGEditor\": \"Dodatkowe poprawne elementy w edytorze WYSIWYG\",\n        \"additionalValidElementsInWYSIWYGEditorInfo\": \"Jeśli edytor WYSIWYG usunie jakieś tagi z Twojego kodu HTML tutaj możesz podać dodatkowe elementy dozwolone.<br> Na przykład: <strong>v-select[*],v-dropdown[*]</strong> zezwoli na niestandardowe tagi v-select i v-dropdown z dowolnymi atrybutami.\",\n        \"advancedOptions\": \"Opcje zaawansowane\",\n        \"advancedPreviewDesc\": \"Zaawansowany podglad pozwala na renderowanie strony bez otwierania przeglądarki\",\n        \"alwaysAddIndexHTMLInURLs\": \"Zawsze dodawaj index.html w URLach\",\n        \"alwaysSaveSearchState\": \"Zawsze zapisuj stan wyszukiwania\",\n        \"anchorLink\": \"Link do kotwicy\",\n        \"appSettings\": \"Ustawienia aplikacji\",\n        \"appSettingsSavedMsg\": \"Ustawienia aplikacji zostały pomyślnie zapisane.\",\n        \"appSettingsSaveErrorMsg\": \"Wystąpił błąd podczas zapisywania ustawień aplikacji. Proszę spróbować ponownie.\",\n        \"appUIZoomLevel\": \"Dostosowanie skali interfejsu\",\n        \"appUIZoomLevelInfo\": \"Dostosuj skalę interfejsu użytkownika, aby zwiększyć widoczność i komfort podczas zarządzania treścią. Ta opcja pozwala dostosować rozmiar interfejsu użytkownika do preferowanego rozmiaru, zapewniając, że tekst, ikony i inne elementy UI są łatwo czytelne i dostępne.\",\n        \"ascending\": \"Rosnąco\",\n        \"authorPage\": \"Strona autora\",\n        \"authorPages\": \"Strony autora\",\n        \"authorPageTitleVariables\": \"Następujące zmienne mogą zostać użyte: %authorname, %sitename.\",\n        \"authorPrefix\": \"Prefiks Autora:\",\n        \"authorPrefixInfo\": \"Definiuje przedrostek, który pojawia się przed slugiem autora w adresie URL np. <strong>https://example.com/AUTHORS_PREFIX/author-slug</strong>.<br>\",\n        \"authorPrefixInfoExtended\": \"Definiuje przedrostek, który pojawia się przed slugiem autora w adresie URL np. <strong>https://example.com/POSTS_PREFIX/AUTHORS_PREFIX/author-slug</strong>.<br>\",\n        \"authorsPrefixAfterPostsPrefix\": \"Umieść prefiks autorów po prefiksie wpisów\",\n        \"authorsPrefixCannotBeEmpty\": \"Prefiks autorów nie może być pusty.\",\n        \"authorsPrefixCannotEqualTagsPrefix\": \"Prefiks autorów nie może być taki sam jak prefiks tagów.\",\n        \"backupLocation\": \"Lokalizacja kopii zapasowej\",\n        \"backupLocationChangedConfirmMsg\": \"Zmieniono lokalizację przechowywania kopii zapasowych. Wszystkie nowe kopie zapasowe będą przechowywane w tym katalogu. Jeśli potrzebujesz dostępu do wcześniejszych kopii zapasowych, możesz je ręcznie przenieść do nowego katalogu.\",\n        \"badge\": \"Badge\",\n        \"badgeAndCustomLink\": \"Badge + link niestandardowy\",\n        \"badgeLabel\": \"Etykieta plakietki\",\n        \"bannerPosition\": \"Pozycja bannera\",\n        \"basicSettings\": \"Ustawienia podstawowe\",\n        \"byIDAscending\": \"Według ID rosnąco\",\n        \"byIDDescending\": \"Według ID malejąco\",\n        \"cannotAddIndexHTMLInURLsInfo\": \"Włącz tę opcję jeśli nie możesz domyślnie włączyć ładowania plików index.html kiedy katalog na Twoim serwerze jest otwarty.\",\n        \"cardTypes\": \"Typy kart:\",\n        \"changeSitesLocationWithoutCopyingFiles\": \"Zmień lokalizację stron bez przenoszenia istniejących stron do nowego katalogu\",\n        \"closePostEditorOnSave\": \"Zamknij edytor wpisów po zapisie\",\n        \"codeEditor\": \"Edytor kodu (CodeMirror)\",\n        \"colorTheme\": \"Motyw kolorystyczny\",\n        \"content\": \"Treść\",\n        \"continueSync\": \"Kontynuuj i synchronizuj\",\n        \"continueSyncNoRemoteFiles\": \"Publii nie może znaleźć listy plików na twoim serwerze. Jeśli będziesz kontynuować proces synchronizacji, WSZYSTKIE wygenerowane pliki strony internetowej zostaną przesłane na twój serwer. Możesz anulować proces synchronizacji, aby sprawdzić przyczynę problemu, a następnie spróbować ponownie.\",\n        \"convertToWebp\": \"Konwertuj do WebP\",\n        \"convertToWebpInfo\": \"Włącz tę opcję, jeśli chcesz konwertować wszystkie grafiki w formatach JPG, JPEG oraz PNG do formatu WebP. Po włączeniu tej opcji konieczna jest ponowna regeneracja miniaturek. Opcja ta zadziała tylko gdy używasz biblioteki Sharp do tworzenia miniaturek.\",\n        \"convertToWebpJimpWarning\": \"Ta opcja nie działa z obecnie używanym silnikiem tworzenia miniaturek - Jimp. Proszę użyć silnika Sharp aby móc używać tej opcji.\",\n        \"cookieGroups\": \"Grupy plików cookies\",\n        \"cookieBanner\": \"Banner cookies\",\n        \"cookieBasic\": \"Podstawowy\",\n        \"cookieBasicDescription\": \"Ta sekcja zawiera ustawienia odpowiedzialne za wyświetlanie się banera plików cookie, który służy do poinformowania odwiedzających twoją witrynę o używaniu cookies.\",\n        \"cookieAdvanced\": \"Zaawansowany\",\n        \"cookieAdvancedDescription\": \"Ta sekcja zawiera opcje wyświetlania wyskakującego okienka z zaawansowanymi ustawieniami plików cookie, w którym użytkownik może zarządzać zgodami dla określonej grupy plików cookie.\",\n        \"createXMLSitemap\": \"Stwórz XML mapy strony\",\n        \"currentTheme\": \"Aktualny motyw:\",\n        \"currentThemeHasOverrides\": \"Wygląda na to, że nadpisujesz pliki motywu, a Twoje modyfikacje mogą nie być w pełni zgodne z jego nową wersją. Aktualizacja motywu wymaga przejrzenia i scalenia zawartości tych plików. Kliknij 'OK', aby kontynuować aktualizację lub 'Anuluj', aby najpierw przejrzeć swoje modyfikacje.\",\n        \"customElementsInWYSIWYGEditor\": \"Niestandardowe elementy dostępne w edytorze WYSIWYG\",\n        \"customElementsInWYSIWYGEditorInfo\": \"Jeśli edytor WYSIWYG automatycznie usuwa niektóre tagi z Twojego kodu HTML, możesz dodać je tutaj jako niestandardowe elementy.<br> Na przykład: <strong>v-select,v-dropdown</strong>\",\n        \"customFeedTitle\": \"Własny tytuł kanału\",\n        \"customLanguageCode\": \"Niestandardowy kod języka:\",\n        \"darkMode\": \"Tryb ciemny\",\n        \"defaultAuthorsOrdering\": \"Domyślna kolejność autorów:\",\n        \"defaultOrderingOnLists\": \"Domyślna kolejność na listach\",\n        \"defaultPagesOrdering\": \"Domyślna kolejność stron:\",\n        \"defaultPostsOrdering\": \"Domyślna kolejność wpisów:\",\n        \"defaultTagsOrdering\": \"Domyślna kolejność tagów:\",\n        \"descending\": \"Malejąco\",\n        \"disableAuthorsPagination\": \"Wyłącz stronicowanie autorów\",\n        \"disableAuthorsPaginationIndexing\": \"Wyłącz indeksowanie stronicowania autorów\",\n        \"disableAuthorsPaginationIndexingInfo\": \"Jeśli ta opcja jest włączona, pliki paginacji stron autorów zostaną wykluczone z mapy witryny i otrzymają <strong>noindex, follow</strong> metatag robotów.\",\n        \"disableAuthorsPaginationInfo\": \"Jeśli ta opcja jest włączona, paginacja autorów nie zostanie wygenerowana.\",\n        \"disableHomepagePagination\": \"Wyłącz stronicowanie strony głównej\",\n        \"disableHomepagePaginationInfo\": \"Jeśli ta opcja jest włączona, paginacja strony głównej nie zostanie wygenerowana.\",\n        \"disableHomepagePaginationIndexing\": \"Wyłącz indeksowanie paginacji strony głównej\",\n        \"disableTagsPagination\": \"Wyłącz stronicowanie tagów\",\n        \"disableTagsPaginationIndexing\": \"Wyłącz indeksowanie stronicowania tagów\",\n        \"disableTagsPaginationIndexingInfo\": \"Jeśli ta opcja jest włączona pliki z Twoimi tagami paginacji zostaną wykluczone z mapy witryny i otrzymają <strong>noindex, follow</strong> metatag robotów.\",\n        \"disableTagsPaginationInfo\": \"Jeśli ta opcja jest włączona, paginacja tagów nie zostanie wygenerowana.\",\n        \"displayEmptyAuthors\": \"Pokazuj autorów bez wpisów\",\n        \"displayEmptyAuthorsInfo\": \"Jeśli ta opcja jest włączona, autorzy bez przypisanych do nich wpisów nadal będą mieć utworzone swoje podstrony i pojawiać się na liście autorów.\",\n        \"displayEmptyTags\": \"Wyświetlaj tagi bez wpisów\",\n        \"displayEmptyTagsInfo\": \"Jeśli ta opcja jest włączona tagi bez przypisanych do nich wpisów nadal będa miały utworzone swoje podstrony i pokażą się na liście tagów.\",\n        \"dontIndexThisPage\": \"Poproś wyszukiwarkę, aby nie indeksowała całej witryny.\",\n        \"editorFontFamily\": \"Krój pisma\",\n        \"editorFontFamilySansSerif\": \"Bezszeryfowy\",\n        \"editorFontFamilySerif\": \"Szeryfowy\",\n        \"editorFontSize\": \"Rozmiar tekstu (px)\",\n        \"editors\": \"Edytory\",\n        \"embedConsents\": \"Zgody na osadzane treści\",\n        \"embedConsentsDescription\": \"Ta sekcja umożliwia blokowanie treści ładowanych w ramkach iframe od zewnętrznych dostawców treści, którzy mogą ustawiać pliki cookie. Podając domenę adresu URL iframe np. <strong>youtube.com</strong>, wiadomość, przycisk akceptacji i przypisujac do istniejącej grupy plików cookie, ramka iframe zostanie zastąpiona bannerem informującym odwiedzających, że treść jest zablokowana z powodu ich ustawień zgody (odwiedzający nie wyraził zgodę na rodzaje plików cookie używanych przez element iframe w banerze plików cookie).\",\n        \"embedVideos\": \"Osadzanie video\",\n        \"enableAdvancedPreview\": \"Włącz zaawansowany podgląd\",\n        \"enableAutoIndent\": \"Włącz automatyczne wcięcia\",\n        \"enableCSSCompression\": \"Włącz kompresję CSS\",\n        \"enableHTMLCompression\": \"Włącz kompresję HTML\",\n        \"enableJSONFeed\": \"Włącz kanał JSON\",\n        \"enableMediaLazyLoad\": \"Włącz lazy load dla mediów\",\n        \"enableMediaLazyLoadInfo\": \"Włącz tę opcję, jeśli chcesz używać natywnego lazy loading, które wolno ładuje obrazy, filmy i ramki iframe.\",\n        \"enableResponsiveImages\": \"Włącz responsywne obrazy\",\n        \"enableResponsiveImagesInfo\": \"Włącz tę opcję, jeśli chcesz dostarczać obrazy o różnych rozmiarach przy różnych rozdzielczościach ekranu w zależności od punktów przerwania zdefiniowanych w pliku config.json w folderze motywu.\",\n        \"enableRSSFeed\": \"Włącz kanał RSS\",\n        \"enableSharingButtons\": \"Włącz przyciski udostępniania\",\n        \"enableSpellchecker\": \"Włącz sprawdzanie pisowni\",\n        \"errorPage\": \"Strona błędu:\",\n        \"errorPageFilenameCannotBeEmpty\": \"Nazwa pliku strony błędu nie może być pusta.\",\n        \"errorPageFilenameCannotEqualSearchPageFilename\": \"Nazwa pliku strony błędu nie może być taka sama jak nazwa pliku strony wyszukiwania.\",\n        \"errorPageTitleVariables\": \"Następujące zmienne mogą zostać użyte: %sitename.\",\n        \"excludedFiles\": \"Wykluczone pliki\",\n        \"excludeFeaturedPosts\": \"Wyklucz wyróżnione wpisy\",\n        \"excludeFilesFromSitemapInfo\": \"Wpisz rozdzieloną przecinkami listę plików HTML lub katalogów, które mają być wykluczone z mapy witryny.<br>Na przykład: <strong>avoid-this-file.html,avoid-this-catalog-too</strong>.\",\n        \"experimentalFeaturesWarning\": \"Zawsze szukamy sposobów na ulepszenie naszej aplikacji. Ta sekcja zawiera eksperymentalne funkcje zaprojektowane w celu rozszerzenia funkcjonalności Publii. Nie możemy zagwarantować ich poprawnego działania; aktywujesz je na własne ryzyko.\",\n        \"experimentalFeatureAppAutoBeautifySourceCode\": \"Automatycznie upiększaj kod źródłowy w edytorze WYSIWYG\",\n        \"experimentalFeatureAppAutoBeautifySourceCodeDesc\": \"Ta funkcja włącza automatyczne upiększanie kodu w edytorze kodu źródłowego edytora WYSIWYG\",\n        \"experimentalFeatureAppFtpAlt\": \"Używaj alternatywnej biblioteki FTP do uploadu strony\",\n        \"experimentalFeatureAppFtpAltDesc\": \"Użyj tej opcji, szczególnie jeśli Twój hosting używa IPv6 do adresacji serwerów\",\n        \"experimentalFileManagerInSidebar\": \"Pokaż menadżer plików w pasku bocznym\",\n        \"externalImages\": \"Zewnętrzne obrazy\",\n        \"externalPage\": \"Strona zewnętrzna\",\n        \"facebook\": \"Facebook\",\n        \"facebookAppID\": \"ID Aplikacji z Facebook\",\n        \"facebookAppIDInfo\": \"Przeczytaj jak uzyskać <a href=\\\"https://developers.facebook.com/docs/apps/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Facebook App ID</a>.\",\n        \"fallbackImage\": \"Obraz zastępczy\",\n        \"fallbackLogoImage\": \"Zastępczy obraz logo\",\n        \"fallbackLogoImageInfo\": \"Logo musi mieścić się w rozmiarze 60 &times; 600 pixeli.\",\n        \"featuredPostsOrderBy\": \"Sortuj wyróżnione wpisy po:\",\n        \"featuredPostsOrdering\": \"Sortowanie wyróżnionych wpisów:\",\n        \"feedsRelativeUrlsInfo\": \"Aby wyświetlić ustawienia kanałów RSS/JSON, wyłącz opcję 'Używaj relatywnych URLi' w ustawieniach serwera; w przeciwnym wypadku kanał nie zostanie wygenerowany\",\n        \"feedTitle\": \"Tytuł kanału:\",\n        \"feedUpdatedDateType\": \"Data aktualizacji wpisu ustawiana jako:\",\n        \"filesLocation\": \"Lokalizacja plików\",\n        \"footerText\": \"Tekst w stopce\",\n        \"frontpage\": \"Pierwsza strona\",\n        \"frontpagePageCannotBeEmpty\": \"Wybór strony głównej nie może być pusty.\",\n        \"gConsentModeDefaultState\": \"Stan domyślny\",\n        \"gConsentModeEnabled\": \"Włącz Google Consent Mode\",\n        \"gConsentMode\": {\n            \"addGroup\": \"Dodaj ustawienia grupy cookies\",\n            \"cookieGroup\": \"Grupa cookies\",\n            \"description\": \"Ta sekcja pozwala na określenie domyślnego stanu Google Consent Mode oraz określenie sygnałów, które będa wysyłane w momencie gdy konkretna grupa plików cookies zostanie zaakceptowana przez użytkownika. Użyj tych opcji jeśli potrzebujesz kompatybilności z Google Consent Mode. Jeśli nie używasz Google Analytics, Google Ads lub podobnych narzędzi, najprawdopodobniej nie potrzebujesz włączać tych funkcji.\",\n            \"title\": \"Google Consent Mode\"\n        },\n        \"GDPR\": \"Ustawienia prywatności\",\n        \"gdprBannerPosition\": {\n            \"centered\": \"Wycentrowany\",\n            \"left\": \"Z lewej\",\n            \"right\": \"Z prawej\",\n            \"bar\": \"Pasek\"\n        },\n        \"gdprAllowAdvancedConfiguration\": \"Włącz zaawansowaną konfigurację plików cookies\",\n        \"gdprAdvancedConfigurationLinkLabel\": \"Etykieta przycisku zaawansowanej konfiguracji\",\n        \"gdprAdvancedConfigurationLinkPlaceholder\": \"Zarządzaj preferencjami\",\n        \"gdprAdvancedConfigurationAcceptButtonLabel\": \"Etykieta przycisku akceptacji\",\n        \"gdprAdvancedConfigurationRejectButtonLabel\": \"Etykieta przycisku odrzucenia\",\n        \"gdprAdvancedConfigurationSaveButtonLabel\": \"Etykieta przycisku zapisu\",\n        \"gdprAdvancedConfigurationTitle\": \"Tytuł w popupie\",\n        \"gdprAdvancedConfigurationDescription\": \"Opis w popupie\",\n        \"gdprAdvancedConfigurationPrivacyLink\": \"Link do polityki prywatności\",\n        \"gdprAdvancedConfigurationPrivacyLinkDescription\":\"Wyświetl link do strony polityki prywatności w popupie. Jeśli opcja 'Pokaż link do polityki prywatności' w sekcji 'Podstawowy' jest wyłączona, link również nie zostanie wyświetlony w popupie.\",\n        \"gdprBehaviourInfo\": \"Pamiętaj o umieszczeniu linku z powyższą kotwicą w nawigacji Twojej strony lub w treści strony (np. <a href=\\\"#cookie-settings\\\">Ustawienia plików cookies</a>). W przeciwnym razie użytkownicy Twojej witryny mogą nie mieć możliwości zmiany swoich indywidualnych preferencji dotyczących plików cookie.\",\n        \"gdprCookieSettingsRevision\": \"Wersja ustawień\",\n        \"gdprCookieSettingsTTL\": \"Przechowuj ustawienia użytkownika przez X dni\",\n        \"gdprDebugMode\": \"Włącz tryb debugowania\",\n        \"gdprShowRejectButton\": \"Pokaż przycisk odrzucenia\",\n        \"gdprRejectButtonLabel\": \"Etykieta przycisku odrzucenia\",\n        \"gdprBannerTitle\": \"Tytuł banera\",\n        \"gdprBannerMessage\": \"Opis w banerze\",\n        \"generateOpenGraphTags\": \"Generuj tagi Open Graph\",\n        \"generateTwitterCards\": \"Generuj karty Twitter'a\",\n        \"googleAnalyticsTrackingID\": \"ID Google Analytics Tracking\",\n        \"hiddenPostsOrderBy\": \"Sortuj ukryte wpisy po:\",\n        \"hiddenPostsOrdering\": \"Sortowanie ukrytych wpisów:\",\n        \"hideCustomExcerptsOnPostPages\": \"Ukryj własne zajawki na stronach wpisów\",\n        \"hideCustomExcerptsOnPostPagesInfo\": \"Jeśli ta opcja jest włączona strony wpisów nie będą pokazywać tekstu umieszczonego nad elementem \\\"Czytaj więcej\\\" w edytorze wpisu.\",\n        \"hideCustomExcerptsOnPagePages\": \"Ukryj własne zajawki na stronach\",\n        \"hideCustomExcerptsOnPagePagesInfo\": \"Jeśli ta opcja jest włączona strony nie będą pokazywać tekstu umieszczonego nad elementem \\\"Czytaj więcej\\\" w edytorze strony.\",\n        \"homepage\": \"Strona główna\",\n        \"homepageNoIndexPagination\": \"Jeśli ta opcja jest włączona, pliki paginacji strony głównej zostaną wykluczone z mapy witryny i otrzymają <strong>noindex, follow</strong> metatag robotów.\",\n        \"homepagePagination\": \"Paginacja strony głównej\",\n        \"howToPrepareYourThemeForGDPRInfo\": \"Włączenie tej opcji spowoduje wyświetlenie banera plików cookie w Twojej witrynie. Aby uzyskać więcej informacji na temat prawidłowej konfiguracji banera zapoznaj się z następującym artykułem <a href=\\\"https://getpublii.com/docs/gdpr-cookie-banner-configuration.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">GDPR Cookie Banner Configuration</a> \",\n        \"howYtNoCookiesWorks\": \"Po włączeniu tej opcji, domena adresu URL umieszczonego filmu 'www.youtube.com' zostanie automatycznie zmieniona na 'www.youtube-nocookie.com', aby uniemożliwić personalizowanie sposobu przeglądania YouTube ani w tym odtwarzaczu, ani w samym serwisie, a także zapewni, że żadne dane nie są wykorzystywane do dostosowywania reklam wyświetlanych widzom poza Twoją witryną. Dowiedz się więcej o rozszerzonym trybie prywatności odwiedzając <a href=\\\"https://support.google.com/youtube/answer/171780?hl=en#zippy=%2Cturn-on-privacy-enhanced-mode\\\">stronę pomocy YouTube.</a>.\",\n        \"howVimeoNoTrackWorks\": \"Po włączeniu tej opcji, dodatkowy parametr dnt=1 zostanie dodany do adresu URL umieszczonego odtwarzacza Vimeo (player.vimeo.com/video), aby uniemożliwić śledzenie jakichkolwiek danych sesji, w tym wszystkich plików cookie i danych analitycznych. Dowiedz się więcej o parametrach odtwarzacza Vimeo odwiedzając <a href=\\\"https://vimeo.zendesk.com/hc/en-us/articles/360001494447-Player-parameters-overview\\\">stronę pomocy Vimeo</a>.\",\n        \"imageResizeEngineInfo\": \"Silnik zmiany rozmiaru obrazów The Sharp jest znacznie szybszy niż Jimp, ale może powodować problemy z niektórymi obrazami. Jeśli napotkasz problemy podczas tworzenia lub ponownego generowania miniatur, spróbuj przełączyć się na mechanizm zmiany rozmiaru Jimp. Jeśli chcesz używać obrazów WebP, musisz użyć mechanizmu zmiany rozmiaru Sharp.\",\n        \"imagesResizeEngine\": \"Silnik zmiany rozmiaru obrazów:\",\n        \"imageResizeEngineWarning\": \"Jeśli używany jest silnik Jimp, opcja wykorzystania grafik WebP nie będzie działać poprawnie. Upewnij się, że wszystkie Twoje strony mają opcję 'Szybkość strony / Konwertuj do WebP' wyłączoną i wygeneruj miniatury ponownie aby zapewnić poprawne wyświetlanie grafik.\",\n        \"indentSize\": \"Rozmiar wcięć (spacje)\",\n        \"internalPage\": \"Strona wewnętrzna\",\n        \"leaveBlankForDefaultBackupsDir\": \"Pozostaw puste, aby użyć domyślnego katalogu kopii zapasowych\",\n        \"leaveBlankForDefaultPreviewDirectory\": \"Pozostaw puste, aby użyć domyślnego katalogu dla podglądów\",\n        \"leaveBlankToUseDefaultPageTitle\": \"Pozostaw puste aby użyć domyślnego tytułu strony.\",\n        \"leaveBlankToUseDefaultSitesDir\": \"Pozostaw puste, aby użyć domyślnego katalogu witryn\",\n        \"lightMode\": \"Tryb jasny\",\n        \"linkedIn\": \"LinkedIn\",\n        \"linkLabel\": \"Etykieta linku\",\n        \"loadAtStart\": \"Załaduj na początku:\",\n        \"metaDescription\": \"Opis Meta:\",\n        \"metaRobots\": \"Meta Roboty:\",\n        \"noIndexForChatGPTBot\": \"Blokuj bota GPTBot\",\n        \"noIndexForChatGPTUser\": \"Blokuj bota ChatGPT-User\",\n        \"noIndexForChatGPTBotInfo\": \"Gdy ta opcja jest włączona, zapobiega indeksowaniu zawartości witryny przez bota GPTBot (<a href=\\\"https://platform.openai.com/docs/gptbot\\\" target=\\\"_blank\\\">używanego przez ChatGPT do indeksowania treści</a>). Pamiętaj, że zadziała tylko wtedy, gdy nie używasz własnego pliku robots.txt.\",\n        \"noIndexForChatGPTUserInfo\": \"Gdy ta opcja jest włączona, zapobiega skanowaniu zawartości witryny przez bota ChatGPT-User (<a href=\\\"https://platform.openai.com/docs/plugins/bot\\\" target=\\\"_blank\\\">używanego przez wtyczki ChatGPT</a>). Pamiętaj, że zadziała tylko wtedy, gdy nie używasz własnego pliku robots.txt.\",\n        \"noIndexForCommonCrawlBots\": \"Blokuj boty Common Crawl\",\n        \"noIndexForCommonCrawlBotsInfo\": \"Gdy ta opcja jest włączona, zapobiega indeksowaniu zawartości witryny przez boty Common Crawl. Pamiętaj, że zadziała tylko wtedy, gdy nie używasz własnego pliku robots.txt.\",\n        \"noIndexWebsite\": \"Nieindeksowanie strony\",\n        \"notSelected\": \"Nie wybrano\",\n        \"numberOfPostsInFeed\": \"Liczba wpisów w kanale\",\n        \"openDevtoolsInMainW\": \"Automatycznie otwórz DevTools w oknie głównym\",\n        \"openGraph\": \"Open Graph\",\n        \"openLastUsedWebsite\": \"Otwórz ostatnio używaną witrynę\",\n        \"openPopupWindowBy\": \"Otwórz banner w\",\n        \"optionsForDevelopers\": \"Opcje dla programistów\",\n        \"optionsForEditors\": \"Options for editors\",\n        \"optionsForExperimentalFeatures\": \"Funkcje eksperymentalne\",\n        \"ordering\": {\n            \"authorASC\": \"Po autorze (rosnąco)\",\n            \"authorDESC\": \"Po autorze (malejąco)\",\n            \"createdASC\": \"Po dacie utworzenia (rosnąco)\",\n            \"createdDESC\": \"Po dacie utworzenia (malejąco)\",\n            \"hierarchical\": \"Hierarchicznie\",\n            \"idASC\": \"Po ID (rosnąco)\",\n            \"idDESC\": \"Po ID (malejąco)\",\n            \"modifiedASC\": \"Po dacie modyfikacji (rosnąco)\",\n            \"modifiedDESC\": \"Po dacie modyfikacji (malejąco)\",\n            \"nameASC\": \"Po nazwie (rosnąco)\",\n            \"nameDESC\": \"Po nazwie (malejąco)\",\n            \"postsCounterASC\": \"Po liczbie wpisów (rosnąco)\",\n            \"postsCounterDESC\": \"Po liczbie wpisów (malejąco)\",\n            \"titleASC\": \"Po tytule (rosnąco)\",\n            \"titleDESC\": \"Po tytule (malejąco)\"\n        },\n        \"pageAsFrontpage\": \"Wybierz podstronę\",\n        \"pageTitle\": \"Tytuł Strony\",\n        \"pageTitleVariables\": \"Następujące zmienne mogą zostać użyte: %pagetitle, %sitename, %authorname\",\n        \"paginationPhrase\": \"Fraza paginacji:\",\n        \"paginationPhraseCannotBeEmpty\": \"Fraza paginacji nie może być pusta.\",\n        \"paginationPhraseInfo\": \"Definiuje frazę używaną przed numerem strony w adresie URL np. <strong>https://example.com/tags/tag-slug/page/2</strong>.<br>\",\n        \"password\": {\n            \"alwaysAskForPassword\": \"Zawsze proś o hasło\",\n            \"password\": \"Hasło\",\n            \"passwordFieldCantBeEmpty\": \"Pole hasła nie może być puste.\"\n        },\n        \"pinterest\": \"Pinterest\",\n        \"postCreationDate\": \"Data utworzenia wpisu\",\n        \"postID\": \"ID wpisu\",\n        \"postModificationDate\": \"Data modyfikacji wpisu\",\n        \"postPageTitleVariables\": \"W tytule strony wpisu można użyć następujących zmiennych: %posttitle, %sitename, %authorname.\",\n        \"postsListing\": \"Lista wpisów\",\n        \"postsIndex\": \"Indeks wpisów\",\n        \"postsOrderBy\": \"Sortuj wpisy po:\",\n        \"postsOrdering\": \"Sortowanie wpisów:\",\n        \"postPrefixInfo\": \"Wprowadzony tutaj prefix zostanie dodany przed slugiem wpisu w URL. Na przykład: https://example.com/PREFIKS_POSTA/post-slug. Ta opcja jest konieczna, jeśli ustawisz <a href=\\\"#\\\" data-internal-link=\\\"SEO\\\">podstronę jako stronę główną</a> i potrzebujesz stronicowania wpisów. W tym przypadku, stronicowanie wpisów będzie dostępne pod adresem https://example.com/PREFIKS_POSTA/page/X. Dodatkowo, tagi będą renderowane pod adresem https://example.com/PREFIKS_POSTA/PREFIKS_TAGA/tag-slug.\",\n        \"postsPrefix\": \"Prefiks postów\",\n        \"postTitle\": \"Tytuł wpisu\",\n        \"previewLocation\": \"Lokalizacja podglądów\",\n        \"previewLocationChangedConfirmMsg\": \"Miejsce przechowywania podglądu zostało zmienione. Pamiętaj, że istniejące pliki w tym folderze zostaną usunięte po utworzeniu podglądu Twojej witryny.\",\n        \"privacyPolicyLinkLabel\": \"Etykieta linka polityki prywatności\",\n        \"privacyPolicyPage\": \"Wybierz stronę\",\n        \"privacyPolicyPageURL\": \"Wpisz adres URL strony\",\n        \"privacyPolicyURL\": \"Źródło linka polityki prywatności\",\n        \"provideFacebookAppID\": \"Proszę podać <a href=\\\"https://developers.facebook.com/docs/apps/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Facebook App ID</a>\",\n        \"random\": \"Losowe\",\n        \"readAboutOurRecommendedServerSettings\": \"Dowiedz się więcej o zalecanych konfiguracjach serwerów odwiedzając oficjalną stronę <a href=\\\"https://getpublii.com/docs/recommended-server-settings.html\\\" target=\\\"_blank\\\">dokumentacji</a>.\",\n        \"relatedPostsOptions\": \"Opcje powiązanych wpisów\",\n        \"relatedPostsOptionsInfo\": \"Po włączeniu wpisy powiązane będą pobierane ze wszystkich tagów. Po wyłączeniu wpisy powiązane będą generowane tylko na podstawie tych samych tagów, co bieżący wpis.\",\n        \"relatedPostsOrdering\": \"Sortowanie powiązanych wpisów:\",\n        \"relatedPostsSelectionMechanism\": \"Mechanizm wyboru powiązanych wpisów:\",\n        \"removeHTMLComments\": \"Usuń komentarze HTML\",\n        \"responsiveImagesQuality\": \"Jakość responsywnych obrazów\",\n        \"responsiveImagesAlphaQuality\": \"Jakość alpha responsywnych obrazów (tylko WebP)\",\n        \"RSSJSONFeed\": \"Kanał RSS/JSON\",\n        \"saveButtonLabel\": \"Etykieta przycisku zapisu\",\n        \"saveSearchStateInfo\": \"Po włączeniu Publii zapisze bieżące wyniki wyszukiwania, nawet podczas tworzenia nowego wpisu, umożliwiając powrót do listy. Domyślnie Publii zapisuje wyniki wyszukiwania tylko podczas otwierania wpisu do edycji.\",\n        \"requiresRestartingApp\": \"Wymaga ponowengo uruchomienia aplikacji.\",\n        \"saveSettings\": \"Zapisz Ustawienia\",\n        \"searchPage\": \"Strona wyszukiwania:\",\n        \"searchPageFilenameCannotBeEmpty\": \"Nazwa pliku strony wyszukiwania nie może być pusta.\",\n        \"searchPageTitleVariables\": \"Następujące zmienne mogą zostać użyte: %sitename.\",\n        \"selectedDirInvalid\": \"Wybrana ścieżka nie istnieje.\",\n        \"selectPage\": \"Wybierz stronę\",\n        \"showFeaturedImage\": \"Pokaż wyróżniony obraz\",\n        \"showFeaturedImageInfo\": \"Wyświetl w kanale obraz wyróżniony wpisu.\",\n        \"showFullText\": \"Pokaż pełen tekst\",\n        \"showFullTextInFeedInfo\": \"Wyświetl w kanale pełny tekst wpisu.\",\n        \"showModificationDate\": \"Pokaż datę modyfikacji\",\n        \"showModificationDateAsColumn\": \"Pokaż datę modyfikacji jako kolumnę\",\n        \"showOnlyFeaturedPosts\": \"Pokaż tylko wyróżnione wpisy\",\n        \"showPostSlugsOnTheListing\": \"Pokaż slug na liście wpisów/stron\",\n        \"showPostTagsOnTheListing\": \"Pokaż tagi wpisu na liście\",\n        \"showPrivacyPolicyLink\": \"Pokaż link do polityki prywatności\",\n        \"sitemap\": \"Mapa witryny\",\n        \"sitemapLinkInfo\": \"Mapę XML można znaleźć tutaj: <a href=\\\"{sitemapLink}\\\" class=\\\"sitemap-external-link\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">sitemap.xml</a>\",\n        \"sitemapRelativeUrlsInfo\": \"Aby wyświetlić ustawienia mapy witryny, wyłącz opcję 'Używaj relatywnych URLi' w ustawieniach serwera; w przeciwnym wypadku mapa witryny nie zostanie wygenerowana\",\n        \"siteSettings\": \"Ustawienia strony\",\n        \"siteSettingsSaveDuplicateNameErrorMessage\": \"Podana nazwa witryny jest używana. Podaj inną nazwę.\",\n        \"siteSettingsSaveEmptyNameErrorMessage\": \"Nazwa witryny nie może być pusta. Podaj inną nazwę.\",\n        \"siteSettingsSaveNoKeyringErrorMessage\": \"Publii nie możne zapisać ustawień z powodu problemu z oprogramowaniem do bezpiecznego przechowywania haseł. Uruchom ponownie aplikację i spróbuj jeszcze raz. Jeśli problem będzie się powtarzał, zgłoś go naszemu zespołowi za pośrednictwem forum.\",\n        \"siteSettingsSaveNoKeyringErrorMessageLinux\": \"Publii nie możne zapisać ustawień, ponieważ nie jest zainstalowane żadne oprogramowanie do bezpiecznego przechowywania haseł. Postępuj zgodnie z instrukcjami instalacji dla Node Keytar via https://github.com/atom/node-keytar/ i spróbuj ponownie. Prawdopodobnie w Twoim systemie brakuje paczek libsecret-1-dev i gnome-keyring.\",\n        \"siteSettingsSaveSiteNotExistsErrorMessage\": \" nie istnieje. Uruchom ponownie aplikację.\",\n        \"siteSettingsSaveSuccessMessage\": \"Ustawienia witryny zostały pomyślnie zapisane.\",\n        \"sitesLocation\": \"Lokalizacja witryn\",\n        \"sitesLocationChangedConfirmMsg\": \"Zmieniono lokalizację witryn. Nowy folder docelowy zostanie wyczyszczony przed przeniesieniem tam plików witryn. Czy chcesz kontynuować?\",\n        \"spellcheckerDoesNotSupportLanguage\": \"Sprawdznie pisowni nie wspiera wybranego języka strony. Zalecamy wyłączenie tej funkcji.\",\n        \"system\": \"System\",\n        \"systemColors\": \"Użyj kolorów systemowych\",\n        \"tagPages\": \"Strony taga\",\n        \"tagPageTitleVariables\": \"Następujące zmienne mogą zostać użyte: %tagname, %sitename.\",\n        \"tagPrefix\": \"Prefiks Taga:\",\n        \"tagPrefixInfo\": \"Wprowadzone tutaj prefiksy zostaną dodane przed umieszczeniem tagu w adresie URL np. <strong>https://example.com/TAGS_PREFIX/tag-slug</strong>.<br>Ten prefiks jest również używany do generowania strony z listą tagów (jeśli jest obsługiwana w Twoim motywie) jako <strong>https://example.com/TAGS_PREFIX/index.html</strong>.\",\n        \"tagPrefixInfoExtended\": \"Wprowadzone tutaj prefiksy zostaną dodane przed umieszczeniem tagu w adresie URL np. <strong>https://example.com/POSTS_PREFIX/TAGS_PREFIX/tag-slug</strong>.<br>Ten prefiks jest również używany do generowania strony z listą tagów (jeśli jest obsługiwana w Twoim motywie) jako <strong>https://example.com/POSTS_PREFIX/TAGS_PREFIX/index.html</strong>.\",\n        \"tagsListPage\": \"Strona listy tagów\",\n        \"tagsListPageTitleVariables\": \"Następujące zmienne mogą zostać użyte: %sitename.\",\n        \"tagsPrefixAfterPostsPrefix\": \"Umieść prefix tagów po prefiksie wpisów\",\n        \"tagsPrefixCannotBeEmpty\": \"Prefiks tagów nie może być pusty, jeśli włączone są ładne adresy URL.\",\n        \"themeDoesNotHaveSupportedFeaturesList\": \"<p>Plik Twojego motywu <strong>config.json</strong> nie zawiera sekcji <strong>supportedFeatures</strong>. Proszę zaktualizować lub zmodyfikować motyw aby uzyskać dokładny komunikat o funkcjach, które nie są wspierane przez aktualnie używany motyw. <a href=\\\"https://getpublii.com/dev/theme-supported-features\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">Czytaj więcej o wspieranych funkcjach</a>.</p>\",\n        \"themeDoesNotSupport404ErrorPage\": \"Twój motyw nie obsługuje strony błędu 404.\",\n        \"themeDoesNotSupportAuthorPages\": \"Twój motyw nie obsługuje stron autorów.\",\n        \"themeDoesNotSupportErrorPages\": \"Twój motyw nie obsługuje stron błędów.\",\n        \"themeDoesNotSupportSearchPages\": \"Twój motyw nie obsługuje stron wyszukiwania.\",\n        \"themeDoesNotSupportTagPages\": \"Twój motyw nie obsługuje stron tagów.\",\n        \"themeDoesNotSupportTagsListPage\": \"Twój motyw nie wspiera stron listy tagów.\",\n        \"themeDoesNotSupportEmbedConsents\": \"Twój motyw nie wspiera zgód dla osadzanych treści.\",\n        \"themePrimaryColor\": \"Kolor przewodni motywu\",\n        \"timeFormat\": \"Format czasu:\",\n        \"toViewSitemapEnableIndexingInfo\": \"Aby wyświetlić ustawienia mapy witryny, wyłącz opcję 'Poproś wyszukiwarki, aby nie indeksowały tej witryny' w zakładce Ustawienia SEO; w przeciwnym wypadku mapa witryny nie zostanie wygenerowana.\",\n        \"tumblr\": \"Tumblr\",\n        \"twitter\": \"Twitter\",\n        \"twitterCards\": \"Karty Twitter'a\",\n        \"twitterUsername\": \"Nazwa użytkownika na Twitter\",\n        \"twitterSummaryCard\": \"Karta podsumowania\",\n        \"twitterSummaryCardLargeImage\": \"Karta podsumowania z dużym obrazem\",\n        \"urls\": \"URLe\",\n        \"useAsTitlePageTitle\": \"Użyj tytułu strony jako tytułu\",\n        \"useAsTitlePageTitleInfo\": \"Kiedy ta opcja jest włączona, metatagi og:title i twitter:title będą zawierały tytuł strony zamiast tytułu wpisu, nazwy taga lub nazwy autora.\",\n        \"useGlobalConfiguration\": \"Użyj konfiguracji globalnej\",\n        \"useOnlyTags\": \"Używaj tylko tagów\",\n        \"useOnlyTitles\": \"Używaj tylko tytułów\",\n        \"usePrettyURLs\": \"Użyj ładnych URLi\",\n        \"usePrettyURLsInfo\": \"Jeśli ta opcja jest włączona URLe Twoich wspisów nie będą zawierały przyrostka np. URL <strong>https://example.com/post.html</strong> zostanie zamieniony na <strong>https://example.com/post/</strong>. Dla podstron hierarchia nie będzie uwzględniana w URL, jeśli ta opcja jest wyłączona.\",\n        \"usePageAsFrontpage\": \"Ustaw podstronę jako stronę główną\",\n        \"usePageAsFrontpageNotice\": \"Włączenie tej opcji spowoduje, że strona główna będzie wyświetlać zawartość wybranej podstrony. Wszystkie wpisy na stronie głównej zostaną zastąpione zawartością tej podstrony. Należy pamiętać, że pozostawienie pola <a href=\\\"#\\\" data-internal-link=\\\"URLe\\\">Prefiks postów</a> pustym spowoduje wyłączenie stronicowania wpisów.\",\n        \"useSiteGlobalSettings\": \"Użyj globalnych ustawień strony\",\n        \"useTitlesAndTags\": \"Używaj tytułów i tagów\",\n        \"useWideScrollbars\": \"Użyj szerszych pasków przewijania\",\n        \"versionParameter\": \"Parametr wersji\",\n        \"versionParameterInfo\": \"Dodaj parametr wersji w adresach URL plików CSS/JS, aby pominąć pamięć podręczną przeglądarki. Ta opcja może spowodować, że podczas wdrażania konieczne będzie zsynchronizowanie większej liczby plików niż zwykle.\",\n        \"vimeoNoTrack\": \"Włącz tryb 'Bez śledzenia' dla video z Vimeo\",\n        \"websiteName\": \"Nazwa witryny\",\n        \"websiteSpeed\": \"Szybkość witryny\",\n        \"webpLossless\": \"Włącz bezstratną kompresję dla grafik w formacie WebP\",\n        \"whatsApp\": \"WhatsApp\",\n        \"youMustReviewGdprSettings\": \"Powinieneś sprawdzić swoje ustawienia Prywatności (poprzednio GDPR) ze względu na istotne zmiany w Publii v.0.40. <a href=\\\"https://getpublii.com/blog/release-040.html#cookie-banner\\\" target=\\\"_blank\\\">Czytaj więcej</a>\",\n        \"ytNoCookies\": \"Włącz tryb rozszerzonej prywatności dla video z YouTube\"\n    },\n    \"site\": {\n        \"addNewWebsite\": \"Dodaj nową stronę internetową\",\n        \"cloneWebsite\": \"Klonuj stronę internetową\",\n        \"cloneWebsiteSuccessMsg\": \"Witryna została sklonowana. Przełączono na: \",\n        \"createNewWebsite\": \"Utwórz nową stronę\",\n        \"createWebsite\": \"Utwórz stronę\",\n        \"createYourFirstWebsite\": \"Utwórz swoją pierwszą stronę\",\n        \"creationInProgress\": \"Trwa tworzenie...\",\n        \"deleteWebsite\": \"Usuń stronę internetową\",\n        \"deleteWebsiteConfirmMsg\": \"Czy na pewno chcesz usunąć tę witrynę? Tej czynności nie można cofnąć.\",\n        \"deleteWebsiteCSuccessMsg\": \"Witryna została usunięta.\",\n        \"deleteWebsiteSuccessMsg\": \"Witryna została usunięta. Przełączono na: \",\n        \"duplicateWebsite\": \"Duplikuj stronę internetową\",\n        \"erroOcurredDuringSiteDatabaseCreationInfo\": \"Wystąpił błąd poczas tworzenia bazy danych dla strony. Sprawdź swój program antywirusowy i spróbuj ponownie. Możliwe, że musisz również usunąć niepoprawny katalog strony z katalogu stron Publii.\",\n        \"installFromBackup\": \"Zainstaluj z kopii zapasowej\",\n        \"removeWebsite\": \"Usuń stronę internetową\",\n        \"restoreFromBackup\": {\n            \"createWebsite\": \"Utwórz stronę\",\n            \"invalidBackupContent\": \"Wybrany plik nie jest poprawnym plikiem kopii zapasowej.\",\n            \"invalidSiteData\": \"Wybrana kopia zapasowa jest uszkodzona.\",\n            \"iWantChangeName\": \"Zmień nazwę\",\n            \"restoreFailed\": \"Wystapił błąd podczas tworzenia strony z kopii zapasowej. Spróbuj ponownie.\",\n            \"selectSiteName\": \"Wybierz nazwę strony\",\n            \"siteExistsWantOverride\": \"W katalogu o wybranej nazwie istnieje już strona. Czy chcesz kontynuować? UTRACISZ dane poprzedniej strony i dane strony z kopii zapasowej NADPISZĄ istniejącą stronę\",\n            \"siteNameCannotBeEmpty\": \"Nazwa strony nie może być pusta\",\n            \"unsupportedFormat\": \"Niewspierany format - proszę użyć kopii zapasowej w formacie TAR.\",\n            \"unpackError\": \"Wystapił błąd podczas wypakowywania kopii zapasowej - spróbuj ponownie.\",\n            \"yesPleaseOverride\": \"Tak, nadpisz\"\n        },\n        \"selectedBackupFile\": \"Wybrano plik kopii zapasowej: \",\n        \"siteDescription\": \"Opis strony (wewnętrzny):\",\n        \"siteLoadingErrorMsg\": \"Wystąpił błąd podczas ładowania wybranej witryny. Sprawdź pliki witryny i spróbuj ponownie.\",\n        \"siteName\": \"Nazwa strony:\",\n        \"siteSettingsSaveSuccessMsg\": \"Ustawienia witryny zostały pomyślnie zapisane.\",\n        \"siteWithThisNameExists\": \"Strona o podanej nazwie już istnieje!\",\n        \"specifyNameForWebsiteDuplicate\": \"Podaj nową nazwę duplikatu witryny:\",\n        \"websiteAuthorRequired\": \"nazwa autora strony jest wymagana i powinna zawierać litery\",\n        \"websiteName\": \"Nazwa strony\",\n        \"websiteNameAlreadyInUseMsg\": \"Wybrana nazwa jest używana przez inną stronę internetową. Proszę spróbować ponownie.\",\n        \"websiteNameCantBeEmpty\": \"Nazwa witryny nie może być pusta. Proszę spróbować ponownie.\",\n        \"websiteNameRequired\": \"nazwa strony jest wymagana\",\n        \"websiteNotFound\": \"Nie znaleziono strony internetowej\"\n    },\n    \"supportedFeatures\": {\n        \"featureNames\": {\n            \"authorImages\": \"wyróżnione grafiki autorów\",\n            \"authorPages\": \"podstrony autorów\",\n            \"blockEditor\": \"edytor blokowy\",\n            \"customComments\": \"systemy komentarzy\",\n            \"customSearch\": \"wyszukiwarki\",\n            \"errorPage\": \"podstrona błędów\",\n            \"searchPage\": \"podstrona wyszukiwania\",\n            \"tagImages\": \"wyróżnione grafiki tagów\",\n            \"tagsList\": \"podstrona listy tagów\",\n            \"tagPages\": \"podstrony tagów\"\n        },\n        \"yourThemeDoNotSupport\": \"Twój obecnie używany motyw nie wspiera wymaganej funkcji: {featureName}\"\n    },\n    \"sync\": {\n        \"accessID\": \"ID dostępowe\",\n        \"accessIDFieldCantBeEmpty\": \"Pole ID dostępowe nie może być puste\",\n        \"acl\": \"ACL\",\n        \"afterInitialSyncSiteWillBeAvailableOnline\": \"Po początkowej synchronizacji Twoja witryna będzie dostępna online\",\n        \"allFilesUploadedPart1\": \"Wszystkie pliki zostały pomyślnie wysłane na Twój serwer. \", \n        \"allFilesUploadedPart2\": \"Aby odwiedzić stronę, kliknij przycisk poniżej.\",\n        \"apiRateLimiting\": \"Ograniczenie szybkości interfejsu API\",\n        \"apiRateLimitingNote\": \"Wyłącz tę opcję tylko wtedy, gdy używasz Github Enterprise z wyłączonym ograniczaniem szybkości interfejsu API. W przeciwnym razie wyłączenie tej opcji może spowodować błędy wdrażania.\",\n        \"apiServer\": \"Serwer API\",\n        \"apiServerNote\": \"Zmień tę wartość tylko wtedy, gdy korzystasz z własnej instancji GitHub (wersja Enterprise).\",\n        \"authenticationMethod\": \"Metoda uwierzytelnienia\",\n        \"branch\": \"Gałąź\",\n        \"branchExampleGitNote\": \"Przykłady: <strong>main</strong>, <strong>gh-pages</strong> or <strong>docs</strong>.\",\n        \"branchExampleGitHubNote\": \"Przykłady: <strong>gh-pages</strong>, <strong>docs</strong> lub <strong>main</strong>.\",\n        \"branchExampleGitLabeNote\": \"Przykład: <strong>main</strong>.\",\n        \"branchFieldCantBeEmpty\": \"Pole gałęzi nie może być puste\",\n        \"bucket\": \"Bucket\",\n        \"bucketFieldCantBeEmpty\": \"Pole bucket nie może być puste\",\n        \"certificates\": \"Certyfikaty\",\n        \"changeServerType\": \"Zmień rodzaj serwera\",\n        \"checkingConnection\": \"Sprawdzanie połączenia...\",\n        \"clickToChangeDeploymentMethod\": \"Kliknij, aby zmienić aktualnie używaną metodę wdrażania\",\n        \"commitAuthor\": \"Autor zmian\",\n        \"commitAuthorFieldCantBeEmpty\": \"Pole autora zmian nie może być puste\",\n        \"commitEmail\": \"Adres e-mail autora zmian\",\n        \"commitMessage\": \"Opis zmian\",\n        \"commitMessageFieldCantBeEmpty\": \"Pole opisu zmian nie może być puste\",\n        \"configureServer\": \"Skonfiguruj serwer\",\n        \"connectedToServer\": \"Połączono z serwerem\",\n        \"connectingToServer\": \"Łączenie z serwerem...\",\n        \"connectionToServerErrorAdditionalMessage\": \"Wystąpił błąd podczas łączenia się z serwerem: \",\n        \"connectionToServerErrorMessage\": \"Wystąpił błąd podczas łączenia się z serwerem. Proszę sprawdzić ustawienia serwera i spróbować ponownie.\",\n        \"connectionToServerErrorText\": \"Wystąpił błąd podczas łączenia się z serwerem...\",\n        \"connectToServerCantStoreFilesErrorMsg\": \"Błąd! Aplikacja mogła połączyć się z Twoim serwerem, ale nie mogła zapisać plików. Sprawdź uprawnienia do plików na Twoim serwerze\",\n        \"connectToServerErrorMsg\": \"Błąd! Aplikacja nie mogła połączyć się z Twoim serwerem.\",\n        \"connectToServerSuccessMsg\": \"Sukces! Aplikacja mogła połączyć się z Twoim serwerem.\",\n        \"deploymentMethodFilesPubliiMsg\": \"Wybrana metoda synchronizacji korzysta z pliku <strong>files.publii.json</strong> do synchronizacji. Rozważ ochronę dostępu do tego pliku w swojej konfiguracji serwera, jeśli jest to potrzebne - <a href=\\\"https://getpublii.com/docs/recommended-server-settings.html#sync-file\\\" target=\\\"_blank\\\">dowiedz się więcej</a>\",\n        \"deploymentMethodFtpMsg\": \"Protokół FTP wykorzystuje transmisję nieszyfrowaną, co oznacza, że wszelkie dane przesyłane za jego pośrednictwem, w tym nazwa użytkownika i hasło, mogą zostać odczytane przez każdego, kto może przechwycić Twoją transmisję. Zdecydowanie zalecamy korzystanie z protokołów FTPS lub SFTP, jeśli to możliwe.\",\n        \"deploymentMethodGitMsg\": \"Aby uzyskać szczegółowe informacje o tym, jak skonfigurować stronę internetową za pomocą Git, zobacz <a href=\\\"https://getpublii.com/docs/host-static-website-git-repository.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dokumentację</a> online.\",\n        \"deploymentMethodGithubPagesMsg\": \"Aby uzyskać szczegółowe informacje o tym, jak skonfigurować stronę internetową za pomocą Stron Github, zobacz <a href=\\\"https://getpublii.com/docs/host-static-website-github-pages.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dokumentację</a> online.\",\n        \"deploymentMethodGitNote\": \"Pamiętaj, że w wypadku korzystania z własnej domeny, musisz umieścić plik CNAME w menadżerze plików w katalogu głównym\",\n        \"deploymentMethodGithubPagesNote\": \"To będzie ścieżka Twojego repozytorium Github, która powinna mieć następujący format: <strong>NAZWA_UZYTKOWNIKA.github.io/NAZWA_REPOZYTORIUM</strong>.<br>Jeśli używasz niestandardowej nazwy domeny, ustaw w tym polu tylko niestandardową nazwę domeny.\",\n        \"deploymentMethodGitlabPagesMsg\": \"Aby uzyskać szczegółowe informacje o tym, jak skonfigurować stronę internetową za pomocą Stron Gitlab, zobacz <a href=\\\"https://getpublii.com/docs/host-static-website-gitlab-pages.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dokumentację</a> online.\",\n        \"deploymentMethodGoogleCloudMsg\": \"Aby uzyskać szczegółowe informacje o tym, jak skonfigurować stronę internetową za pomocą Chmury Google, zobacz <a href=\\\"https://getpublii.com/docs/make-static-website-google-cloud.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dokumentację</a> online.\",\n        \"deploymentMethodNetlifyMsg\": \"Aby uzyskać szczegółowe informacje o tym, jak skonfigurować stronę internetową za pomocą Netlify, zobacz <a href=\\\"https://getpublii.com/docs/build-a-static-website-with-netlify.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dokumentację</a> online.\",\n        \"deploymentMethodS3Msg\": \"Aby uzyskać szczegółowe informacje o tym, jak skonfigurować stronę internetową za pomocą S3, zobacz <a href=\\\"https://getpublii.com/docs/setup-static-website-hosting-amazon-s3.html\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dokumentację</a> online.\",\n        \"deploymentSettingDatHyperIpfsProtocolNote\": \"Protokół \\\"dat://\\\", \\\"hyper://\\\", \\\"dweb://\\\" i \\\"ipfs://\\\" jest przydatny tylko wtedy, gdy planujesz korzystać ze swojej witryny w sieciach P2P. Przeczytaj więcej o <a href=\\\"https://datproject.org/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dat://</a>, <a href=\\\"https://hypercore-protocol.org/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">hyper://</a>, <a href=\\\"https://dwebx.org/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">dweb://</a> i <a href=\\\"https://ipfs.io/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">IPFS</a>\",\n        \"deploymentSettingDoubleSlashProtocolNote\": \"Uwaga: poczas korzystania z \\\"//\\\" jako protokołu, niektóre funkcje, takie jak tagi Open Graph, przyciski udostępniania itp., nie działają poprawnie.\",\n        \"deploymentSettingFileProtocolNote\": \"Protokół \\\"file://\\\" jest przydatny tylko wtedy, gdy korzystasz z ręcznej metody wdrażania dla witryn intranetowych.\",\n        \"deploymentSettingRelativeUrlsNote\": \"Uwaga: podczas korzystania ze względnych URLi, niektóre funkcje, takie jak tagi Open Graph, mapy witryn, kanały RSS, kanały JSON itp., zostaną wyłączone.\",\n        \"deprecated\": \"Przestarzałe\",\n        \"destinationServerNotConfiguredErrorMessage\": \"Twoja witryna nie może być obecnie zsynchronizowana, ponieważ serwer docelowy nie został poprawnie skonfigurowany. <br>Sprawdź ustawienia serwera, aby upewnić się, że wprowadzono prawidłowe informacje.\",\n        \"destinationServerNotConfiguredErrorText\": \"Upewnij się, że serwer docelowy jest poprawnie skonfigurowany.\",\n        \"domainNameNotSetErrorMessage\": \"Twoja witryna nie może być obecnie zsynchronizowana, ponieważ wygląda na to, że w ustawieniach brakuje nazwy domeny. <br>Sprawdź ustawienia serwera, aby upewnić się, że wprowadzono nazwę domeny.\",\n        \"domainNameNotSetErrorText\": \"Upewnij się, że nazwa domeny jest ustawiona.\",\n        \"duringSyncYouCantChangeFilesLocation\": \"W trakcie synchronizacji nie możesz zmienić lokalizacji plików.\",\n        \"fieldIsCaseSensitive\": \"W tym polu rozróżniana jest wielkość liter.\",\n        \"filesNotSyncedErrorMessage\": \"Proszę sprawdzić plik hard-upload-errors-log.txt przy pomocy narzędzia Narzędzia -&gt; Przeglądarka logów.\",\n        \"filesNotSyncedErrorText\": \"Niektóre pliki nie zostały poprawnie zsynchronizowane.\",\n        \"ftp\": \"FTP\",\n        \"ftps\": \"FTPS\",\n        \"ftpWithSSLTLS\": \"FTP z SSL/TLS\",\n        \"generatePreviewFiles\": \"Wygeneruj pliki podglądu\",\n        \"getWebsiteFiles\": \"Pobierz pliki strony\",\n        \"git\": \"Git\",\n        \"github\": \"GitHub\",\n        \"githubPages\": \"Strony GitHub\",\n        \"githubSyncedPart1\": \"Zmiany na Github Pages mogą być widoczne po kilku minutach od wysłania, \",\n        \"githubSyncedPart2\": \"zatem należy odczekać chwilę przed weryfikacją zmian.\",\n        \"gitlabPages\": \"Strony Gitlab\",\n        \"gitlabSyncedPart1\": \"Zmiany na Gitlab Pages mogą być widoczne po kilku minutach od wysłania, \",\n        \"gitlabSyncedPart2\": \"zatem należy odczekać chwilę przed weryfikacją zmian.\",\n        \"gitPassword\": \"Hasło / Token\",\n        \"gitPasswordNote\": \"Jeśli używasz 2FA aby chronić swoje konto, zamiast hasła musisz użyć uprzednio wygenerowanego tokenu dostępowego\",\n        \"googleCloud\": \"Chmura Google\",\n        \"googleCloudPrefixNote\": \"Możesz umieścić swoją stronę internetową w podkatalogu. Unikaj ukośnika na początku (np. <strong>/blog/</strong>) - spowoduje to utworzenie dodatkowego katalogu z pustą nazwą. Przykład prawidłowego prefiksu: <strong>blog</strong>.\",\n        \"goToAppSettings\": \"Idź do ustawień aplikacji\",\n        \"goToSettings\": \"Idź do ustawień\",\n        \"htmlCacheControl\": \"Nagłówek Cache-Control dla plików HTML\",\n        \"keyFile\": \"Plik klucza\",\n        \"lastRendered\": \"Ostatnie renderowanie\",\n        \"lastSync\": \"Ostatnia synchronizacja\",\n        \"leaveBlankToUseDefaultOutputDirectory\": \"Pozostaw puste aby użyć domyślnego katalogu docelowego\",\n        \"manualDeployment\": \"Wdrażanie ręczne\",\n        \"manualOutputFieldCantBeEmpty\": \"Ręczny wybór wyjścia nie może być pusty\",\n        \"ManualUpload\": \"Przesyłanie ręczne\",\n        \"netlify\": \"Netlify\",\n        \"netlifyToken\": \"Token Netlify\",\n        \"nonCompressedCatalog\": \"Katalog nieskompresowany\",\n        \"note\": \"Uwaga:\",\n        \"operationsDone\": \"wykonanych operacji\",\n        \"otherCacheControl\": \"Nagłówek Cache-Control dla innych plików\",\n        \"outputDirectory\": \"Katalog docelowy\",\n        \"outputDirectoryNote\": \"W wybranym katalogu zostanie utworzony katalog/plik {siteName}-files. Jeśli już istnieje - zostanie zastąpiony nowymi plikami.\",\n        \"outputType\": \"Typ wyjścia\",\n        \"outputTypeNote\": \"Publii wygeneruje katalog lub archiwum ZIP/TAR z Twoją stroną internetową. Następnie możesz ręcznie przesłać te pliki na dowolny serwer docelowy.\",\n        \"parallelUploads\": \"Przesyłanie równoległe\",\n        \"parallelUploadsNote\": \"Więcej równoległych operacji może prowadzić do błędów przesyłania przy wolnych połączeniach internetowych lub błędu 403 z powodu limitów szybkości interfejsu API.\",\n        \"passphraseForKey\": \"Hasło do klucza\",\n        \"passphraseForKeyNote\": \"Użyj tego pola tylko wtedy, gdy Twój klucz wymaga hasła\",\n        \"port\": \"Port\",\n        \"portFormatNote\": \"Pole portu nie może być puste i musi być dodatnią liczbą całkowitą między 1 i 65535.\",\n        \"prefix\": \"Prefiks\",\n        \"preparationError\": \"Błąd przygotowania\",\n        \"preparedToUpload\": \"Gotowe do wysłania na serwer\",\n        \"preparingFiles\": \"Przygotowywanie plików\",\n        \"previewCatalogDoesNotExistInfo\": \"Katalog do podglądu nie istnieje. Wybierz katalog do podglądów strony w ustawieniach aplikacji. \",\n        \"previewChanges\": \"Zobacz swoje zmiany\",\n        \"provideAccessData\": \"Podaj dane dostępu\",\n        \"provideFTPPasswordFor\": \"Proszę podać hasło FTP dla \",\n        \"provideFTPPasswordForServer\": \"Proszę podać hasło FTP dla następującego serwera: \",\n        \"region\": \"Region\",\n        \"regionFieldCantBeEmpty\": \"Pole regiony nie może byc puste\",\n        \"remotePath\": \"Ścieżka zdalna\",\n        \"remotePathMatchServerOrRootPathNote\": \"Ścieżka powinna pasować do ścieżki serweranp. public_html/, public_html/blog/ lub ścieżce głównej np. /home/username/public_html/.\",\n        \"remotePathMatchServerRootPathNote\": \"Ścieżka powinna odpowiadać ścieżce głównej serwera np. /public_html/, /public_html/blog/.\",\n        \"repository\": \"Repozytorium\",\n        \"repositoryFieldCantBeEmpty\": \"Pole repozytorium nie może być puste\",\n        \"repositoryUrl\": \"URL repozytorium\",\n        \"repositoryUrlFieldCantBeEmpty\": \"Adres URL repozytorium nie może być pusty\",\n        \"repositoryUrlNote\": \"URL do Twojego repozytorium Git przechowującego pliki strony\",\n        \"requireCertificateForConnection\": \"Do połączenia wymagaj ważnego certyfikatu\",\n        \"requireFTPPassAlwaysOnSync\": \"Wymagaj hasła FTP przy każdej synchronizacji witryny\",\n        \"s3CompatibleStorage\": \"Pamięć kompatybilna z S3\",\n        \"s3PrefixNote\": \"Możesz umieścić swoją stronę internetową w podkatalogu. Unikaj ukośnika na początku (np. <strong>/blog/</strong>) - spowoduje to utworzenie dodatkowego katalogu z pustą nazwą. Przykład prawidłowego prefiksu: <strong>blog/</strong>.\",\n        \"s3ProviderEndpoint\": \"Endpoint dostawcy S3\",\n        \"s3ProviderEndpointNote\": \"Niestandardowy endpoint nie może być pusty, gdy opcja „Użyj niestandardowego dostawcy S3” jest ustawiona na \\\"tak\\\"\",\n        \"secretKey\": \"Tajny klucz\",\n        \"secretKeyFieldCantBeEmpty\": \"Pole klucza tajnego nie może być puste\",\n        \"selectServerType\": \"Wybierz rodzaj serwera:\",\n        \"serverFieldCantBeEmpty\": \"Pole serwera nie może być puste.\",\n        \"serverGitLabNote\": \"Zmień tę wartość tylko wtedy, gdy używasz własnej instancji GitLab.\",\n        \"serverSettingsSaveErrorMsg\": \"Publii nie może zapisać ustawień z powodu problemu z oprogramowaniem do bezpiecznego przechowywania haseł. Uruchom ponownie aplikację i spróbuj ponownie. Jeśli problem będzie się powtarzał, zgłoś go naszemu zespołowi za pośrednictwem forum.\",\n        \"serverSettingsSaveLinuxErrorMsg\": \"Publii nie może zapisać ustawień, ponieważ nie jest zainstalowane żadne oprogramowanie do bezpiecznego przechowywania haseł. Postępuj zgodnie z instrukcjami instalacji dla Node Keytar via https://github.com/atom/node-keytar/ i spróbuj ponownie. Najprawdopodobniej w Twoim systemie brakuje paczek libsecret-1-dev i gnome-keyring packages.\",\n        \"serverSettingsSaveSuccessMsg\": \"Ustawienia serwera zostały pomyślnie zapisane.\",\n        \"settings\": \"Ustawienia\",\n        \"sftp\": \"SFTP\",\n        \"sftpKeyNote\": \"Proszę wybrać plik klucza\",\n        \"sftpWithKey\": \"SFTP (z kluczem)\",\n        \"sftpWithPassword\": \"SFTP (z hasłem)\",\n        \"siteFieldCantBeEmpty\": \"Pole ID strony nie może być puste\",\n        \"siteID\": \"ID strony\",\n        \"siteIsInSync\": \"Strona zsynchronizowana\",\n        \"syncFTPNoPasswordMsg\": \"Bez hasła nie można zsynchronizować witryny. Proszę spróbować ponownie\",\n        \"syncInProgress\": \"Trwa synchronizacja\",\n        \"syncInProgressMessage\": \"Podczas procesu synchronizacji nie możesz zmienić nazwy strony.\",\n        \"syncYourWebsite\": \"Synchronizuj stronę\",\n        \"tarArchive\": \"Archiwum TAR\",\n        \"testConnection\": \"Testuj połączenie\",\n        \"testConnectionNoPasswordMsg\": \"Bez hasła nie można przetestować połączenia. Proszę spróbuj ponownie\",\n        \"token\": \"Token\",\n        \"tokenFieldCantBeEmpty\": \"Pole tokena nie może być puste\",\n        \"uploadingWebsite\": \"Przesyłanie strony internetowej\",\n        \"useCustomS3Provider\": \"Użyj niestandardowego dostawcy S3\",\n        \"useCustomS3ProviderNote\": \"Uwaga: AWS jest domyślnym dostawcą S3. Korzystając z alternatywnego dostawcy, musisz wypełnić pole „Endpoint dostawcy S3”.\",\n        \"useFtps\": \"Stosuj FTP z SSL/TLS\",\n        \"useRelativeURLs\": \"Użyj względnych URLi\",\n        \"username\": \"Nazwa użytkownika\",\n        \"usernameFieldCantBeEmpty\": \"Pole nazwy użytkownika nie może być puste.\",\n        \"usernameOrganization\": \"Nazwa użytkownika / organizacji\",\n        \"visitWebsite\": \"Odwiedź stronę\",\n        \"visitYourWebsite\": \"Odwiedź swoją stronę\",\n        \"websiteFilesPreparedInfo\": \"Twoja strona internetowa została przygotowana. Użyj poniższego przycisku \\\"Pobierz pliki strony\\\" <br>aby uzyskać pliki w celu ich ręcznego umieszczenia na serwerze.\",\n        \"websiteLinkInvalidMsg\": \"Przepraszamy! Link do witryny wydaje się być nieprawidłowy.\",\n        \"websiteSynchronization\": \"Synchronizacja strony internetowej\",\n        \"websiteSynchronizationInfo\": \"Wszelkie zduplikowane pliki lub nazwy plików już istniejące w lokalizacji docelowej <br>, które pasują do plików generowanych przez Publii, zostaną nadpisane.\",\n        \"websiteURL\": \"URL strony internetowej\",\n        \"youHaventSelectedAnyThemeInfo\": \"Nie wybrano żadnego motywu. Najpierw wybierz motyw w ustawieniach.\",\n        \"yourJSONKey\": \"Twój klucz JSON\",\n        \"yourJSONKeyFieldCantBeEmpty\": \"Pole klucza JSON nie może być puste\",\n        \"yourPrivateKey\": \"Twój klucz prywatny\",\n        \"yourWebsiteIsInSync\": \"Twoja witryna jest teraz zsynchronizowana\",\n        \"zipArchive\": \"Archiwum ZIP\"\n    },\n    \"tag\": {\n        \"addNewTag\": \"Dodaj nowy tag\",\n        \"addThisAsNewTag\": \"Dodaj jako nowy tag\",\n        \"createFirstTag\": \"Nie masz jeszcze żadnych tagów. Utwórzmy pierwszy!\",\n        \"editTag\": \"Edytuj tag\",\n        \"filterOrSearchTags\": \"Filtruj lub wyszukaj tagi...\",\n        \"hideTag\": \"Ukryj tag\",\n        \"leaveBlankToUseDefaultTagPageURL\": \"Pozostaw puste aby użyć domyślnego URL dla strony taga.\",\n        \"mainTag\": \"Główny tag\",\n        \"newTagHasBeenCreated\": \"Nowy tag został utworzony\",\n        \"noMainTagForPostMsg\": \"Jeśli wpis ma tagi, ale nie ustawiono głównego tagu, domyślnie zostanie użyty pierwszy tag w kolejności alfabetycznej.\",\n        \"noSupportFoFeaturedImagesForTags\": \"Twój motyw nie wspiera wyróżnionych obrazków dla tagów.\",\n        \"noTagsAvailable\": \"Brak dostępnych tagów\",\n        \"noTagsMatchingYourCriteria\": \"Brak tagów spełniających Twoje kryteria.\",\n        \"removeTagMessage\": \"Na pewno chcesz usunąć wybrane tagi?\",\n        \"removeTagSuccessMessage\": \"Wybrane tagi zostały usunięte\",\n        \"themeDoesNotSupportForTagPagesInTheme\": \"Opcja \\\"Zapis i Podgląd\\\" nie jest dostępna z powodu braku wsparcia dla stron tagów w Twoim motywie.\",\n        \"selectTag\": \"Wybierz tag\",\n        \"selectTagPage\": \"Wybierz stronę taga\",\n        \"tag\": \"Tag\",\n        \"tagDataParsingErrorMessage\": \"Wystąpił błąd podczas parsowania danych taga dla ID: \",\n        \"tagHasBeenEdited\": \"Tag został przeedytowany\",\n        \"tagIsNotAllowed\": \"Ten tag nie jest dozwolony\",\n        \"tagLink\": \"Link do taga\",\n        \"tagName\": \"Nazwa taga:\",\n        \"tagNameCannotBeEmptyErrorMessage\": \"Nazwa taga nie może być pusta. Proszę podać inną nazwę.\",\n        \"tagNameInUseErrorMessage\": \"Podana nazwa taga jest już używana. Proszę podać inną nazwę.\",\n        \"tagNameNotAllowedErrorMessage\": \"Podana nazwa taga/slug nie jest dozwolona.\",\n        \"tagNameSimilarInUseErrorMessage\": \"Podobna nazwa taga (bez rozróżniania wielkości liter) jest już używana. Proszę podać inną nazwę.\",\n        \"tagPage\": \"Strona taga\",\n        \"tagsListLink\": \"Link do listy tagów\",\n        \"tagStatusChangeSuccessMessage\": \"Status wybranych tagów został zmieniony\",\n        \"tagWillNotAppearInGeneratedTagLists\": \"Tag nie pojawi się na żadnej z wygenerowanych list tagów takich jak strona wpisu lub taga.\",\n        \"thisTagIsHidden\": \"Ten tag jest ukryty\",\n        \"toUseThisOptionEnableIndexingTagPages\": \"Aby użyć tej opcji najpierw włącz, w ustawieniach SEO, indeksowanie stron tagów.\",\n        \"url\": \"URL\"\n    },\n    \"theme\": {\n        \"addThemeSuccessMessage\": \"Motyw został dodany.\",\n        \"authorOptions\": \"Opcje autorów\",\n        \"authorOptionsInfo\": \"Sekcja opcji autora pozwala na ustawienie globalnych opcji dla dodatkowych informacji zawartych w autorach. Zmiany wrpowadzone w tej sekcji będą miały wpływ na wszystkich autorów na Twojej stronie ale możesz również, jeśli tego potrzebujesz, nadpisać ustawienia aplikacji, dla każdego z autorów osobno, w widoku edycji autora.\",\n        \"authorsPostsPerPage\": \"Autorzy na stronie:\",\n        \"authorsPostsPerPageInfo\": \"Podaj wartość -1 jeśli chcesz wyświetlać wszystkich autorów na stronie.\",\n        \"changeAppTheme\": \"Zmień motyw aplikacji\",\n        \"customSettings\": \"Ustawienia własne\",\n        \"defaultPageTemplate\": \"Domyślny szablon podstrony\",\n        \"defaultPostTemplate\": \"Domyślny szablon wpisu\",\n        \"defaultTemplate\": \"Szablon domyślny\",\n        \"deleteTheme\": \"Usuń motyw\",\n        \"dropYourThemeHere\": \"Upuść tutaj swój motyw\",\n        \"excerptLength\": \"Długość zajawki:\",\n        \"getMoreThemes\": \"Pobierz więcej motywów\",\n        \"goToThemesManager\": \"Przejdż do menadżera motywów\",\n        \"installTheme\": \"Zainstaluj motyw\",\n        \"leaveBlankToUseDefault\": \"Pozostaw puste aby użyć wartości domyślnej\",\n        \"newVersionAvailable\": \"Dostępna jest nowa wersja\",\n        \"pageOptions\": \"Opcje stron\",\n        \"pageOptionsInfo\": \"Sekcja opcji stron pozwala na ustawienie globalnych opcji dla dodatkowych informacji zawartych w stronach. Zmiany wrpowadzone w tej sekcji będą miały wpływ na wszystkie strony na Twojej stronie ale możesz również, jeśli tego potrzebujesz, nadpisać ustawienia aplikacji, dla każdej ze stron osobno, w widoku edycji strony.\",\n        \"postOptions\": \"Opcje wpisów\",\n        \"postOptionsInfo\": \"Sekcja opcji wpisu pozwala na ustawienie globalnych opcji dla dodatkowych informacji zawartych we wpisach. Zmiany wrpowadzone w tej sekcji będą miały wpływ na wszystkie wpisy na Twojej stronie ale możesz również, jeśli tego potrzebujesz, nadpisać ustawienia aplikacji, dla każdego z wpisów osobno, w widoku edycji wpisu.\",\n        \"postsPerPage\": \"Wpisy na stronie:\",\n        \"postsPerPageInfo\": \"Podaj wartość -1 jeśli chcesz wyświetlać wszystkie wpisy na stronie.\",\n        \"removeThemeMessage\": \"Czy na pewno chcesz usunąć motyw {themeName}?\",\n        \"removeThemeSuccessMessage\": \"Motyw został usunięty.\",\n        \"resetThemeSettings\": \"Resetuj ustawienia motywu\",\n        \"saveSettingsSuccessMessage\": \"Ustawienia motywu zosały zapisane.\",\n        \"selectTheme\": \"Wybierz motyw\",\n        \"settingsResetMessage\": \"Na pewno chcesz zresetować ustawienia motywu?\",\n        \"settingsResetSuccessMessage\": \"Ustawienia motywu zostały zresetowane\",\n        \"tagOptions\": \"Opcje tagów\",\n        \"tagOptionsInfo\": \"Sekcja opcji tagu pozwala na ustawienie globalnych opcji dla dodatkowych informacji zawartych w tagach. Zmiany wrpowadzone w tej sekcji będą miały wpływ na wszystkie tagi na Twojej stronie ale możesz również, jeśli tego potrzebujesz, nadpisać ustawienia aplikacji, dla każdego z tagów osobno, w widoku edycji tagu.\",\n        \"tagsPostsPerPage\": \"Tagi na stronie:\",\n        \"tagsPostsPerPageInfo\": \"Podaj wartość -1 jeśli chcesz wyświetlać wszystkie tagi na stronie.\",\n        \"themes\": \"Motywy\",\n        \"themeSettings\": \"Ustawienia Motywu\",\n        \"translations\": \"Tłumaczenia\",\n        \"translationsInfo\": \"Jeśli chcesz przetłumaczyć frazy motywu na język inny niż angielski poczytaj najpierw o <a href=\\\"https://getpublii.com/dev/translations-api/\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">API Tłumaczeń</a> w naszej dokumentacji.<br><br>\",\n        \"updateThemeSuccessMessage\": \"Motyw został zaktualizowany.\",\n        \"uploadThemeErrorMessage\": \"Przesłane pliki są niepoprawne. Proszę przesłać katalog motywu lub plik ZIP motywu.\",\n        \"websiteLogo\": \"Logo strony:\"\n    },\n    \"tools\": {\n        \"css\": {\n            \"customCSS\": \"Niestandardowy CSS\",\n            \"customCSSSaveSuccessMsg\": \"Niestandardowy kod CSS zpsatł zapisany.\",\n            \"normal\": \"Normalny\",\n            \"putCustomCSSComment\": \"Tutaj umieść swój niestandardowy kod CSS\"\n        },\n        \"customHTML\": \"Niestandardowy HTML\",\n        \"find\": \"Znajdź:\",\n        \"findAndReplace\": \"Znajdź i zastąp:\",\n        \"findAndReplaceShortcut\": \"Ctrl + Alt + F\",\n        \"findAndReplaceShortcutMac\": \"Cmd + Alt + F\",\n        \"findShortcut\": \"Ctrl + F\",\n        \"findShortcutMac\": \"Cmd + F\",\n        \"goToTools\": \"Idź do narzędzi\",\n        \"logFileEmpty\": \"Ten plik logów jest pusty...\",\n        \"logViewer\": \"Przeglądarka logów\",\n        \"pluginActivationError\": \"Nie udało się aktywować wtyczki - spróbuj ponownie\",\n        \"pluginDeactivationError\": \"Nie udało się deaktywować wtyczki - spróbuj ponownie\",\n        \"selectFileToLoad\": \"Wybierz plik do załadowania\",\n        \"thumbnails\": {\n            \"listRegeneratedFiles\": \"Lista zregenerowanych plików:\",\n            \"processingRegenerateThumbnailsInfo\": \"Naciśnięcie przycisku Regeneruj miniatury rozpocznie generowanie nowych rozmiarów obrazów zdefiniowanych przez Twój nowy motyw.\",\n            \"progress\": \"Postęp: \",\n            \"regenerateThumbnails\": \"Regeneruj miniatury\",\n            \"regenerateThumbnailsInfo\": \"Jeśli zmieniłeś motyw lub masz problemy z responsywnymi obrazami, możesz je ponownie wygenerować za pomocą przycisku poniżej. Może to chwilę potrwać, jeśli Twoja witryna zawiera dużo obrazów, więc prosimy o cierpliwość.\",\n            \"regenerateThumbnailsNotNecessaryInfo\": \"Obecnie nie masz wybranego motywu dla tej witryny. Regeneracja miniatur nie jest konieczna.\",\n            \"regeneratingThumbnails\": \"Regenerowanie miniatur...\",\n            \"skipRegeneration\": \"Pomiń regenrację\",\n            \"themeOrThumbnailsSettingsChanged\": \"Twój motyw lub ustawienia miniatur zostały zmienione.\",\n            \"thumbnailsCreated\": \"Wszystkie miniatury zostały utworzone.\",\n            \"thumbnailsRegenerationCancelled\": \"Regeneracja miniatur została anulowana.\"\n        },\n        \"wpImport\": {\n            \"addTagsToContentAutomatically\": \"Automatycznie dodaj tagi <p> i <br> do treści wpisu\",\n            \"categories\": \"Kategorie\",\n            \"checkingWXRFile\": \"Sprawdzanie wybranego pliku WXR\",\n            \"contentFormatting\": \"Formatowanie treści:\",\n            \"duringWXRAnalyzeWeHaveFound\": \"Podczas anlizy WXR znaleźliśmy\",\n            \"importAuthors\": \"Importuj autorów\",\n            \"importData\": \"Importuj dane\",\n            \"importingData\": \"Importowanie danych\",\n            \"importNote\": \"Wpisy zostaną zaimportowane jako zgodne z edytorem WYSIWYG.\",\n            \"importSelectedTypesOfPosts\": \"Importuj wybrane rodzaje wpisów:\",\n            \"pages\": \"strony\",\n            \"postAuthors\": \"Autorzy wpisów\",\n            \"selectWXRFileLabel\": \"Proszę wybrać plik WXR:\",\n            \"selectWXRFilePlaceholder\": \"Wybierz plik WXR do zaimportowania\",\n            \"usedTaxonomyForPosts\": \"Taksonomia używana dla wpisów:\",\n            \"useMainAuthor\": \"Użyj głównego autora z Publii\",\n            \"wpImporter\": \"Importer WP\",\n            \"wpImportGoToRegenerateMsg\": \"Twoje dane WordPress zostały zaimportowane. Regeneracja miniatur może być konieczna, jeśli wcześniej wybrałeś motyw.\"\n        }\n    },\n    \"toolsPlugin\": {\n        \"pluginSettingsLoadError\": \"Wystąpił błąd podczas wczytywania ustawień wtyczki\",\n        \"pluginSettingsSaveSuccess\": \"Ustawienia wtyczki zostały zapisane\",\n        \"pluginSettingsSaveError\": \"Wystąpił błąd podczas zapisu ustawień wtyczki\",\n        \"tabsLabel\": \"Ustawienia wtyczki\",\n        \"thisPluginHasNoOptions\": \"Ta wtyczka nie ma dodatkowych opcji...\"\n    },\n    \"ui\": {\n        \"aboutPublii\": \"O Publii\",\n        \"alternativeText\": \"Tekst alternatywny\",\n        \"appConfiguration\": \"Konfiguracja aplikacji\",\n        \"appIsUnableToGetNotifications\": \"Nie udało się pobrać powiadomień. Spróbuj ponownie później. Błąd: \",\n        \"applyChanges\": \"Zatwierdź zmiany\",\n        \"authors\": \"Autorzy\",\n        \"backToTools\": \"Wróć do narzędzi\",\n        \"backToPages\": \"Strony\",\n        \"backToPosts\": \"Posty\",\n        \"basicInformation\": \"Informacje podstawowe\",\n        \"beautifyCode\": \"Upiększ kod\",\n        \"blogIndexLink\": \"Link do listy wpisów\",\n        \"cancel\": \"Anuluj\",\n        \"cancelWarningMsg\": \"Utracisz wszystkie niezapisane zmiany – czy chcesz kontynuować?\",\n        \"canonicalURL\": \"Kanoniczny URL\",\n        \"caption\": \"Podpis\",\n        \"checkDocumentation\": \"Sprawdź dokumentację Publii\",\n        \"close\": \"Zamknij\",\n        \"copyToClipboard\": \"Skopiuj do schowka\",\n        \"credits\": \"Uznania\",\n        \"customLink\": \"Link własny\",\n        \"customTemplate\": \"Własny szablon\",\n        \"delete\": \"Usuń\",\n        \"description\": \"Opis\",\n        \"donate\": \"Przekaż darowiznę\",\n        \"edit\": \"Edytuj\",\n        \"enablePrettyURLs\": \"Enable pretty URLs\",\n        \"featuredImage\": \"Obrazek wyróżniony\",\n        \"fillAllRequiredFields\": \"Proszę uzupełnić wszystkie wymagane pola\",\n        \"frontpageLink\": \"Link do strony startowej\",\n        \"gdprBreakingChangesConfirmMsg\": \"Wprowadziliśmy istotne zmiany w sekcji Ustawienia Prywatności (poprzednio GDPR). Zalecamy sprawdzenie ustawień i ponowne ich zapisanie.\",\n        \"githubRepository\": \"Repozytorium Github\",\n        \"goBack\": \"Wróć\",\n        \"goToPluginCustomOptions\": \"Ukryj dodatkowe opcje\",\n        \"goToPluginStandardOptions\": \"Pokaż dodatkowe opcje\",\n        \"goToSettings\": \"Idź do ustawień Prywatności\",\n        \"help\": \"Pomoc\",\n        \"hide\": \"Ukryj\",\n        \"hideThisNotification\": \"Schowaj to powiadominie\",\n        \"id\": \"ID\",\n        \"ifCanonicalUrlIsSetMetaRobotsTagIsIgnored\": \"Jeśłi kanoiczny URL jest ustawiony, tagi meta robotów są ignorowane.\",\n        \"indexFollow\": \"index, follow\",\n        \"indexNofollow\": \"index, nofollow\",\n        \"indexFollowNoArchive\": \"index, follow, noarchive\",\n        \"indexNofollowNoArchive\": \"index, nofollow, noarchive\",\n        \"iUnderstand\": \"OK, rozumiem\",\n        \"lastModified\": \"Ostatnia modyfikacja\",\n        \"learnMore\": \"Więcej informacji\",\n        \"leaveBlankToUseDefaultPageTitle\": \"Pozostaw puste aby zastosować domyślny tytuł strony\",\n        \"linkTarget\": \"Otwórz w \",\n        \"loading\": \"Ładowanie...\",\n        \"menus\": \"Menu\",\n        \"metaDescription\": \"Meta opisy\",\n        \"metaRobotsIndex\": \"Index meta robotów\",\n        \"minimize\": \"Zminimalizuj\",\n        \"more\": \"Więcej\",\n        \"moreInformationOnPublii\": \"Więcej informacji o Publii\",\n        \"moreItems\": \"Więcej elementów\",\n        \"name\": \"Nazwa\",\n        \"newWindow\": \"Nowym oknie\",\n        \"noindexFollow\": \"noindex, follow\",\n        \"noindexNofollow\": \"noindex, nofollow\",\n        \"none\": \"None\",\n        \"notAvailableInYourTheme\": \"Niedostępne w Twoim motywie\",\n        \"notSet\": \"Nie ustawiono\",\n        \"of\": \"z\",\n        \"ok\": \"OK\",\n        \"otherOptions\": \"Inne opcje\",\n        \"pages\": \"Strony\",\n        \"pagesSupportNotEnabledMessage\": \"Twój obecny motyw nie obsługuje stron. Proszę wybierz motyw z wbudowaną obsługą stron lub zmodyfikuj obecnie używany. Szczegółowe informacje na temat implementacji obsługi stron w motywie znajdziesz po kliknięciu przycisku 'Więcej informacji'\",\n        \"pageTitle\": \"Tytuł Strony\",\n        \"pleaseFillAllRequiredFields\": \"Proszę wypełnić wszystkie wymagane pola\",\n        \"posts\": \"Wpisy\",\n        \"preview\": \"Podgląd\",\n        \"previewFrontPageOnly\": \"Podejrzyj tylko stronę główną\",\n        \"previewFullWebsite\": \"Podejrzyj całą stronę\",\n        \"publiiOnGithub\": \"Zobacz Publii na Github\",\n        \"publishAndClose\": \"Opublikuj i zamknij\",\n        \"publishPost\": \"Opublikuj wpis\",\n        \"reloadFile\": \"Wczytaj ponownie plik\",\n        \"renderFrontPageOnly\": \"Renderuj tylko stronę główną\",\n        \"renderFullWebsite\": \"Renderuj całą stronę\",\n        \"reportBugInSupportDesk\": \"Zgłoś błąd na forum\",\n        \"reportIssue\": \"Zgłoś problem\",\n        \"sameWindow\": \"Tym samym oknie\",\n        \"save\": \"Zapisz\",\n        \"saveAndClose\": \"Zapisz i zamknij\",\n        \"saveAndPreview\": \"Zapisz i podejrzyj\",\n        \"saveAndRender\": \"Zapisz i renderuj\",\n        \"saveAsDraft\": \"Zapisz jako szkic\",\n        \"saveChanges\": \"Zapisz zmiany\",\n        \"saveDraft\": \"Zapisz szkic\",\n        \"saveDraftAndClose\": \"Zapisz szkic i zamknij\",\n        \"search\": \"Szukaj...\",\n        \"selectOption\": \"Wybierz opcję\",\n        \"selectWebsite\": \"Wybierz witrynę\",\n        \"seo\": \"SEO\",\n        \"server\": \"Serwer\",\n        \"show\": \"Pokaż\",\n        \"slug\": \"Slug\",\n        \"supportPublii\": \"Wspieraj Publii i przekaż darowiznę już dzisiaj!\",\n        \"tags\": \"Tagi\",\n        \"theme\": \"Motywy\",\n        \"tools\": \"Narzędzia & Pluginy\",\n        \"unhide\": \"Odkryj\",\n        \"updateSlug\": \"Zaktualizuj slug\",\n        \"uploadInProgress\": \"Trwa przesyłanie...\",\n        \"whatsNew\": \"Co się zmieniło?\"\n    }\n}\n"
  },
  {
    "path": "app/default-files/default-languages/pl/wysiwyg.json",
    "content": "{\n    \"Action\": \"Akcja\",\n    \"Activity\": \"Aktywność\",\n    \"Address\": \"Adres\",\n    \"Advanced\": \"Zaawansowane\",\n    \"Align\": \"Wyrównaj\",\n    \"Align center\": \"Wyrównaj do środka\",\n    \"Align left\": \"Wyrównaj do lewej\",\n    \"Align right\": \"Wyrównaj do prawej\",\n    \"Alignment\": \"Wyrównanie\",\n    \"All\": \"Wszystkie\",\n    \"Alternative source\": \"Alternatywne źródło\",\n    \"Alternative source URL\": \"Alternatywny URL źródła\",\n    \"Anchor\": \"Kotwica\",\n    \"Anchor...\": \"Kotwica\",\n    \"Anchors\": \"Kotwice\",\n    \"Animals and Nature\": \"Zwierzęta i natura\",\n    \"Arrows\": \"Strzałki\",\n    \"B\": \"B\",\n    \"Background color\": \"Kolor tła\",\n    \"Black\": \"Czarny\",\n    \"Block\": \"Zablokuj\",\n    \"Blockquote\": \"Blok cytatu\",\n    \"Blue\": \"Niebieski\",\n    \"Body\": \"Treść\",\n    \"Bold\": \"Pogrubienie\",\n    \"Border\": \"Ramka\",\n    \"Border color\": \"Kolor ramki\",\n    \"Border style\": \"Styl ramki\",\n    \"Border width\": \"Grubość ramki\",\n    \"Bottom\": \"Dół\",\n    \"Browse for an image\": \"Przeglądaj zdjęcia\",\n    \"Bullet list\": \"Lista wypunktowana\",\n    \"Cancel\": \"Anuluj\",\n    \"Capitalization\": \"Jak w zdaniu\",\n    \"Caption\": \"Tytuł\",\n    \"Cell\": \"Komórka\",\n    \"Cell padding\": \"Dopełnienie komórki\",\n    \"Cell properties\": \"Właściwości komórki\",\n    \"Cell spacing\": \"Odstępy komórek\",\n    \"Cell type\": \"Typ komórki\",\n    \"Center\": \"Środek\",\n    \"Characters\": \"Znaki\",\n    \"Characters (no spaces)\": \"Znaki (bez spacji)\",\n    \"Circle\": \"Kółko\",\n    \"Class\": \"Klasa CSS\",\n    \"Clear formatting\": \"Wyczyść formatowanie\",\n    \"Close\": \"Zamknij\",\n    \"Code\": \"Kod\",\n    \"Code sample\": \"Przykład kodu źródłowego\",\n    \"Color\": \"Kolor\",\n    \"Color Picker\": \"Selektor kolorów\",\n    \"Color swatch\": \"Próbka koloru\",\n    \"Cols\": \"Kol.\",\n    \"Column\": \"Kolumna\",\n    \"Column group\": \"Grupa kolumn\",\n    \"Constrain proportions\": \"Zachowaj proporcje\",\n    \"Copy\": \"Kopiuj\",\n    \"Copy row\": \"Kopiuj wiersz\",\n    \"Could not find the specified string.\": \"Nie znaleziono poszukiwanego tekstu.\",\n    \"Could not load emoticons\": \"Nie można załadować emoji\",\n    \"Count\": \"Liczba\",\n    \"Currency\": \"Waluta\",\n    \"Current window\": \"Bieżące okno\",\n    \"Custom color\": \"Kolor niestandardowy\",\n    \"Custom...\": \"Niestandardowy...\",\n    \"Cut\": \"Wytnij\",\n    \"Cut row\": \"Wytnij wiersz\",\n    \"Dark Blue\": \"Ciemnoniebieski\",\n    \"Dark Gray\": \"Ciemnoszary\",\n    \"Dark Green\": \"Ciemnozielony\",\n    \"Dark Orange\": \"Ciemnopomarańczowy\",\n    \"Dark Purple\": \"Ciemnopurpurowy\",\n    \"Dark Red\": \"Ciemnoczerwony\",\n    \"Dark Turquoise\": \"Ciemnoturkusowy\",\n    \"Dark Yellow\": \"Ciemnożółty\",\n    \"Date/time\": \"Data/Czas\",\n    \"Decrease indent\": \"Zmniejsz wcięcie\",\n    \"Default\": \"Domyślne\",\n    \"Delete column\": \"Usuń kolumnę\",\n    \"Delete row\": \"Usuń wiersz\",\n    \"Delete table\": \"Usuń tabelę\",\n    \"Dimensions\": \"Wymiary\",\n    \"Disc\": \"Dysk\",\n    \"Div\": \"Div\",\n    \"Document\": \"Dokument\",\n    \"Document properties\": \"Właściwości dokumentu\",\n    \"Drop an image here\": \"Upuść obraz tutaj\",\n    \"Edit\": \"Edycja\",\n    \"Edit image\": \"Edytuj obrazek\",\n    \"Embed\": \"Osadź\",\n    \"Emoticons\": \"Emoji\",\n    \"Emoticons...\": \"Emoji\",\n    \"Error\": \"Błąd\",\n    \"Extended Latin\": \"Rozszerzony łaciński\",\n    \"Failed to upload image: {0}\": \"Nie udało się przesłać obrazu: {0}\",\n    \"Find\": \"Znajdź\",\n    \"Find and replace\": \"Znajdź i zamień\",\n    \"Find and replace...\": \"Znajdź i zamień...\",\n    \"Find whole words only\": \"Znajdź tylko całe wyrazy\",\n    \"Finish\": \"Zakończ\",\n    \"Flags\": \"Flagi\",\n    \"Font\": \"Font\",\n    \"Font Sizes\": \"Rozmiar fontu\",\n    \"Fonts\": \"Fonty\",\n    \"Food and Drink\": \"Jedzenie i picie\",\n    \"Footer\": \"Stopka\",\n    \"Format\": \"Format\",\n    \"Format Painter\": \"Malarz formatów\",\n    \"Formats\": \"Formaty\",\n    \"Fullscreen\": \"Pełny ekran\",\n    \"G\": \"G\",\n    \"General\": \"Ogólne\",\n    \"Gray\": \"Szary\",\n    \"Green\": \"Zielony\",\n    \"H Align\": \"Wyrównanie w pionie\",\n    \"Header\": \"Nagłówek\",\n    \"Header 1\": \"Nagłówek H1\",\n    \"Header 2\": \"Nagłówek H2\",\n    \"Header 3\": \"Nagłówek H3\",\n    \"Header 4\": \"Nagłówek H4\",\n    \"Header 5\": \"Nagłówek H5\",\n    \"Header 6\": \"Nagłówek H6\",\n    \"Header cell\": \"Komórka nagłówka\",\n    \"Headers\": \"Nagłówki\",\n    \"Heading 1\": \"Nagłówek H1\",\n    \"Heading 2\": \"Nagłówek H2\",\n    \"Heading 3\": \"Nagłówek H3\",\n    \"Heading 4\": \"Nagłówek H4\",\n    \"Heading 5\": \"Nagłówek H5\",\n    \"Heading 6\": \"Nagłówek H6\",\n    \"Headings\": \"Nagłówki\",\n    \"Height\": \"Wysokość\",\n    \"Help\": \"Pomoc\",\n    \"Horizontal line\": \"Pozioma linia\",\n    \"Horizontal space\": \"Odstęp poziomy\",\n    \"Id\": \"Identyfikator\",\n    \"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.\": \"Identyfikator powinien zaczynać się zawsze literą, dozwolone są litery, numery, ukośniki, kropki, dwukropki i podłogi\",\n    \"Ignore\": \"Ignoruj\",\n    \"Ignore all\": \"Ignoruj wszystko\",\n    \"Image\": \"Obraz\",\n    \"Image description\": \"Opis obrazka\",\n    \"Image list\": \"Lista obrazków\",\n    \"Image options\": \"Opcje obrazu\",\n    \"Image title\": \"Tytuł obrazu\",\n    \"Image...\": \"Obraz\",\n    \"Increase indent\": \"Zwiększ wcięcie\",\n    \"Inline\": \"W tekście\",\n    \"Insert\": \"Wstaw\",\n    \"Insert column after\": \"Wstaw kolumnę po\",\n    \"Insert column before\": \"Wstaw kolumnę przed\",\n    \"Insert date/time\": \"Wstaw datę/czas\",\n    \"Insert image\": \"Wstaw obrazek\",\n    \"Insert link\": \"Wstaw link\",\n    \"Insert row after\": \"Wstaw wiersz po\",\n    \"Insert row before\": \"Wstaw wiersz przed\",\n    \"Insert table\": \"Wstaw tabelę\",\n    \"Insert video\": \"Wstaw wideo\",\n    \"Insert/Edit Link\": \"Wstaw/Edytuj link\",\n    \"Insert/edit iframe\": \"Wstaw/edytuj iframe\",\n    \"Insert/edit image\": \"Wstaw lub zmień obrazek\",\n    \"Insert/edit link\": \"Wstaw/edytuj link\",\n    \"Insert/edit media\": \"Wstaw/Edytuj media\",\n    \"Insert/edit video\": \"Wstaw/edytuj wideo\",\n    \"Italic\": \"Kursywa\",\n    \"Justify\": \"Wyjustuj\",\n    \"Keyboard Navigation\": \"Nawigacja za pomocą klawiatury\",\n    \"Language\": \"Język\",\n    \"Left\": \"Lewo\",\n    \"Left to right\": \"Od lewej do prawej\",\n    \"Light Blue\": \"Jasnoniebieski\",\n    \"Light Gray\": \"Jasnoszary\",\n    \"Light Green\": \"Jasnozielony\",\n    \"Light Purple\": \"Jasnopurpurowy\",\n    \"Light Red\": \"Jasnoczerwony\",\n    \"Light Yellow\": \"Jasnożółty\",\n    \"Link\": \"Adres linku\",\n    \"Link list\": \"Lista linków\",\n    \"Link...\": \"Link...\",\n    \"Loading emoticons...\": \"Ładowanie emoji...\",\n    \"Lower Alpha\": \"Małe litery\",\n    \"Lower Greek\": \"Małe greckie\",\n    \"Lower Roman\": \"Małe rzymskie\",\n    \"Match case\": \"Dopasuj wielkość liter\",\n    \"Mathematical\": \"Matematyczne\",\n    \"Media\": \"Media\",\n    \"Media poster (Image URL)\": \"Okładka (URL obrazu)\",\n    \"Media...\": \"Multimedia...\",\n    \"Medium Blue\": \"średnioniebieski\",\n    \"Medium Gray\": \"Średnioszary\",\n    \"Medium Purple\": \"średniopurpurowy\",\n    \"Merge cells\": \"Połącz komórki\",\n    \"Middle\": \"Środek\",\n    \"Midnight Blue\": \"Nocny błękit\",\n    \"More...\": \"Więcej...\",\n    \"Name\": \"Nazwa\",\n    \"Navy Blue\": \"Ciemnoniebieski\",\n    \"New window\": \"Nowe okno\",\n    \"Next\": \"Nast.\",\n    \"No\": \"Nie\",\n    \"No color\": \"Bez koloru\",\n    \"Nonbreaking space\": \"Niełamliwa spacja\",\n    \"None\": \"Brak\",\n    \"Numbered list\": \"Lista numerowana\",\n    \"OR\": \"LUB\",\n    \"Objects\": \"Obiekty\",\n    \"Ok\": \"Ok\",\n    \"Open help dialog\": \"Otwórz okno dialogowe pomocy\",\n    \"Open link in...\": \"Otwórz link w...\",\n    \"Orange\": \"Pomarańczowy\",\n    \"Page break\": \"Podział strony\",\n    \"Paragraph\": \"Akapit\",\n    \"Paste\": \"Wklej\",\n    \"Paste as text\": \"Wklej jako zwykły tekst\",\n    \"Paste or type a link\": \"Wklej lub wpisz adres linka\",\n    \"Paste row after\": \"Wklej wiersz po\",\n    \"Paste row before\": \"Wklej wiersz przed\",\n    \"Paste your embed code below:\": \"Wklej tutaj kod do osadzenia:\",\n    \"People\": \"Ludzie\",\n    \"Permanent Pen Properties\": \"Właściwości markera\",\n    \"Permanent pen properties...\": \"Właściwości markera...\",\n    \"Poster\": \"Okładka\",\n    \"Powered by {0}\": \"Powered by {0}\",\n    \"Pre\": \"Tekst preformatowany\",\n    \"Preferences\": \"Ustawienia\",\n    \"Preformatted\": \"Wstępne formatowanie\",\n    \"Prev\": \"Poprz.\",\n    \"Preview\": \"Podgląd\",\n    \"Previous\": \"Poprzedni\",\n    \"Print\": \"Drukuj\",\n    \"Print...\": \"Drukuj...\",\n    \"Purple\": \"Purpurowy\",\n    \"Quotations\": \"Cudzysłowy\",\n    \"R\": \"R\",\n    \"Red\": \"Czerwony\",\n    \"Redo\": \"Powtórz\",\n    \"Remove color\": \"Usuń kolor\",\n    \"Remove link\": \"Usuń link\",\n    \"Replace\": \"Zamień\",\n    \"Replace all\": \"Zamień wszystko\",\n    \"Replace with\": \"Zamień na\",\n    \"Restore last draft\": \"Przywróć ostatni szkic\",\n    \"Rich Text Area. Press ALT-0 for help.\": \"Obszar tekstu sformatowanego. Naciśnij ALT-0, aby uzyskać pomoc.\",\n    \"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help\": \"Obszar Edycji. ALT-F9 - menu. ALT-F10 - pasek narzędzi. ALT-0 - pomoc\",\n    \"Right\": \"Prawo\",\n    \"Right to left\": \"Od prawej do lewej\",\n    \"Row\": \"Wiersz\",\n    \"Row group\": \"Grupa wierszy\",\n    \"Row properties\": \"Właściwości wiersza\",\n    \"Row type\": \"Typ wiersza\",\n    \"Rows\": \"Wiersz.\",\n    \"Save\": \"Zapisz\",\n    \"Scope\": \"Kontekst\",\n    \"Search\": \"Wyszukaj\",\n    \"Select all\": \"Zaznacz wszystko\",\n    \"Select...\": \"Wybierz...\",\n    \"Selection\": \"Zaznaczenie\",\n    \"Shortcut\": \"Skrót\",\n    \"Show caption\": \"Pokaż podpis\",\n    \"Show invisible characters\": \"Pokaż niewidoczne znaki\",\n    \"Size\": \"Rozmiar\",\n    \"Source\": \"Kod HTML\",\n    \"Source code\": \"Kod źródłowy\",\n    \"Special character\": \"Znak specjalny\",\n    \"Special character...\": \"Znak specjalny\",\n    \"Spellcheck\": \"Sprawdzanie pisowni\",\n    \"Split cell\": \"Podziel komórki\",\n    \"Square\": \"Kwadrat\",\n    \"Strikethrough\": \"Przekreślenie\",\n    \"Style\": \"Styl\",\n    \"Subscript\": \"Indeks dolny\",\n    \"Superscript\": \"Indeks górrny\",\n    \"Switch to or from fullscreen mode\": \"Włącz lub wyłącz tryb pełnoekranowy\",\n    \"Symbols\": \"Symbole\",\n    \"System Font\": \"Font systemowy\",\n    \"Table\": \"Tabela\",\n    \"Table of Contents\": \"Spis treści\",\n    \"Table properties\": \"Właściwości tabeli\",\n    \"Target\": \"Cel\",\n    \"Text\": \"Tekst\",\n    \"Text color\": \"Kolor tekstu\",\n    \"Text to display\": \"Tekst do wyświetlenia\",\n    \"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?\": \"Wprowadzony adres wygląda na adres e-mail. Czy chcesz dodać mailto: jako prefiks?\",\n    \"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?\": \"Wprowadzony adres wygląda na link zewnętrzny. Czy chcesz dodać http:// jako prefiks?\",\n    \"Title Case\": \"Jak Nazwy Własne\",\n    \"To open the popup, press Shift+Enter\": \"Aby otworzyć okienko, naciśnij Shift+Enter\",\n    \"Tools\": \"Narzędzia\",\n    \"Top\": \"Góra\",\n    \"Travel and Places\": \"Podróże i miejsca\",\n    \"Turquoise\": \"Turkusowy\",\n    \"UPPERCASE\": \"WIELKIE LITERY\",\n    \"Underline\": \"Podkreślenie\",\n    \"Undo\": \"Cofnij\",\n    \"Update\": \"Aktualizuj\",\n    \"Upload\": \"Prześlij\",\n    \"Upper Alpha\": \"Wielkie litery\",\n    \"Upper Roman\": \"Wielkie rzymskie\",\n    \"Url\": \"URL\",\n    \"User Defined\": \"Własny\",\n    \"V Align\": \"Wyrównanie w poziomie\",\n    \"Valid\": \"Prawidłowe\",\n    \"Version\": \"Wersja\",\n    \"Vertical space\": \"Odstęp pionowy\",\n    \"View\": \"Widok\",\n    \"Warn\": \"Ostrzeżenie\",\n    \"White\": \"Biały\",\n    \"Whole words\": \"Całe słowa\",\n    \"Width\": \"Szerokość\",\n    \"Word count\": \"Liczba słów\",\n    \"Words\": \"Słowa\",\n    \"Words: {0}\": \"Słów: {0}\",\n    \"Yellow\": \"Żółty\",\n    \"Yes\": \"Tak\",\n    \"You have unsaved changes are you sure you want to navigate away?\": \"Masz niezapisane zmiany. Czy na pewno chcesz opuścić ten widok?\",\n    \"alignment\": \"wyrównanie\",\n    \"comments\": \"komentarze\",\n    \"example\": \"przykład\",\n    \"formatting\": \"formatowanie\",\n    \"history\": \"historia\",\n    \"indentation\": \"wcięcie\",\n    \"lowercase\": \"małe litery\",\n    \"permanent pen\": \"marker\",\n    \"styles\": \"style\",\n    \"{0} characters\": \"{0} znaków\",\n    \"{0} words\": \"{0} słów\"\n}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/404.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"page page--error\">\n  <div class=\"content\">\n      <div class=\"hero\">\n         <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n            <div class=\"wrapper\">\n               <h1>{{ translate 'error.404' }}</h1>\n\n               <div class=\"page__desc\">\n                  <p>{{ translate 'error.message' }}</p>\n               </div>\n               \n               <a href=\"{{@website.url}}\" class=\"btn hero__cta btn--icon\">\n                  <svg width=\"18\" height=\"18\" aria-hidden=\"true\">\n                     <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-prev\"/>\n                  </svg>\n                  <span>{{ translate 'error.button' }}</span>\n               </a>\n            </div>\n         </header>\n      </div>\n   </div>\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/CHANGELOG.md",
    "content": "# Changelog\n\n## [3.2.1.0] - 2026-01-06\n### Fixed\n- Fixed an issue where the search popup would not open if the input element was missing\n\n## [3.2.0.0] - 2025-11-04\n### Added\n- Added new variable fonts\n### Improved\n- Improved videos/iframe handling\n\n## [3.1.4.0] - 2025-10-15\n### Improved\n- Adjust textarea gap and author date margin for improved layout\n\n## [3.1.3.0] - 2025-04-26\n### Added\n- Added support for post prefix page. A dedicated posts.hbs template is now used when a post prefix path (e.g., /blog/) is defined, instead of falling back to the homepage index.hbs. This improves consistency and provides a clearer separation between homepage and post prefix listings.\n\n## [3.1.2.0] - 2025-02-08\n### Improved\n- Enhanced post image styling (margins) and add blog index body class\n### Removed\n- Removed unnecessary onclick attribute from back to top button in footer\n\n## [3.1.1.0] - 2025-01-21\n### Improved\n- Some UI improvements\n### Fixed\n- Resolved issue with horizontal scrollbar on Windows OS,\n### Removed\n- Removed unnecessary gallery CSS rule \n\n## [3.1.0.0] - 2024-12-10\n### Removed\n- Removed built-in gallery to support external gallery plugins\n\n## [3.0.0.0] - 2024-07-07\n### Added\n- Added support for italic fonts \n- Redesigned the theme\n\n## [2.9.0.0] - 2024-06-30\n### Added\n- Added support for Pages\n- Added new typography options: now you can adjust letter spacing or line height for both body and heading elements.\n- Added a new baseline option to defines a core vertical rhythm unit for consistent spacing across the website\n\n### Improved\n- Minor CSS enhancements\n\n## [2.8.3.0] - 2023-11-03\n### Fixed\n- Resolved issue with missing the responsive thumbnails on the post page\n\n## [2.8.2.0] - 2023-10-25\n### Fixed\n- Resolved checkIf function issue caused by parsing error in Handlebars, occurring when SocialSharing plugin was missing\n### Updated\n- Updated X Twitter icon\n\n## [2.8.1.0] - 2023-10-21\n### Fixed\n- Fixed the CSS error related to the missing ‘gray-2’ CSS variable\n\n## [2.8.0.0] - 2023-10-20\n**Note:** The new features and enhancements require at least Publii version 0.43.1!\n### Added\n- Added a ‘customSocialSharing’ custom HTML position to support the Social Sharing plugin\n\n## [2.7.0.0] - 2023-10-03\n### Added\n- Added more variable fonts\n### Improved\n- Made minor CSS improvements\n\n## [2.6.2.0] - 2023-04-17\n### Added\n- Added option to disable tag counter on Tags page\n### Improved\n- Minor CSS enhancements\n\n## [2.6.1.0] - 2023-04-03\n### Changed\n- Reorganized and renamed theme options to improve navigation and readability\n\n## [2.6.0.0] - 2023-03-29\n**Note:** The new features and enhancements require at least Publii version 0.42.x!\n### Added\n- Implemented support for new tag and author options\n- Added support for new extended menu assignment options\n- Removed AMP-related files\n- Added the NoScript tag to handle images when JavaScript is disabled\n### Improved\n- Made minor CSS improvements\n\n## [2.5.0.0] - 2022-07-07\n**Note:** The new features and enhancements require at least Publii version 0.40.x!\n### Added\n- Added support for Search plugins\n- Added a script to calculate the aspect ratio of the iframes automatically\n- Added support for Emended Content Consent\n- Updated menu script\n- Updated supportedFeatures section\n\n## [2.4.1.0] - 2022-03-20\n### Adjusted\n- Slight Dark mode CSS adjustment\n\n## [2.4.0.0] - 2022-03-16\n**Note:** The new features and enhancements require at least Publii version 0.39.x!\n### Changed\n- Changed how Google fonts are handled, from loading them from a CDN to hosting them locally\n### Added\n- Added Dark Mode (now it supports light, dark, and auto mode)\n- Added support for Comment plugins\n- Updated Facebook social icon\n- Updated avatar code markup\n- Changed the way the “Back to top” button works\n\n## [2.3.3.0] - 2021-11-07\n### Fixed\n- Fixed Photoswipe script to open gallery image in the pop-up window directly by firing image URL\n- Updated Disqus script (now uses an Intersection Observer to detect / load comment section)\n- Updated menu script\n### Improved\n- Improved accessibility of the navigation menu\n### Changed\n- Changed the way CSS variables work in the theme (now uses theme-variables.js)\n- Small CSS improvements\n\n## [2.3.0.0] - 2020-09-25\n**Note:** The new features and enhancements require Publii version 0.37.x!\n### Added\n- Added support for tags page\n- Added support for tag featured image\n- Added support for author featured image\n- Added support for author website field\n- Added supported features flags\n- Added option to define the number of related posts\n- Updated Disqus script\n\n## [2.2.3.0] - 2020-06-16\n### Updated\n- Updated menu script to support the anchors in mobile view\n### Fixed\n- Fixed mobile menu styling\n\n## [2.2.2.0] - 2020-06-04\n### Added\n- Added aria-label to the search input\n### Fixed\n- Fixed zoom/in/out gallery option\n- Updated font.hbs file\n- Fixed WhatsApp share button\n\n## [2.2.1.0] - 2020-05-25\n**Note:** Before installing, make sure you have installed Publii at least version 0.36.0! If you need to keep using Publii 0.35.3 you can always download the previous version of the theme from [here](https://cdn.getpublii.com/themes/simple_2.1.0.0.zip).\n### Added\n- Added support for „Enable Responsive Images” option\n- Added support for native Lazy Loading\n### Adjusted\n- Typo adjustment\n### Updated\n- Updated Photoswipe gallery to 4.1.3 version\n### Fixed\n- Fixed gallery UI buttons behavior on hover\n### Rewritten\n- Rewritten to use CSS variables\n\n## [2.1.0.0] - 2020-01-13\n### Added\n- Added Block editor support\n\n## [2.0.4.0] - 2019-11-27\n**Note:** Before installing, make sure you have installed Publii at least version 35.3\n### Added\n- Added support for better display of SVG images\n\n## [2.0.3.0] - 2019-08-04\n### Optimized\n- Optimized image lazyload for the smoothest, fastest experience possible\n### Fixed\n- Fixed page scrolling when the mobile menu is opened (iOS only)\n### Added\n- Added WhatsApp share button\n\n## [2.0.2.1] - 2019-05-25\n### Fixed\n- Fixed display of the image in the post content\n\n## [2.0.2.0] - 2019-05-13\n### Improved\n- Improved the way the first menu level is displayed; now the menu items break down on the next line when it is needed\n### Fixed\n- Fixed the hero section by removing the space between the image and the right side of the browser window\n\n## [2.0.1.0] - 2019-05-09\n### Rewritten\n- A minor redesign, with a rewritten code to make it more efficient\n### Added\n- An option for displaying a featured image on post list pages\n- Overhauled menu system! New scripts, two mobile menu styles (Sidebar and overlay), and a reactive submenu that shifts position when it gets too close to the edge of the browser window\n- Responsive iframes (for things like videos, maps etc…)\n- No more jQuery; now Simple uses Vanilla scripts\n- Expanded hero section controls\n- New font selection options\n- New ‘Back to Top’ option\n- A new gallery style and optimized image lazyload for the smoothest, fastest experience possible\n\n## [1.7.2.0] - 2019-03-28\n### Added\n- Added option to change the overlay of hero section; now it supports gradient or solid color\n\n## [1.7.1.0] - 2019-03-25\n### Removed\n- If the menu is unassigned to any menu position, its HTML markup is no longer displayed\n- Removed Google+ service due to its shutdown on April 2\n\n## [1.7.0.0] - 2019-03-02\n### Changed\n- Gallery styling has been changed\n### Centered\n- Centered caption in a gallery lightbox\n### Fixed\n- Fixed pagination ordering\n- Fixed Pinterest share button\n\n## [1.6.1.0] - 2018-12-12\n### Added\n- Added support for a table of content\n- Updated the lazysizes script to v. 1.4.5\n- Added preload option to an image gallery\n- jQuery JavaScript library is now served locally\n\n## [1.6.0.0] - 2018-10-22\n### Changed\n- Changed the CSS style of the <hr> tag\n- Moved share icons to the bottom of article\n- Updated StumbleUpon share button, now it’s Mix.com\n### Fixed\n- Fixed the image logo, now it looks well on the mobile devices too\n### Removed\n- Removed the lazyload from the hero image to speed up its loading\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/css/editor.css",
    "content": "/* \n * Add your own CSS code for the WYSIWYG editor \n */\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/css/main.css",
    "content": "*,\n*:before,\n*:after {\n  -webkit-box-sizing: border-box;\n          box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\narticle,\naside,\nfooter,\nheader,\nhgroup,\nmain,\nnav,\nsection {\n  display: block;\n}\n\nli {\n  list-style: none;\n}\n\nimg {\n  height: auto;\n  max-width: 100%;\n  vertical-align: top;\n}\n\nbutton,\ninput,\nselect,\ntextarea {\n  font: inherit;\n}\n\naddress {\n  font-style: normal;\n}\n\n::-moz-selection {\n  background: var(--color);\n  color: var(--white);\n}\n\n::selection {\n  background: var(--color);\n  color: var(--white);\n}\n\n:focus-visible {\n  outline: 2px solid var(--color) !important;\n  outline-offset: 2px;\n}\n\nhtml {\n  font-size: var(--font-size);\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  scroll-behavior: smooth;\n  scrollbar-gutter: stable;\n}\nhtml.no-scroll {\n  overflow: hidden;\n  position: fixed;\n}\n\nbody {\n  background: var(--page-bg);\n  color: var(--text-color);\n  font-family: var(--body-font);\n  font-variation-settings: \"wght\" var(--font-weight-normal);\n  letter-spacing: var(--letter-spacing);\n  line-height: var(--line-height);\n  -ms-scroll-chaining: none;\n      overscroll-behavior: none;\n}\n\na {\n  text-decoration: none;\n}\na {\n  color: var(--link-color);\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n}\na:hover {\n  color: var(--link-color-hover);\n}\na:active {\n  color: var(--link-color-hover);\n}\na:focus {\n  outline: none;\n}\n\n.invert {\n  color: var(--link-color-hover);\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n}\n.invert:hover {\n  color: var(--link-color);\n}\n.invert:active {\n  color: var(--link-color);\n}\n.invert:focus {\n  outline: none;\n}\n\np,\nul,\nol,\ndl {\n  margin-top: calc(var(--baseline) * 4 + 0.25vw);\n}\n\nblockquote,\nfigure,\nhr,\npre,\ntable {\n  margin-top: calc(var(--baseline) * 6 + 0.5vw);\n  margin-bottom: calc(var(--baseline) * 6 + 0.5vw);\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  color: var(--headings-color);\n  font-family: var(--heading-font);\n  font-variation-settings: \"wght\" var(--headings-weight);\n  font-style: var(--headings-style);\n  -ms-hyphens: manual;\n      hyphens: manual;\n  letter-spacing: var(--headings-letter-spacing);\n  line-height: var(--headings-line-height);\n  margin-top: calc(var(--baseline) * 6 + 1vw);\n  text-transform: var(--headings-transform);\n}\n\nh1,\n.h1 {\n  font-size: clamp(1.5710900065rem, 1.5710900065rem + 1.424540906 * (100vw - 20rem) / 70, 2.9956309125rem);\n  font-family: var(--heading-font);\n}\n\nh2,\n.h2 {\n  font-size: clamp(1.3808408252rem, 1.3808408252rem + 0.9332127447 * (100vw - 20rem) / 70, 2.3140535699rem);\n}\n\nh3,\n.h3 {\n  font-size: clamp(1.2136296308rem, 1.2136296308rem + 0.4621997101 * (100vw - 20rem) / 70, 1.6758293408rem);\n}\n\nh4,\n.h4 {\n  font-size: clamp(1.1377777785rem, 1.1377777785rem + 0.1567604947 * (100vw - 20rem) / 70, 1.2945382732rem);\n}\n\nh5,\n.h5 {\n  font-size: clamp(1.066666667rem, 1.066666667rem + 0.0711111115 * (100vw - 20rem) / 70, 1.1377777785rem);\n}\n\nh6,\n.h6 {\n  font-size: clamp(1rem, 1rem + 0 * (100vw - 20rem) / 70, 1rem);\n}\n\nh2 + *,\nh3 + *,\nh4 + *,\nh5 + *,\nh6 + * {\n  margin-top: calc(var(--baseline) * 2 + 0.25vw);\n}\n\nb,\nstrong {\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n}\n\nblockquote {\n  border-top: 2px solid var(--dark);\n  border-bottom: 2px solid var(--dark);\n  color: var(--headings-color);\n  font-family: var(--heading-font);\n  font-style: italic;\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  padding: calc(var(--baseline) * 6 + 1vw) 2rem;\n  font-size: clamp(1.1377777785rem, 1.1377777785rem + 0.1567604947 * (100vw - 20rem) / 70, 1.2945382732rem);\n}\nblockquote > :nth-child(1) {\n  margin-top: 0;\n}\n\nul,\nol {\n  margin-left: 3ch;\n}\nul > li,\nol > li {\n  list-style: inherit;\n  padding: 0 0 var(--baseline) 1ch;\n}\n\ndl dt {\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n}\n\npre {\n  background-color: var(--lighter);\n  font-size: 0.8239746086rem;\n  padding: calc(var(--baseline) * 6);\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\npre > code {\n  color: var(--text-color);\n  display: inline-block;\n  font-size: inherit;\n  padding: 0;\n}\n\ncode {\n  background-color: var(--lighter);\n  color: var(--color);\n  font-size: 0.8239746086rem;\n  font-family: Menlo, Monaco, Consolas, Courier New, monospace;\n}\n\ntable {\n  border: 1px solid var(--light);\n  border-collapse: collapse;\n  border-spacing: 0;\n  vertical-align: top;\n  text-align: left;\n  width: 100%;\n}\n@media all and (max-width: 37.4375em) {\n  table {\n    display: block;\n    overflow-x: auto;\n  }\n}\ntable th {\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  padding: calc(var(--baseline) * 2.5) calc(var(--baseline) * 4);\n}\ntable td {\n  border-top: 1px solid var(--light);\n  padding: calc(var(--baseline) * 2.5) calc(var(--baseline) * 4);\n}\n\n.table-striped tr:nth-child(2n) {\n  background: var(--lighter);\n}\n.table-bordered th,\n.table-bordered td {\n  border: 1px solid var(--light);\n}\n.table-title th {\n  background: var(--lighter);\n}\n\nfigcaption {\n  clear: both;\n  color: var(--gray);\n  font-style: italic;\n  font-size: 0.7241964329rem;\n  padding: calc(var(--baseline) * 3) 0 0;\n  text-align: center;\n}\n\nkbd {\n  background: var(--dark);\n  border-radius: 2px;\n  color: var(--white);\n  font-family: Menlo, Monaco, Consolas, Courier New, monospace;\n  font-size: 0.8789062495rem;\n  padding: calc(var(--baseline) * 0.5) calc(var(--baseline) * 1.5);\n}\n\nsub,\nsup {\n  font-size: 65%;\n}\n\nsmall {\n  font-size: 0.8789062495rem;\n}\n\nhr,\n.separator {\n  background: none;\n  border: none;\n  height: auto;\n  line-height: 1;\n  max-width: none;\n  text-align: center;\n}\nhr::before,\n.separator::before {\n  content: \"•••\";\n  color: var(--dark);\n  font-size: 1rem;\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  letter-spacing: 1.1377777785rem;\n  padding-left: 1.1377777785rem;\n}\n\n.separator--dot::before {\n  content: \"•\";\n  color: var(--dark);\n  font-size: 1rem;\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  letter-spacing: 1.1377777785rem;\n  padding-left: 1.1377777785rem;\n}\n.separator--long-line {\n  position: relative;\n}\n.separator--long-line::before {\n  content: \"\";\n  height: 1rem;\n}\n.separator--long-line::after {\n  border-top: 1px solid var(--light);\n  content: \"\";\n  height: 1px;\n  position: absolute;\n  width: 100%;\n  top: 50%;\n  left: 0;\n}\n\n.btn, [type=button],\n[type=submit],\nbutton {\n  align-items: center;\n  background: none;\n  border: 1px solid var(--dark);\n  border-radius: calc(var(--border-radius) * 10);\n  color: var(--dark);\n  cursor: pointer;\n  display: inline-flex;\n  font-family: var(--menu-font);\n  font-size: 0.8239746086rem;\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  overflow: hidden;\n  padding: calc(var(--baseline) * 2) calc(var(--baseline) * 4);\n  text-align: center;\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n  vertical-align: middle;\n  will-change: transform;\n}\n@media all and (max-width: 19.9375em) {\n  .btn, [type=button],\n  [type=submit],\n  button {\n    width: 100%;\n  }\n}\n.btn:hover, [type=button]:hover,\n[type=submit]:hover,\nbutton:hover, .btn:active, [type=button]:active,\n[type=submit]:active,\nbutton:active, .btn:focus, [type=button]:focus,\n[type=submit]:focus,\nbutton:focus {\n  background-color: var(--dark);\n  color: var(--helper);\n}\n.btn--icon {\n  gap: 0.3rem;\n  justify-content: center;\n}\n.btn--icon svg {\n  stroke: currentColor;\n}\n@media all and (min-width: 20em) {\n  .btn + .btn, [type=button] + .btn,\n  [type=submit] + .btn,\n  button + .btn, .btn + [type=button], [type=button] + [type=button],\n  [type=submit] + [type=button],\n  button + [type=button],\n  .btn + [type=submit],\n  [type=button] + [type=submit],\n  [type=submit] + [type=submit],\n  button + [type=submit],\n  .btn + button,\n  [type=button] + button,\n  [type=submit] + button,\n  button + button {\n    margin-left: calc(var(--baseline) * 2);\n  }\n}\n@media all and (max-width: 37.4375em) {\n  .btn + .btn, [type=button] + .btn,\n  [type=submit] + .btn,\n  button + .btn, .btn + [type=button], [type=button] + [type=button],\n  [type=submit] + [type=button],\n  button + [type=button],\n  .btn + [type=submit],\n  [type=button] + [type=submit],\n  [type=submit] + [type=submit],\n  button + [type=submit],\n  .btn + button,\n  [type=button] + button,\n  [type=submit] + button,\n  button + button {\n    margin-bottom: calc(var(--baseline) * 2);\n  }\n}\n.btn:disabled, [type=button]:disabled,\n[type=submit]:disabled,\nbutton:disabled, .btn[disabled], [disabled][type=button],\n[disabled][type=submit],\nbutton[disabled] {\n  background-color: var(--light);\n  border-color: var(--light);\n  color: var(--gray);\n  cursor: not-allowed;\n  pointer-events: none;\n}\n\n[type=button],\n[type=submit],\nbutton {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n}\n\n::-webkit-search-cancel-button {\n  -webkit-appearance: none;\n}\n\nfieldset {\n  border: 1px solid var(--light);\n  margin: calc(var(--baseline) * 6 + 1vw) 0 0;\n  padding: calc(var(--baseline) * 6);\n}\nfieldset > legend {\n  margin-left: -1rem;\n  padding: 0 1rem;\n}\n\nlegend {\n  font-variation-settings: \"wght\" 500;\n  padding: 0;\n}\n\nlabel {\n  font-variation-settings: \"wght\" 500;\n  margin: 0 calc(var(--baseline) * 4) calc(var(--baseline) * 3) 0;\n}\n\n[type=text],\n[type=url],\n[type=tel],\n[type=number],\n[type=email],\n[type=search],\ntextarea,\nselect {\n  background-color: var(--page-bg);\n  border: none;\n  border: 1px solid var(--light);\n  color: var(--text-color);\n  font-size: 1rem;\n  outline: none;\n  padding: calc(var(--baseline) * 1.2) calc(var(--baseline) * 3);\n  vertical-align: middle;\n  width: 100%;\n  -webkit-appearance: none;\n  -moz-appearance: none;\n}\n@media all and (min-width: 37.5em) {\n  [type=text],\n  [type=url],\n  [type=tel],\n  [type=number],\n  [type=email],\n  [type=search],\n  textarea,\n  select {\n    width: auto;\n  }\n}\n[type=text]:focus,\n[type=url]:focus,\n[type=tel]:focus,\n[type=number]:focus,\n[type=email]:focus,\n[type=search]:focus,\ntextarea:focus,\nselect:focus {\n  border-color: var(--dark);\n}\n\ninput[type=checkbox],\ninput[type=radio] {\n  opacity: 0;\n  position: absolute;\n}\ninput[type=checkbox] + label,\ninput[type=radio] + label {\n  position: relative;\n  margin-left: -1px;\n  cursor: pointer;\n  padding: 0;\n}\ninput[type=checkbox] + label:before,\ninput[type=radio] + label:before {\n  background-color: var(--white);\n  border: 1px solid var(--light);\n  border-radius: 2px;\n  content: \"\";\n  display: inline-block;\n  height: calc(var(--baseline) * 5);\n  line-height: calc(var(--baseline) * 5);\n  margin-right: calc(var(--baseline) * 4);\n  vertical-align: middle;\n  text-align: center;\n  width: calc(var(--baseline) * 5);\n}\ninput[type=checkbox]:checked + label:before,\ninput[type=radio]:checked + label:before {\n  content: \"\";\n  background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 8'%3e%3cpolygon points='9.53 0 4.4 5.09 1.47 2.18 0 3.64 2.93 6.54 4.4 8 5.87 6.54 11 1.46 9.53 0' fill='%23d73a42'/%3e%3c/svg%3e\");\n  background-repeat: no-repeat;\n  background-size: 11px 8px;\n  background-position: 50% 50%;\n}\n\ninput[type=radio] + label:before {\n  border-radius: 50%;\n}\ninput[type=radio]:checked + label:before {\n  background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3ccircle cx='4' cy='4' r='4' fill='%23d73a42'/%3e%3c/svg%3e\");\n}\n\n[type=file] {\n  margin-bottom: calc(var(--baseline) * 6);\n  width: 100%;\n}\n\nselect {\n  max-width: 100%;\n  width: auto;\n  position: relative;\n}\nselect:not([multiple]) {\n  background: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 6 6\"><polygon points=\"3 6 3 6 0 0 6 0 3 6\" fill=\"%238a8b8c\"/></svg>') no-repeat 90% 50%;\n  background-size: 8px;\n  padding-right: calc(var(--baseline) * 12);\n}\n\nselect[multiple] {\n  border: 1px solid var(--light);\n  padding: calc(var(--baseline) * 6);\n  width: 100%;\n}\nselect[multiple]:hover {\n  border-color: var(--light);\n}\nselect[multiple]:focus {\n  border-color: var(--dark);\n}\nselect[multiple]:disabled {\n  background-color: var(--light);\n  cursor: not-allowed;\n}\nselect[multiple]:disabled:hover {\n  border-color: var(--light);\n}\n\ntextarea {\n  display: block;\n  overflow: auto;\n  resize: vertical;\n  max-width: 100%;\n}\n\n.top {\n  align-items: center;\n  display: flex;\n  height: var(--navbar-height);\n  position: relative;\n  padding: 0 var(--page-margin);\n  -webkit-transition: background 0.5s ease;\n  transition: background 0.5s ease;\n  width: 100%;\n  z-index: 2;\n}\n@media all and (min-width: 56.25em) {\n  .top {\n    justify-content: space-between;\n    height: var(--navbar-height);\n  }\n}\n.top.sticky {\n  background: var(--page-bg);\n  position: sticky;\n  top: -100px;\n  -webkit-animation: slideDown 0.5s cubic-bezier(0.17, 0.67, 0, 1) forwards;\n          animation: slideDown 0.5s cubic-bezier(0.17, 0.67, 0, 1) forwards;\n}\n@-webkit-keyframes slideDown {\n  from {\n    opacity: 0;\n    top: -100px;\n  }\n  to {\n    opacity: 1;\n    top: 0;\n  }\n}\n@keyframes slideDown {\n  from {\n    opacity: 0;\n    top: -100px;\n  }\n  to {\n    opacity: 1;\n    top: 0;\n  }\n}\n\n.logo {\n  color: var(--logo-color) !important;\n  font-size: 1.2136296308rem;\n  font-family: var(--logo-font);\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  margin-right: auto;\n  order: 1;\n}\n.logo > img {\n  height: var(--navbar-height);\n  -o-object-fit: contain;\n     object-fit: contain;\n  padding: calc(var(--baseline) * 2) 0;\n  width: auto;\n}\n\n.search {\n  order: 2;\n}\n@media all and (min-width: 56.25em) {\n  .search {\n    order: 3;\n  }\n}\n.search__btn {\n  border-color: var(--light);\n  margin: 0;\n  height: 2.2rem;\n  padding: 0;\n  width: 2.2rem;\n}\n@media all and (min-width: 56.25em) {\n  .search__btn {\n    margin-left: 2rem;\n  }\n}\n.search__btn:hover, .search__btn:focus {\n  border-color: var(--dark);\n}\n.search__form {\n  display: flex;\n  justify-content: space-between;\n  width: 100%;\n}\n.search__form button {\n  width: auto;\n  flex-shrink: 0;\n}\n.search__input {\n  background: none;\n  border: none;\n  border-bottom: 1px solid var(--dark);\n  display: block;\n  font-family: var(--heading-font);\n  padding: 0;\n  width: 90%;\n}\n.search__input:focus-visible {\n  outline: none !important;\n}\n.search__overlay {\n  background-color: var(--page-bg);\n  -webkit-box-shadow: 0 3px 30px rgba(0, 0, 0, 0.05);\n          box-shadow: 0 3px 30px rgba(0, 0, 0, 0.05);\n  left: 0;\n  opacity: 0;\n  position: fixed;\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n  top: 0;\n  visibility: hidden;\n  width: 100%;\n  z-index: 2005;\n}\n.search__overlay-inner {\n  -webkit-animation: slideininput 0.24s 1s forwards;\n          animation: slideininput 0.24s 1s forwards;\n  align-items: center;\n  display: flex;\n  height: calc(var(--navbar-height) * 3);\n  justify-content: space-between;\n  padding: 0 var(--page-margin);\n  opacity: 0;\n  scale: 0.9;\n}\n@-webkit-keyframes slideininput {\n  60% {\n    opacity: 0;\n    scale: 0.9;\n  }\n  100% {\n    opacity: 1;\n    scale: 1;\n  }\n}\n@keyframes slideininput {\n  60% {\n    opacity: 0;\n    scale: 0.9;\n  }\n  100% {\n    opacity: 1;\n    scale: 1;\n  }\n}\n.search__overlay.expanded {\n  -webkit-transform: translate(0, 0);\n          transform: translate(0, 0);\n  opacity: 1;\n  display: block;\n  visibility: visible;\n}\n\n.navbar {\n  order: 3;\n}\n.navbar .navbar__menu {\n  display: flex;\n  flex-wrap: wrap;\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n@media all and (max-width: 56.1875em) {\n  .navbar .navbar__menu {\n    display: none;\n  }\n}\n.navbar .navbar__menu li {\n  font-family: var(--menu-font);\n  display: block;\n  font-size: 0.8789062495rem;\n  line-height: var(--line-height);\n  font-variation-settings: \"wght\" 500;\n  padding: 0;\n  position: relative;\n  width: auto;\n}\n.navbar .navbar__menu li a,\n.navbar .navbar__menu li span[aria-haspopup=true] {\n  color: var(--nav-link-color);\n  display: block;\n  padding: 0 0.6rem;\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n}\n.navbar .navbar__menu li a:active, .navbar .navbar__menu li a:focus, .navbar .navbar__menu li a:hover,\n.navbar .navbar__menu li span[aria-haspopup=true]:active,\n.navbar .navbar__menu li span[aria-haspopup=true]:focus,\n.navbar .navbar__menu li span[aria-haspopup=true]:hover {\n  color: var(--nav-link-color-hover);\n}\n.navbar .navbar__menu li span {\n  color: var(--nav-link-color);\n  cursor: default;\n  display: block;\n  padding: 0 0.6rem;\n}\n.navbar .navbar__menu > li:hover > a, .navbar .navbar__menu > li:hover > span[aria-haspopup=true] {\n  color: var(--nav-link-color-hover);\n}\n.navbar .has-submenu:active > .navbar__submenu,\n.navbar .has-submenu:focus > .navbar__submenu,\n.navbar .has-submenu:hover > .navbar__submenu {\n  left: 0;\n  opacity: 1;\n  -webkit-transform: scale(1);\n          transform: scale(1);\n  visibility: visible;\n  margin-top: 0.8rem;\n}\n.navbar .has-submenu:active > .navbar__submenu:before,\n.navbar .has-submenu:focus > .navbar__submenu:before,\n.navbar .has-submenu:hover > .navbar__submenu:before {\n  content: \"\";\n  height: 1rem;\n  left: 0;\n  position: absolute;\n  width: 100%;\n  top: -1rem;\n}\n.navbar .has-submenu:active > .navbar__submenu.is-right-submenu,\n.navbar .has-submenu:focus > .navbar__submenu.is-right-submenu,\n.navbar .has-submenu:hover > .navbar__submenu.is-right-submenu {\n  left: auto;\n  right: 0;\n  -webkit-transform-origin: right top;\n          transform-origin: right top;\n}\n.navbar .has-submenu .has-submenu:active > .navbar__submenu,\n.navbar .has-submenu .has-submenu:focus > .navbar__submenu,\n.navbar .has-submenu .has-submenu:hover > .navbar__submenu {\n  top: 0;\n  margin-top: 0;\n}\n.navbar .has-submenu .has-submenu:active > .navbar__submenu.is-right-submenu,\n.navbar .has-submenu .has-submenu:focus > .navbar__submenu.is-right-submenu,\n.navbar .has-submenu .has-submenu:hover > .navbar__submenu.is-right-submenu {\n  top: 0;\n  margin-top: 0;\n}\n.navbar .navbar__submenu {\n  background: var(--lighter);\n  border-radius: calc(var(--border-radius) * 4);\n  left: -9999px;\n  list-style-type: none;\n  margin: 0;\n  padding: 1rem 0.85rem;\n  position: absolute;\n  visibility: hidden;\n  white-space: nowrap;\n  z-index: 1;\n  opacity: 0;\n  -webkit-transform: scale(0.8);\n          transform: scale(0.8);\n  -webkit-transform-origin: 0 top;\n          transform-origin: 0 top;\n  -webkit-transition: opacity 0.15s, -webkit-transform 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n  transition: opacity 0.15s, -webkit-transform 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n  transition: opacity 0.15s, transform 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n  transition: opacity 0.15s, transform 0.3s cubic-bezier(0.275, 1.375, 0.8, 1), -webkit-transform 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n}\n.navbar .navbar__submenu__submenu {\n  z-index: 2;\n}\n.navbar .navbar__submenu li {\n  line-height: 1.5;\n  font-size: 0.8789062495rem;\n}\n.navbar .navbar__submenu li a,\n.navbar .navbar__submenu li span[aria-haspopup=true] {\n  border-radius: calc(var(--border-radius) * 3);\n  color: var(--nav-link-color-hover);\n  padding: 0.5rem 1rem;\n  -webkit-transition: all 0.24s ease;\n  transition: all 0.24s ease;\n}\n.navbar .navbar__submenu li a:active, .navbar .navbar__submenu li a:focus, .navbar .navbar__submenu li a:hover,\n.navbar .navbar__submenu li span[aria-haspopup=true]:active,\n.navbar .navbar__submenu li span[aria-haspopup=true]:focus,\n.navbar .navbar__submenu li span[aria-haspopup=true]:hover {\n  background: var(--page-bg);\n  color: var(--nav-link-color);\n}\n.navbar .navbar__submenu li span {\n  color: var(--nav-link-color-hover) !important;\n  padding: 0.5rem 1rem;\n}\n.navbar .navbar__submenu li:hover > a, .navbar .navbar__submenu li:hover > span[aria-haspopup=true] {\n  color: var(--nav-link-color);\n}\n.navbar .navbar__toggle {\n  background: var(--black);\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  border: none;\n  cursor: pointer;\n  display: block;\n  line-height: 1;\n  margin: 0;\n  overflow: visible;\n  padding: 0;\n  position: relative;\n  right: 0;\n  margin-left: 0.75rem;\n  text-transform: none;\n  z-index: 2004;\n  height: 3.2rem;\n  padding: 0;\n  width: 3.2rem;\n}\n@media all and (min-width: 56.25em) {\n  .navbar .navbar__toggle {\n    display: none;\n  }\n}\n.navbar .navbar__toggle:hover, .navbar .navbar__toggle:focus {\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  outline: none;\n  -webkit-transform: none;\n          transform: none;\n}\n.navbar .navbar__toggle-box {\n  width: 20px;\n  height: 14px;\n  display: inline-block;\n  position: relative;\n}\n.navbar .navbar__toggle-inner {\n  display: block;\n  top: 50%;\n  text-indent: -9999999em;\n}\n.navbar .navbar__toggle-inner::before {\n  content: \"\";\n  display: block;\n  top: -5px;\n}\n.navbar .navbar__toggle-inner::after {\n  content: \"\";\n  display: block;\n  bottom: -5px;\n}\n.navbar .navbar__toggle-inner, .navbar .navbar__toggle-inner::before, .navbar .navbar__toggle-inner::after {\n  width: 20px;\n  height: 1px;\n  background-color: var(--white);\n  position: absolute;\n  -webkit-transition: opacity 0.14s ease-out, -webkit-transform;\n  transition: opacity 0.14s ease-out, -webkit-transform;\n  transition: transform, opacity 0.14s ease-out;\n  transition: transform, opacity 0.14s ease-out, -webkit-transform;\n}\n.navbar .navbar__toggle-inner {\n  -webkit-transition-duration: 0.075s;\n          transition-duration: 0.075s;\n  -webkit-transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n          transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n}\n.navbar .navbar__toggle-inner::before {\n  -webkit-transition: top 0.075s ease 0.12s, opacity 0.075s ease;\n  transition: top 0.075s ease 0.12s, opacity 0.075s ease;\n}\n.navbar .navbar__toggle-inner::after {\n  -webkit-transition: bottom 0.075s ease 0.12s, -webkit-transform 0.075s cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  transition: bottom 0.075s ease 0.12s, -webkit-transform 0.075s cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  transition: bottom 0.075s ease 0.12s, transform 0.075s cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  transition: bottom 0.075s ease 0.12s, transform 0.075s cubic-bezier(0.55, 0.055, 0.675, 0.19), -webkit-transform 0.075s cubic-bezier(0.55, 0.055, 0.675, 0.19);\n}\n.navbar .navbar__toggle.is-active .navbar__toggle-inner {\n  -webkit-transform: rotate(45deg);\n          transform: rotate(45deg);\n  -webkit-transition-delay: 0.12s;\n          transition-delay: 0.12s;\n  -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n          transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n}\n.navbar .navbar__toggle.is-active .navbar__toggle-inner::before {\n  top: 0;\n  opacity: 0;\n  -webkit-transition: top 0.075s ease, opacity 0.075s ease 0.12s;\n  transition: top 0.075s ease, opacity 0.075s ease 0.12s;\n}\n.navbar .navbar__toggle.is-active .navbar__toggle-inner::after {\n  bottom: 0;\n  -webkit-transform: rotate(-90deg);\n          transform: rotate(-90deg);\n  -webkit-transition: bottom 0.075s ease, -webkit-transform 0.075s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s;\n  transition: bottom 0.075s ease, -webkit-transform 0.075s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s;\n  transition: bottom 0.075s ease, transform 0.075s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s;\n  transition: bottom 0.075s ease, transform 0.075s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s, -webkit-transform 0.075s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s;\n}\n\n.navbar_mobile_overlay {\n  background: var(--page-bg);\n  height: calc(100vh - 4.4rem);\n  left: 0;\n  opacity: 1;\n  overflow: auto;\n  pointer-events: auto;\n  position: fixed;\n  top: 4.4rem;\n  -webkit-transition: all 0.3s cubic-bezier(0, 0, 0.3, 1);\n  transition: all 0.3s cubic-bezier(0, 0, 0.3, 1);\n  width: 100%;\n  z-index: 1001;\n}\n.navbar_mobile_overlay.is-hidden {\n  opacity: 0;\n  pointer-events: none;\n}\n.navbar_mobile_overlay .navbar__menu {\n  margin: 24px;\n}\n.navbar_mobile_overlay .navbar__menu li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n  text-align: center;\n}\n.navbar_mobile_overlay .navbar__menu li a,\n.navbar_mobile_overlay .navbar__menu li span {\n  color: var(--dark);\n  display: block;\n  padding: calc(var(--baseline) * 2);\n  position: relative;\n}\n.navbar_mobile_overlay .navbar__menu li a:active, .navbar_mobile_overlay .navbar__menu li a:focus, .navbar_mobile_overlay .navbar__menu li a:hover,\n.navbar_mobile_overlay .navbar__menu li span:active,\n.navbar_mobile_overlay .navbar__menu li span:focus,\n.navbar_mobile_overlay .navbar__menu li span:hover {\n  color: var(--dark);\n}\n.navbar_mobile_overlay .navbar__menu li a[aria-haspopup=true]::after,\n.navbar_mobile_overlay .navbar__menu li span[aria-haspopup=true]::after {\n  content: \"\";\n  width: 0;\n  height: 0;\n  border-style: solid;\n  border-width: 5px 5px 0 5px;\n  border-color: currentColor transparent transparent transparent;\n  left: calc(var(--baseline) * 2);\n  top: 15px;\n  position: relative;\n}\n.navbar_mobile_overlay .navbar__submenu {\n  margin: 0;\n  padding: 0;\n  visibility: hidden;\n}\n.navbar_mobile_overlay .navbar__submenu[aria-hidden=false] {\n  visibility: visible;\n}\n.navbar_mobile_overlay .navbar__submenu_wrapper {\n  height: 0;\n  opacity: 0;\n  overflow: hidden;\n  -webkit-transition: all 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n  transition: all 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n}\n.navbar_mobile_overlay .navbar__submenu_wrapper.is-active {\n  height: auto;\n  opacity: 1;\n}\n\n.navbar_mobile_sidebar {\n  background: var(--page-bg);\n  -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\n          box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\n  height: 100vh;\n  left: 0;\n  max-width: 400px;\n  overflow: auto;\n  position: fixed;\n  top: 0;\n  -webkit-transition: all 0.3s cubic-bezier(0, 0, 0.3, 1);\n  transition: all 0.3s cubic-bezier(0, 0, 0.3, 1);\n  width: 80%;\n  z-index: 1001;\n}\n.navbar_mobile_sidebar.is-hidden {\n  left: -400px;\n}\n.navbar_mobile_sidebar .navbar__menu {\n  margin: 24px;\n}\n.navbar_mobile_sidebar .navbar__menu li {\n  font-family: var(--menu-font);\n  font-size: 16px;\n  list-style: none;\n  line-height: 1.3;\n  margin: 0;\n  padding: 0;\n}\n.navbar_mobile_sidebar .navbar__menu li a,\n.navbar_mobile_sidebar .navbar__menu li .is-separator {\n  color: var(--dark);\n  display: block;\n  padding: 10px 20px 10px 0;\n  position: relative;\n}\n.navbar_mobile_sidebar .navbar__menu li a:active, .navbar_mobile_sidebar .navbar__menu li a:focus, .navbar_mobile_sidebar .navbar__menu li a:hover,\n.navbar_mobile_sidebar .navbar__menu li .is-separator:active,\n.navbar_mobile_sidebar .navbar__menu li .is-separator:focus,\n.navbar_mobile_sidebar .navbar__menu li .is-separator:hover {\n  color: var(--dark);\n}\n.navbar_mobile_sidebar .navbar__menu li a[aria-haspopup=true]::after,\n.navbar_mobile_sidebar .navbar__menu li .is-separator[aria-haspopup=true]::after {\n  content: \"\";\n  width: 0;\n  height: 0;\n  border-style: solid;\n  border-width: 5px 5px 0 5px;\n  border-color: currentColor transparent transparent transparent;\n  right: 0;\n  top: 18px;\n  position: absolute;\n}\n.navbar_mobile_sidebar .navbar__submenu {\n  margin: 0 0 0 24px;\n  padding: 0;\n  visibility: hidden;\n}\n.navbar_mobile_sidebar .navbar__submenu[aria-hidden=false] {\n  visibility: visible;\n}\n.navbar_mobile_sidebar .navbar__submenu_wrapper {\n  height: 0;\n  opacity: 0;\n  overflow: hidden;\n  -webkit-transition: all 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n  transition: all 0.3s cubic-bezier(0.275, 1.375, 0.8, 1);\n}\n.navbar_mobile_sidebar .navbar__submenu_wrapper.is-active {\n  height: auto;\n  opacity: 1;\n}\n.navbar_mobile_sidebar__overlay {\n  background: rgba(0, 0, 0, 0.5);\n  height: 100%;\n  opacity: 1;\n  pointer-events: auto;\n  position: fixed;\n  top: 0;\n  -webkit-transition: all 0.3s cubic-bezier(0, 0, 0.3, 1);\n  transition: all 0.3s cubic-bezier(0, 0, 0.3, 1);\n  width: 100%;\n  z-index: 10;\n}\n.navbar_mobile_sidebar__overlay.is-hidden {\n  opacity: 0;\n  pointer-events: none;\n}\n\n.wrapper {\n  -webkit-box-sizing: content-box;\n          box-sizing: content-box;\n  max-width: var(--page-width);\n  margin-left: auto;\n  margin-right: auto;\n  padding-left: var(--page-margin);\n  padding-right: var(--page-margin);\n}\n\n.entry-wrapper {\n  -webkit-box-sizing: content-box;\n          box-sizing: content-box;\n  max-width: var(--entry-width);\n  margin-left: auto;\n  margin-right: auto;\n  padding-left: var(--page-margin);\n  padding-right: var(--page-margin);\n}\n\n.hero {\n  position: relative;\n  z-index: 1;\n}\n.hero--noimage::after {\n  background: var(--dark);\n  content: \"\";\n  display: block;\n  height: 1px;\n  bottom: 0;\n  width: calc(100% - var(--page-margin) * 2);\n  z-index: -1;\n  max-width: var(--page-width);\n  position: absolute;\n  left: 50%;\n  -webkit-transform: translate(-50%, 0);\n          transform: translate(-50%, 0);\n}\n.hero__content {\n  padding-bottom: calc(var(--baseline) * 6 + 1.5vw);\n}\n.hero__content h1 > sup {\n  font-size: 1.066666667rem;\n  vertical-align: top;\n}\n.hero__content--centered {\n  text-align: center;\n}\n.hero__content--centered .content__meta {\n  justify-content: center;\n}\n.hero__cta {\n  margin-top: calc(var(--baseline) * 6);\n}\n.hero__image {\n  margin: 0 var(--page-margin);\n}\n.hero__image-wrapper {\n  position: relative;\n  background: var(--lighter);\n  border-radius: calc(var(--border-radius) * 4);\n}\n@media all and (min-width: 56.25em) {\n  .hero__image-wrapper {\n    height: var(--hero-height);\n  }\n}\n.hero__image-wrapper img {\n  border-radius: inherit;\n  display: block;\n  height: 100%;\n  -o-object-fit: cover;\n     object-fit: cover;\n  width: 100%;\n}\n@media all and (min-width: 56.25em) {\n  .hero__image-wrapper img {\n    height: var(--hero-height);\n  }\n}\n.hero__image > figcaption {\n  background: var(--page-bg);\n}\n@media all and (min-width: 56.25em) {\n  .hero__image > figcaption {\n    text-align: right;\n  }\n}\n\n.feed__item {\n  display: flex;\n  flex-wrap: wrap;\n  gap: calc(2rem + 2vw);\n  margin-top: calc(var(--baseline) * 12 + 2vw);\n}\n@media all and (min-width: 37.5em) {\n  .feed__item {\n    flex-wrap: nowrap;\n  }\n}\n.feed__item--centered {\n  justify-content: center;\n}\n.feed__content {\n  max-width: var(--entry-width);\n}\n.feed__image {\n  background: var(--lighter);\n  border-radius: calc(var(--border-radius) * 4);\n  flex-shrink: 0;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n}\n@media all and (min-width: 37.5em) {\n  .feed__image {\n    height: calc(var(--feed-image-size) + 4vw);\n    width: calc(var(--feed-image-size) + 4vw);\n  }\n}\n.feed__image > img {\n  border-radius: inherit;\n  display: inline-block;\n  height: 100%;\n  -o-object-fit: cover;\n     object-fit: cover;\n  width: 100%;\n}\n.feed__image--wide {\n  max-width: var(--page-width);\n}\n.feed__meta {\n  align-items: center;\n  color: var(--gray);\n  display: flex;\n  font-size: 0.8239746086rem;\n  gap: 0.8rem;\n  margin-bottom: calc(var(--baseline) * 3);\n}\n.feed__author {\n  font-family: var(--menu-font);\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  text-decoration: none;\n}\n.feed__author-thumb {\n  border-radius: 50%;\n  height: 1.7rem;\n  margin-right: -0.2rem;\n  width: 1.7rem;\n}\n.feed__date {\n  color: var(--gray);\n  font-style: italic;\n}\n.feed__author + .feed__date::before {\n  content: \"\";\n  background: var(--light);\n  display: inline-block;\n  height: 1px;\n  margin-right: 4px;\n  width: 1rem;\n  vertical-align: middle;\n}\n.feed__readmore {\n  margin-top: calc(var(--baseline) * 4 + 0.25vw);\n}\n.feed__title {\n  margin-top: 0;\n}\n.feed--grid {\n  margin: 0;\n}\n@media all and (min-width: 37.5em) {\n  .feed--grid {\n    display: grid;\n    grid-template-columns: 100%;\n    gap: 0 3rem;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .feed--grid {\n    grid-template-columns: repeat(2, 1fr);\n  }\n}\n.feed--grid h2 {\n  margin-top: 0;\n}\n.feed--grid sup {\n  font-size: 1.066666667rem;\n  vertical-align: top;\n}\n.feed--grid li {\n  align-items: center;\n  list-style: none;\n  gap: 2rem;\n  padding: 0;\n}\n\n.content {\n  overflow: hidden;\n}\n.content__meta {\n  margin-top: calc(var(--baseline) * 4 + 0.25vw);\n  margin-bottom: 0;\n}\n.content__meta--centered {\n  justify-content: center;\n}\n.content__entry {\n  margin-top: calc(var(--baseline) * 6 + 1.5vw);\n  overflow-wrap: break-word;\n}\n.content__entry > :nth-child(1) {\n  margin-top: 0;\n}\n.content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a) {\n  color: var(--link-color-hover);\n  text-decoration: underline;\n  text-decoration-thickness: 1px;\n  text-underline-offset: 0.2em;\n  -webkit-text-decoration-skip: ink;\n          text-decoration-skip-ink: auto;\n}\n.content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a):hover, .content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a):active, .content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a):focus {\n  color: var(--link-color);\n}\n.content__entry--nospace {\n  margin-top: 0;\n}\n.content__avatar-thumbs {\n  border-radius: 50%;\n  height: 4.5rem;\n  width: 4.5rem;\n}\n.content__footer {\n  margin-top: calc(var(--baseline) * 9 + 1vw);\n}\n.content__updated {\n  color: var(--gray);\n  font-size: 0.8789062495rem;\n  font-style: italic;\n}\n.content__actions {\n  align-items: baseline;\n  display: flex;\n  flex-basis: auto;\n  gap: 2rem;\n  justify-content: space-between;\n  margin-top: calc(var(--baseline) * 4 + 0.25vw);\n  position: relative;\n}\n.content__share {\n  flex-shrink: 0;\n}\n.content__share-button {\n  border-color: var(--light);\n}\n.content__share-popup {\n  background: var(--lighter);\n  border-radius: calc(var(--border-radius) * 4);\n  bottom: 140%;\n  display: none;\n  padding: 1rem 0.85rem;\n  position: absolute;\n  right: 0;\n  text-align: left;\n  z-index: 1;\n}\n.content__share-popup.is-visible {\n  -webkit-animation: share-popup 0.48s cubic-bezier(0.17, 0.67, 0.6, 1.34) backwards;\n          animation: share-popup 0.48s cubic-bezier(0.17, 0.67, 0.6, 1.34) backwards;\n  display: block;\n}\n@-webkit-keyframes share-popup {\n  from {\n    -webkit-transform: scale(0.9);\n            transform: scale(0.9);\n  }\n  to {\n    -webkit-transform: scale(1);\n            transform: scale(1);\n  }\n}\n@keyframes share-popup {\n  from {\n    -webkit-transform: scale(0.9);\n            transform: scale(0.9);\n  }\n  to {\n    -webkit-transform: scale(1);\n            transform: scale(1);\n  }\n}\n.content__share-popup > a {\n  border-radius: calc(var(--border-radius) * 3);\n  color: var(--text-color);\n  display: block;\n  font-family: var(--menu-font);\n  font-size: 0.8239746086rem;\n  padding: 0.4rem 0.8rem;\n}\n.content__share-popup > a:hover {\n  background: var(--page-bg);\n  color: var(--text-color);\n  text-decoration: none;\n}\n.content__share-popup > a > svg {\n  fill: var(--text-color);\n  display: inline-block;\n  height: 0.9rem;\n  margin-right: 0.5666666667rem;\n  pointer-events: none;\n  vertical-align: middle;\n  width: 0.9rem;\n}\n.content__tag {\n  margin: 0;\n  font-family: var(--menu-font);\n  font-size: 0.8239746086rem;\n}\n.content__tag > li {\n  display: inline-flex;\n  margin: 0.3rem 0.3rem 0.3rem 0;\n  padding: 0;\n}\n.content__tag > li > a {\n  border: 1px solid var(--light);\n  border-radius: calc(var(--border-radius) * 10);\n  color: var(--dark);\n  font-size: 0.7241964329rem;\n  font-variation-settings: \"wght\" var(--font-weight-normal);\n  padding: calc(var(--baseline) * 1) calc(var(--baseline) * 2.5);\n}\n.content__tag > li > a:hover {\n  border-color: var(--dark);\n}\n.content__bio {\n  display: flex;\n  margin: calc(var(--baseline) * 12 + 1vw) 0;\n}\n@media all and (min-width: 37.5em) {\n  .content__bio {\n    align-items: center;\n  }\n}\n@media all and (min-width: 37.5em) {\n  .content__bio::before {\n    content: \"\";\n    border-top: 1px solid var(--light);\n    height: 1px;\n    margin-right: 2rem;\n    width: 30%;\n  }\n}\n.bio__avatar {\n  border-radius: 50%;\n  flex-shrink: 0;\n  height: 2.5rem;\n  margin-right: 1.2rem;\n  width: 2.5rem;\n}\n@media all and (min-width: 37.5em) {\n  .bio__avatar {\n    height: 4rem;\n    margin-right: 2rem;\n    width: 4rem;\n  }\n}\n.bio__name {\n  margin: 0;\n}\n.bio__desc {\n  font-family: var(--body-font);\n  font-size: 0.8789062495rem;\n  line-height: 1.5;\n}\n.bio__desc > :nth-child(1) {\n  margin-top: calc(var(--baseline) * 2);\n}\n.bio__desc a {\n  text-decoration: underline;\n  text-decoration-thickness: 1px;\n  text-underline-offset: 0.2em;\n  -webkit-text-decoration-skip: ink;\n          text-decoration-skip-ink: auto;\n}\n.bio__desc a {\n  color: var(--link-color-hover);\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n}\n.bio__desc a:hover {\n  color: var(--link-color);\n}\n.bio__desc a:active {\n  color: var(--link-color);\n}\n.bio__desc a:focus {\n  outline: none;\n}\n\n.content__nav {\n  margin-top: calc(var(--baseline) * 16 + 1vw);\n}\n.content__nav-inner {\n  border-top: 1px solid var(--dark);\n  border-bottom: 1px solid var(--dark);\n  padding: calc(var(--baseline) * 16) 0;\n}\n@media all and (min-width: 37.5em) {\n  .content__nav-inner {\n    display: flex;\n    gap: 1rem;\n    justify-content: space-between;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .content__nav-inner {\n    gap: 2rem;\n  }\n}\n@media all and (max-width: 37.4375em) {\n  .content__nav-prev + .content__nav-next {\n    margin-top: calc(var(--baseline) * 6 + 1vw);\n  }\n}\n@media all and (min-width: 37.5em) {\n  .content__nav-next {\n    margin-left: auto;\n    text-align: right;\n  }\n}\n.content__nav-link {\n  font-family: var(--heading-font);\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  font-style: italic;\n  height: 100%;\n  line-height: 1.5;\n  display: flex;\n  gap: 1rem;\n  justify-content: space-between;\n  align-items: center;\n}\n@media all and (min-width: 37.5em) {\n  .content__nav-link {\n    gap: 2rem;\n  }\n}\n.content__nav-link > div {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: -webkit-box;\n  -webkit-line-clamp: 3;\n  -webkit-box-orient: vertical;\n}\n@media all and (min-width: 56.25em) {\n  .content__nav-link > div {\n    -webkit-line-clamp: 4;\n  }\n}\n.content__nav-link span {\n  color: var(--gray);\n  display: block;\n  font-size: 0.7724761953rem;\n  font-family: var(--menu-font);\n  font-style: normal;\n  font-variation-settings: \"wght\" var(--font-weight-normal);\n  margin-bottom: var(--baseline);\n}\n.content__nav-image {\n  flex: 1 0 4rem;\n  margin: 0;\n  height: 4rem;\n}\n@media all and (min-width: 37.5em) and (max-width: 56.1875em) {\n  .content__nav-image {\n    flex-basis: 6rem;\n    height: 6rem;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .content__nav-image {\n    flex-basis: 8rem;\n    height: 8rem;\n  }\n}\n.content__nav-image > img {\n  border-radius: calc(var(--border-radius) * 4);\n  display: block;\n  height: 100%;\n  -o-object-fit: cover;\n     object-fit: cover;\n  width: 100%;\n}\n.content__related {\n  background: var(--lighter);\n  margin-top: calc(var(--baseline) * 16 + 1vw);\n  padding: calc(var(--baseline) * 12 + 3vw) 0;\n}\n.related__title {\n  margin-top: 0;\n}\n\n.content__comments {\n  margin-top: calc(var(--baseline) * 9);\n  overflow: hidden;\n}\n\n.post__image:not(.post__image--wide):not(.post__image--full) {\n  display: inline-block;\n  margin-bottom: calc(var(--baseline) * 2 + 0.25vw);\n}\n.post__image a,\n.post__image img {\n  border-radius: calc(var(--border-radius) * 4);\n  display: inline-block;\n}\n.post__image > figcaption {\n  background-color: var(--page-bg);\n}\n.post__image--left {\n  float: left;\n  margin-right: 2rem;\n  max-width: 50%;\n}\n.post__image--right {\n  float: right;\n  margin-left: 2rem;\n  max-width: 50%;\n}\n.post__image--center {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n  text-align: center;\n}\n.post__image--wide {\n  display: block;\n}\n@media all and (min-width: 56.25em) {\n  .post__image--wide {\n    margin-left: calc(-50vw + 50%);\n    margin-right: calc(-50vw + 50%);\n    padding: 0 var(--page-margin);\n    text-align: center;\n  }\n  .post__image--wide a,\n  .post__image--wide img {\n    display: block;\n    height: auto;\n    margin: auto;\n    max-width: var(--page-width);\n    width: 100%;\n  }\n}\n.post__image--full {\n  background-color: var(--lighter);\n  display: block;\n  margin-left: calc(-50vw + 50%);\n  margin-right: calc(-50vw + 50%);\n  text-align: center;\n}\n.post__image--full a,\n.post__image--full img {\n  border-radius: 0;\n  display: block;\n  height: auto;\n  width: 100%;\n}\n.post__video, .post__iframe {\n  display: block;\n  margin-top: calc(var(--baseline) * 6 + 0.5vw);\n  margin-bottom: calc(var(--baseline) * 6 + 0.5vw);\n  overflow: hidden;\n  padding: 0;\n  position: relative;\n  width: 100%;\n}\n.post__video::before, .post__iframe::before {\n  display: block;\n  content: \"\";\n  padding-top: var(--embed-aspect-ratio);\n}\n.post__video iframe[height*=\"%\"][width*=\"%\"],\n.post__video video[height*=\"%\"][width*=\"%\"],\n.post__video iframe[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]),\n.post__video video[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]), .post__iframe iframe[height*=\"%\"][width*=\"%\"],\n.post__iframe video[height*=\"%\"][width*=\"%\"],\n.post__iframe iframe[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]),\n.post__iframe video[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]) {\n  border: none;\n  height: 100%;\n  left: 0;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  width: 100%;\n}\n.post__video:has(iframe:not([height]))::before, .post__video:has(iframe:not([width]))::before, .post__video:has(video:not([height]))::before, .post__video:has(video:not([width]))::before, .post__iframe:has(iframe:not([height]))::before, .post__iframe:has(iframe:not([width]))::before, .post__iframe:has(video:not([height]))::before, .post__iframe:has(video:not([width]))::before {\n  display: none;\n}\n.post__toc {\n  margin-top: calc(var(--baseline) * 6 + 0.5vw);\n}\n.post__toc h3 {\n  border-bottom: 1px solid var(--dark);\n  font-size: 1rem;\n  margin: 0;\n  padding-bottom: calc(var(--baseline) * 2 + 0.25vw);\n}\n.post__toc ul {\n  counter-reset: item;\n  list-style: decimal;\n  margin: calc(var(--baseline) * 3 + 0.25vw) 0 0 3ch;\n}\n.post__toc ul li {\n  counter-increment: item;\n  padding: 0;\n}\n.post__toc ul ul {\n  margin-top: 0;\n}\n.post__toc ul ul li {\n  display: block;\n}\n.post__toc ul ul li:before {\n  content: counters(item, \".\") \". \";\n  margin-left: -3ch;\n}\n\n.banner {\n  text-align: center;\n}\n.banner--after-content {\n  margin-top: calc(var(--baseline) * 9 + 1vw);\n}\n\n.page__desc a {\n  text-decoration: underline;\n  text-decoration-thickness: 1px;\n  text-underline-offset: 0.2em;\n  -webkit-text-decoration-skip: ink;\n          text-decoration-skip-ink: auto;\n}\n@media all and (min-width: 37.5em) {\n  .page--author__wrapper {\n    display: flex;\n    gap: 2rem;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .page--author__wrapper {\n    gap: 3rem;\n  }\n}\n.page--author__avatar {\n  border-radius: 50%;\n  height: calc(var(--baseline) * 10 + 2vw);\n  margin-top: calc(var(--baseline) * 6 + 1vw);\n  width: calc(var(--baseline) * 10 + 2vw);\n}\n.page--author__website {\n  margin-top: calc(var(--baseline) * 4 + 0.25vw);\n}\n.page--search form {\n  align-items: flex-start;\n  display: flex;\n  flex-wrap: wrap;\n}\n@media all and (max-width: 37.4375em) {\n  .page--search input {\n    margin-bottom: calc(var(--baseline) * 2);\n  }\n}\n@media all and (min-width: 20em) {\n  .page--search input {\n    flex: 1 0 auto;\n    margin-right: calc(var(--baseline) * 2);\n  }\n}\n@media all and (max-width: 37.4375em) {\n  .page--search button {\n    width: 100%;\n  }\n}\n\n.subpages__title {\n  border-top: 1px solid var(--light);\n  padding-top: calc(var(--baseline) * 6 + 0.5vw);\n}\n.subpages__list {\n  list-style: initial;\n  margin-left: 2ch;\n}\n.subpages__list ul {\n  list-style: initial;\n  margin: 0 0 0 2ch;\n}\n.subpages__list li {\n  padding-bottom: 0;\n}\n\n.readmore {\n  display: inline-block;\n  color: var(--gray);\n  font-size: 0.9374999997rem;\n  font-style: italic;\n  text-decoration: underline;\n  -webkit-text-decoration-skip: ink;\n          text-decoration-skip-ink: auto;\n}\n\n.align-left {\n  text-align: left;\n}\n\n.align-right {\n  text-align: right;\n}\n\n.align-center {\n  text-align: center;\n}\n\n.align-justify {\n  text-align: justify;\n}\n\n.msg {\n  background-color: var(--lighter);\n  border-left: 3px solid var(--light);\n  font-size: 0.9374999997rem;\n  padding: calc(var(--baseline) * 4) calc(var(--baseline) * 6);\n  position: relative;\n}\n.msg--highlight {\n  border-left-color: var(--highlight-color);\n}\n.msg--info {\n  border-left-color: var(--info-color);\n}\n.msg--success {\n  border-left-color: var(--success-color);\n}\n.msg--warning {\n  border-left-color: var(--warning-color);\n}\n\n.ordered-list {\n  counter-reset: listCounter;\n}\n.ordered-list li {\n  counter-increment: listCounter;\n  list-style: none;\n  padding-left: 0.3rem;\n  position: relative;\n}\n.ordered-list li::before {\n  color: var(--color);\n  content: counter(listCounter, decimal-leading-zero) \".\";\n  font-variation-settings: \"wght\" var(--font-weight-bold);\n  left: -2rem;\n  position: absolute;\n}\n\n.dropcap:first-letter {\n  color: var(--headings-color);\n  float: left;\n  font-size: 3.6355864383rem;\n  line-height: 0.7;\n  margin-right: 0.6rem;\n  padding: calc(var(--baseline) * 2) calc(var(--baseline) * 2) calc(var(--baseline) * 2) 0;\n}\n\n.pec-wrapper {\n  height: 100%;\n  left: 0;\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n.pec-overlay {\n  align-items: center;\n  background-color: var(--lighter);\n  font-size: 14px;\n  display: none;\n  height: inherit;\n  justify-content: center;\n  line-height: 1.4;\n  padding: 1rem;\n  position: relative;\n  text-align: center;\n}\n@media all and (min-width: 37.5em) {\n  .pec-overlay {\n    font-size: 16px;\n    line-height: var(--line-height);\n    padding: 1rem 2rem;\n  }\n}\n.pec-overlay.is-active {\n  display: flex;\n}\n.pec-overlay-inner p {\n  margin: 0 0 1rem;\n}\n\n.pagination {\n  display: flex;\n  gap: calc(var(--baseline) * 2);\n  justify-content: center;\n  margin-top: calc(var(--baseline) * 12 + 1vw);\n}\n@media all and (min-width: 56.25em) {\n  .pagination {\n    margin-top: calc(var(--baseline) * 18 + 1vw);\n  }\n}\n\n/* Footer ------------------------ */\n.footer {\n  border-top: 1px solid var(--light);\n  font-size: 0.9374999997rem;\n  padding: calc(var(--baseline) * 9 + 1vw) 0 calc(var(--baseline) * 6 + 1vw);\n  margin: calc(var(--baseline) * 12 + 1vw) auto 0;\n  max-width: var(--page-width);\n  text-align: center;\n}\n.footer--glued {\n  border: none;\n  padding-top: 0;\n}\n.footer__nav ul {\n  list-style: none;\n  margin: 0;\n}\n.footer__nav ul li {\n  display: inline-block;\n  padding: var(--baseline) 0.5rem;\n}\n* + .footer__social {\n  margin-top: calc(var(--baseline) * 6 + 0.5vw);\n}\n.footer__social svg {\n  fill: var(--dark);\n  height: 1rem;\n  margin: 0 0.5rem;\n  -webkit-transition: all 0.12s linear 0s;\n  transition: all 0.12s linear 0s;\n  width: 1rem;\n}\n.footer__social svg:hover {\n  fill: var(--gray);\n}\n.footer__copyright {\n  font-size: 0.8239746086rem;\n  margin-top: var(--baseline);\n}\n.footer__copyright > *:first-child {\n  margin: 0;\n}\n.footer__bttop {\n  background: var(--page-bg);\n  bottom: calc(var(--baseline) * 5);\n  border-radius: 50%;\n  border-color: var(--light);\n  line-height: 1;\n  opacity: 0;\n  padding: calc(var(--baseline) * 2);\n  position: fixed;\n  right: 2rem;\n  text-align: center;\n  width: auto !important;\n  visibility: hidden;\n  z-index: 999;\n}\n@media all and (min-width: 56.25em) {\n  .footer__bttop {\n    bottom: calc(var(--baseline) * 10);\n  }\n}\n.footer__bttop:hover {\n  border-color: var(--dark);\n  opacity: 1;\n}\n.footer__bttop.is-visible {\n  visibility: visible;\n  opacity: 1;\n}\n\n.gallery {\n  margin: calc(var(--baseline) * 6 + 1vw) calc(var(--gallery-gap) * -1);\n}\n@media all and (min-width: 20em) {\n  .gallery {\n    display: flex;\n    flex-wrap: wrap;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .gallery-wrapper--wide {\n    display: flex;\n    justify-content: center;\n    margin-left: calc(-50vw + 50%);\n    margin-right: calc(-50vw + 50%);\n    padding: 0 var(--page-margin);\n  }\n  .gallery-wrapper--wide .gallery {\n    max-width: var(--page-width);\n  }\n}\n@media all and (min-width: 56.25em) {\n  .gallery-wrapper--full {\n    margin-left: calc(-50vw + 50%);\n    margin-right: calc(-50vw + 50%);\n    padding: 0 var(--page-margin);\n  }\n}\n@media all and (min-width: 20em) {\n  .gallery[data-columns=\"1\"] .gallery__item {\n    flex: 1 0 100%;\n  }\n}\n@media all and (min-width: 30em) {\n  .gallery[data-columns=\"2\"] .gallery__item {\n    flex: 1 0 50%;\n  }\n}\n@media all and (min-width: 37.5em) {\n  .gallery[data-columns=\"3\"] .gallery__item {\n    flex: 1 0 33.333%;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .gallery[data-columns=\"4\"] .gallery__item {\n    flex: 0 1 25%;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .gallery[data-columns=\"5\"] .gallery__item {\n    flex: 0 1 20%;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .gallery[data-columns=\"6\"] .gallery__item {\n    flex: 0 1 16.666%;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .gallery[data-columns=\"7\"] .gallery__item {\n    flex: 1 0 14.285%;\n  }\n}\n@media all and (min-width: 56.25em) {\n  .gallery[data-columns=\"8\"] .gallery__item {\n    flex: 1 0 12.5%;\n  }\n}\n.gallery__item {\n  margin: 0;\n  padding: var(--gallery-gap);\n  position: relative;\n}\n@media all and (min-width: 20em) {\n  .gallery__item {\n    flex: 1 0 50%;\n  }\n}\n@media all and (min-width: 30em) {\n  .gallery__item {\n    flex: 1 0 33.333%;\n  }\n}\n@media all and (min-width: 37.5em) {\n  .gallery__item {\n    flex: 1 0 25%;\n  }\n}\n.gallery__item a {\n  border-radius: calc(var(--border-radius) * 4);\n  display: block;\n  height: 100%;\n  width: 100%;\n}\n.gallery__item a::after {\n  background: -webkit-gradient(linear, left bottom, left top, from(rgba(0, 0, 0, 0.4)), to(rgba(0, 0, 0, 0)));\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 100%);\n  border-radius: inherit;\n  bottom: var(--gallery-gap);\n  content: \"\";\n  display: block;\n  opacity: 0;\n  left: var(--gallery-gap);\n  height: calc(100% - var(--gallery-gap) * 2);\n  position: absolute;\n  right: var(--gallery-gap);\n  top: var(--gallery-gap);\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n  width: calc(100% - var(--gallery-gap) * 2);\n}\n.gallery__item a:hover::after {\n  opacity: 1;\n}\n.gallery__item img {\n  border-radius: inherit;\n  display: block;\n  height: 100%;\n  -o-object-fit: cover;\n     object-fit: cover;\n  width: 100%;\n}\n.gallery__item figcaption {\n  bottom: 1.2rem;\n  color: var(--white);\n  left: 50%;\n  opacity: 0;\n  position: absolute;\n  text-align: center;\n  -webkit-transform: translate(-50%, 1.2rem);\n          transform: translate(-50%, 1.2rem);\n  -webkit-transition: all 0.24s ease-out;\n  transition: all 0.24s ease-out;\n}\n.gallery__item:hover figcaption {\n  opacity: 1;\n  -webkit-transform: translate(-50%, 0);\n          transform: translate(-50%, 0);\n}"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/css/style.css",
    "content": "@font-face{font-family:Lora;src:url('../dynamic/fonts/lora/lora.woff2') format('woff2');font-weight:400 700;font-display:swap;font-style:normal}@font-face{font-family:Lora;src:url('../dynamic/fonts/lora/lora-italic.woff2') format('woff2');font-weight:400 700;font-display:swap;font-style:italic}:root{--page-margin:6vw;--page-width:66rem;--entry-width:42rem;--navbar-height:4.4rem;--border-radius:3px;--baseline:0.28333rem;--gallery-gap:calc(var(--baseline) * 1.5);--body-font:'Lora',serif;--heading-font:'Lora',serif;--logo-font:var(--body-font);--menu-font:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Oxygen,Ubuntu,Cantarell,\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";--font-size:clamp(1.1rem, 1.1rem + (0.09999999999999987 * ((100vw - 20rem) / 70)), 1.2rem);--font-weight-normal:400;--font-weight-bold:600;--line-height:1.7;--letter-spacing:0em;--headings-weight:500;--headings-transform:none;--headings-style:normal;--headings-letter-spacing:0em;--headings-line-height:1.2;--hero-height:50vh;--feed-image-size:8rem;--white:#FFFFFF;--black:#17181E;--helper:#FFFFFF;--dark:#17181E;--gray:#57585a;--light:#CACBCF;--lighter:#F3F3F3;--page-bg:#FFFFFF;--color:#D73A42;--text-color:#17181E;--headings-color:#17181E;--link-color:#17181E;--link-color-hover:#D73A42;--nav-link-color:#17181E;--nav-link-color-hover:#17181E;--logo-color:#17181E;--highlight-color:#FFC700;--info-color:#67B1F3;--success-color:#00A563;--warning-color:#EE4E4E}@media all and (min-width:56.25em){:root{--navbar-height:6rem}}@media (prefers-color-scheme:dark){:root{--white:#FFFFFF;--black:#1e1e1e;--helper:#1e1e1e;--dark:#CECBCB;--gray:#9D9D9D;--light:#373737;--lighter:#1e1e1e;--page-bg:#181818;--color:#FFC074;--text-color:#BFBFBF;--headings-color:#EEEDED;--link-color:#EEEDED;--link-color-hover:#FFC074;--nav-link-color:rgba(255,255,255,1);--nav-link-color-hover:rgba(255,255,255,.7);--logo-color:#FFFFFF;--highlight-color:#F6DC90;--info-color:#5B9ED5;--success-color:#54A468;--warning-color:#FB6762}}*,:after,:before{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0}article,aside,footer,header,hgroup,main,nav,section{display:block}li{list-style:none}img{height:auto;max-width:100%;vertical-align:top}button,input,select,textarea{font:inherit}address{font-style:normal}::-moz-selection{background:var(--color);color:var(--white)}::selection{background:var(--color);color:var(--white)}:focus-visible{outline:2px solid var(--color)!important;outline-offset:2px}html{font-size:var(--font-size);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scroll-behavior:smooth}html.no-scroll{overflow:hidden;position:fixed}body{background:var(--page-bg);color:var(--text-color);font-family:var(--body-font);font-variation-settings:\"wght\" var(--font-weight-normal);letter-spacing:var(--letter-spacing);line-height:var(--line-height);-ms-scroll-chaining:none;overscroll-behavior:none}a{text-decoration:none}a{color:var(--link-color);-webkit-transition:all .24s ease-out;transition:all .24s ease-out}a:hover{color:var(--link-color-hover)}a:active{color:var(--link-color-hover)}a:focus{outline:0}.invert{color:var(--link-color-hover);-webkit-transition:all .24s ease-out;transition:all .24s ease-out}.invert:hover{color:var(--link-color)}.invert:active{color:var(--link-color)}.invert:focus{outline:0}dl,ol,p,ul{margin-top:calc(var(--baseline) * 4 + .25vw)}blockquote,figure,hr,pre,table{margin-top:calc(var(--baseline) * 6 + .5vw);margin-bottom:calc(var(--baseline) * 6 + .5vw)}h1,h2,h3,h4,h5,h6{color:var(--headings-color);font-family:var(--heading-font);font-variation-settings:\"wght\" var(--headings-weight);font-style:var(--headings-style);-ms-hyphens:manual;hyphens:manual;letter-spacing:var(--headings-letter-spacing);line-height:var(--headings-line-height);margin-top:calc(var(--baseline) * 6 + 1vw);text-transform:var(--headings-transform)}.h1,h1{font-size:clamp(1.5710900065rem, 1.5710900065rem + 1.424540906 * (100vw - 20rem) / 70, 2.9956309125rem);font-family:var(--heading-font)}.h2,h2{font-size:clamp(1.3808408252rem, 1.3808408252rem + .9332127447 * (100vw - 20rem) / 70, 2.3140535699rem)}.h3,h3{font-size:clamp(1.2136296308rem, 1.2136296308rem + .4621997101 * (100vw - 20rem) / 70, 1.6758293408rem)}.h4,h4{font-size:clamp(1.1377777785rem, 1.1377777785rem + .1567604947 * (100vw - 20rem) / 70, 1.2945382732rem)}.h5,h5{font-size:clamp(1.066666667rem, 1.066666667rem + .0711111115 * (100vw - 20rem) / 70, 1.1377777785rem)}.h6,h6{font-size:clamp(1rem, 1rem + 0 * (100vw - 20rem) / 70, 1rem)}h2+*,h3+*,h4+*,h5+*,h6+*{margin-top:calc(var(--baseline) * 2 + .25vw)}b,strong{font-variation-settings:\"wght\" var(--font-weight-bold)}blockquote{border-top:2px solid var(--dark);border-bottom:2px solid var(--dark);color:var(--headings-color);font-family:var(--heading-font);font-style:italic;font-variation-settings:\"wght\" var(--font-weight-bold);padding:calc(var(--baseline) * 6 + 1vw) 2rem;font-size:clamp(1.1377777785rem, 1.1377777785rem + .1567604947 * (100vw - 20rem) / 70, 1.2945382732rem)}blockquote>:first-child{margin-top:0}ol,ul{margin-left:3ch}ol>li,ul>li{list-style:inherit;padding:0 0 var(--baseline) 1ch}dl dt{font-variation-settings:\"wght\" var(--font-weight-bold)}pre{background-color:var(--lighter);font-size:.8239746086rem;padding:calc(var(--baseline) * 6);white-space:pre-wrap;word-wrap:break-word}pre>code{color:var(--text-color);display:inline-block;font-size:inherit;padding:0}code{background-color:var(--lighter);color:var(--color);font-size:.8239746086rem;font-family:Menlo,Monaco,Consolas,Courier New,monospace}table{border:1px solid var(--light);border-collapse:collapse;border-spacing:0;vertical-align:top;text-align:left;width:100%}@media all and (max-width:37.4375em){table{display:block;overflow-x:auto}}table th{font-variation-settings:\"wght\" var(--font-weight-bold);padding:calc(var(--baseline) * 2.5) calc(var(--baseline) * 4)}table td{border-top:1px solid var(--light);padding:calc(var(--baseline) * 2.5) calc(var(--baseline) * 4)}.table-striped tr:nth-child(2n){background:var(--lighter)}.table-bordered td,.table-bordered th{border:1px solid var(--light)}.table-title th{background:var(--lighter)}figcaption{clear:both;color:var(--gray);font-style:italic;font-size:.7241964329rem;padding:calc(var(--baseline) * 3) 0 0;text-align:center}kbd{background:var(--dark);border-radius:2px;color:var(--white);font-family:Menlo,Monaco,Consolas,Courier New,monospace;font-size:.8789062495rem;padding:calc(var(--baseline) * .5) calc(var(--baseline) * 1.5)}sub,sup{font-size:65%}small{font-size:.8789062495rem}.separator,hr{background:0 0;border:none;height:auto;line-height:1;max-width:none;text-align:center}.separator::before,hr::before{content:\"•••\";color:var(--dark);font-size:1rem;font-variation-settings:\"wght\" var(--font-weight-bold);letter-spacing:1.1377777785rem;padding-left:1.1377777785rem}.separator--dot::before{content:\"•\";color:var(--dark);font-size:1rem;font-variation-settings:\"wght\" var(--font-weight-bold);letter-spacing:1.1377777785rem;padding-left:1.1377777785rem}.separator--long-line{position:relative}.separator--long-line::before{content:\"\";height:1rem}.separator--long-line::after{border-top:1px solid var(--light);content:\"\";height:1px;position:absolute;width:100%;top:50%;left:0}.btn,[type=button],[type=submit],button{align-items:center;background:0 0;border:1px solid var(--dark);border-radius:calc(var(--border-radius) * 10);color:var(--dark);cursor:pointer;display:inline-flex;font-family:var(--menu-font);font-size:.8239746086rem;font-variation-settings:\"wght\" var(--font-weight-bold);overflow:hidden;padding:calc(var(--baseline) * 2) calc(var(--baseline) * 4);text-align:center;-webkit-transition:all .24s ease-out;transition:all .24s ease-out;vertical-align:middle;will-change:transform}@media all and (max-width:19.9375em){.btn,[type=button],[type=submit],button{width:100%}}.btn:active,.btn:focus,.btn:hover,[type=button]:active,[type=button]:focus,[type=button]:hover,[type=submit]:active,[type=submit]:focus,[type=submit]:hover,button:active,button:focus,button:hover{background-color:var(--dark);color:var(--helper)}.btn--icon{gap:.3rem;justify-content:center}.btn--icon svg{stroke:currentColor}@media all and (min-width:20em){.btn+.btn,.btn+[type=button],.btn+[type=submit],.btn+button,[type=button]+.btn,[type=button]+[type=button],[type=button]+[type=submit],[type=button]+button,[type=submit]+.btn,[type=submit]+[type=button],[type=submit]+[type=submit],[type=submit]+button,button+.btn,button+[type=button],button+[type=submit],button+button{margin-left:calc(var(--baseline) * 2)}}@media all and (max-width:37.4375em){.btn+.btn,.btn+[type=button],.btn+[type=submit],.btn+button,[type=button]+.btn,[type=button]+[type=button],[type=button]+[type=submit],[type=button]+button,[type=submit]+.btn,[type=submit]+[type=button],[type=submit]+[type=submit],[type=submit]+button,button+.btn,button+[type=button],button+[type=submit],button+button{margin-bottom:calc(var(--baseline) * 2)}}.btn:disabled,.btn[disabled],[disabled][type=button],[disabled][type=submit],[type=button]:disabled,[type=submit]:disabled,button:disabled,button[disabled]{background-color:var(--light);border-color:var(--light);color:var(--gray);cursor:not-allowed;pointer-events:none}[type=button],[type=submit],button{-webkit-appearance:none;-moz-appearance:none}::-webkit-search-cancel-button{-webkit-appearance:none}fieldset{border:1px solid var(--light);margin:calc(var(--baseline) * 6 + 1vw) 0 0;padding:calc(var(--baseline) * 6)}fieldset>legend{margin-left:-1rem;padding:0 1rem}legend{font-variation-settings:\"wght\" 500;padding:0}label{font-variation-settings:\"wght\" 500;margin:0 calc(var(--baseline) * 4) calc(var(--baseline) * 3) 0}[type=email],[type=number],[type=search],[type=tel],[type=text],[type=url],select,textarea{background-color:var(--page-bg);border:none;border:1px solid var(--light);color:var(--text-color);font-size:1rem;outline:0;padding:calc(var(--baseline) * 1.2) calc(var(--baseline) * 3);vertical-align:middle;width:100%;-webkit-appearance:none;-moz-appearance:none}@media all and (min-width:37.5em){[type=email],[type=number],[type=search],[type=tel],[type=text],[type=url],select,textarea{width:auto}}[type=email]:focus,[type=number]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=url]:focus,select:focus,textarea:focus{border-color:var(--dark)}input[type=checkbox],input[type=radio]{opacity:0;position:absolute}input[type=checkbox]+label,input[type=radio]+label{position:relative;margin-left:-1px;cursor:pointer;padding:0}input[type=checkbox]+label:before,input[type=radio]+label:before{background-color:var(--white);border:1px solid var(--light);border-radius:2px;content:\"\";display:inline-block;height:calc(var(--baseline) * 5);line-height:calc(var(--baseline) * 5);margin-right:calc(var(--baseline) * 4);vertical-align:middle;text-align:center;width:calc(var(--baseline) * 5)}input[type=checkbox]:checked+label:before,input[type=radio]:checked+label:before{content:\"\";background-image:url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 8'%3e%3cpolygon points='9.53 0 4.4 5.09 1.47 2.18 0 3.64 2.93 6.54 4.4 8 5.87 6.54 11 1.46 9.53 0' fill='%23d73a42'/%3e%3c/svg%3e\");background-repeat:no-repeat;background-size:11px 8px;background-position:50% 50%}input[type=radio]+label:before{border-radius:50%}input[type=radio]:checked+label:before{background-image:url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3ccircle cx='4' cy='4' r='4' fill='%23d73a42'/%3e%3c/svg%3e\")}[type=file]{margin-bottom:calc(var(--baseline) * 6);width:100%}select{max-width:100%;width:auto;position:relative}select:not([multiple]){background:url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 6 6\"><polygon points=\"3 6 3 6 0 0 6 0 3 6\" fill=\"%238a8b8c\"/></svg>') no-repeat 90% 50%;background-size:8px;padding-right:calc(var(--baseline) * 12)}select[multiple]{border:1px solid var(--light);padding:calc(var(--baseline) * 6);width:100%}select[multiple]:hover{border-color:var(--light)}select[multiple]:focus{border-color:var(--dark)}select[multiple]:disabled{background-color:var(--light);cursor:not-allowed}select[multiple]:disabled:hover{border-color:var(--light)}textarea{display:block;overflow:auto;resize:vertical;max-width:100%}.top{align-items:center;display:flex;height:var(--navbar-height);position:relative;padding:0 var(--page-margin);-webkit-transition:background .5s ease;transition:background .5s ease;width:100%;z-index:2}@media all and (min-width:56.25em){.top{justify-content:space-between;height:var(--navbar-height)}}.top.sticky{background:var(--page-bg);position:sticky;top:-100px;-webkit-animation:slideDown .5s cubic-bezier(.17,.67,0,1) forwards;animation:slideDown .5s cubic-bezier(.17,.67,0,1) forwards}@-webkit-keyframes slideDown{from{opacity:0;top:-100px}to{opacity:1;top:0}}@keyframes slideDown{from{opacity:0;top:-100px}to{opacity:1;top:0}}.logo{color:var(--logo-color)!important;font-size:1.2136296308rem;font-family:var(--logo-font);font-variation-settings:\"wght\" var(--font-weight-bold);margin-right:auto;order:1}.logo>img{height:var(--navbar-height);-o-object-fit:contain;object-fit:contain;padding:calc(var(--baseline) * 2) 0;width:auto}.search{order:2}@media all and (min-width:56.25em){.search{order:3}}.search__btn{border-color:var(--light);margin:0;height:2.2rem;padding:0;width:2.2rem}@media all and (min-width:56.25em){.search__btn{margin-left:2rem}}.search__btn:focus,.search__btn:hover{border-color:var(--dark)}.search__form{display:flex;justify-content:space-between;width:100%}.search__form button{width:auto;flex-shrink:0}.search__input{background:0 0;border:none;border-bottom:1px solid var(--dark);display:block;font-family:var(--heading-font);padding:0;width:90%}.search__input:focus-visible{outline:0!important}.search__overlay{background-color:var(--page-bg);-webkit-box-shadow:0 3px 30px rgba(0,0,0,.05);box-shadow:0 3px 30px rgba(0,0,0,.05);left:0;opacity:0;position:fixed;-webkit-transition:all .24s ease-out;transition:all .24s ease-out;top:0;visibility:hidden;width:100%;z-index:2005}.search__overlay-inner{-webkit-animation:slideininput .24s 1s forwards;animation:slideininput .24s 1s forwards;align-items:center;display:flex;height:calc(var(--navbar-height) * 3);justify-content:space-between;padding:0 var(--page-margin);opacity:0;scale:0.9}@-webkit-keyframes slideininput{60%{opacity:0;scale:0.9}100%{opacity:1;scale:1}}@keyframes slideininput{60%{opacity:0;scale:0.9}100%{opacity:1;scale:1}}.search__overlay.expanded{-webkit-transform:translate(0,0);transform:translate(0,0);opacity:1;display:block;visibility:visible}.navbar{order:3}.navbar .navbar__menu{display:flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}@media all and (max-width:56.1875em){.navbar .navbar__menu{display:none}}.navbar .navbar__menu li{font-family:var(--menu-font);display:block;font-size:.8789062495rem;line-height:var(--line-height);font-variation-settings:\"wght\" 500;padding:0;position:relative;width:auto}.navbar .navbar__menu li a,.navbar .navbar__menu li span[aria-haspopup=true]{color:var(--nav-link-color);display:block;padding:0 .6rem;-webkit-transition:all .24s ease-out;transition:all .24s ease-out}.navbar .navbar__menu li a:active,.navbar .navbar__menu li a:focus,.navbar .navbar__menu li a:hover,.navbar .navbar__menu li span[aria-haspopup=true]:active,.navbar .navbar__menu li span[aria-haspopup=true]:focus,.navbar .navbar__menu li span[aria-haspopup=true]:hover{color:var(--nav-link-color-hover)}.navbar .navbar__menu li span{color:var(--nav-link-color);cursor:default;display:block;padding:0 .6rem}.navbar .navbar__menu>li:hover>a,.navbar .navbar__menu>li:hover>span[aria-haspopup=true]{color:var(--nav-link-color-hover)}.navbar .has-submenu:active>.navbar__submenu,.navbar .has-submenu:focus>.navbar__submenu,.navbar .has-submenu:hover>.navbar__submenu{left:0;opacity:1;-webkit-transform:scale(1);transform:scale(1);visibility:visible;margin-top:.8rem}.navbar .has-submenu:active>.navbar__submenu:before,.navbar .has-submenu:focus>.navbar__submenu:before,.navbar .has-submenu:hover>.navbar__submenu:before{content:\"\";height:1rem;left:0;position:absolute;width:100%;top:-1rem}.navbar .has-submenu:active>.navbar__submenu.is-right-submenu,.navbar .has-submenu:focus>.navbar__submenu.is-right-submenu,.navbar .has-submenu:hover>.navbar__submenu.is-right-submenu{left:auto;right:0;-webkit-transform-origin:right top;transform-origin:right top}.navbar .has-submenu .has-submenu:active>.navbar__submenu,.navbar .has-submenu .has-submenu:focus>.navbar__submenu,.navbar .has-submenu .has-submenu:hover>.navbar__submenu{top:0;margin-top:0}.navbar .has-submenu .has-submenu:active>.navbar__submenu.is-right-submenu,.navbar .has-submenu .has-submenu:focus>.navbar__submenu.is-right-submenu,.navbar .has-submenu .has-submenu:hover>.navbar__submenu.is-right-submenu{top:0;margin-top:0}.navbar .navbar__submenu{background:var(--lighter);border-radius:calc(var(--border-radius) * 4);left:-9999px;list-style-type:none;margin:0;padding:1rem .85rem;position:absolute;visibility:hidden;white-space:nowrap;z-index:1;opacity:0;-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:0 top;transform-origin:0 top;-webkit-transition:opacity .15s,-webkit-transform .3s cubic-bezier(.275, 1.375, .8, 1);transition:opacity .15s,-webkit-transform .3s cubic-bezier(.275, 1.375, .8, 1);transition:opacity .15s,transform .3s cubic-bezier(.275, 1.375, .8, 1);transition:opacity .15s,transform .3s cubic-bezier(.275, 1.375, .8, 1),-webkit-transform .3s cubic-bezier(.275, 1.375, .8, 1)}.navbar .navbar__submenu__submenu{z-index:2}.navbar .navbar__submenu li{line-height:1.5;font-size:.8789062495rem}.navbar .navbar__submenu li a,.navbar .navbar__submenu li span[aria-haspopup=true]{border-radius:calc(var(--border-radius) * 3);color:var(--nav-link-color-hover);padding:.5rem 1rem;-webkit-transition:all .24s ease;transition:all .24s ease}.navbar .navbar__submenu li a:active,.navbar .navbar__submenu li a:focus,.navbar .navbar__submenu li a:hover,.navbar .navbar__submenu li span[aria-haspopup=true]:active,.navbar .navbar__submenu li span[aria-haspopup=true]:focus,.navbar .navbar__submenu li span[aria-haspopup=true]:hover{background:var(--page-bg);color:var(--nav-link-color)}.navbar .navbar__submenu li span{color:var(--nav-link-color-hover)!important;padding:.5rem 1rem}.navbar .navbar__submenu li:hover>a,.navbar .navbar__submenu li:hover>span[aria-haspopup=true]{color:var(--nav-link-color)}.navbar .navbar__toggle{background:var(--black);-webkit-box-shadow:none;box-shadow:none;border:none;cursor:pointer;display:block;line-height:1;margin:0;overflow:visible;padding:0;position:relative;right:0;margin-left:.75rem;text-transform:none;z-index:2004;height:3.2rem;padding:0;width:3.2rem}@media all and (min-width:56.25em){.navbar .navbar__toggle{display:none}}.navbar .navbar__toggle:focus,.navbar .navbar__toggle:hover{-webkit-box-shadow:none;box-shadow:none;outline:0;-webkit-transform:none;transform:none}.navbar .navbar__toggle-box{width:20px;height:14px;display:inline-block;position:relative}.navbar .navbar__toggle-inner{display:block;top:50%;text-indent:-9999999em}.navbar .navbar__toggle-inner::before{content:\"\";display:block;top:-5px}.navbar .navbar__toggle-inner::after{content:\"\";display:block;bottom:-5px}.navbar .navbar__toggle-inner,.navbar .navbar__toggle-inner::after,.navbar .navbar__toggle-inner::before{width:20px;height:1px;background-color:var(--white);position:absolute;-webkit-transition:opacity .14s ease-out,-webkit-transform;transition:opacity .14s ease-out,-webkit-transform;transition:transform,opacity .14s ease-out;transition:transform,opacity .14s ease-out,-webkit-transform}.navbar .navbar__toggle-inner{-webkit-transition-duration:75ms;transition-duration:75ms;-webkit-transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19)}.navbar .navbar__toggle-inner::before{-webkit-transition:top 75ms ease .12s,opacity 75ms ease;transition:top 75ms ease .12s,opacity 75ms ease}.navbar .navbar__toggle-inner::after{-webkit-transition:bottom 75ms ease .12s,-webkit-transform 75ms cubic-bezier(.55, .055, .675, .19);transition:bottom 75ms ease .12s,-webkit-transform 75ms cubic-bezier(.55, .055, .675, .19);transition:bottom 75ms ease .12s,transform 75ms cubic-bezier(.55, .055, .675, .19);transition:bottom 75ms ease .12s,transform 75ms cubic-bezier(.55, .055, .675, .19),-webkit-transform 75ms cubic-bezier(.55, .055, .675, .19)}.navbar .navbar__toggle.is-active .navbar__toggle-inner{-webkit-transform:rotate(45deg);transform:rotate(45deg);-webkit-transition-delay:0.12s;transition-delay:0.12s;-webkit-transition-timing-function:cubic-bezier(0.215,0.61,0.355,1);transition-timing-function:cubic-bezier(0.215,0.61,0.355,1)}.navbar .navbar__toggle.is-active .navbar__toggle-inner::before{top:0;opacity:0;-webkit-transition:top 75ms ease,opacity 75ms ease .12s;transition:top 75ms ease,opacity 75ms ease .12s}.navbar .navbar__toggle.is-active .navbar__toggle-inner::after{bottom:0;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);-webkit-transition:bottom 75ms ease,-webkit-transform 75ms cubic-bezier(.215, .61, .355, 1) .12s;transition:bottom 75ms ease,-webkit-transform 75ms cubic-bezier(.215, .61, .355, 1) .12s;transition:bottom 75ms ease,transform 75ms cubic-bezier(.215, .61, .355, 1) .12s;transition:bottom 75ms ease,transform 75ms cubic-bezier(.215, .61, .355, 1) .12s,-webkit-transform 75ms cubic-bezier(.215, .61, .355, 1) .12s}.navbar_mobile_overlay{background:var(--page-bg);height:calc(100vh - 4.4rem);left:0;opacity:1;overflow:auto;pointer-events:auto;position:fixed;top:4.4rem;-webkit-transition:all .3s cubic-bezier(0, 0, .3, 1);transition:all .3s cubic-bezier(0, 0, .3, 1);width:100%;z-index:1001}.navbar_mobile_overlay.is-hidden{opacity:0;pointer-events:none}.navbar_mobile_overlay .navbar__menu{margin:24px}.navbar_mobile_overlay .navbar__menu li{list-style:none;margin:0;padding:0;text-align:center}.navbar_mobile_overlay .navbar__menu li a,.navbar_mobile_overlay .navbar__menu li span{color:var(--dark);display:block;padding:calc(var(--baseline) * 2);position:relative}.navbar_mobile_overlay .navbar__menu li a:active,.navbar_mobile_overlay .navbar__menu li a:focus,.navbar_mobile_overlay .navbar__menu li a:hover,.navbar_mobile_overlay .navbar__menu li span:active,.navbar_mobile_overlay .navbar__menu li span:focus,.navbar_mobile_overlay .navbar__menu li span:hover{color:var(--dark)}.navbar_mobile_overlay .navbar__menu li a[aria-haspopup=true]::after,.navbar_mobile_overlay .navbar__menu li span[aria-haspopup=true]::after{content:\"\";width:0;height:0;border-style:solid;border-width:5px 5px 0 5px;border-color:currentColor transparent transparent transparent;left:calc(var(--baseline) * 2);top:15px;position:relative}.navbar_mobile_overlay .navbar__submenu{margin:0;padding:0;visibility:hidden}.navbar_mobile_overlay .navbar__submenu[aria-hidden=false]{visibility:visible}.navbar_mobile_overlay .navbar__submenu_wrapper{height:0;opacity:0;overflow:hidden;-webkit-transition:all .3s cubic-bezier(.275, 1.375, .8, 1);transition:all .3s cubic-bezier(.275, 1.375, .8, 1)}.navbar_mobile_overlay .navbar__submenu_wrapper.is-active{height:auto;opacity:1}.navbar_mobile_sidebar{background:var(--page-bg);-webkit-box-shadow:0 0 5px rgba(0,0,0,.25);box-shadow:0 0 5px rgba(0,0,0,.25);height:100vh;left:0;max-width:400px;overflow:auto;position:fixed;top:0;-webkit-transition:all .3s cubic-bezier(0, 0, .3, 1);transition:all .3s cubic-bezier(0, 0, .3, 1);width:80%;z-index:1001}.navbar_mobile_sidebar.is-hidden{left:-400px}.navbar_mobile_sidebar .navbar__menu{margin:24px}.navbar_mobile_sidebar .navbar__menu li{font-family:var(--menu-font);font-size:16px;list-style:none;line-height:1.3;margin:0;padding:0}.navbar_mobile_sidebar .navbar__menu li .is-separator,.navbar_mobile_sidebar .navbar__menu li a{color:var(--dark);display:block;padding:10px 20px 10px 0;position:relative}.navbar_mobile_sidebar .navbar__menu li .is-separator:active,.navbar_mobile_sidebar .navbar__menu li .is-separator:focus,.navbar_mobile_sidebar .navbar__menu li .is-separator:hover,.navbar_mobile_sidebar .navbar__menu li a:active,.navbar_mobile_sidebar .navbar__menu li a:focus,.navbar_mobile_sidebar .navbar__menu li a:hover{color:var(--dark)}.navbar_mobile_sidebar .navbar__menu li .is-separator[aria-haspopup=true]::after,.navbar_mobile_sidebar .navbar__menu li a[aria-haspopup=true]::after{content:\"\";width:0;height:0;border-style:solid;border-width:5px 5px 0 5px;border-color:currentColor transparent transparent transparent;right:0;top:18px;position:absolute}.navbar_mobile_sidebar .navbar__submenu{margin:0 0 0 24px;padding:0;visibility:hidden}.navbar_mobile_sidebar .navbar__submenu[aria-hidden=false]{visibility:visible}.navbar_mobile_sidebar .navbar__submenu_wrapper{height:0;opacity:0;overflow:hidden;-webkit-transition:all .3s cubic-bezier(.275, 1.375, .8, 1);transition:all .3s cubic-bezier(.275, 1.375, .8, 1)}.navbar_mobile_sidebar .navbar__submenu_wrapper.is-active{height:auto;opacity:1}.navbar_mobile_sidebar__overlay{background:rgba(0,0,0,.5);height:100%;opacity:1;pointer-events:auto;position:fixed;top:0;-webkit-transition:all .3s cubic-bezier(0, 0, .3, 1);transition:all .3s cubic-bezier(0, 0, .3, 1);width:100%;z-index:10}.navbar_mobile_sidebar__overlay.is-hidden{opacity:0;pointer-events:none}.wrapper{-webkit-box-sizing:content-box;box-sizing:content-box;max-width:var(--page-width);margin-left:auto;margin-right:auto;padding-left:var(--page-margin);padding-right:var(--page-margin)}.entry-wrapper{-webkit-box-sizing:content-box;box-sizing:content-box;max-width:var(--entry-width);margin-left:auto;margin-right:auto;padding-left:var(--page-margin);padding-right:var(--page-margin)}.hero{position:relative;z-index:1}.hero--noimage::after{background:var(--dark);content:\"\";display:block;height:1px;bottom:0;width:calc(100% - var(--page-margin) * 2);z-index:-1;max-width:var(--page-width);position:absolute;left:50%;-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}.hero__content{padding-bottom:calc(var(--baseline) * 6 + 1.5vw)}.hero__content h1>sup{font-size:1.066666667rem;vertical-align:top}.hero__content--centered{text-align:center}.hero__content--centered .content__meta{justify-content:center}.hero__cta{margin-top:calc(var(--baseline) * 6)}.hero__image{margin:0 var(--page-margin)}.hero__image-wrapper{position:relative;background:var(--lighter);border-radius:calc(var(--border-radius) * 4)}@media all and (min-width:56.25em){.hero__image-wrapper{height:var(--hero-height)}}.hero__image-wrapper img{border-radius:inherit;display:block;height:100%;-o-object-fit:cover;object-fit:cover;width:100%}@media all and (min-width:56.25em){.hero__image-wrapper img{height:var(--hero-height)}}.hero__image>figcaption{background:var(--page-bg)}@media all and (min-width:56.25em){.hero__image>figcaption{text-align:right}}.feed__item{display:flex;flex-wrap:wrap;gap:calc(2rem + 2vw);margin-top:calc(var(--baseline) * 12 + 2vw)}@media all and (min-width:37.5em){.feed__item{flex-wrap:nowrap}}.feed__item--centered{justify-content:center}.feed__content{max-width:var(--entry-width)}.feed__image{background:var(--lighter);border-radius:calc(var(--border-radius) * 4);flex-shrink:0;height:100%;margin:0;width:100%}@media all and (min-width:37.5em){.feed__image{height:calc(var(--feed-image-size) + 4vw);width:calc(var(--feed-image-size) + 4vw)}}.feed__image>img{border-radius:inherit;display:inline-block;height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.feed__image--wide{max-width:var(--page-width)}.feed__meta{align-items:center;color:var(--gray);display:flex;font-size:.8239746086rem;gap:.8rem;margin-bottom:calc(var(--baseline) * 3)}.feed__author{font-family:var(--menu-font);font-variation-settings:\"wght\" var(--font-weight-bold);text-decoration:none}.feed__author-thumb{border-radius:50%;height:1.7rem;margin-right:-.2rem;width:1.7rem}.feed__date{color:var(--gray);font-style:italic}.feed__author+.feed__date::before{content:\"\";background:var(--light);display:inline-block;height:1px;margin-right:4px;width:1rem;vertical-align:middle}.feed__readmore{margin-top:calc(var(--baseline) * 4 + .25vw)}.feed__title{margin-top:0}.feed--grid{margin:0}@media all and (min-width:37.5em){.feed--grid{display:grid;grid-template-columns:100%;gap:0 3rem}}@media all and (min-width:56.25em){.feed--grid{grid-template-columns:repeat(2,1fr)}}.feed--grid h2{margin-top:0}.feed--grid sup{font-size:1.066666667rem;vertical-align:top}.feed--grid li{align-items:center;list-style:none;gap:2rem;padding:0}.content{overflow:hidden}.content__meta{margin-top:calc(var(--baseline) * 4 + .25vw);margin-bottom:0}.content__meta--centered{justify-content:center}.content__entry{margin-top:calc(var(--baseline) * 6 + 1.5vw);overflow-wrap:break-word}.content__entry>:first-child{margin-top:0}.content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a){color:var(--link-color-hover);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:0.2em;-webkit-text-decoration-skip:ink;text-decoration-skip-ink:auto}.content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a):active,.content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a):focus,.content__entry a:not(.btn):not([type=button]):not([type=submit]):not(button):not(.post__toc a):not(.gallery__item a):hover{color:var(--link-color)}.content__entry--nospace{margin-top:0}.content__avatar-thumbs{border-radius:50%;height:4.5rem;width:4.5rem}.content__footer{margin-top:calc(var(--baseline) * 9 + 1vw)}.content__updated{color:var(--gray);font-size:.8789062495rem;font-style:italic}.content__actions{align-items:baseline;display:flex;flex-basis:auto;gap:2rem;justify-content:space-between;margin-top:calc(var(--baseline) * 4 + .25vw);position:relative}.content__share{flex-shrink:0}.content__share-button{border-color:var(--light)}.content__share-popup{background:var(--lighter);border-radius:calc(var(--border-radius) * 4);bottom:140%;display:none;padding:1rem .85rem;position:absolute;right:0;text-align:left;z-index:1}.content__share-popup.is-visible{-webkit-animation:share-popup .48s cubic-bezier(.17,.67,.6,1.34) backwards;animation:share-popup .48s cubic-bezier(.17,.67,.6,1.34) backwards;display:block}@-webkit-keyframes share-popup{from{-webkit-transform:scale(.9);transform:scale(.9)}to{-webkit-transform:scale(1);transform:scale(1)}}@keyframes share-popup{from{-webkit-transform:scale(.9);transform:scale(.9)}to{-webkit-transform:scale(1);transform:scale(1)}}.content__share-popup>a{border-radius:calc(var(--border-radius) * 3);color:var(--text-color);display:block;font-family:var(--menu-font);font-size:.8239746086rem;padding:.4rem .8rem}.content__share-popup>a:hover{background:var(--page-bg);color:var(--text-color);text-decoration:none}.content__share-popup>a>svg{fill:var(--text-color);display:inline-block;height:.9rem;margin-right:.5666666667rem;pointer-events:none;vertical-align:middle;width:.9rem}.content__tag{margin:0;font-family:var(--menu-font);font-size:.8239746086rem}.content__tag>li{display:inline-flex;margin:.3rem .3rem .3rem 0;padding:0}.content__tag>li>a{border:1px solid var(--light);border-radius:calc(var(--border-radius) * 10);color:var(--dark);font-size:.7241964329rem;font-variation-settings:\"wght\" var(--font-weight-normal);padding:calc(var(--baseline) * 1) calc(var(--baseline) * 2.5)}.content__tag>li>a:hover{border-color:var(--dark)}.content__bio{display:flex;margin:calc(var(--baseline) * 12 + 1vw) 0}@media all and (min-width:37.5em){.content__bio{align-items:center}}@media all and (min-width:37.5em){.content__bio::before{content:\"\";border-top:1px solid var(--light);height:1px;margin-right:2rem;width:30%}}.bio__avatar{border-radius:50%;flex-shrink:0;height:2.5rem;margin-right:1.2rem;width:2.5rem}@media all and (min-width:37.5em){.bio__avatar{height:4rem;margin-right:2rem;width:4rem}}.bio__name{margin:0}.bio__desc{font-family:var(--body-font);font-size:.8789062495rem;line-height:1.5}.bio__desc>:first-child{margin-top:calc(var(--baseline) * 2)}.bio__desc a{text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:0.2em;-webkit-text-decoration-skip:ink;text-decoration-skip-ink:auto}.bio__desc a{color:var(--link-color-hover);-webkit-transition:all .24s ease-out;transition:all .24s ease-out}.bio__desc a:hover{color:var(--link-color)}.bio__desc a:active{color:var(--link-color)}.bio__desc a:focus{outline:0}.content__nav{margin-top:calc(var(--baseline) * 16 + 1vw)}.content__nav-inner{border-top:1px solid var(--dark);border-bottom:1px solid var(--dark);padding:calc(var(--baseline) * 16) 0}@media all and (min-width:37.5em){.content__nav-inner{display:flex;gap:1rem;justify-content:space-between}}@media all and (min-width:56.25em){.content__nav-inner{gap:2rem}}@media all and (max-width:37.4375em){.content__nav-prev+.content__nav-next{margin-top:calc(var(--baseline) * 6 + 1vw)}}@media all and (min-width:37.5em){.content__nav-next{margin-left:auto;text-align:right}}.content__nav-link{font-family:var(--heading-font);font-variation-settings:\"wght\" var(--font-weight-bold);font-style:italic;height:100%;line-height:1.5;display:flex;gap:1rem;justify-content:space-between;align-items:center}@media all and (min-width:37.5em){.content__nav-link{gap:2rem}}.content__nav-link>div{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}@media all and (min-width:56.25em){.content__nav-link>div{-webkit-line-clamp:4}}.content__nav-link span{color:var(--gray);display:block;font-size:.7724761953rem;font-family:var(--menu-font);font-style:normal;font-variation-settings:\"wght\" var(--font-weight-normal);margin-bottom:var(--baseline)}.content__nav-image{flex:1 0 4rem;margin:0;height:4rem}@media all and (min-width:37.5em) and (max-width:56.1875em){.content__nav-image{flex-basis:6rem;height:6rem}}@media all and (min-width:56.25em){.content__nav-image{flex-basis:8rem;height:8rem}}.content__nav-image>img{border-radius:calc(var(--border-radius) * 4);display:block;height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.content__related{background:var(--lighter);margin-top:calc(var(--baseline) * 16 + 1vw);padding:calc(var(--baseline) * 12 + 3vw) 0}.related__title{margin-top:0}.content__comments{margin-top:calc(var(--baseline) * 9);overflow:hidden}.post__image:not(.post__image--wide):not(.post__image--full){display:inline-block;margin-bottom:calc(var(--baseline) * 2 + .25vw)}.post__image a,.post__image img{border-radius:calc(var(--border-radius) * 4);display:inline-block}.post__image>figcaption{background-color:var(--page-bg)}.post__image--left{float:left;margin-right:2rem;max-width:50%}.post__image--right{float:right;margin-left:2rem;max-width:50%}.post__image--center{display:block;margin-left:auto;margin-right:auto;text-align:center}.post__image--wide{display:block}@media all and (min-width:56.25em){.post__image--wide{margin-left:calc(-50vw + 50%);margin-right:calc(-50vw + 50%);padding:0 var(--page-margin);text-align:center}.post__image--wide a,.post__image--wide img{display:block;height:auto;margin:auto;max-width:var(--page-width);width:100%}}.post__image--full{background-color:var(--lighter);display:block;margin-left:calc(-50vw + 50%);margin-right:calc(-50vw + 50%);text-align:center}.post__image--full a,.post__image--full img{border-radius:0;display:block;height:auto;width:100%}.post__iframe,.post__video{display:block;margin-top:calc(var(--baseline) * 6 + .5vw);margin-bottom:calc(var(--baseline) * 6 + .5vw);overflow:hidden;padding:0;position:relative;width:100%}.post__iframe::before,.post__video::before{display:block;content:\"\";padding-top:var(--embed-aspect-ratio)}.post__iframe iframe[height*=\"%\"][width*=\"%\"],.post__iframe iframe[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]),.post__iframe video[height*=\"%\"][width*=\"%\"],.post__iframe video[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]),.post__video iframe[height*=\"%\"][width*=\"%\"],.post__video iframe[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]),.post__video video[height*=\"%\"][width*=\"%\"],.post__video video[height]:not([height*=\"%\"])[width]:not([width*=\"%\"]){border:none;height:100%;left:0;position:absolute;top:0;bottom:0;width:100%}.post__iframe:has(iframe:not([height]))::before,.post__iframe:has(iframe:not([width]))::before,.post__iframe:has(video:not([height]))::before,.post__iframe:has(video:not([width]))::before,.post__video:has(iframe:not([height]))::before,.post__video:has(iframe:not([width]))::before,.post__video:has(video:not([height]))::before,.post__video:has(video:not([width]))::before{display:none}.post__toc{margin-top:calc(var(--baseline) * 6 + .5vw)}.post__toc h3{border-bottom:1px solid var(--dark);font-size:1rem;margin:0;padding-bottom:calc(var(--baseline) * 2 + .25vw)}.post__toc ul{counter-reset:item;list-style:decimal;margin:calc(var(--baseline) * 3 + .25vw) 0 0 3ch}.post__toc ul li{counter-increment:item;padding:0}.post__toc ul ul{margin-top:0}.post__toc ul ul li{display:block}.post__toc ul ul li:before{content:counters(item, \".\") \". \";margin-left:-3ch}.banner{text-align:center}.banner--after-content{margin-top:calc(var(--baseline) * 9 + 1vw)}.page__desc a{text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:0.2em;-webkit-text-decoration-skip:ink;text-decoration-skip-ink:auto}@media all and (min-width:37.5em){.page--author__wrapper{display:flex;gap:2rem}}@media all and (min-width:56.25em){.page--author__wrapper{gap:3rem}}.page--author__avatar{border-radius:50%;height:calc(var(--baseline) * 10 + 2vw);margin-top:calc(var(--baseline) * 6 + 1vw);width:calc(var(--baseline) * 10 + 2vw)}.page--author__website{margin-top:calc(var(--baseline) * 4 + .25vw)}.page--search form{align-items:flex-start;display:flex;flex-wrap:wrap}@media all and (max-width:37.4375em){.page--search input{margin-bottom:calc(var(--baseline) * 2)}}@media all and (min-width:20em){.page--search input{flex:1 0 auto;margin-right:calc(var(--baseline) * 2)}}@media all and (max-width:37.4375em){.page--search button{width:100%}}.subpages__title{border-top:1px solid var(--light);padding-top:calc(var(--baseline) * 6 + .5vw)}.subpages__list{list-style:initial;margin-left:2ch}.subpages__list ul{list-style:initial;margin:0 0 0 2ch}.subpages__list li{padding-bottom:0}.readmore{display:inline-block;color:var(--gray);font-size:.9374999997rem;font-style:italic;text-decoration:underline;-webkit-text-decoration-skip:ink;text-decoration-skip-ink:auto}.align-left{text-align:left}.align-right{text-align:right}.align-center{text-align:center}.align-justify{text-align:justify}.msg{background-color:var(--lighter);border-left:3px solid var(--light);font-size:.9374999997rem;padding:calc(var(--baseline) * 4) calc(var(--baseline) * 6);position:relative}.msg--highlight{border-left-color:var(--highlight-color)}.msg--info{border-left-color:var(--info-color)}.msg--success{border-left-color:var(--success-color)}.msg--warning{border-left-color:var(--warning-color)}.ordered-list{counter-reset:listCounter}.ordered-list li{counter-increment:listCounter;list-style:none;padding-left:.3rem;position:relative}.ordered-list li::before{color:var(--color);content:counter(listCounter,decimal-leading-zero) \".\";font-variation-settings:\"wght\" var(--font-weight-bold);left:-2rem;position:absolute}.dropcap:first-letter{color:var(--headings-color);float:left;font-size:3.6355864383rem;line-height:.7;margin-right:.6rem;padding:calc(var(--baseline) * 2) calc(var(--baseline) * 2) calc(var(--baseline) * 2) 0}.pec-wrapper{height:100%;left:0;position:absolute;top:0;width:100%}.pec-overlay{align-items:center;background-color:var(--lighter);font-size:14px;display:none;height:inherit;justify-content:center;line-height:1.4;padding:1rem;position:relative;text-align:center}@media all and (min-width:37.5em){.pec-overlay{font-size:16px;line-height:var(--line-height);padding:1rem 2rem}}.pec-overlay.is-active{display:flex}.pec-overlay-inner p{margin:0 0 1rem}.pagination{display:flex;gap:calc(var(--baseline) * 2);justify-content:center;margin-top:calc(var(--baseline) * 12 + 1vw)}@media all and (min-width:56.25em){.pagination{margin-top:calc(var(--baseline) * 18 + 1vw)}}.footer{border-top:1px solid var(--light);font-size:.9374999997rem;padding:calc(var(--baseline) * 9 + 1vw) 0 calc(var(--baseline) * 6 + 1vw);margin:calc(var(--baseline) * 12 + 1vw) auto 0;max-width:var(--page-width);text-align:center}.footer--glued{border:none;padding-top:0}.footer__nav ul{list-style:none;margin:0}.footer__nav ul li{display:inline-block;padding:var(--baseline) .5rem}*+.footer__social{margin-top:calc(var(--baseline) * 6 + .5vw)}.footer__social svg{fill:var(--dark);height:1rem;margin:0 .5rem;-webkit-transition:all .12s linear 0s;transition:all .12s linear 0s;width:1rem}.footer__social svg:hover{fill:var(--gray)}.footer__copyright{font-size:.8239746086rem;margin-top:var(--baseline)}.footer__copyright>:first-child{margin:0}.footer__bttop{background:var(--page-bg);bottom:calc(var(--baseline) * 5);border-radius:50%;border-color:var(--light);line-height:1;opacity:0;padding:calc(var(--baseline) * 2);position:fixed;right:2rem;text-align:center;width:auto!important;visibility:hidden;z-index:999}@media all and (min-width:56.25em){.footer__bttop{bottom:calc(var(--baseline) * 10)}}.footer__bttop:hover{border-color:var(--dark);opacity:1}.footer__bttop.is-visible{visibility:visible;opacity:1}.gallery{margin:calc(var(--baseline) * 6 + 1vw) calc(var(--gallery-gap) * -1)}@media all and (min-width:20em){.gallery{display:flex;flex-wrap:wrap}}@media all and (min-width:56.25em){.gallery-wrapper--wide{display:flex;justify-content:center;margin-left:calc(-50vw + 50%);margin-right:calc(-50vw + 50%);padding:0 var(--page-margin)}.gallery-wrapper--wide .gallery{max-width:var(--page-width)}}@media all and (min-width:56.25em){.gallery-wrapper--full{margin-left:calc(-50vw + 50%);margin-right:calc(-50vw + 50%);padding:0 var(--page-margin)}}@media all and (min-width:20em){.gallery[data-columns=\"1\"] .gallery__item{flex:1 0 100%}}@media all and (min-width:30em){.gallery[data-columns=\"2\"] .gallery__item{flex:1 0 50%}}@media all and (min-width:37.5em){.gallery[data-columns=\"3\"] .gallery__item{flex:1 0 33.333%}}@media all and (min-width:56.25em){.gallery[data-columns=\"4\"] .gallery__item{flex:0 1 25%}}@media all and (min-width:56.25em){.gallery[data-columns=\"5\"] .gallery__item{flex:0 1 20%}}@media all and (min-width:56.25em){.gallery[data-columns=\"6\"] .gallery__item{flex:0 1 16.666%}}@media all and (min-width:56.25em){.gallery[data-columns=\"7\"] .gallery__item{flex:1 0 14.285%}}@media all and (min-width:56.25em){.gallery[data-columns=\"8\"] .gallery__item{flex:1 0 12.5%}}.gallery__item{margin:0;padding:var(--gallery-gap);position:relative}@media all and (min-width:20em){.gallery__item{flex:1 0 50%}}@media all and (min-width:30em){.gallery__item{flex:1 0 33.333%}}@media all and (min-width:37.5em){.gallery__item{flex:1 0 25%}}.gallery__item a{border-radius:calc(var(--border-radius) * 4);display:block;height:100%;width:100%}.gallery__item a::after{background:-webkit-gradient(linear,left bottom,left top,from(rgba(0,0,0,.4)),to(rgba(0,0,0,0)));background:linear-gradient(to top,rgba(0,0,0,.4) 0,rgba(0,0,0,0) 100%);border-radius:inherit;bottom:var(--gallery-gap);content:\"\";display:block;opacity:0;left:var(--gallery-gap);height:calc(100% - var(--gallery-gap) * 2);position:absolute;right:var(--gallery-gap);top:var(--gallery-gap);-webkit-transition:all .24s ease-out;transition:all .24s ease-out;width:calc(100% - var(--gallery-gap) * 2)}.gallery__item a:hover::after{opacity:1}.gallery__item img{border-radius:inherit;display:block;height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.gallery__item figcaption{bottom:1.2rem;color:var(--white);left:50%;opacity:0;position:absolute;text-align:center;-webkit-transform:translate(-50%,1.2rem);transform:translate(-50%,1.2rem);-webkit-transition:all .24s ease-out;transition:all .24s ease-out}.gallery__item:hover figcaption{opacity:1;-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}img[loading]{opacity:0}img.is-loaded{opacity:1;transition:opacity 1s cubic-bezier(.215, .61, .355, 1)}"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/adventpro/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/albertsans/OFL.txt",
    "content": "Copyright 2021 The Albert Sans Project Authors (https://github.com/usted/Albert-Sans)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/aleo/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/andadapro/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/antonio/OFL.txt",
    "content": "Copyright 2013 The Antonio Project Authors (https://github.com/googlefonts/antonioFont)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/archivonarrow/OFL.txt",
    "content": "Copyright 2019 The Archivo Narrow Project Authors (https://github.com/Omnibus-Type/ArchivoNarrow)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/asap/OFL.txt",
    "content": "Copyright 2018 The Asap Project Authors (https://github.com/Omnibus-Type/Asap)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/assistant/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/besley/OFL.txt",
    "content": "Copyright 2020 The Besley Project Authors (https://github.com/indestructible-type/Besley)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/bigshouldersdisplay/OFL.txt",
    "content": "Copyright 2019 The Big Shoulders Project Authors (https://github.com/xotypeco/big_shoulders)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/bitcount/OFL.txt",
    "content": "Copyright 1980 The Bitcount Project Authors (https://github.com/petrvanblokland/TYPETR-Bitcount)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/bitter/OFL.txt",
    "content": "Copyright 2011 The Bitter Project Authors (https://github.com/solmatas/BitterPro)\r\nwith Reserved Font Name \"Bitter Pro\".\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/bodonimoda/OFL.txt",
    "content": "Copyright 2020 The Bodoni Moda Project Authors (https://github.com/indestructible-type/Bodoni)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/brygada1918/OFL.txt",
    "content": "Copyright 2020 The Brygada 1918 Project Authors (https://github.com/kosmynkab/Brygada-1918)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/cabin/OFL.txt",
    "content": "Copyright 2009 The Cairo Project Authors (https://github.com/Gue3bara/Cairo)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/cairo/OFL.txt",
    "content": "Copyright 2009 The Cairo Project Authors (https://github.com/Gue3bara/Cairo)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/cinzel/OFL.txt",
    "content": "Copyright 2020 The Cinzel Project Authors (https://github.com/NDISCOVER/Cinzel)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/comfortaa/OFL.txt",
    "content": "Copyright 2011 The Comfortaa Project Authors (https://github.com/alexeiva/comfortaa), with Reserved Font Name \"Comfortaa\".\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/comme/OFL.txt",
    "content": "Copyright 2014 The Comme Project Authors (https://github.com/googlefonts/comme)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/dancingscript/OFL.txt",
    "content": "Copyright 2016 The Dancing Script Project Authors (https://github.com/googlefonts/DancingScript), with Reserved Font Name 'Dancing Script'.\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/danfo/OFL.txt",
    "content": "Copyright 2023 The Danfo Project Authors (https://github.com/Afrotype/danfo)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/dmsans/OFL.txt",
    "content": "Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/domine/OFL.txt",
    "content": "Copyright 2020 The Domine Project Authors (https://github.com/googlefonts/domine)\r\nCopyright (c) 2012, Pablo Impallari (www.impallari.com|impallari@gmail.com),\r\nCopyright (c) 2012, Pablo Impallari (www.impallari.com|impallari@gmail.com),\r\nCopyright (c) 2012, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com),\r\nCopyright (c) 2012, Brenda Gallo (gbrenda1987@gmail.com), with Reserved Font Name Domine.\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/dosis/OFL.txt",
    "content": "Copyright 2011 The Dosis Project Authors (https://github.com/googlefonts/dosis-vf)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/doto/OFL.txt",
    "content": "Copyright 2024 The Doto Project Authors (https://github.com/oliverlalan/Doto)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/dynapuff/OFL.txt",
    "content": "Copyright 2021 The DynaPuff Project Authors (https://github.com/googlefonts/dynapuff)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/exo/OFL.txt",
    "content": "Copyright 2017 The Exo Project Authors (https://github.com/NDISCOVER/Exo-1.0)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/familjengrotesk/OFL.txt",
    "content": "Copyright 2021 The Familjen Grotesk Project Authors (https://github.com/Familjen-Sthlm/Familjen-Grotesk)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/faustina/OFL.txt",
    "content": "Copyright 2016 The Faustina Project Authors (https://github.com/Omnibus-Type/Faustina)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/figtree/OFL.txt",
    "content": "Copyright 2016 The Faustina Project Authors (https://github.com/Omnibus-Type/Faustina)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/finlandica/OFL.txt",
    "content": "Copyright 2015 The Finlandica Project Authors (https://github.com/HelsinkiTypeStudio/Finlandica)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/frankruhllibre/OFL.txt",
    "content": "Copyright 2016 The Faustina Project Authors (https://github.com/Omnibus-Type/Faustina)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/fredoka/OFL.txt",
    "content": "Copyright 2016 The Fredoka Project Authors (https://github.com/hafontia/Fredoka-One)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/funneldisplay/OFL.txt",
    "content": "Copyright 2024 The Funnel Project Authors (https://github.com/Dicotype/Funnel)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/gantari/OFL.txt",
    "content": "Copyright 2022 The Gantari Project Authors (https://github.com/Lafontype/Gantari)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/geistmono/OFL.txt",
    "content": "Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/glory/OFL.txt",
    "content": "Copyright 2016-2020 The Glory Project Authors (https://github.com/googlefonts/glory)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttps://openfontlicense.org\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/gluten/OFL.txt",
    "content": "Copyright 2020 The Gluten Project Authors (https://github.com/Etcetera-Type-Co/Gluten)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/googlesanscode/OFL.txt",
    "content": "Copyright 2025 The Google Sans Code Project Authors (github.com/googlefonts/googlesans-code)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/grenzegotisch/OFL.txt",
    "content": "Copyright 2020 The Grenze Gotisch Project Authors (https://github.com/Omnibus-Type/Grenze-Gotisch)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/handjet/OFL.txt",
    "content": "Copyright 2018 The Handjet Project Authors (https://github.com/rosettatype/Handjet/)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/heebo/OFL.txt",
    "content": "Copyright 2014 The Heebo Project Authors (https://github.com/OdedEzer/heebo)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/hostgrotesk/OFL.txt",
    "content": "Copyright 2023 The Host Grotesk Project Authors (https://github.com/Element-Type/HostGrotesk)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/imbue/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/inclusivesans/OFL.txt",
    "content": "Copyright 2022 The Inclusive Sans Project Authors (https://github.com/LivKing/Inclusive-Sans)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/instrumentsans/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/jetbrainsmono/OFL.txt",
    "content": "Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/jura/OFL.txt",
    "content": "Copyright 2019 The Jura Project Authors (https://github.com/ossobuffo/jura)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/kalnia/OFL.txt",
    "content": "Copyright 2022 The Kalnia Project Authors (https://github.com/fridamedrano/Kalnia-Typeface.git)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/karla/OFL.txt",
    "content": "Copyright 2019 The Karla Project Authors (https://github.com/googlefonts/karla)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/kreon/OFL.txt",
    "content": "Copyright 2018 The Kreon Project Authors (https://github.com/googlefonts/kreon), with Reserved Font Name \"Kreon\".\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttps://openfontlicense.org\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/kumbhsans/OFL.txt",
    "content": "Copyright 2020 The KumbhSans Project Authors (https://github.com/xconsau/KumbhSans)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/labrada/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/leaguespartan/OFL.txt",
    "content": "Copyright 2020 The League Spartan Project Authors (https://github.com/theleagueof/league-spartan)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/lemonada/OFL.txt",
    "content": "Copyright 2011 The Lemonada Project Authors (https://github.com/Gue3bara/Lemonada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/lexend/OFL.txt",
    "content": "Copyright 2011 The Lemonada Project Authors (https://github.com/Gue3bara/Lemonada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/lexenddeca/OFL.txt",
    "content": "Copyright 2018 The Lexend Project Authors (https://github.com/googlefonts/lexend), with Reserved Font Name “RevReading Lexend”.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/librefranklin/OFL.txt",
    "content": "Copyright 2020 The LibreFranklin Project Authors (https://github.com/impallari/Libre-Franklin)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/lora/OFL.txt",
    "content": "Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name \"Lora\".\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttp://scripts.sil.org/OFL\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/manrope/OFL.txt",
    "content": "Copyright 2018 The Manrope Project Authors (https://github.com/sharanda/manrope)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/manuale/OFL.txt",
    "content": "Copyright 2019 The Manuale Project Authors (https://github.com/Omnibus-Type/Manuale)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/mavenpro/OFL.txt",
    "content": "Copyright 2018 The Manrope Project Authors (https://github.com/sharanda/manrope)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/merriweathersans/OFL.txt",
    "content": "Copyright 2019 The Merriweather Project Authors (https://github.com/SorkinType/Merriweather-Sans)\r\nwith Reserved Font Name 'Merriweather'\r\n\r\nMerriweather is a trademark of Sorkin Type Co.\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/montserrat/OFL.txt",
    "content": "Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/mulish/OFL.txt",
    "content": "Copyright 2016 The Mulish Project Authors (https://github.com/googlefonts/mulish)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/nunito/OFL.txt",
    "content": "Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/orbitron/OFL.txt",
    "content": "Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/oswald/OFL.txt",
    "content": "Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/outfit/OFL.txt",
    "content": "Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/oxanium/OFL.txt",
    "content": "Copyright 2019 The Oxanium Project Authors (https://github.com/sevmeyer/oxanium)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/parkinsans/OFL.txt",
    "content": "Copyright 2024 The Parkinsans Project Authors (https://github.com/redstonedesign/parkinsans)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/petrona/OFL.txt",
    "content": "Copyright 2019 The Petrona Project Authors (https://github.com/RingoSeeber/Petrona)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/playfairdisplay/OFL.txt",
    "content": "Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name \"Playfair Display\"\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/playwriteusmodern/OFL.txt",
    "content": "Copyright 2023 The Playwrite Project Authors (https://github.com/TypeTogether/Playwrite)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/playwriteustrad/OFL.txt",
    "content": "Copyright 2023 The Playwrite Project Authors (https://github.com/TypeTogether/Playwrite)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/plusjakartasans/OFL.txt",
    "content": "Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name \"Playfair Display\"\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/pontanosans/OFL.txt",
    "content": "Copyright 2012 The Pontano Sans Project Authors (https://github.com/googlefonts/PontanoSansFont)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/publicsans/OFL.txt",
    "content": "Copyright (c) 2015, Pablo Impallari, Rodrigo Fuenzalida (Modified by Dan O. Williams and USWDS) (https://github.com/uswds/public-sans)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/quicksand/OFL.txt",
    "content": "Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily), with Reserved Font Name “Quicksand”.\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/radiocanadabig/OFL.txt",
    "content": "Copyright 2022 The Radio Canada Big Project Authors (https://github.com/googlefonts/radiocanadadisplay)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/raleway/OFL.txt",
    "content": "Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name \"Raleway\".\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/redhatdisplay/OFL.txt",
    "content": "Copyright 2021 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/redhatmono/OFL.txt",
    "content": "Copyright 2021 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/redhattext/OFL.txt",
    "content": "Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/redrose/OFL.txt",
    "content": "Copyright 2018 The Red Rose Project Authors (https://github.com/magictype/redrose)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/rem/OFL.txt",
    "content": "Copyright 2019 The REM Project Authors (https://github.com/octaviopardo/REM)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/robotoflex/LICENSE.txt",
    "content": "\r\n                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright [yyyy] [name of copyright owner]\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/robotoslab/LICENSE.txt",
    "content": "\r\n                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright [yyyy] [name of copyright owner]\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/rokkitt/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/rubik/OFL.txt",
    "content": "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/ruda/OFL.txt",
    "content": "Copyright 2019 The Ruda Project Authors (https://github.com/marmonsalve/Ruda-new)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/smoochsans/OFL.txt",
    "content": "Copyright 2016 The Smooch Sans Project Authors (https://github.com/googlefonts/smooch-sans)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttps://openfontlicense.org\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/sora/OFL.txt",
    "content": "Copyright 2019 The Sora Project Authors (https://github.com/sora-xor/sora-font)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/sourcecodepro/OFL.txt",
    "content": "Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/spartan/OFL.txt",
    "content": "Copyright 2020 The Spartan Project Authors (https://github.com/bghryct/Spartan-MB)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/sticknobills/OFL.txt",
    "content": "Copyright 2021 The Stick No Bills Project Authors (https://github.com/mooniak/stick-no-bills-font)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/susemono/OFL.txt",
    "content": "Copyright 2025 The SUSE Project Authors (https://github.com/SUSE/suse-font)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/teachers/OFL.txt",
    "content": "Copyright 2023 The Teachers Project Authors (https://github.com/chankfonts/Teachers-fonts)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/tektur/OFL.txt",
    "content": "Copyright 2023 The Tektur Project Authors (https://www.github.com/hyvyys/Tektur)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/tourney/OFL.txt",
    "content": "Copyright 2020 The Tourney Project Authors (https://github.com/Etcetera-Type-Co/Tourney)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/urbanist/OFL.txt",
    "content": "Copyright 2021 The Urbanist Project Authors (https://github.com/coreyhu/Urbanist)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/varta/OFL.txt",
    "content": "Copyright 2019 The Varta Project Authors (https://github.com/SorkinType/Varta)\n\nVarta is a trademark of Sorkin Type Co.\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/victormono/OFL.txt",
    "content": "Copyright 2023 The Victor Mono Project Authors (https://github.com/rubjo/victor-mono-font)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/wixmadefortext/OFL.txt",
    "content": "Copyright 2023 The Wix Madefor Project Authors (https://github.com/wix/wixmadefor)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/workbench/OFL.txt",
    "content": "Copyright 2021 The Workbench Project Authors (https://github.com/jenskutilek/homecomputer-fonts)\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/worksans/OFL.txt",
    "content": "Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/yanonekaffeesatz/OFL.txt",
    "content": "Copyright 2010 The Yanone Kaffeesatz Project Authors (https://github.com/alexeiva/yanone-kaffeesatz)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/yrsa/OFL.txt",
    "content": "Copyright 2011 The Andada Pro Project Authors (https://github.com/huertatipografica/Andada)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/zalandosans/OFL.txt",
    "content": "Copyright 2025 The Zalando Sans Project Authors (https://github.com/zalando/sans), with Reserved Font Name “Zalando”\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/dynamic/fonts/zalandosansexpanded/OFL.txt",
    "content": "Copyright 2025 The Zalando Sans Project Authors (https://github.com/zalando/sans), with Reserved Font Name “Zalando”\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is copied below, and is also available with a FAQ at:\nhttps://openfontlicense.org\n\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded, \nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION & CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/js/scripts.js",
    "content": "// Sticky header position on page scrolling up\r\nconst header = document.querySelector('.js-header');\r\nconst stickyClass = 'sticky';\r\nlet lastScrollTop = 0;\r\nlet isWaiting = false;\r\n\r\nwindow.addEventListener('scroll', () => {\r\n    if (!isWaiting) {\r\n        window.requestAnimationFrame(() => {\r\n            let currentScroll = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;\r\n\r\n            if (currentScroll > lastScrollTop) {\r\n                header.classList.remove(stickyClass);\r\n            } else if (currentScroll < lastScrollTop && currentScroll > 0) {\r\n                header.classList.add(stickyClass);\r\n            } else if (currentScroll <= 0) {\r\n                header.classList.remove(stickyClass);\r\n            }\r\n\r\n            lastScrollTop = currentScroll;\r\n            isWaiting = false;\r\n        });\r\n        isWaiting = true;\r\n    }\r\n}, false);\r\n\r\n// Dropdown menu\r\n(function (menuConfig) {\r\n    /**\r\n     * Merge default config with the theme overrided ones\r\n     */\r\n    var defaultConfig = {\r\n        // behaviour\r\n        mobileMenuMode: 'overlay',\r\n        animationSpeed: 300,\r\n        submenuWidth: 300,\r\n        doubleClickTime: 500,\r\n        mobileMenuExpandableSubmenus: false,\r\n        isHoverMenu: true,\r\n        // selectors\r\n        wrapperSelector: '.navbar',\r\n        buttonSelector: '.navbar__toggle',\r\n        menuSelector: '.navbar__menu',\r\n        submenuSelector: '.navbar__submenu',\r\n        mobileMenuSidebarLogoSelector: null,\r\n        mobileMenuSidebarLogoUrl: null,\r\n        relatedContainerForOverlayMenuSelector: null,\r\n        // attributes \r\n        ariaButtonAttribute: 'aria-haspopup',\r\n        // CSS classes\r\n        separatorItemClass: 'is-separator',\r\n        parentItemClass: 'has-submenu',\r\n        submenuLeftPositionClass: 'is-left-submenu',\r\n        submenuRightPositionClass: 'is-right-submenu',\r\n        mobileMenuOverlayClass: 'navbar_mobile_overlay',\r\n        mobileMenuSubmenuWrapperClass: 'navbar__submenu_wrapper',\r\n        mobileMenuSidebarClass: 'navbar_mobile_sidebar',\r\n        mobileMenuSidebarOverlayClass: 'navbar_mobile_sidebar__overlay',\r\n        hiddenElementClass: 'is-hidden',\r\n        openedMenuClass: 'is-active',\r\n        noScrollClass: 'no-scroll',\r\n        relatedContainerForOverlayMenuClass: 'is-visible'\r\n    };\r\n\r\n    var config = {};\r\n\r\n    Object.keys(defaultConfig).forEach(function (key) {\r\n        config[key] = defaultConfig[key];\r\n    });\r\n\r\n    if (typeof menuConfig === 'object') {\r\n        Object.keys(menuConfig).forEach(function (key) {\r\n            config[key] = menuConfig[key];\r\n        });\r\n    }\r\n\r\n    /**\r\n     * Menu initializer\r\n     */\r\n    function init() {\r\n        if (!document.querySelectorAll(config.wrapperSelector).length) {\r\n            return;\r\n        }\r\n\r\n        initSubmenuPositions();\r\n\r\n        if (config.mobileMenuMode === 'overlay') {\r\n            initMobileMenuOverlay();\r\n        } else if (config.mobileMenuMode === 'sidebar') {\r\n            initMobileMenuSidebar();\r\n        }\r\n\r\n        initClosingMenuOnClickLink();\r\n\r\n        if (!config.isHoverMenu) {\r\n            initAriaAttributes();\r\n        }\r\n    };\r\n\r\n    /**\r\n     * Function responsible for the submenu positions\r\n     */\r\n    function initSubmenuPositions() {\r\n        var submenuParents = document.querySelectorAll(config.wrapperSelector + ' .' + config.parentItemClass);\r\n\r\n        for (var i = 0; i < submenuParents.length; i++) {\r\n            var eventTrigger = config.isHoverMenu ? 'mouseenter' : 'click';\r\n\r\n            submenuParents[i].addEventListener(eventTrigger, function () {\r\n                var submenu = this.querySelector(config.submenuSelector);\r\n                var itemPosition = this.getBoundingClientRect().left;\r\n                var widthMultiplier = 2;\r\n\r\n                if (this.parentNode === document.querySelector(config.menuSelector)) {\r\n                    widthMultiplier = 1;\r\n                }\r\n\r\n                if (config.submenuWidth !== 'auto') {\r\n                    var submenuPotentialPosition = itemPosition + (config.submenuWidth * widthMultiplier);\r\n\r\n                    if (window.innerWidth < submenuPotentialPosition) {\r\n                        submenu.classList.remove(config.submenuLeftPositionClass);\r\n                        submenu.classList.add(config.submenuRightPositionClass);\r\n                    } else {\r\n                        submenu.classList.remove(config.submenuRightPositionClass);\r\n                        submenu.classList.add(config.submenuLeftPositionClass);\r\n                    }\r\n                } else {\r\n                    var submenuPotentialPosition = 0;\r\n                    var submenuPosition = 0;\r\n\r\n                    if (widthMultiplier === 1) {\r\n                        submenuPotentialPosition = itemPosition + submenu.clientWidth;\r\n                    } else {\r\n                        submenuPotentialPosition = itemPosition + this.clientWidth + submenu.clientWidth;\r\n                    }\r\n\r\n                    if (window.innerWidth < submenuPotentialPosition) {\r\n                        submenu.classList.remove(config.submenuLeftPositionClass);\r\n                        submenu.classList.add(config.submenuRightPositionClass);\r\n                        submenuPosition = -1 * submenu.clientWidth;\r\n                        submenu.removeAttribute('style');\r\n\r\n                        if (widthMultiplier === 1) {\r\n                            submenuPosition = 0;\r\n                            submenu.style.right = submenuPosition + 'px';\r\n                        } else {\r\n                            submenu.style.right = this.clientWidth + 'px';\r\n                        }\r\n                    } else {\r\n                        submenu.classList.remove(config.submenuRightPositionClass);\r\n                        submenu.classList.add(config.submenuLeftPositionClass);\r\n                        submenuPosition = this.clientWidth;\r\n\r\n                        if (widthMultiplier === 1) {\r\n                            submenuPosition = 0;\r\n                        }\r\n\r\n                        submenu.removeAttribute('style');\r\n                        submenu.style.left = submenuPosition + 'px';\r\n                    }\r\n                }\r\n\r\n                submenu.setAttribute('aria-hidden', false);\r\n            });\r\n\r\n            if (config.isHoverMenu) {\r\n                submenuParents[i].addEventListener('mouseleave', function () {\r\n                    var submenu = this.querySelector(config.submenuSelector);\r\n                    submenu.removeAttribute('style');\r\n                    submenu.setAttribute('aria-hidden', true);\r\n                });\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Function used to init mobile menu - overlay mode\r\n     */\r\n    function initMobileMenuOverlay() {\r\n        var menuWrapper = document.createElement('div');\r\n        menuWrapper.classList.add(config.mobileMenuOverlayClass);\r\n        menuWrapper.classList.add(config.hiddenElementClass);\r\n        var menuContentHTML = document.querySelector(config.menuSelector).outerHTML;\r\n        menuWrapper.innerHTML = menuContentHTML;\r\n        document.body.appendChild(menuWrapper);\r\n\r\n        // Init toggle submenus\r\n        if (config.mobileMenuExpandableSubmenus) {\r\n            wrapSubmenusIntoContainer(menuWrapper);\r\n            initToggleSubmenu(menuWrapper);\r\n        } else {\r\n            setAriaForSubmenus(menuWrapper);\r\n        }\r\n\r\n        // Init button events\r\n        var button = document.querySelector(config.buttonSelector);\r\n\r\n        button.addEventListener('click', function () {\r\n            var relatedContainer = document.querySelector(config.relatedContainerForOverlayMenuSelector);\r\n            menuWrapper.classList.toggle(config.hiddenElementClass);\r\n            button.classList.toggle(config.openedMenuClass);\r\n            button.setAttribute(config.ariaButtonAttribute, button.classList.contains(config.openedMenuClass));\r\n\r\n            if (button.classList.contains(config.openedMenuClass)) {\r\n                document.documentElement.classList.add(config.noScrollClass);\r\n\r\n                if (relatedContainer) {\r\n                    relatedContainer.classList.add(config.relatedContainerForOverlayMenuClass);\r\n                }\r\n            } else {\r\n                document.documentElement.classList.remove(config.noScrollClass);\r\n\r\n                if (relatedContainer) {\r\n                    relatedContainer.classList.remove(config.relatedContainerForOverlayMenuClass);\r\n                }\r\n            }\r\n        });\r\n    }\r\n\r\n    /**\r\n     * Function used to init mobile menu - sidebar mode\r\n     */\r\n    function initMobileMenuSidebar() {\r\n        // Create menu structure\r\n        var menuWrapper = document.createElement('div');\r\n        menuWrapper.classList.add(config.mobileMenuSidebarClass);\r\n        menuWrapper.classList.add(config.hiddenElementClass);\r\n        var menuContentHTML = '';\r\n\r\n        if (config.mobileMenuSidebarLogoSelector !== null) {\r\n            menuContentHTML = document.querySelector(config.mobileMenuSidebarLogoSelector).outerHTML;\r\n        } else if (config.mobileMenuSidebarLogoUrl !== null) {\r\n            menuContentHTML = '<img src=\"' + config.mobileMenuSidebarLogoUrl + '\" alt=\"\" />';\r\n        }\r\n\r\n        menuContentHTML += document.querySelector(config.menuSelector).outerHTML;\r\n        menuWrapper.innerHTML = menuContentHTML;\r\n\r\n        var menuOverlay = document.createElement('div');\r\n        menuOverlay.classList.add(config.mobileMenuSidebarOverlayClass);\r\n        menuOverlay.classList.add(config.hiddenElementClass);\r\n\r\n        document.body.appendChild(menuOverlay);\r\n        document.body.appendChild(menuWrapper);\r\n\r\n        // Init toggle submenus\r\n        if (config.mobileMenuExpandableSubmenus) {\r\n            wrapSubmenusIntoContainer(menuWrapper);\r\n            initToggleSubmenu(menuWrapper);\r\n        } else {\r\n            setAriaForSubmenus(menuWrapper);\r\n        }\r\n\r\n        // Menu events\r\n        menuWrapper.addEventListener('click', function (e) {\r\n            e.stopPropagation();\r\n        });\r\n\r\n        menuOverlay.addEventListener('click', function () {\r\n            menuWrapper.classList.add(config.hiddenElementClass);\r\n            menuOverlay.classList.add(config.hiddenElementClass);\r\n            button.classList.remove(config.openedMenuClass);\r\n            button.setAttribute(config.ariaButtonAttribute, false);\r\n            document.documentElement.classList.remove(config.noScrollClass);\r\n        });\r\n\r\n        // Init button events\r\n        var button = document.querySelector(config.buttonSelector);\r\n\r\n        button.addEventListener('click', function () {\r\n            menuWrapper.classList.toggle(config.hiddenElementClass);\r\n            menuOverlay.classList.toggle(config.hiddenElementClass);\r\n            button.classList.toggle(config.openedMenuClass);\r\n            button.setAttribute(config.ariaButtonAttribute, button.classList.contains(config.openedMenuClass));\r\n            document.documentElement.classList.toggle(config.noScrollClass);\r\n        });\r\n    }\r\n\r\n    /**\r\n     * Set aria-hidden=\"false\" for submenus\r\n     */\r\n    function setAriaForSubmenus(menuWrapper) {\r\n        var submenus = menuWrapper.querySelectorAll(config.submenuSelector);\r\n\r\n        for (var i = 0; i < submenus.length; i++) {\r\n            submenus[i].setAttribute('aria-hidden', false);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Wrap all submenus into div wrappers\r\n     */\r\n    function wrapSubmenusIntoContainer(menuWrapper) {\r\n        var submenus = menuWrapper.querySelectorAll(config.submenuSelector);\r\n\r\n        for (var i = 0; i < submenus.length; i++) {\r\n            var submenuWrapper = document.createElement('div');\r\n            submenuWrapper.classList.add(config.mobileMenuSubmenuWrapperClass);\r\n            submenus[i].parentNode.insertBefore(submenuWrapper, submenus[i]);\r\n            submenuWrapper.appendChild(submenus[i]);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Initialize submenu toggle events\r\n     */\r\n    function initToggleSubmenu(menuWrapper) {\r\n        // Init parent menu item events\r\n        var parents = menuWrapper.querySelectorAll('.' + config.parentItemClass);\r\n\r\n        for (var i = 0; i < parents.length; i++) {\r\n            // Add toggle events\r\n            parents[i].addEventListener('click', function (e) {\r\n                e.stopPropagation();\r\n                var submenu = this.querySelector('.' + config.mobileMenuSubmenuWrapperClass);\r\n                var content = submenu.firstElementChild;\r\n\r\n                if (submenu.classList.contains(config.openedMenuClass)) {\r\n                    var height = content.clientHeight;\r\n                    submenu.style.height = height + 'px';\r\n\r\n                    setTimeout(function () {\r\n                        submenu.style.height = '0px';\r\n                    }, 0);\r\n\r\n                    setTimeout(function () {\r\n                        submenu.removeAttribute('style');\r\n                        submenu.classList.remove(config.openedMenuClass);\r\n                    }, config.animationSpeed);\r\n\r\n                    content.setAttribute('aria-hidden', true);\r\n                    content.parentNode.firstElementChild.setAttribute('aria-expanded', false);\r\n                } else {\r\n                    var height = content.clientHeight;\r\n                    submenu.classList.add(config.openedMenuClass);\r\n                    submenu.style.height = '0px';\r\n\r\n                    setTimeout(function () {\r\n                        submenu.style.height = height + 'px';\r\n                    }, 0);\r\n\r\n                    setTimeout(function () {\r\n                        submenu.removeAttribute('style');\r\n                    }, config.animationSpeed);\r\n\r\n                    content.setAttribute('aria-hidden', false);\r\n                    content.parentNode.firstElementChild.setAttribute('aria-expanded', true);\r\n                }\r\n            });\r\n\r\n            // Block links\r\n            var childNodes = parents[i].children;\r\n\r\n            for (var j = 0; j < childNodes.length; j++) {\r\n                if (childNodes[j].tagName === 'A') {\r\n                    childNodes[j].addEventListener('click', function (e) {\r\n                        var lastClick = parseInt(this.getAttribute('data-last-click'), 10);\r\n                        var currentTime = +new Date();\r\n\r\n                        if (isNaN(lastClick)) {\r\n                            e.preventDefault();\r\n                            this.setAttribute('data-last-click', currentTime);\r\n                        } else if (lastClick + config.doubleClickTime <= currentTime) {\r\n                            e.preventDefault();\r\n                            this.setAttribute('data-last-click', currentTime);\r\n                        } else if (lastClick + config.doubleClickTime > currentTime) {\r\n                            e.stopPropagation();\r\n                            closeMenu(this, true);\r\n                        }\r\n                    });\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Set aria-* attributes according to the current activity state\r\n     */\r\n    function initAriaAttributes() {\r\n        var allAriaElements = document.querySelectorAll(config.wrapperSelector + ' ' + '*[aria-hidden]');\r\n\r\n        for (var i = 0; i < allAriaElements.length; i++) {\r\n            var ariaElement = allAriaElements[i];\r\n\r\n            if (\r\n                ariaElement.parentNode.classList.contains('active') ||\r\n                ariaElement.parentNode.classList.contains('active-parent')\r\n            ) {\r\n                ariaElement.setAttribute('aria-hidden', 'false');\r\n                ariaElement.parentNode.firstElementChild.setAttribute('aria-expanded', true);\r\n            } else {\r\n                ariaElement.setAttribute('aria-hidden', 'true');\r\n                ariaElement.parentNode.firstElementChild.setAttribute('aria-expanded', false);\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Close menu on click link\r\n     */\r\n    function initClosingMenuOnClickLink() {\r\n        var links = document.querySelectorAll(config.menuSelector + ' a');\r\n\r\n        for (var i = 0; i < links.length; i++) {\r\n            if (links[i].parentNode.classList.contains(config.parentItemClass)) {\r\n                continue;\r\n            }\r\n\r\n            links[i].addEventListener('click', function (e) {\r\n                closeMenu(this, false);\r\n            });\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Close menu\r\n     */\r\n    function closeMenu(clickedLink, forceClose) {\r\n        if (forceClose === false) {\r\n            if (clickedLink.parentNode.classList.contains(config.parentItemClass)) {\r\n                return;\r\n            }\r\n        }\r\n\r\n        var relatedContainer = document.querySelector(config.relatedContainerForOverlayMenuSelector);\r\n        var button = document.querySelector(config.buttonSelector);\r\n        var menuWrapper = document.querySelector('.' + config.mobileMenuOverlayClass);\r\n\r\n        if (!menuWrapper) {\r\n            menuWrapper = document.querySelector('.' + config.mobileMenuSidebarClass);\r\n        }\r\n\r\n        menuWrapper.classList.add(config.hiddenElementClass);\r\n        button.classList.remove(config.openedMenuClass);\r\n        button.setAttribute(config.ariaButtonAttribute, false);\r\n        document.documentElement.classList.remove(config.noScrollClass);\r\n\r\n        if (relatedContainer) {\r\n            relatedContainer.classList.remove(config.relatedContainerForOverlayMenuClass);\r\n        }\r\n\r\n        var menuOverlay = document.querySelector('.' + config.mobileMenuSidebarOverlayClass);\r\n\r\n        if (menuOverlay) {\r\n            menuOverlay.classList.add(config.hiddenElementClass);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Run menu scripts \r\n     */\r\n    init();\r\n})(window.publiiThemeMenuConfig);\r\n\r\n// Load search input area\r\nconst searchButton = document.querySelector('.js-search-btn');\r\nconst searchOverlay = document.querySelector('.js-search-overlay');\r\n\r\nif (searchButton && searchOverlay) {\r\n    searchButton.addEventListener('click', (event) => {\r\n        event.stopPropagation();\r\n        searchOverlay.classList.toggle('expanded');\r\n\r\n        if (searchOverlay.classList.contains('expanded')) {\r\n            setTimeout(() => {\r\n                const element = searchOverlay.querySelector('input, button');\r\n                if (element) {\r\n                    element.focus();\r\n                }\r\n            }, 60);\r\n        }\r\n    });\r\n\r\n    searchOverlay.addEventListener('click', (event) => {\r\n        event.stopPropagation();\r\n    });\r\n\r\n    document.body.addEventListener('click', () => {\r\n        searchOverlay.classList.remove('expanded');\r\n    });\r\n}\r\n\r\n\r\n// Share buttons pop-up\r\n(function () {\r\n    // share popup\r\n    const shareButton = document.querySelector('.js-content__share-button');\r\n    const sharePopup = document.querySelector('.js-content__share-popup');\r\n\r\n    if (shareButton && sharePopup) {\r\n        sharePopup.addEventListener('click', function (e) {\r\n            e.stopPropagation();\r\n        });\r\n\r\n        shareButton.addEventListener('click', function (e) {\r\n            e.preventDefault();\r\n            e.stopPropagation();\r\n            sharePopup.classList.toggle('is-visible');\r\n        });\r\n\r\n        document.body.addEventListener('click', function () {\r\n            sharePopup.classList.remove('is-visible');\r\n        });\r\n    }\r\n\r\n    // link selector and pop-up window size\r\n    const Config = {\r\n        Link: \".js-share\",\r\n        Width: 500,\r\n        Height: 500\r\n    };\r\n\r\n    // add handler to links\r\n    const shareLinks = document.querySelectorAll(Config.Link);\r\n    shareLinks.forEach(link => {\r\n        link.addEventListener('click', PopupHandler);\r\n    });\r\n\r\n    // create popup\r\n    function PopupHandler(e) {\r\n        e.preventDefault();\r\n\r\n        const target = e.target.closest(Config.Link);\r\n        if (!target) return;\r\n\r\n        // hide share popup\r\n        if (sharePopup) {\r\n            sharePopup.classList.remove('is-visible');\r\n        }\r\n\r\n        // popup position\r\n        const px = Math.floor((window.innerWidth - Config.Width) / 2);\r\n        const py = Math.floor((window.innerHeight - Config.Height) / 2);\r\n\r\n        // open popup\r\n        const linkHref = target.href;\r\n        const popup = window.open(linkHref, \"social\", `\r\n            width=${Config.Width},\r\n            height=${Config.Height},\r\n            left=${px},\r\n            top=${py},\r\n            location=0,\r\n            menubar=0,\r\n            toolbar=0,\r\n            status=0,\r\n            scrollbars=1,\r\n            resizable=1\r\n        `);\r\n\r\n        if (popup) {\r\n            popup.focus();\r\n        }\r\n    }\r\n})();\r\n\r\n// Back to top\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n    const backToTopButton = document.getElementById('backToTop');\r\n\r\n    if (backToTopButton) {\r\n        const backToTopScrollFunction = () => {\r\n            if (document.body.scrollTop > 400 || document.documentElement.scrollTop > 400) {\r\n                backToTopButton.classList.add('is-visible');\r\n            } else {\r\n                backToTopButton.classList.remove('is-visible');\r\n            }\r\n        };\r\n\r\n        const backToTopFunction = () => {\r\n            window.scrollTo({\r\n                top: 0,\r\n                behavior: 'smooth'\r\n            });\r\n        };\r\n\r\n        window.addEventListener('scroll', backToTopScrollFunction);\r\n        backToTopButton.addEventListener('click', backToTopFunction);\r\n    }\r\n});\r\n\r\n\r\n// Responsive embeds script\r\n(function () {\r\n    let wrappers = document.querySelectorAll('.post__video, .post__iframe');\r\n\r\n    for (let i = 0; i < wrappers.length; i++) {\r\n        let embed = wrappers[i].querySelector('iframe, embed, video, object');\r\n\r\n        if (!embed) {\r\n            continue;\r\n        }\r\n\r\n        if (embed.getAttribute('data-responsive') === 'false') {\r\n            continue;\r\n        }\r\n\r\n        let w = embed.getAttribute('width');\r\n        let h = embed.getAttribute('height');\r\n        let ratio = false;\r\n\r\n        if (!w || !h) {\r\n            continue;\r\n        }\r\n\r\n        if (w.indexOf('%') > -1 && h.indexOf('%') > -1) { // percentage mode\r\n            w = parseFloat(w.replace('%', ''));\r\n            h = parseFloat(h.replace('%', ''));\r\n            ratio = h / w;\r\n        } else if (w.indexOf('%') === -1 && h.indexOf('%') === -1) { // pixels mode\r\n            w = parseInt(w, 10);\r\n            h = parseInt(h, 10);\r\n            ratio = h / w;\r\n        }\r\n\r\n        if (ratio !== false) {\r\n            let ratioValue = (ratio * 100) + '%';\r\n            wrappers[i].setAttribute('style', '--embed-aspect-ratio:' + ratioValue);\r\n        }\r\n    }\r\n})();"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/js/svg-fix.js",
    "content": "// SVG map fix\n(function() {\n    var allItems = document.querySelectorAll('use');\n\n    for (var i = 0; i < allItems.length; i++) {\n        var item = allItems[i];\n        var anchor = '#' + item.getAttribute('xlink:href').split('#')[1];\n        var itemData = window.publiiSvgFix[anchor];\n\n        if(!itemData) {\n            console.log('ANCHOR', anchor, i);\n            continue;\n        }\n\n        var svgItem = item.parentNode;\n        svgItem.innerHTML = itemData.content;\n        svgItem.setAttribute('viewBox', itemData.viewbox);\n    }\n})();"
  },
  {
    "path": "app/default-files/default-themes/simple/assets/js/svg-map.js",
    "content": "window.publiiSvgFix = {\n\n    \"#search\": {\n        \"viewbox\": \"0 0 24 24\",\n        \"content\": \"<circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"8\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><path d=\\\"M21 21l-4.3-4.3\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" />\"\n    },\n    \"#share\": {\n        \"viewbox\": \"0 0 24 24\",\n        \"content\": \"<path d=\\\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><polyline points=\\\"16 6 12 2 8 6\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><line x1=\\\"12\\\" y1=\\\"2\\\" x2=\\\"12\\\" y2=\\\"15\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" />\"\n    },\n    \"#arrow-prev\": {\n        \"viewbox\": \"0 0 24 24\",\n        \"content\": \"<path d=\\\"M6 8L2 12L6 16\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><path d=\\\"M2 12H22\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" />\"\n    },\n    \"#arrow-next\": {\n        \"viewbox\": \"0 0 24 24\",\n        \"content\": \"<path d=\\\"M18 8L22 12L18 16\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><path d=\\\"M2 12H22\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" />\"\n    },\n    \"#toparrow\": {\n        \"viewbox\": \"0 0 24 24\",\n        \"content\": \"<path d=\\\"M8 6L12 2L16 6\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><path d=\\\"M12 2V22\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" />\"\n    },\n    \"#website\": {\n        \"viewbox\": \"0 0 24 24\",\n        \"content\": \"<path d=\\\"M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><path d=\\\"M21 3l-9 9\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" /><path d=\\\"M15 3h6v6\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" />\"\n    },\n    \"#facebook\": {\n        \"viewbox\": \"0 0 32 32\",\n        \"content\": \"<path d=\\\"M32,16.1A16,16,0,1,0,13.5,32V20.75H9.44V16.1H13.5V12.55c0-4,2.39-6.26,6-6.26a24.77,24.77,0,0,1,3.58.31v4h-2a2.33,2.33,0,0,0-2.61,2.52v3h4.44l-.71,4.65H18.5V32A16.07,16.07,0,0,0,32,16.1Z\\\" />\"\n    },\n    \"#twitter\": {\n        \"viewbox\": \"0 0 24 24\",\n        \"content\": \"<path d=\\\"m14.093,10.315L22.283,1h-1.941l-7.111,8.089L7.551,1H1l8.589,12.231L1,23h1.941l7.51-8.542,5.998,8.542h6.551l-8.907-12.685h0Zm-2.658,3.024l-.87-1.218L3.64,2.43h2.981l5.588,7.821.87,1.218,7.264,10.166h-2.981l-5.927-8.296h0Z\\\" />\"\n    },\n    \"#instagram\": {\n        \"viewbox\": \"0 0 32 32\",\n        \"content\": \"<path d=\\\"M16,2.883c4.272,0,4.778.016,6.465.093a8.853,8.853,0,0,1,2.971.551,4.957,4.957,0,0,1,1.84,1.2,4.957,4.957,0,0,1,1.2,1.84,8.853,8.853,0,0,1,.551,2.971c.077,1.687.093,2.193.093,6.465s-.016,4.778-.093,6.465a8.853,8.853,0,0,1-.551,2.971,5.3,5.3,0,0,1-3.037,3.037,8.853,8.853,0,0,1-2.971.551c-1.687.077-2.193.093-6.465.093s-4.778-.016-6.465-.093a8.853,8.853,0,0,1-2.971-.551,4.957,4.957,0,0,1-1.84-1.2,4.957,4.957,0,0,1-1.2-1.84,8.853,8.853,0,0,1-.551-2.971C2.9,20.778,2.883,20.272,2.883,16s.016-4.778.093-6.465a8.853,8.853,0,0,1,.551-2.971,4.957,4.957,0,0,1,1.2-1.84,4.957,4.957,0,0,1,1.84-1.2,8.853,8.853,0,0,1,2.971-.551C11.222,2.9,11.728,2.883,16,2.883M16,0c-4.345,0-4.89.018-6.6.1A11.744,11.744,0,0,0,5.519.84,7.843,7.843,0,0,0,2.685,2.685,7.843,7.843,0,0,0,.84,5.519,11.744,11.744,0,0,0,.1,9.4C.018,11.11,0,11.655,0,16s.018,4.89.1,6.6A11.744,11.744,0,0,0,.84,26.481a7.843,7.843,0,0,0,1.845,2.834A7.843,7.843,0,0,0,5.519,31.16,11.744,11.744,0,0,0,9.4,31.9c1.707.078,2.251.1,6.6.1s4.89-.018,6.6-.1a11.744,11.744,0,0,0,3.884-.744,8.181,8.181,0,0,0,4.679-4.679A11.744,11.744,0,0,0,31.9,22.6c.078-1.707.1-2.251.1-6.6s-.018-4.89-.1-6.6a11.744,11.744,0,0,0-.744-3.884,7.843,7.843,0,0,0-1.845-2.834A7.843,7.843,0,0,0,26.481.84,11.744,11.744,0,0,0,22.6.1C20.89.018,20.345,0,16,0Zm0,7.784A8.216,8.216,0,1,0,24.216,16,8.216,8.216,0,0,0,16,7.784Zm0,13.55A5.333,5.333,0,1,1,21.333,16,5.333,5.333,0,0,1,16,21.333ZM24.541,5.539a1.92,1.92,0,1,0,1.92,1.92A1.92,1.92,0,0,0,24.541,5.539Z\\\"/>\"\n    },\n    \"#linkedin\": {\n        \"viewbox\": \"0 0 34.48 32\",\n        \"content\": \"<path d=\\\"M29.632,0H2.362A2.336,2.336,0,0,0,0,2.306V29.691A2.337,2.337,0,0,0,2.362,32h27.27A2.342,2.342,0,0,0,32,29.691V2.306A2.34,2.34,0,0,0,29.632,0ZM9.491,27.268H4.744V12H9.491ZM7.119,9.909a2.752,2.752,0,1,1,2.75-2.753A2.753,2.753,0,0,1,7.119,9.909ZM27.268,27.268H22.525V19.842c0-1.772-.033-4.05-2.466-4.05-2.47,0-2.848,1.929-2.848,3.921v7.555H12.468V12h4.553v2.086h.063a4.986,4.986,0,0,1,4.491-2.467c4.806,0,5.694,3.163,5.694,7.275Z\\\"/>\"\n    },\n    \"#vimeo\": {\n        \"viewbox\": \"0 0 24.999 20.159\",\n        \"content\": \"<path d=\\\"M26.105,5.927q3.628,0.1,3.641,4.123c0,0.176-.006.36-0.016,0.548q-0.16,3.416-5.09,9.306-5.089,6.17-8.634,6.183-2.205,0-3.671-3.779l-1.029-3.454L10.29,15.4q-1.124-3.749-2.406-3.746a7.819,7.819,0,0,0-1.95,1.091l-1.187-1.4,1.842-1.53L8.4,8.3a8.908,8.908,0,0,1,3.7-2.107,3.207,3.207,0,0,1,.341-0.016q2.606,0,3.249,3.746,0.356,2.137.594,3.473t0.387,1.867q0.862,3.544,1.859,3.559,0.766,0,2.36-2.31A9.305,9.305,0,0,0,22.562,13a2.969,2.969,0,0,0,.031-0.434,1.439,1.439,0,0,0-1.7-1.547,5.46,5.46,0,0,0-1.843.359q1.763-5.434,6.775-5.449h0.281Z\\\" transform=\\\"translate(-4.747 -5.927)\\\" />\"\n    },\n    \"#youtube\": {\n        \"viewbox\": \"0 0 32 22.507\",\n        \"content\": \"<path d=\\\"M31.68,9.6a6.924,6.924,0,0,0-1.272-3.176A4.577,4.577,0,0,0,27.2,5.07c-4.478-.324-11.2-0.324-11.2-0.324H15.993s-6.717,0-11.2.324A4.577,4.577,0,0,0,1.592,6.426,6.921,6.921,0,0,0,.32,9.6,48.4,48.4,0,0,0,0,14.781v2.428a48.4,48.4,0,0,0,.32,5.179,6.921,6.921,0,0,0,1.272,3.176A5.426,5.426,0,0,0,5.12,26.932C7.68,27.177,16,27.253,16,27.253s6.724-.01,11.2-0.334a4.577,4.577,0,0,0,3.206-1.355,6.923,6.923,0,0,0,1.272-3.176A48.46,48.46,0,0,0,32,17.209V14.781A48.46,48.46,0,0,0,31.68,9.6ZM12.7,20.151l0-8.991,8.647,4.511Z\\\" transform=\\\"translate(0 -4.747)\\\" />\"\n    },\n    \"#pinterest\": {\n        \"viewbox\": \"0 0 32 32\",\n        \"content\": \"<path d=\\\"M16 0a16 16 0 0 0-5.83 30.9 15.34 15.34 0 0 1 .055-4.59c.29-1.25 1.876-7.953 1.876-7.953a5.776 5.776 0 0 1-.478-2.375c0-2.225 1.29-3.886 2.9-3.886a2.01 2.01 0 0 1 2.024 2.254c0 1.373-.874 3.425-1.325 5.327a2.323 2.323 0 0 0 2.37 2.89c2.844 0 5.03-3 5.03-7.326a6.316 6.316 0 0 0-6.683-6.508 6.926 6.926 0 0 0-7.225 6.944 6.224 6.224 0 0 0 1.188 3.65.478.478 0 0 1 .11.46c-.12.504-.39 1.59-.443 1.814-.07.293-.232.355-.535.214-2-.93-3.248-3.852-3.248-6.2 0-5.047 3.667-9.682 10.572-9.682 5.55 0 9.864 3.955 9.864 9.24 0 5.515-3.477 9.953-8.3 9.953a4.282 4.282 0 0 1-3.667-1.837s-.8 3.055-1 3.8a17.885 17.885 0 0 1-1.99 4.195A16 16 0 1 0 16 0z\\\" />\"\n    },\n    \"#mix\": {\n        \"viewbox\": \"0 0 32 32\",\n        \"content\": \"<path  d=\\\"M6.4,11.44v17.45C6.4,30.58,4.97,32,3.2,32S0,30.58,0,28.88V17.42C3.42,17.45,6.22,14.79,6.4,11.44z\\\"/><path  d=\\\"M31.75,0C24.8,0,19.17,5.6,19.2,12.46v8.48c-0.03,1.69-1.46,3.12-3.2,3.12c-1.79,0-3.22-1.42-3.2-3.12V8.48c-0.02-1.78-1.45-3.2-3.2-3.24C7.9,5.27,6.52,6.57,6.4,8.23c-0.01,0.06-0.01,0.14,0,0.25v2.99c-0.19,3.31-2.98,5.97-6.4,5.98V0H31.75z\\\"/><path d=\\\"M32,0v17.95c0,1.69-1.43,3.12-3.2,3.12c-1.77,0-3.2-1.42-3.2-3.12v-1.62c0-1.81-1.43-3.23-3.2-3.24c-1.77,0.01-3.2,1.43-3.2,3.24v-3.99C19.2,6.07,23.9,0.86,29.98,0H32z\\\"/>\"\n    },\n    \"#buffer\": {\n        \"viewbox\": \"0 0 32 32\",\n        \"content\": \"<path d=\\\"M.556,15.342,3.338,14a.954.954,0,0,1,1.091.025c.458.225,10.229,4.942,10.229,4.942a3.506,3.506,0,0,0,2.682,0s10.038-4.85,10.4-5.017a.689.689,0,0,1,.791-.008c.342.167,2.916,1.408,2.916,1.408.741.358.741.942-.017,1.292L17.333,23.45a3.506,3.506,0,0,1-2.682,0L.556,16.642C-.185,16.283-.185,15.7.556,15.342ZM.573,8.375l14.094,6.808a3.506,3.506,0,0,0,2.682,0L31.444,8.375c.741-.358.741-.942,0-1.3L17.349.267a3.506,3.506,0,0,0-2.682,0L.573,7.075C-.169,7.433-.169,8.017.573,8.375Zm30.871,15.25s-2.574-1.242-2.916-1.408a.689.689,0,0,0-.791.008c-.358.167-10.388,5.025-10.388,5.025a3.506,3.506,0,0,1-2.682,0S4.9,22.533,4.438,22.308a.954.954,0,0,0-1.091-.025L.564,23.625c-.741.358-.741.942,0,1.3l14.094,6.808A3.109,3.109,0,0,0,16,32a3.237,3.237,0,0,0,1.341-.267l14.094-6.808C32.185,24.567,32.185,23.983,31.444,23.625Z\\\" />\"\n    },\n    \"#whatsapp\": {\n        \"viewbox\": \"0 0 32 32\",\n        \"content\": \"<path d=\\\"M24.09,19.64c0.1,0.17,0.1,0.96-0.23,1.88c-0.33,0.93-1.92,1.77-2.69,1.88c-0.69,0.1-1.55,0.14-2.51-0.16c-0.58-0.18-1.32-0.43-2.27-0.83c-3.99-1.72-6.6-5.72-6.8-5.98c-0.2-0.26-1.63-2.15-1.63-4.1c0-1.95,1.03-2.91,1.39-3.31c0.36-0.4,0.8-0.5,1.06-0.5s0.53,0,0.76,0.01c0.24,0.01,0.57-0.09,0.9,0.68c0.33,0.79,1.13,2.74,1.23,2.94c0.1,0.2,0.17,0.43,0.03,0.69c-0.13,0.26-0.2,0.43-0.4,0.66s-0.42,0.52-0.6,0.69c-0.2,0.2-0.41,0.41-0.17,0.81c0.23,0.4,1.03,1.69,2.21,2.74c1.52,1.35,2.8,1.77,3.2,1.97c0.4,0.2,0.63,0.17,0.86-0.1c0.23-0.26,1-1.16,1.26-1.55c0.27-0.4,0.53-0.33,0.9-0.2c0.36,0.13,2.32,1.09,2.72,1.29C23.73,19.37,23.99,19.47,24.09,19.64z M32,15.87c0,8.74-7.15,15.86-15.93,15.86c0,0,0,0,0,0h-0.01c-2.67,0-5.29-0.67-7.61-1.93L0,32l2.26-8.22c-1.39-2.4-2.13-5.13-2.13-7.93C0.14,7.11,7.28,0,16.07,0c4.26,0,8.26,1.65,11.27,4.65C30.35,7.65,32,11.63,32,15.87z M29.31,15.87c0-3.52-1.37-6.83-3.88-9.32c-2.5-2.49-5.83-3.86-9.36-3.87c-7.3,0-13.25,5.91-13.25,13.18c0,2.49,0.7,4.92,2.02,7.01l0.31,0.5l-1.34,4.86l5.01-1.31l0.48,0.29c2.03,1.2,4.36,1.84,6.74,1.84h0.01C23.37,29.05,29.31,23.13,29.31,15.87z\\\" />\"\n    }\n};\n"
  },
  {
    "path": "app/default-files/default-themes/simple/author.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"page page--author\">\n   {{#author}}\n      <div class=\"hero {{#checkIfAll authorViewConfig.displayFeaturedImage featuredImage.url}}\n   {{else}}hero--noimage{{/checkIfAll}}\">\n         <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n            <div class=\"wrapper {{#checkIf @config.custom.alignHero '!=' \"center\" }}page--author__wrapper{{/checkIf}}\">\n                  {{#if authorViewConfig.displayAvatar}}        \n                     {{#if avatar}}\n                        <img \n                           src=\"{{avatarImage.url}}\" \n                           {{ lazyload \"lazy\" }}\n                           height=\"{{avatarImage.height}}\"\n                           width=\"{{avatarImage.width}}\" \n                           class=\"page--author__avatar\" \n                           alt=\"{{avatarImage.alt}}\">\n                     {{/if}}\n                  {{/if}}\n                  <div>\n                     <h1>\n                        {{name}}\n                        {{#if authorViewConfig.displayPostCounter}}<sup>({{postsNumber}})</sup>{{/if}}\n                     </h1>\n                     {{#if authorViewConfig.displayDescription}} \n                        {{#if description}}\n                           <div class=\"page__desc\">{{{description}}}</div>\n                        {{/if}}  \n                     {{/if}}\n                     {{#if authorViewConfig.displayWebsite}}  \n                        {{#if website}}\n                           <div class=\"page--author__website\">\n                              <a href=\"{{website}}\" target=\"_blank\" rel=\"nofollow noreferrer noopener\" class=\"btn btn--icon\">\n                                 <span>{{ translate 'author.visitWebsite' }}</span>\n                                 <svg height=\"18\" width=\"18\" aria-hidden=\"true\">\n                                    <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#website\"/>\n                                 </svg>\n                              </a>\n                           </div>\n                        {{/if}}  \n                     {{/if}} \n                  </div>\n            </div>\n         </header>\n\n         {{#if authorViewConfig.displayFeaturedImage}}\n            {{#featuredImage}}\n               {{#if url}}\n                  <figure class=\"hero__image\">   \n                     <div class=\"hero__image-wrapper\">                   \n                        <img\n                           src=\"{{url}}\"\n                           {{#if @config.site.responsiveImages}}                           \n                                 {{responsiveImageAttributes 'authorImage' srcset sizes}}\n                           {{/if}}\n                           {{ lazyload \"eager\" }}\n                           height=\"{{height}}\"\n                           width=\"{{width}}\"\n                           alt=\"{{alt}}\">\n                        </div>\n                        \n                     {{#checkIfAny caption credits}}\n                        <figcaption>\n                           {{caption}}\n                           {{credits}}\n                        </figcaption>                      \n                     {{/checkIfAny}}                      \n                  </figure>\n               {{/if}}\n            {{/featuredImage}}\n         {{/if}}\n      </div>\n\n      {{#if authorViewConfig.displayPostList}} \n         <div class=\"wrapper feed\">\n            {{#each ../posts}}\n            <article class=\"feed__item {{#checkIf @config.custom.alignFeed '==' \"center\" }}feed__item--centered{{/checkIf}}\">\n               {{#if @config.custom.feedFeaturedImage}}\n                  {{#featuredImage}}\n                     {{#if url}}\n                        <figure class=\"feed__image\">\n                           <img\n                              src=\"{{url}}\"\n                              {{#if @config.site.responsiveImages}}\n                                 {{responsiveImageAttributes 'featuredImage' srcset.feed sizes.feed}}\n                              {{/if}}\n                              {{ lazyload \"lazy\" }}\n                              height=\"{{height}}\"\n                              width=\"{{width}}\"\n                              alt=\"{{alt}}\">\n                        </figure>\n                     {{/if}}\n                  {{/featuredImage}}\n               {{/if}}\n               <div class=\"feed__content\">\n                  <header>\n                     {{#checkIfAny @config.custom.feedAvatar @config.custom.feedAuthor @config.custom.feedDate}}\n                        <div class=\"feed__meta\">\n                           {{#author}}\n                              {{#if @config.custom.feedAvatar}}\n                                 {{#if avatar}}\n                                    {{#if @config.custom.feedAuthor}}\n                                       <img\n                                          src=\"{{avatarImage.url}}\" \n                                          {{ lazyload \"lazy\" }}\n                                          height=\"{{avatarImage.height}}\"\n                                          width=\"{{avatarImage.width}}\"      \n                                          class=\"feed__author-thumb\"\n                                          alt=\"\">\n                                    {{else}}\n                                       <a href=\"{{url}}\" class=\"feed__author-link\">\n                                          <img\n                                             src=\"{{avatarImage.url}}\" \n                                             {{ lazyload \"lazy\" }}\n                                             height=\"{{avatarImage.height}}\"\n                                             width=\"{{avatarImage.width}}\"      \n                                             class=\"feed__author-thumb\"\n                                             alt=\"{{avatarImage.alt}}\">\n                                       </a>\n                                    {{/if}}\n                                 {{/if}}\n                              {{/if}}\n                              {{#if @config.custom.feedAuthor}}\n                                 <a href=\"{{url}}\" class=\"feed__author\">{{name}}</a>\n                              {{/if}}\n                           {{/author}}\n                           {{#if @config.custom.feedDate}}\n                              {{#checkIf @config.custom.feedDateType '==' \"published\" }}\n                                 <time datetime=\"{{date createdAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                    {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                       {{date createdAt @config.custom.formatDate}}\n                                    {{else}}\n                                       {{date createdAt @config.custom.formatDateCustom}}\n                                    {{/checkIf}}\n                                 </time>\n                              {{/checkIf}}\n                              {{#checkIf @config.custom.feedDateType '==' \"modified\" }}\n                                 <time datetime=\"{{date modifiedAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                    {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                       {{date modifiedAt @config.custom.formatDate}}\n                                    {{else}}\n                                       {{date modifiedAt @config.custom.formatDateCustom}}\n                                    {{/checkIf}}\n                                 </time>\n                              {{/checkIf}}\n                           {{/if}}\n                        </div>\n                     {{/checkIfAny}}\n                     <h2 class=\"feed__title\">\n                        <a href=\"{{url}}\">\n                           {{title}}\n                        </a>\n                     </h2>\n                  </header>               \n                  {{#if hasCustomExcerpt}}\n                     {{{ excerpt }}}\n                  {{else}}\n                     <p>{{{ excerpt }}}</p>\n                  {{/if}}\n                  {{#if @config.custom.feedtReadMore}}\n                     <a href=\"{{url}}\" class=\"readmore feed__readmore\">\n                        {{ translate 'post.readMore' }}</a>\n                  {{/if}}\n               </div>\n            </article>\n            {{/each}}\n            {{> pagination}}\n         </div>\n      {{/if}} \n   {{/author}}\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/computed-options.js",
    "content": "module.exports = function (themeConfig) {\n    const fontParams = {\n        'albertsans': { hasItalic: true },\n        'adventpro': { hasItalic: true },\n        'aleo': { hasItalic: true },\n        'andadapro': { hasItalic: true },\n        'antonio': { hasItalic: false },\n        'archivonarrow': { hasItalic: true },\n        'asap': { hasItalic: true },\n        'assistant': { hasItalic: false },\n        'besley': { hasItalic: true },\n        'bitter': { hasItalic: true },\n        'bitcount': { hasItalic: false },\n        'bodonimoda': { hasItalic: true },\n        'brygada1918': { hasItalic: true },\n        'cabin': { hasItalic: true },\n        'cairo': { hasItalic: false },\n        'cinzel': { hasItalic: false },\n        'comfortaa': { hasItalic: false },\n        'comme': { hasItalic: false },\n        'dancingscript': { hasItalic: false },\n        'danfo': { hasItalic: false },\n        'dmsans': { hasItalic: true },\n        'domine': { hasItalic: false },\n        'dosis': { hasItalic: false },\n        'doto': { hasItalic: false },\n        'dynapuff': { hasItalic: false },\n        'exo': { hasItalic: true },\n        'familjengrotesk': { hasItalic: true },\n        'faustina': { hasItalic: true },\n        'figtree': { hasItalic: true },\n        'finlandica': { hasItalic: true },\n        'frankruhllibre': { hasItalic: false },\n        'fredoka': { hasItalic: false },\n        'funneldisplay': { hasItalic: false },\n        'gantari': { hasItalic: true },\n        'geistmono': { hasItalic: false },\n        'glory': { hasItalic: true },\n        'gluten': { hasItalic: false },\n        'googlesanscode': { hasItalic: true },\n        'grenzegotisch': { hasItalic: false },\n        'handjet': { hasItalic: false },\n        'heebo': { hasItalic: false },\n        'hostgrotesk': { hasItalic: true },\n        'imbue': { hasItalic: false },\n        'inclusivesans': { hasItalic: true },\n        'instrumentsans': { hasItalic: true },\n        'jetbrainsmono': { hasItalic: true },\n        'jura': { hasItalic: false },\n        'kalnia': { hasItalic: false },\n        'karla': { hasItalic: true },\n        'kreon': { hasItalic: false },\n        'kumbhsans': { hasItalic: false },\n        'labrada': { hasItalic: true },\n        'leaguespartan': { hasItalic: false },\n        'lemonada': { hasItalic: false },\n        'lexend': { hasItalic: false },\n        'lexenddeca': { hasItalic: false },\n        'librefranklin': { hasItalic: true },\n        'lora': { hasItalic: true },\n        'manuale': { hasItalic: true },\n        'manrope': { hasItalic: false },\n        'mavenpro': { hasItalic: false },\n        'merriweathersans': { hasItalic: true },\n        'montserrat': { hasItalic: true },\n        'mulish': { hasItalic: true },\n        'nunito': { hasItalic: true },\n        'orbitron': { hasItalic: false },\n        'oswald': { hasItalic: false },\n        'outfit': { hasItalic: false },\n        'oxanium': { hasItalic: false },\n        'parkinsans': { hasItalic: false },\n        'petrona': { hasItalic: true },\n        'playfairdisplay': { hasItalic: true },\n        'playwriteusmodern': { hasItalic: false },\n        'playwriteustrad': { hasItalic: false },\n        'plusjakartasans': { hasItalic: true },\n        'pontanosans': { hasItalic: false },\n        'publicsans': { hasItalic: true },\n        'quicksand': { hasItalic: false },\n        'radiocanadabig': { hasItalic: true },\n        'raleway': { hasItalic: true },\n        'redhatdisplay': { hasItalic: true },\n        'redhatmono': { hasItalic: true },\n        'redhattext': { hasItalic: true },\n        'redrose': { hasItalic: false },\n        'rem': { hasItalic: true },\n        'robotoflex': { hasItalic: false },\n        'robotoslab': { hasItalic: false },\n        'rokkitt': { hasItalic: true },\n        'rubik': { hasItalic: true },\n        'ruda': { hasItalic: false },\n        'smoochsans': { hasItalic: false },\n        'sourcecodepro': { hasItalic: true },\n        'sora': { hasItalic: false },\n        'spartan': { hasItalic: false },\n        'sticknobills': { hasItalic: false },\n        'susemono': { hasItalic: true },\n        'system-ui': { hasItalic: false },\n        'teachers': { hasItalic: true },\n        'tektur': { hasItalic: false },\n        'tourney': { hasItalic: true },\n        'urbanist': { hasItalic: true },\n        'varta': { hasItalic: false },\n        'victormono': { hasItalic: true },\n        'wixmadefortext': { hasItalic: true },\n        'workbench': { hasItalic: false },\n        'worksans': { hasItalic: true },\n        'yanonekaffeesatz': { hasItalic: false },\n        'zalandosans': { hasItalic: true },\n        'zalandosansexpanded': { hasItalic: true },\n        'yrsa': { hasItalic: true }\n    };\n\n    let fontBody = themeConfig.customConfig.find(option => option.name === 'fontBody').value;\n    let fontHeadings = themeConfig.customConfig.find(option => option.name === 'fontHeadings').value;\n\n    let disableFontBodyItalic = themeConfig.customConfig.find(option => option.name === 'disableFontBodyItalic').value;\n    let disableFontHeadingsItalic = themeConfig.customConfig.find(option => option.name === 'disableFontHeadingsItalic').value;\n\n    return [\n        {\n            name: 'fontBodyItalic',\n            type: 'checkbox',\n            value: !disableFontBodyItalic && (fontParams[fontBody]?.hasItalic || false)\n        },\n        {\n            name: 'fontHeadingsItalic',\n            type: 'checkbox',\n            value: !disableFontHeadingsItalic && (fontParams[fontHeadings]?.hasItalic || false)\n        }\n    ];\n};\n"
  },
  {
    "path": "app/default-files/default-themes/simple/config.json",
    "content": "{\n    \"name\": \"Simple\",\n    \"version\": \"3.2.1.0\",\n    \"author\": \"TidyCustoms <https://tidycustoms.net/>\",\n    \"menus\": {\n        \"mainMenu\": {\n            \"desc\": \"\",\n            \"name\": \"Main menu\",\n            \"maxLevels\": -1\n        },\n        \"footerMenu\": {\n            \"desc\": \"\",\n            \"name\": \"Footer menu\",\n            \"maxLevels\": 1\n        }\n    },\n    \"renderer\": {\n        \"relatedPostsNumber\": 3,\n        \"renderRelatedPosts\": true,\n        \"renderSimilarPosts\": false,\n        \"renderPrevNextPosts\": true,\n        \"createContentStructure\": true,\n        \"createTagPages\": true,\n        \"createAuthorPages\": true,\n        \"createTagsList\": true,\n        \"createSearchPage\": false,\n        \"create404page\": true,\n        \"customHTML\": {\n            \"afterPost\": \"After every post\",\n            \"afterPage\": \"After every page\"\n        }\n    },\n    \"supportedFeatures\": {\n        \"blockEditor\": true,\n        \"tagsList\": true,\n        \"tagPages\": true,\n        \"tagImages\": true,\n        \"authorPages\": true,\n        \"authorImages\": true,\n        \"searchPage\": true,\n        \"errorPage\": true,\n        \"customSharing\": true,\n        \"customSearch\": true,\n        \"customComments\": true,\n        \"embedConsents\": true,\n        \"pages\": true,\n        \"postsPage\": true\n    },\n    \"pageTemplates\": {\n        \"empty\": \"Empty container\"\n    },\n    \"config\": [\n        {\n            \"name\": \"postsPerPage\",\n            \"label\": \"Posts per page\",\n            \"value\": 5,\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"tagsPostsPerPage\",\n            \"label\": \"Tags posts per page\",\n            \"value\": 5,\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"authorsPostsPerPage\",\n            \"label\": \"Authors posts per page\",\n            \"value\": 5,\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"excerptLength\",\n            \"label\": \"Excerpt length\",\n            \"value\": 45,\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"logo\",\n            \"label\": \"Website logo\",\n            \"value\": \"\",\n            \"type\": \"upload\",\n            \"upload\": true\n        }\n    ],\n    \"customConfig\": [\n        {\n            \"name\": \"pageMargin\",\n            \"label\": \"Page margin\",\n            \"group\": \"Layout\",\n            \"value\": \"6vw\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"pageWidth\",\n            \"label\": \"Page width\",\n            \"group\": \"Layout\",\n            \"note\": \"Defines the overall width of the page. This should always be greater than or equal to the Entry width to ensure proper layout. Set to 100% for full-screen width.\",\n            \"value\": \"66rem\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"entryWidth\",\n            \"label\": \"Entry width\",\n            \"group\": \"Layout\",\n            \"note\": \"Defines the width of content for posts and pages. Ensure that this value is smaller than or equal to the Page width for proper content display. Set to 100% for full-screen width.\",\n            \"value\": \"42rem\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"borderRadius\",\n            \"label\": \"Corner Roundness\",\n            \"group\": \"Layout\",\n            \"note\": \"Adjust the roundness of corners for elements like images, buttons or dropdown menus. Increasing this value (in pixels) will make the corners more rounded.\",\n            \"value\": \"3\",\n            \"type\": \"range\",\n            \"min\": 0,\n            \"max\": 100,\n            \"step\": 1\n        },\n        {\n            \"name\": \"baseline\",\n            \"label\": \"Baseline\",\n            \"group\": \"Layout\",\n            \"note\": \"Defines a core vertical rhythm unit for consistent spacing across the website, used as a multiplier for margins and paddings. Increasing this value enhances spacing, creating an airier layout, while decreasing it compacts content, for tighter visual appeal.\",\n            \"value\": \"0.28333rem\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"Hero section\",\n            \"group\": \"Layout\",\n            \"size\": \"big\"\n        },\n        {\n            \"name\": \"alignHero\",\n            \"label\": \"Content alignment\",\n            \"group\": \"Layout\",\n            \"value\": \"left\",\n            \"type\": \"radio\",\n            \"options\": [\n                {\n                    \"label\": \"Left\",\n                    \"value\": \"left\"\n                },\n                {\n                    \"label\": \"Center\",\n                    \"value\": \"center\"\n                }\n            ]\n        },\n        {\n            \"name\": \"textHero\",\n            \"label\": \"Text\",\n            \"group\": \"Layout\",\n            \"value\": \"<h1>Discover My Blogging Journey: Adventures, Travels, and Hobbies!</h1> <p>Join me as I share captivating stories, travel experiences, and dive into the joys of my favorite hobbies.</p><p><a href=\\\"#\\\" class=\\\"btn\\\">Read more</a></p>\",\n            \"type\": \"wysiwyg\"\n        },\n        {\n            \"name\": \"heightHero\",\n            \"label\": \"Image height\",\n            \"group\": \"Layout\",\n            \"value\": \"50vh\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"uploadHero\",\n            \"label\": \"Image\",\n            \"group\": \"Layout\",\n            \"value\": \"\",\n            \"type\": \"upload\",\n            \"upload\": true\n        },\n        {\n            \"name\": \"uploadHeroAlt\",\n            \"label\": \"Image Alt text\",\n            \"group\": \"Layout\",\n            \"placeholder\": \"Add an alternative text to the hero image\",\n            \"value\": \"\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"uploadHeroCaption\",\n            \"label\": \"Image Caption\",\n            \"group\": \"Layout\",\n            \"placeholder\": \"Add text caption to the hero image\",\n            \"value\": \"\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"Post page\",\n            \"group\": \"Layout\",\n            \"size\": \"big\"\n        },\n        {\n            \"name\": \"relatedPostsNumber\",\n            \"label\": \"Related post\",\n            \"group\": \"Layout\",\n            \"note\": \"Specify number of related posts to show. Zero '0' means no posts.\",\n            \"value\": \"3\",\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Post list\",\n            \"note\": \"This section lets you choose what shows up in your feed on the homepage, tags, and author pages. You can pick and choose which details to show, like author avatars, post dates, and other things.\",\n            \"size\": \"no-line\"\n        },\n        {\n            \"name\": \"alignFeed\",\n            \"label\": \"Content alignment\",\n            \"group\": \"Post list\",\n            \"value\": \"center\",\n            \"type\": \"radio\",\n            \"options\": [\n                {\n                    \"label\": \"Left\",\n                    \"value\": \"left\"\n                },\n                {\n                    \"label\": \"Center\",\n                    \"value\": \"center\"\n                }\n            ]\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Post list\",\n            \"size\": \"small thin \"\n        },\n        {\n            \"name\": \"feedFeaturedImage\",\n            \"group\": \"Post list\",\n            \"label\": \"Show Featured Image\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"feedFeaturedImageSize\",\n            \"label\": \"Image size\",\n            \"group\": \"Post list\",\n            \"note\": \"Adjust the size of the featured image in REM value.\",\n            \"value\": \"8\",\n            \"type\": \"range\",\n            \"min\": 1,\n            \"max\": 50,\n            \"step\": 1,\n            \"dependencies\": [\n                {\n                    \"field\": \"feedFeaturedImage\",\n                    \"value\": true\n                }\n            ]\n        },\n        {\n            \"name\": \"feedAvatar\",\n            \"group\": \"Post list\",\n            \"label\": \"Show author avatar\",\n            \"value\": true,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"feedAuthor\",\n            \"group\": \"Post list\",\n            \"label\": \"Show author name\",\n            \"value\": true,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"feedDate\",\n            \"group\": \"Post list\",\n            \"label\": \"Show date\",\n            \"value\": true,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"feedDateType\",\n            \"group\": \"Post list\",\n            \"label\": \"\",\n            \"value\": \"published\",\n            \"type\": \"radio\",\n            \"options\": [\n                {\n                    \"label\": \"Published date\",\n                    \"value\": \"published\"\n                },\n                {\n                    \"label\": \"Modified date\",\n                    \"value\": \"modified\"\n                }\n            ],\n            \"dependencies\": [\n                {\n                    \"field\": \"feedDate\",\n                    \"value\": true\n                }\n            ]\n        },\n        {\n            \"name\": \"feedtReadMore\",\n            \"group\": \"Post list\",\n            \"label\": \"Show 'Read more' button\",\n            \"value\": true,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"navbarHeight\",\n            \"group\": \"Navbar\",\n            \"label\": \"Navbar height\",\n            \"value\": \"6rem\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"Dropdown menu\",\n            \"group\": \"Navbar\",\n            \"size\": \"big\"\n        },\n        {\n            \"name\": \"submenu\",\n            \"label\": \"Width\",\n            \"group\": \"Navbar\",\n            \"value\": \"auto\",\n            \"type\": \"radio\",\n            \"options\": [\n                {\n                    \"label\": \"Auto\",\n                    \"value\": \"auto\"\n                },\n                {\n                    \"label\": \"Custom\",\n                    \"value\": \"custom\"\n                }\n            ]\n        },\n        {\n            \"name\": \"submenuWidth\",\n            \"group\": \"Navbar\",\n            \"note\": \"The submenu width in PX unit\",\n            \"label\": \"\",\n            \"value\": \"240\",\n            \"type\": \"range\",\n            \"min\": 0,\n            \"max\": 1000,\n            \"step\": 10,\n            \"dependencies\": [\n                {\n                    \"field\": \"submenu\",\n                    \"value\": \"custom\"\n                }\n            ]\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"Mobile menu\",\n            \"group\": \"Navbar\",\n            \"size\": \"big\"\n        },\n        {\n            \"name\": \"mobilemenu\",\n            \"label\": \"Type\",\n            \"group\": \"Navbar\",\n            \"value\": \"sidebar\",\n            \"type\": \"radio\",\n            \"options\": [\n                {\n                    \"label\": \"Sidebar\",\n                    \"value\": \"sidebar\"\n                },\n                {\n                    \"label\": \"Overlay\",\n                    \"value\": \"overlay\"\n                }\n            ]\n        },\n        {\n            \"name\": \"mobilemenuExpandableSubmenus\",\n            \"label\": \"Expandable submenus\",\n            \"group\": \"Navbar\",\n            \"value\": true,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"colorScheme\",\n            \"label\": \"Select color scheme\",\n            \"group\": \"Colors\",\n            \"note\": \"<br>The auto color scheme feature automatically adjusts the color scheme of the interface to match the user's operating system settings. If the operating system does not support this feature, the light version of the color scheme is used instead\",\n            \"value\": \"auto\",\n            \"type\": \"radio\",\n            \"options\": [\n                {\n                    \"label\": \"Light\",\n                    \"value\": \"light\"\n                },\n                {\n                    \"label\": \"Dark\",\n                    \"value\": \"dark\"\n                },\n                {\n                    \"label\": \"Auto\",\n                    \"value\": \"auto\"\n                }\n            ]\n        },\n        {\n            \"name\": \"primaryColor\",\n            \"label\": \"Primary color (Light mode)\",\n            \"group\": \"Colors\",\n            \"value\": \"#D73A42\",\n            \"type\": \"colorpicker\"\n        },\n        {\n            \"name\": \"primaryDarkColor\",\n            \"label\": \"Primary color (Dark mode)\",\n            \"group\": \"Colors\",\n            \"value\": \"#FFC074\",\n            \"type\": \"colorpicker\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"Main font settings\",\n            \"group\": \"Fonts\",\n            \"note\": \"To explore an extensive list of available fonts, along with detailed information about their typefaces, complete range of weights, and other specifications, please visit our <a href='https://getpublii.com/docs/fonts.html' target='_blank'>documentation</a>.\",\n            \"size\": \"small\"\n        },\n        {\n            \"name\": \"fontBody\",\n            \"label\": \"Body font\",\n            \"group\": \"Fonts\",\n            \"value\": \"system-ui\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"OS Default Font\",\n                    \"value\": \"system-ui\",\n                    \"group\": \"System\"\n                },\n                {\n                    \"label\": \"Aleo\",\n                    \"value\": \"aleo\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Andada Pro\",\n                    \"value\": \"andadapro\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Besley\",\n                    \"value\": \"besley\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Bitter\",\n                    \"value\": \"bitter\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Bodoni Moda\",\n                    \"value\": \"bodonimoda\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Brygada 1918\",\n                    \"value\": \"brygada1918\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Cinzel\",\n                    \"value\": \"cinzel\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Danfo\",\n                    \"value\": \"danfo\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Domine\",\n                    \"value\": \"domine\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Faustina\",\n                    \"value\": \"faustina\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Frank Ruhl Libre\",\n                    \"value\": \"frankruhllibre\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Grenze Gotisch\",\n                    \"value\": \"grenzegotisch\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Imbue\",\n                    \"value\": \"imbue\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Kalnia\",\n                    \"value\": \"kalnia\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Kreon\",\n                    \"value\": \"kreon\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Labrada\",\n                    \"value\": \"labrada\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Lora\",\n                    \"value\": \"lora\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Manuale\",\n                    \"value\": \"manuale\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Petrona\",\n                    \"value\": \"petrona\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Playfair Display\",\n                    \"value\": \"playfairdisplay\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Red Rose\",\n                    \"value\": \"redrose\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Roboto Slab\",\n                    \"value\": \"robotoslab\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Rokkitt\",\n                    \"value\": \"rokkitt\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Yrsa\",\n                    \"value\": \"yrsa\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Advent Pro\",\n                    \"value\": \"adventpro\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Albert Sans\",\n                    \"value\": \"albertsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Antonio\",\n                    \"value\": \"antonio\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Archivo Narrow\",\n                    \"value\": \"archivonarrow\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Asap\",\n                    \"value\": \"asap\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Assistant\",\n                    \"value\": \"assistant\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Cabin\",\n                    \"value\": \"cabin\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Cairo\",\n                    \"value\": \"cairo\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Comme\",\n                    \"value\": \"comme\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"DM Sans\",\n                    \"value\": \"dmsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Dosis\",\n                    \"value\": \"dosis\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Doto\",\n                    \"value\": \"doto\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Dyna Puff\",\n                    \"value\": \"dynapuff\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Exo\",\n                    \"value\": \"exo\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Familjen Grotesk\",\n                    \"value\": \"familjengrotesk\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Figtree\",\n                    \"value\": \"figtree\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Finlandica\",\n                    \"value\": \"finlandica\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Fredoka\",\n                    \"value\": \"fredoka\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Funnel Display\",\n                    \"value\": \"funneldisplay\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Gantari\",\n                    \"value\": \"gantari\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Glory\",\n                    \"value\": \"glory\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Handjet\",\n                    \"value\": \"handjet\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Heebo\",\n                    \"value\": \"heebo\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Host Grotesk\",\n                    \"value\": \"hostgrotesk\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Inclusive Sans\",\n                    \"value\": \"inclusivesans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Instrument Sans\",\n                    \"value\": \"instrumentsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Jura\",\n                    \"value\": \"jura\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Karla\",\n                    \"value\": \"karla\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Kumbh Sans\",\n                    \"value\": \"kumbhsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"League Spartan\",\n                    \"value\": \"leaguespartan\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Lexend\",\n                    \"value\": \"lexend\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Lexend Deca\",\n                    \"value\": \"lexenddeca\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Libre Franklin\",\n                    \"value\": \"librefranklin\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Manrope\",\n                    \"value\": \"manrope\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Maven Pro\",\n                    \"value\": \"mavenpro\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Merriweather Sans\",\n                    \"value\": \"merriweathersans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Montserrat\",\n                    \"value\": \"montserrat\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Mulish\",\n                    \"value\": \"mulish\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Nunito\",\n                    \"value\": \"nunito\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Orbitron\",\n                    \"value\": \"orbitron\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Oswald\",\n                    \"value\": \"oswald\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Outfit\",\n                    \"value\": \"outfit\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Oxanium\",\n                    \"value\": \"oxanium\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Parkinsans\",\n                    \"value\": \"parkinsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Plus Jakarta Sans\",\n                    \"value\": \"plusjakartasans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Pontano Sans\",\n                    \"value\": \"pontanosans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Public Sans\",\n                    \"value\": \"publicsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Quicksand\",\n                    \"value\": \"quicksand\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Radio Canada Big\",\n                    \"value\": \"radiocanadabig\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Raleway\",\n                    \"value\": \"raleway\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Red Hat Display\",\n                    \"value\": \"redhatdisplay\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Red Hat Text\",\n                    \"value\": \"redhattext\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"REM\",\n                    \"value\": \"rem\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Roboto Flex\",\n                    \"value\": \"robotoflex\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Rubik\",\n                    \"value\": \"rubik\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Ruda\",\n                    \"value\": \"ruda\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Smooch Sans\",\n                    \"value\": \"smoochsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Sora\",\n                    \"value\": \"sora\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Spartan\",\n                    \"value\": \"spartan\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Stick No Bills\",\n                    \"value\": \"sticknobills\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Teachers\",\n                    \"value\": \"teachers\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Tektur\",\n                    \"value\": \"tektur\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Tourney\",\n                    \"value\": \"tourney\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Urbanist\",\n                    \"value\": \"urbanist\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Varta\",\n                    \"value\": \"varta\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Wix Madefor Text\",\n                    \"value\": \"wixmadefortext\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Workbench\",\n                    \"value\": \"workbench\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Work Sans\",\n                    \"value\": \"worksans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Yanone Kaffeesatz\",\n                    \"value\": \"yanonekaffeesatz\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Zalando Sans\",\n                    \"value\": \"zalandosans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Zalando Sans Expanded\",\n                    \"value\": \"zalandosansexpanded\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Comfortaa\",\n                    \"value\": \"comfortaa\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Dancing Script\",\n                    \"value\": \"dancingscript\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Gluten\",\n                    \"value\": \"gluten\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Lemonada\",\n                    \"value\": \"lemonada\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Playwrite US Modern\",\n                    \"value\": \"playwriteusmodern\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Playwrite US Traditional\",\n                    \"value\": \"playwriteustrad\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Bitcount\",\n                    \"value\": \"bitcount\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Geist Mono\",\n                    \"value\": \"geistmono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Google Sans Code\",\n                    \"value\": \"googlesanscode\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"JetBrains Mono\",\n                    \"value\": \"jetbrainsmono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Red Hat Mono\",\n                    \"value\": \"redhatmono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Source Code Pro\",\n                    \"value\": \"sourcecodepro\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"SUSE Mono\",\n                    \"value\": \"susemono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Victor Mono\",\n                    \"value\": \"victormono\",\n                    \"group\": \"Monospace\"\n                }\n            ]\n        },\n        {\n            \"name\": \"disableFontBodyItalic\",\n            \"label\": \"Disable italic style\",\n            \"group\": \"Fonts\",\n            \"note\": \"This option allows you to disable the loading of the dedicated italic version for the body font. The italic font is automatically loaded when selecting a body font that supports it, like Lora, for optimal appearance. However, for performance reasons, you can choose to prevent loading the dedicated italic version by enabling this option.\",\n            \"type\": \"checkbox\",\n            \"value\": false,\n            \"dependencies\": [\n                {\n                    \"field\": \"fontBody\",\n                    \"value\": \"albertsans,adventpro,aleo,andadapro,archivonarrow,asap,besley,bitter,bodonimoda,brygada1918,cabin,dmsans,exo,familjengrotesk,faustina,figtree,finlandica,gantari,glory,googlesanscode,hostgrotesk,inclusivesans,instrumentsans,jetbrainsmono,karla,labrada,librefranklin,lora,manuale,merriweathersans,montserrat,mulish,nunito,petrona,playfairdisplay,plusjakartasans,publicsans,radiocanadabig,raleway,redhatdisplay,redhatmono,redhattext,rem,rokkitt,rubik,sourcecodepro,susemono,teachers,tourney,urbanist,victormono,wixmadefortext,worksans,zalandosans,zalandosansexpanded,yrsa\"\n                }\n            ]\n        },\n        {\n            \"name\": \"fontHeadings\",\n            \"label\": \"Headings font (H1-H6)\",\n            \"group\": \"Fonts\",\n            \"value\": \"system-ui\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"OS Default Font\",\n                    \"value\": \"system-ui\",\n                    \"group\": \"System\"\n                },\n                {\n                    \"label\": \"Aleo\",\n                    \"value\": \"aleo\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Andada Pro\",\n                    \"value\": \"andadapro\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Besley\",\n                    \"value\": \"besley\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Bitter\",\n                    \"value\": \"bitter\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Bodoni Moda\",\n                    \"value\": \"bodonimoda\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Brygada 1918\",\n                    \"value\": \"brygada1918\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Cinzel\",\n                    \"value\": \"cinzel\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Danfo\",\n                    \"value\": \"danfo\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Domine\",\n                    \"value\": \"domine\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Faustina\",\n                    \"value\": \"faustina\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Frank Ruhl Libre\",\n                    \"value\": \"frankruhllibre\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Grenze Gotisch\",\n                    \"value\": \"grenzegotisch\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Imbue\",\n                    \"value\": \"imbue\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Kalnia\",\n                    \"value\": \"kalnia\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Kreon\",\n                    \"value\": \"kreon\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Labrada\",\n                    \"value\": \"labrada\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Lora\",\n                    \"value\": \"lora\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Manuale\",\n                    \"value\": \"manuale\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Petrona\",\n                    \"value\": \"petrona\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Playfair Display\",\n                    \"value\": \"playfairdisplay\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Red Rose\",\n                    \"value\": \"redrose\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Roboto Slab\",\n                    \"value\": \"robotoslab\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Rokkitt\",\n                    \"value\": \"rokkitt\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Yrsa\",\n                    \"value\": \"yrsa\",\n                    \"group\": \"Serif\"\n                },\n                {\n                    \"label\": \"Advent Pro\",\n                    \"value\": \"adventpro\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Albert Sans\",\n                    \"value\": \"albertsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Antonio\",\n                    \"value\": \"antonio\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Archivo Narrow\",\n                    \"value\": \"archivonarrow\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Asap\",\n                    \"value\": \"asap\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Assistant\",\n                    \"value\": \"assistant\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Cabin\",\n                    \"value\": \"cabin\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Cairo\",\n                    \"value\": \"cairo\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Comme\",\n                    \"value\": \"comme\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"DM Sans\",\n                    \"value\": \"dmsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Dosis\",\n                    \"value\": \"dosis\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Doto\",\n                    \"value\": \"doto\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Dyna Puff\",\n                    \"value\": \"dynapuff\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Exo\",\n                    \"value\": \"exo\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Familjen Grotesk\",\n                    \"value\": \"familjengrotesk\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Figtree\",\n                    \"value\": \"figtree\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Finlandica\",\n                    \"value\": \"finlandica\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Fredoka\",\n                    \"value\": \"fredoka\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Funnel Display\",\n                    \"value\": \"funneldisplay\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Gantari\",\n                    \"value\": \"gantari\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Glory\",\n                    \"value\": \"glory\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Handjet\",\n                    \"value\": \"handjet\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Heebo\",\n                    \"value\": \"heebo\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Host Grotesk\",\n                    \"value\": \"hostgrotesk\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Inclusive Sans\",\n                    \"value\": \"inclusivesans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Instrument Sans\",\n                    \"value\": \"instrumentsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Jura\",\n                    \"value\": \"jura\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Karla\",\n                    \"value\": \"karla\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Kumbh Sans\",\n                    \"value\": \"kumbhsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"League Spartan\",\n                    \"value\": \"leaguespartan\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Lexend\",\n                    \"value\": \"lexend\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Lexend Deca\",\n                    \"value\": \"lexenddeca\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Libre Franklin\",\n                    \"value\": \"librefranklin\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Manrope\",\n                    \"value\": \"manrope\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Maven Pro\",\n                    \"value\": \"mavenpro\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Merriweather Sans\",\n                    \"value\": \"merriweathersans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Montserrat\",\n                    \"value\": \"montserrat\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Mulish\",\n                    \"value\": \"mulish\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Nunito\",\n                    \"value\": \"nunito\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Orbitron\",\n                    \"value\": \"orbitron\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Oswald\",\n                    \"value\": \"oswald\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Outfit\",\n                    \"value\": \"outfit\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Oxanium\",\n                    \"value\": \"oxanium\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Parkinsans\",\n                    \"value\": \"parkinsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Plus Jakarta Sans\",\n                    \"value\": \"plusjakartasans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Pontano Sans\",\n                    \"value\": \"pontanosans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Public Sans\",\n                    \"value\": \"publicsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Quicksand\",\n                    \"value\": \"quicksand\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Radio Canada Big\",\n                    \"value\": \"radiocanadabig\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Raleway\",\n                    \"value\": \"raleway\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Red Hat Display\",\n                    \"value\": \"redhatdisplay\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Red Hat Text\",\n                    \"value\": \"redhattext\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"REM\",\n                    \"value\": \"rem\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Roboto Flex\",\n                    \"value\": \"robotoflex\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Rubik\",\n                    \"value\": \"rubik\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Ruda\",\n                    \"value\": \"ruda\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Smooch Sans\",\n                    \"value\": \"smoochsans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Sora\",\n                    \"value\": \"sora\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Spartan\",\n                    \"value\": \"spartan\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Stick No Bills\",\n                    \"value\": \"sticknobills\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Teachers\",\n                    \"value\": \"teachers\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Tektur\",\n                    \"value\": \"tektur\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Tourney\",\n                    \"value\": \"tourney\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Urbanist\",\n                    \"value\": \"urbanist\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Varta\",\n                    \"value\": \"varta\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Wix Madefor Text\",\n                    \"value\": \"wixmadefortext\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Workbench\",\n                    \"value\": \"workbench\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Work Sans\",\n                    \"value\": \"worksans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Yanone Kaffeesatz\",\n                    \"value\": \"yanonekaffeesatz\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Zalando Sans\",\n                    \"value\": \"zalandosans\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Zalando Sans Expanded\",\n                    \"value\": \"zalandosansexpanded\",\n                    \"group\": \"Sans Serif\"\n                },\n                {\n                    \"label\": \"Comfortaa\",\n                    \"value\": \"comfortaa\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Dancing Script\",\n                    \"value\": \"dancingscript\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Gluten\",\n                    \"value\": \"gluten\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Lemonada\",\n                    \"value\": \"lemonada\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Playwrite US Modern\",\n                    \"value\": \"playwriteusmodern\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Playwrite US Traditional\",\n                    \"value\": \"playwriteustrad\",\n                    \"group\": \"Cursive\"\n                },\n                {\n                    \"label\": \"Bitcount\",\n                    \"value\": \"bitcount\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Geist Mono\",\n                    \"value\": \"geistmono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Google Sans Code\",\n                    \"value\": \"googlesanscode\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"JetBrains Mono\",\n                    \"value\": \"jetbrainsmono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Red Hat Mono\",\n                    \"value\": \"redhatmono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Source Code Pro\",\n                    \"value\": \"sourcecodepro\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"SUSE Mono\",\n                    \"value\": \"susemono\",\n                    \"group\": \"Monospace\"\n                },\n                {\n                    \"label\": \"Victor Mono\",\n                    \"value\": \"victormono\",\n                    \"group\": \"Monospace\"\n                }\n            ]\n        },\n        {\n            \"name\": \"disableFontHeadingsItalic\",\n            \"label\": \"Disable italic style\",\n            \"group\": \"Fonts\",\n            \"note\": \"This option allows you to disable loading of the dedicated italic version for the headings font. The italic font is automatically loaded when selecting a headings font that supports it, like Lora, for optimal appearance. However, for performance reasons, you can choose to prevent loading of the dedicated italic version by enabling this option.\",\n            \"type\": \"checkbox\",\n            \"value\": false,\n            \"dependencies\": [\n                {\n                    \"field\": \"fontHeadings\",\n                    \"value\": \"albertsans,adventpro,aleo,andadapro,archivonarrow,asap,besley,bitter,bodonimoda,brygada1918,cabin,dmsans,exo,familjengrotesk,faustina,figtree,finlandica,gantari,glory,googlesanscode,hostgrotesk,inclusivesans,instrumentsans,jetbrainsmono,karla,labrada,librefranklin,lora,manuale,merriweathersans,montserrat,mulish,nunito,petrona,playfairdisplay,plusjakartasans,publicsans,radiocanadabig,raleway,redhatdisplay,redhatmono,redhattext,rem,rokkitt,rubik,sourcecodepro,susemono,teachers,tourney,urbanist,victormono,wixmadefortext,worksans,zalandosans,zalandosansexpanded,yrsa\"\n                }\n            ]\n        },\n        {\n            \"name\": \"fontMenu\",\n            \"label\": \"Menu font\",\n            \"group\": \"Fonts\",\n            \"value\": \"var(--body-font)\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"OS Default Font\",\n                    \"value\": \"system-ui\"\n                },\n                {\n                    \"label\": \"Select the font used for the Body\",\n                    \"value\": \"var(--body-font)\"\n                },\n                {\n                    \"label\": \"Select the font used for the Headings\",\n                    \"value\": \"var(--heading-font)\"\n                }\n            ]\n        },\n        {\n            \"name\": \"fontLogo\",\n            \"label\": \"Logo font\",\n            \"group\": \"Fonts\",\n            \"value\": \"var(--body-font)\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"OS Default Font\",\n                    \"value\": \"system-ui\"\n                },\n                {\n                    \"label\": \"Select the font used for the Body\",\n                    \"value\": \"var(--body-font)\"\n                },\n                {\n                    \"label\": \"Select the font used for the Headings\",\n                    \"value\": \"var(--heading-font)\"\n                }\n            ]\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Fonts\",\n            \"size\": \"big thin\"\n        },\n        {\n            \"name\": \"minFontSize\",\n            \"label\": \"Minimum font size\",\n            \"group\": \"Fonts\",\n            \"note\": \"The font-size of the root element in REM unit\",\n            \"value\": \"1.1\",\n            \"type\": \"range\",\n            \"min\": 1,\n            \"max\": 3,\n            \"step\": 0.05\n        },\n        {\n            \"name\": \"maxFontSize\",\n            \"label\": \"Maximum font size\",\n            \"group\": \"Fonts\",\n            \"note\": \"The font-size of the root element in REM unit\",\n            \"value\": \"1.2\",\n            \"type\": \"range\",\n            \"min\": 1,\n            \"max\": 3,\n            \"step\": 0.05\n        },\n        {\n            \"name\": \"lineHeight\",\n            \"label\": \"Line height\",\n            \"group\": \"Fonts\",\n            \"note\": \"The default line-height for text within body elements, excluding headings.\",\n            \"value\": \"1.7\",\n            \"type\": \"range\",\n            \"min\": 1,\n            \"max\": 3,\n            \"step\": 0.05\n        },\n        {\n            \"name\": \"letterSpacing\",\n            \"label\": \"Letter spacing\",\n            \"group\": \"Fonts\",\n            \"note\": \"Adjusts the spacing between characters for body text elements, excluding headings, in EM.\",\n            \"value\": \"0\",\n            \"type\": \"range\",\n            \"min\": -1,\n            \"max\": 1,\n            \"step\": 0.01\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Fonts\",\n            \"note\": \"Note that not all fonts support the full range of weights; instead, they will typically support a standard range between 400 and 700. To see the exact range available for the selected body font, please visit our <a href='https://getpublii.com/docs/fonts.html' target='_blank'>documentation</a>.\",\n            \"size\": \"small thin\"\n        },\n        {\n            \"name\": \"fontBodyWeight\",\n            \"label\": \"Normal font weight\",\n            \"group\": \"Fonts\",\n            \"value\": \"400\",\n            \"type\": \"range\",\n            \"min\": 100,\n            \"max\": 900,\n            \"step\": 1\n        },\n        {\n            \"name\": \"fontBoldWeight\",\n            \"label\": \"Bold font weight\",\n            \"group\": \"Fonts\",\n            \"value\": \"600\",\n            \"type\": \"range\",\n            \"min\": 100,\n            \"max\": 900,\n            \"step\": 1\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"Headings\",\n            \"group\": \"Fonts\",\n            \"size\": \"big\"\n        },\n        {\n            \"name\": \"fontHeadignsWeight\",\n            \"label\": \"H1-H6 font weight\",\n            \"group\": \"Fonts\",\n            \"value\": \"500\",\n            \"type\": \"range\",\n            \"min\": 100,\n            \"max\": 900,\n            \"step\": 1\n        },\n        {\n            \"name\": \"fontHeadingsStyle\",\n            \"label\": \"H1-H6 font style\",\n            \"group\": \"Fonts\",\n            \"value\": \"normal\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Normal\",\n                    \"value\": \"normal\"\n                },\n                {\n                    \"label\": \"Italic\",\n                    \"value\": \"italic\"\n                },\n                {\n                    \"label\": \"Oblique\",\n                    \"value\": \"oblique\"\n                }\n            ]\n        },\n        {\n            \"name\": \"fontHeadingsLineHeight\",\n            \"label\": \"H1-H6 line height\",\n            \"group\": \"Fonts\",\n            \"note\": \"The default line-height for heading elements (h1, h2, h3, etc.).\",\n            \"value\": \"1.2\",\n            \"type\": \"range\",\n            \"min\": 1,\n            \"max\": 3,\n            \"step\": 0.05\n        },\n        {\n            \"name\": \"fontHeadingsletterSpacing\",\n            \"label\": \"H1-H6 letter spacing\",\n            \"group\": \"Fonts\",\n            \"note\": \"Adjusts the spacing between characters for heading elements (h1, h2, h3, etc.) in EM.\",\n            \"value\": \"0\",\n            \"type\": \"range\",\n            \"min\": -1,\n            \"max\": 1,\n            \"step\": 0.01\n        },\n        {\n            \"name\": \"fontHeadingsTransform\",\n            \"label\": \"H1-H6 text transform\",\n            \"group\": \"Fonts\",\n            \"value\": \"none\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"None\",\n                    \"value\": \"none\"\n                },\n                {\n                    \"label\": \"Capitalize\",\n                    \"value\": \"capitalize\"\n                },\n                {\n                    \"label\": \"Lowercase\",\n                    \"value\": \"lowercase\"\n                },\n                {\n                    \"label\": \"Uppercase\",\n                    \"value\": \"uppercase\"\n                }\n            ]\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Share Buttons\",\n            \"note\": \"This section allows you to choose your preferred share buttons for various platforms. If you wish to add more, you can install the <a href='https://marketplace.getpublii.com/plugins/social-sharing/' target='_blank'>Social Sharing plugin</a> available on the Publii Marketplace.\",\n            \"size\": \"no-line\"\n        },\n        {\n            \"name\": \"shareFacebook\",\n            \"group\": \"Share Buttons\",\n            \"label\": \"Facebook\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"shareTwitter\",\n            \"group\": \"Share Buttons\",\n            \"label\": \"X (formerly Twitter)\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"shareTwitterName\",\n            \"group\": \"Share Buttons\",\n            \"note\": \"Enter your X account's handle here; it will be used when the X share button is clicked on your site e.g. 'via @XHandle'. If left blank, the default username of @SiteName will be used\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"shareTwitter\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"sharePinterest\",\n            \"group\": \"Share Buttons\",\n            \"label\": \"Pinterest\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"shareMix\",\n            \"group\": \"Share Buttons\",\n            \"label\": \"Mix (formerly StumbleUpon)\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"shareLinkedin\",\n            \"group\": \"Share Buttons\",\n            \"label\": \"LinkedIn\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"shareBuffer\",\n            \"group\": \"Share Buttons\",\n            \"label\": \"Buffer\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"shareWhatsApp\",\n            \"group\": \"Share Buttons\",\n            \"label\": \"WhatsApp\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Footer\",\n            \"note\": \"This section allows you to add follow buttons for various social media platforms. If you want to expand the available options, you can install the <a href='https://marketplace.getpublii.com/plugins/follow-buttons/' target='_blank'>Follow Buttons plugin</a> available on the Publii Marketplace.\",\n            \"size\": \"no-line\"\n        },\n        {\n            \"name\": \"socialButtons\",\n            \"group\": \"Footer\",\n            \"label\": \"Follow Buttons\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"socialFacebook\",\n            \"label\": \"Facebook\",\n            \"group\": \"Footer\",\n            \"placeholder\": \"Please enter your Facebook URL\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"socialButtons\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"socialTwitter\",\n            \"label\": \"X (formerly Twitter)\",\n            \"group\": \"Footer\",\n            \"placeholder\": \"Please enter your X URL\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"socialButtons\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"socialInstagram\",\n            \"label\": \"Instagram\",\n            \"group\": \"Footer\",\n            \"placeholder\": \"Please enter your Instagram URL\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"socialButtons\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"socialLinkedin\",\n            \"label\": \"LinkedIn\",\n            \"group\": \"Footer\",\n            \"placeholder\": \"Please enter your LinkedIn URL\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"socialButtons\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"socialVimeo\",\n            \"label\": \"Vimeo\",\n            \"group\": \"Footer\",\n            \"placeholder\": \"Please enter your Vimeo URL\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"socialButtons\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"socialPinterest\",\n            \"label\": \"Pinterest\",\n            \"group\": \"Footer\",\n            \"placeholder\": \"Please enter your Pinterest URL\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"socialButtons\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"socialYoutube\",\n            \"label\": \"Youtube\",\n            \"group\": \"Footer\",\n            \"placeholder\": \"Please enter your Youtube URL\",\n            \"value\": \"\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"socialButtons\",\n                    \"value\": \"true\"\n                }\n            ]\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Footer\",\n            \"size\": \"small thin\"\n        },\n        {\n            \"name\": \"copyrightText\",\n            \"label\": \"Copyright text\",\n            \"group\": \"Footer\",\n            \"value\": \"Powered by Publii\",\n            \"type\": \"wysiwyg\"\n        },\n        {\n            \"name\": \"searchFeature\",\n            \"label\": \"Search\",\n            \"group\": \"Search\",\n            \"note\": \"Enable / disable the search box, which is usually located next to the top menu. In order to use the search function, you also need to install the search plugin.\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"createSearchPage\",\n            \"label\": \"Search subpage\",\n            \"group\": \"Search\",\n            \"note\": \"Enabling this option will create an additional search.html page which may be required by some search plugins to display the search results on a separate page.\",\n            \"value\": false,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Gallery\",\n            \"note\": \"To use the gallery functionality, you need to install the appropriate plugin. Please check the available gallery plugins in the <a rel=\\\"noopener noreferrer\\\" href=\\\"https://marketplace.getpublii.com/plugins/\\\" target=\\\"_blank\\\">Publii Marketplace</a>.\",\n            \"size\": \"no-line\"\n        },\n        {\n            \"name\": \"galleryItemGap\",\n            \"label\": \"Gallery item spacing\",\n            \"group\": \"Gallery\",\n            \"note\": \"You can use the --baseline variable and multiply it, but you can also use other values like px or rem.\",\n            \"value\": \"calc(var(--baseline) * 1.5)\",\n            \"type\": \"text\"\n        },\n        {\n            \"name\": \"backToTopButton\",\n            \"group\": \"Additional\",\n            \"label\": \"\\\"Back to top\\\" button\",\n            \"value\": true,\n            \"type\": \"checkbox\"\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Additional\",\n            \"size\": \"small\"\n        },\n        {\n            \"name\": \"formatDate\",\n            \"label\": \"Date format\",\n            \"group\": \"Additional\",\n            \"value\": \"MMMM D, YYYY\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Nov 1, 2016\",\n                    \"value\": \"MMM D, YYYY\"\n                },\n                {\n                    \"label\": \"1 Nov 2016\",\n                    \"value\": \"D MMM YYYY\"\n                },\n                {\n                    \"label\": \"November 1, 2016\",\n                    \"value\": \"MMMM D, YYYY\"\n                },\n                {\n                    \"label\": \"1 November 2016\",\n                    \"value\": \"D MMMM YYYY\"\n                },\n                {\n                    \"label\": \"Sunday, November 1, 2016\",\n                    \"value\": \"dddd, MMMM D, YYYY\"\n                },\n                {\n                    \"label\": \"Sunday, 1 November 2016\",\n                    \"value\": \"dddd, D MMMM YYYY\"\n                },\n                {\n                    \"label\": \"November 1, 2016 10:02:05\",\n                    \"value\": \"MMMM D, YYYY HH:mm:ss\"\n                },\n                {\n                    \"label\": \"1 November 2016 10:02:05\",\n                    \"value\": \"D MMMM YYYY HH:mm:ss\"\n                },\n                {\n                    \"label\": \"01/21/2016\",\n                    \"value\": \"MM/DD/YYYY\"\n                },\n                {\n                    \"label\": \"21/01/2016\",\n                    \"value\": \"DD/MM/YYYY\"\n                },\n                {\n                    \"label\": \"Custom - create your own date format\",\n                    \"value\": \"custom\"\n                }\n            ]\n        },\n        {\n            \"name\": \"formatDateCustom\",\n            \"group\": \"Additional\",\n            \"label\": \"Create a custom date format\",\n            \"note\": \"More details you can find <a href='https://github.com/taylorhakes/fecha' target='_blank'>here.</a>\",\n            \"value\": \"HH:mm:ss YY/MM/DD\",\n            \"type\": \"text\",\n            \"dependencies\": [\n                {\n                    \"field\": \"formatDate\",\n                    \"value\": \"custom\"\n                }\n            ]\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Additional\",\n            \"size\": \"small\"\n        },\n        {\n            \"name\": \"lazyLoadEffect\",\n            \"label\": \"Lazy load effects\",\n            \"group\": \"Additional\",\n            \"note\": \"This option works only if the 'Media lazy load' option is enabled in the Website Speed tab under Site Settings\",\n            \"value\": \"fadein\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Fade in\",\n                    \"value\": \"fadein\"\n                },\n                {\n                    \"label\": \"None\",\n                    \"value\": \"none\"\n                }\n            ]\n        },\n        {\n            \"name\": \"separator\",\n            \"type\": \"separator\",\n            \"label\": \"\",\n            \"group\": \"Additional\",\n            \"size\": \"small\"\n        },\n        {\n            \"name\": \"faviconUpload\",\n            \"label\": \"Upload favicon file\",\n            \"group\": \"Additional\",\n            \"note\": \"The ideal size of a favicon is 16x16 pixels. Save your favicon as either favicon.png or favicon.ico\",\n            \"value\": \"\",\n            \"type\": \"smallupload\",\n            \"upload\": true\n        },\n        {\n            \"name\": \"faviconExtension\",\n            \"label\": \"Favicon extension\",\n            \"group\": \"Additional\",\n            \"value\": \"image/x-icon\",\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \".ico\",\n                    \"value\": \"image/x-icon\"\n                },\n                {\n                    \"label\": \".png\",\n                    \"value\": \"image/png\"\n                }\n            ]\n        }\n    ],\n    \"postConfig\": [\n        {\n            \"name\": \"displayDate\",\n            \"label\": \"Display date\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayAuthor\",\n            \"label\": \"Display author\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayLastUpdatedDate\",\n            \"label\": \"Display last updated date\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayTags\",\n            \"label\": \"Display tags\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayShareButtons\",\n            \"label\": \"Display share buttons\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayAuthorBio\",\n            \"label\": \"Display author bio\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayPostNavigation\",\n            \"label\": \"Display post navigation\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayRelatedPosts\",\n            \"label\": \"Display related posts\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayComments\",\n            \"label\": \"Display comments\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        }\n    ],\n    \"pageConfig\": [\n        {\n            \"name\": \"displayDate\",\n            \"label\": \"Display date\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"pageTemplates\": \"!empty\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayAuthor\",\n            \"label\": \"Display author\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"pageTemplates\": \"!empty\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayLastUpdatedDate\",\n            \"label\": \"Display last updated date\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"pageTemplates\": \"!empty\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayShareButtons\",\n            \"label\": \"Display share buttons\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"pageTemplates\": \"!empty\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayAuthorBio\",\n            \"label\": \"Display author bio\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"pageTemplates\": \"!empty\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayChildPages\",\n            \"label\": \"Display child pages\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayComments\",\n            \"label\": \"Display comments\",\n            \"value\": 0,\n            \"type\": \"select\",\n            \"pageTemplates\": \"!empty\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        }\n    ],\n    \"authorConfig\": [\n        {\n            \"name\": \"displayFeaturedImage\",\n            \"label\": \"Display featured image\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayAvatar\",\n            \"label\": \"Display avatar\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayPostCounter\",\n            \"label\": \"Display post counter\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayDescription\",\n            \"label\": \"Display author biography\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayWebsite\",\n            \"label\": \"Display author website\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayPostList\",\n            \"label\": \"Display post list\",\n            \"note\": \"Disabling the post list can cause duplicate content issues. To avoid this, please disable authors pagination in the SEO section of your site settings.\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        }\n    ],\n    \"tagConfig\": [\n        {\n            \"name\": \"displayFeaturedImage\",\n            \"label\": \"Display featured image\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayPostCounter\",\n            \"label\": \"Display post counter\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayDescription\",\n            \"label\": \"Display tag description\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        },\n        {\n            \"name\": \"displayPostList\",\n            \"label\": \"Display post list\",\n            \"note\": \"Disabling the post list can cause duplicate content issues. To avoid this, please disable tag pagination in the SEO section of your site settings.\",\n            \"value\": 1,\n            \"type\": \"select\",\n            \"options\": [\n                {\n                    \"label\": \"Enabled\",\n                    \"value\": 1\n                },\n                {\n                    \"label\": \"Disabled\",\n                    \"value\": 0\n                }\n            ]\n        }\n    ],\n    \"files\": {\n        \"ignoreAssets\": [\n            \"scss\",\n            \".DS_Store\"\n        ],\n        \"assetsPath\": \"assets\",\n        \"useDynamicAssets\": true,\n        \"partialsPath\": \"partials\",\n        \"responsiveImages\": {\n            \"contentImages\": {\n                \"sizes\": \"(max-width: 1920px) 100vw, 1920px\",\n                \"dimensions\": {\n                    \"xs\": {\n                        \"width\": 640,\n                        \"height\": \"auto\"\n                    },\n                    \"sm\": {\n                        \"width\": 768,\n                        \"height\": \"auto\"\n                    },\n                    \"md\": {\n                        \"width\": 1024,\n                        \"height\": \"auto\"\n                    },\n                    \"lg\": {\n                        \"width\": 1366,\n                        \"height\": \"auto\"\n                    },\n                    \"xl\": {\n                        \"width\": 1600,\n                        \"height\": \"auto\"\n                    },\n                    \"2xl\": {\n                        \"width\": 1920,\n                        \"height\": \"auto\"\n                    }\n                }\n            },\n            \"featuredImages\": {\n                \"sizes\": {\n                    \"hero\": \"88vw\",\n                    \"feed\": \"(min-width: 600px) calc(4.38vw + 143px), 87.86vw\"\n                },\n                \"dimensions\": {\n                    \"xs\": {\n                        \"width\": 640,\n                        \"height\": \"auto\",\n                        \"group\": \"hero,feed\"\n                    },\n                    \"sm\": {\n                        \"width\": 768,\n                        \"height\": \"auto\",\n                        \"group\": \"hero,feed\"\n                    },\n                    \"md\": {\n                        \"width\": 1024,\n                        \"height\": \"auto\",\n                        \"group\": \"hero,feed\"\n                    },\n                    \"lg\": {\n                        \"width\": 1366,\n                        \"height\": \"auto\",\n                        \"group\": \"hero\"\n                    },\n                    \"xl\": {\n                        \"width\": 1600,\n                        \"height\": \"auto\",\n                        \"group\": \"hero\"\n                    },\n                    \"2xl\": {\n                        \"width\": 1920,\n                        \"height\": \"auto\",\n                        \"group\": \"hero\"\n                    }\n                }\n            },\n            \"tagImages\": {\n                \"sizes\": \"88vw\",\n                \"dimensions\": {\n                    \"xs\": {\n                        \"width\": 640,\n                        \"height\": \"auto\"\n                    },\n                    \"sm\": {\n                        \"width\": 768,\n                        \"height\": \"auto\"\n                    },\n                    \"md\": {\n                        \"width\": 1024,\n                        \"height\": \"auto\"\n                    },\n                    \"lg\": {\n                        \"width\": 1366,\n                        \"height\": \"auto\"\n                    },\n                    \"xl\": {\n                        \"width\": 1600,\n                        \"height\": \"auto\"\n                    },\n                    \"2xl\": {\n                        \"width\": 1920,\n                        \"height\": \"auto\"\n                    }\n                }\n            },\n            \"authorImages\": {\n                \"sizes\": \"88vw\",\n                \"dimensions\": {\n                    \"xs\": {\n                        \"width\": 640,\n                        \"height\": \"auto\"\n                    },\n                    \"sm\": {\n                        \"width\": 768,\n                        \"height\": \"auto\"\n                    },\n                    \"md\": {\n                        \"width\": 1024,\n                        \"height\": \"auto\"\n                    },\n                    \"lg\": {\n                        \"width\": 1366,\n                        \"height\": \"auto\"\n                    },\n                    \"xl\": {\n                        \"width\": 1600,\n                        \"height\": \"auto\"\n                    },\n                    \"2xl\": {\n                        \"width\": 1920,\n                        \"height\": \"auto\"\n                    }\n                }\n            },\n            \"optionImages\": {\n                \"sizes\": \"88vw\",\n                \"dimensions\": {\n                    \"xs\": {\n                        \"width\": 640,\n                        \"height\": \"auto\"\n                    },\n                    \"sm\": {\n                        \"width\": 768,\n                        \"height\": \"auto\"\n                    },\n                    \"md\": {\n                        \"width\": 1024,\n                        \"height\": \"auto\"\n                    },\n                    \"lg\": {\n                        \"width\": 1366,\n                        \"height\": \"auto\"\n                    },\n                    \"xl\": {\n                        \"width\": 1600,\n                        \"height\": \"auto\"\n                    },\n                    \"2xl\": {\n                        \"width\": 1920,\n                        \"height\": \"auto\"\n                    }\n                }\n            },\n            \"galleryImages\": {\n                \"sizes\": \"\",\n                \"dimensions\": {\n                    \"thumbnail\": {\n                        \"width\": 720,\n                        \"height\": \"auto\"\n                    }\n                }\n            }\n        }\n    },\n    \"customElements\": [\n        {\n            \"label\": \"Ordered list\",\n            \"cssClasses\": \"ordered-list\",\n            \"selector\": \"ol\"\n        },\n        {\n            \"label\": \"Drop cap\",\n            \"cssClasses\": \"dropcap\",\n            \"selector\": \"p\"\n        },\n        {\n            \"label\": \"Info\",\n            \"cssClasses\": \"msg msg--info\",\n            \"selector\": \"p\"\n        },\n        {\n            \"label\": \"Highlight\",\n            \"cssClasses\": \"msg msg--highlight \",\n            \"selector\": \"p\"\n        },\n        {\n            \"label\": \"Success\",\n            \"cssClasses\": \"msg msg--success\",\n            \"selector\": \"p\"\n        },\n        {\n            \"label\": \"Warning\",\n            \"cssClasses\": \"msg msg--warning\",\n            \"selector\": \"p\"\n        },\n        {\n            \"label\": \"Table bordered\",\n            \"cssClasses\": \"table-bordered\",\n            \"selector\": \"table\"\n        },\n        {\n            \"label\": \"Table striped\",\n            \"cssClasses\": \"table-striped\",\n            \"selector\": \"table\"\n        },\n        {\n            \"label\": \"Table title\",\n            \"cssClasses\": \"table-title\",\n            \"selector\": \"table\"\n        }\n    ]\n}"
  },
  {
    "path": "app/default-files/default-themes/simple/dynamic-assets-mapping.js",
    "content": "module.exports = function (themeConfig) {\n  let fontBody = themeConfig.customConfig['fontBody'];\n  let fontHeadings = themeConfig.customConfig['fontHeadings'];\n  let fontBodyItalic = themeConfig.customConfig['fontBodyItalic'];\n  let fontHeadingsItalic = themeConfig.customConfig['fontHeadingsItalic'];\n\n  let assets = new Set();\n\n  const fontParams = {\n    'albertsans': { hasItalic: true },\n    'adventpro': { hasItalic: true },\n    'aleo': { hasItalic: true },\n    'andadapro': { hasItalic: true },\n    'antonio': { hasItalic: false },\n    'archivonarrow': { hasItalic: true },\n    'asap': { hasItalic: true },\n    'assistant': { hasItalic: false },\n    'besley': { hasItalic: true },\n    'bitter': { hasItalic: true },\n    'bitcount': { hasItalic: false },\n    'bodonimoda': { hasItalic: true },\n    'brygada1918': { hasItalic: true },\n    'cabin': { hasItalic: true },\n    'cairo': { hasItalic: false },\n    'cinzel': { hasItalic: false },\n    'comfortaa': { hasItalic: false },\n    'comme': { hasItalic: false },\n    'dancingscript': { hasItalic: false },\n    'danfo': { hasItalic: false },\n    'dmsans': { hasItalic: true },\n    'domine': { hasItalic: false },\n    'dosis': { hasItalic: false },\n    'doto': { hasItalic: false },\n    'dynapuff': { hasItalic: false },\n    'exo': { hasItalic: true },\n    'familjengrotesk': { hasItalic: true },\n    'faustina': { hasItalic: true },\n    'figtree': { hasItalic: true },\n    'finlandica': { hasItalic: true },\n    'frankruhllibre': { hasItalic: false },\n    'fredoka': { hasItalic: false },\n    'funneldisplay': { hasItalic: false },\n    'gantari': { hasItalic: true },\n    'geistmono': { hasItalic: false },\n    'glory': { hasItalic: true },\n    'gluten': { hasItalic: false },\n    'googlesanscode': { hasItalic: true },\n    'grenzegotisch': { hasItalic: false },\n    'handjet': { hasItalic: false },\n    'heebo': { hasItalic: false },\n    'hostgrotesk': { hasItalic: true },\n    'imbue': { hasItalic: false },\n    'inclusivesans': { hasItalic: true },\n    'instrumentsans': { hasItalic: true },\n    'jetbrainsmono': { hasItalic: true },\n    'jura': { hasItalic: false },\n    'kalnia': { hasItalic: false },\n    'karla': { hasItalic: true },\n    'kreon': { hasItalic: false },\n    'kumbhsans': { hasItalic: false },\n    'labrada': { hasItalic: true },\n    'leaguespartan': { hasItalic: false },\n    'lemonada': { hasItalic: false },\n    'lexend': { hasItalic: false },\n    'lexenddeca': { hasItalic: false },\n    'librefranklin': { hasItalic: true },\n    'lora': { hasItalic: true },\n    'manuale': { hasItalic: true },\n    'manrope': { hasItalic: false },\n    'mavenpro': { hasItalic: false },\n    'merriweathersans': { hasItalic: true },\n    'montserrat': { hasItalic: true },\n    'mulish': { hasItalic: true },\n    'nunito': { hasItalic: true },\n    'orbitron': { hasItalic: false },\n    'oswald': { hasItalic: false },\n    'outfit': { hasItalic: false },\n    'oxanium': { hasItalic: false },\n    'parkinsans': { hasItalic: false },\n    'petrona': { hasItalic: true },\n    'playfairdisplay': { hasItalic: true },\n    'playwriteusmodern': { hasItalic: false },\n    'playwriteustrad': { hasItalic: false },\n    'plusjakartasans': { hasItalic: true },\n    'pontanosans': { hasItalic: false },\n    'publicsans': { hasItalic: true },\n    'quicksand': { hasItalic: false },\n    'radiocanadabig': { hasItalic: true },\n    'raleway': { hasItalic: true },\n    'redhatdisplay': { hasItalic: true },\n    'redhatmono': { hasItalic: true },\n    'redhattext': { hasItalic: true },\n    'redrose': { hasItalic: false },\n    'rem': { hasItalic: true },\n    'robotoflex': { hasItalic: false },\n    'robotoslab': { hasItalic: false },\n    'rokkitt': { hasItalic: true },\n    'rubik': { hasItalic: true },\n    'ruda': { hasItalic: false },\n    'smoochsans': { hasItalic: false },\n    'sourcecodepro': { hasItalic: true },\n    'sora': { hasItalic: false },\n    'spartan': { hasItalic: false },\n    'sticknobills': { hasItalic: false },\n    'susemono': { hasItalic: true },\n    'system-ui': { hasItalic: false },\n    'teachers': { hasItalic: true },\n    'tektur': { hasItalic: false },\n    'tourney': { hasItalic: true },\n    'urbanist': { hasItalic: true },\n    'varta': { hasItalic: false },\n    'victormono': { hasItalic: true },\n    'wixmadefortext': { hasItalic: true },\n    'workbench': { hasItalic: false },\n    'worksans': { hasItalic: true },\n    'yanonekaffeesatz': { hasItalic: false },\n    'zalandosans': { hasItalic: true },\n    'zalandosansexpanded': { hasItalic: true },\n    'yrsa': { hasItalic: true }\n  };  \n\n  const addFontAsset = (font, hasItalic) => {\n    assets.add('/fonts/' + font + '/' + font + '.woff2');\n    if (hasItalic && fontParams[font]?.hasItalic) {\n      assets.add('/fonts/' + font + '/' + font + '-italic.woff2');\n    }\n  };\n\n  if (fontBody !== 'system-ui') {\n    addFontAsset(fontBody, fontBodyItalic);\n  }\n\n  if (fontHeadings !== 'system-ui' && fontHeadings !== fontBody) {\n    addFontAsset(fontHeadings, fontHeadingsItalic);\n  } else if (fontHeadingsItalic && !fontBodyItalic && fontParams[fontHeadings]?.hasItalic) {\n    addFontAsset(fontHeadings, fontHeadingsItalic);\n  }\n\n  return Array.from(assets);\n};\n"
  },
  {
    "path": "app/default-files/default-themes/simple/index.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main>\n   <div class=\"hero {{#checkIf @config.custom.uploadHero '&&' @renderer.isFirstPage}}{{else}}hero--noimage{{/checkIf}}\">\n      {{#if @config.custom.textHero}}\n         <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n            <div class=\"wrapper\">\n               {{{@config.custom.textHero}}}\n            </div>\n         </header>\n      {{/if}}\n      {{#if @renderer.isFirstPage}}\n         {{#if @config.custom.uploadHero}}\n            <figure class=\"hero__image\">\n               <div class=\"hero__image-wrapper\">\n                  <img\n                     src=\"{{@config.custom.uploadHero}}\"\n                     {{#if @config.site.responsiveImages}}\n                        {{responsiveImageAttributes @config.custom.uploadHero}}\n                     {{/if}}\n                     {{ lazyload \"eager\" }}\n                     {{imageDimensions @config.custom.uploadHero}}\n                     alt=\"{{@config.custom.uploadHeroAlt}}\">\n                  </div>\n\n               {{#if @config.custom.uploadHeroCaption}}\n                  <figcaption>\n                     {{@config.custom.uploadHeroCaption}}\n                  </figcaption>\n               {{/if}}\n            </figure>\n         {{/if}}\n      {{/if}}\n   </div>\n   <div class=\"wrapper feed\">\n      {{#each posts}}\n         <article class=\"feed__item {{#checkIf @config.custom.alignFeed '==' \"center\" }}feed__item--centered{{/checkIf}}\">\n            {{#if @config.custom.feedFeaturedImage}}\n               {{#featuredImage}}\n                  {{#if url}}\n                     <figure class=\"feed__image\">\n                        <img\n                           src=\"{{url}}\"\n                           {{#if @config.site.responsiveImages}}\n                              {{responsiveImageAttributes 'featuredImage' srcset.feed sizes.feed}}\n                           {{/if}}\n                           {{ lazyload \"lazy\" }}\n                           height=\"{{height}}\"\n                           width=\"{{width}}\"\n                           alt=\"{{alt}}\">\n                     </figure>\n                  {{/if}}\n               {{/featuredImage}}\n            {{/if}}\n            <div class=\"feed__content\">\n               <header>\n                  {{#checkIfAny @config.custom.feedAvatar @config.custom.feedAuthor @config.custom.feedDate}}\n                     <div class=\"feed__meta\">\n                        {{#author}}\n                           {{#if @config.custom.feedAvatar}}\n                              {{#if avatar}}\n                                 {{#if @config.custom.feedAuthor}}\n                                    <img\n                                       src=\"{{avatarImage.url}}\" \n                                       {{ lazyload \"lazy\" }}\n                                       height=\"{{avatarImage.height}}\"\n                                       width=\"{{avatarImage.width}}\"      \n                                       class=\"feed__author-thumb\"\n                                       alt=\"\">\n                                 {{else}}\n                                    <a href=\"{{url}}\" class=\"feed__author-link\">\n                                       <img\n                                          src=\"{{avatarImage.url}}\" \n                                          {{ lazyload \"lazy\" }}\n                                          height=\"{{avatarImage.height}}\"\n                                          width=\"{{avatarImage.width}}\"      \n                                          class=\"feed__author-thumb\"\n                                          alt=\"{{avatarImage.alt}}\">\n                                    </a>\n                                 {{/if}}\n                              {{/if}}\n                           {{/if}}\n                           {{#if @config.custom.feedAuthor}}\n                              <a href=\"{{url}}\" class=\"feed__author\">{{name}}</a>\n                           {{/if}}\n                        {{/author}}\n\n                        {{#if @config.custom.feedDate}}\n                           {{#checkIf @config.custom.feedDateType '==' \"published\" }}\n                              <time datetime=\"{{date createdAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                 {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                    {{date createdAt @config.custom.formatDate}}\n                                 {{else}}\n                                    {{date createdAt @config.custom.formatDateCustom}}\n                                 {{/checkIf}}\n                              </time>\n                           {{/checkIf}}\n                           {{#checkIf @config.custom.feedDateType '==' \"modified\" }}\n                              <time datetime=\"{{date modifiedAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                 {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                    {{date modifiedAt @config.custom.formatDate}}\n                                 {{else}}\n                                    {{date modifiedAt @config.custom.formatDateCustom}}\n                                 {{/checkIf}}\n                              </time>\n                           {{/checkIf}}\n                        {{/if}}\n                     </div>\n                  {{/checkIfAny}}\n                  <h2 class=\"feed__title\">\n                     <a href=\"{{url}}\">\n                        {{title}}\n                     </a>\n                  </h2>\n               </header>               \n               {{#if hasCustomExcerpt}}\n                  {{{ excerpt }}}\n               {{else}}\n                  <p>{{{ excerpt }}}</p>\n               {{/if}}\n               {{#if @config.custom.feedtReadMore}}\n                  <a href=\"{{url}}\" class=\"readmore feed__readmore\">\n                     {{ translate 'post.readMore' }}</a>\n               {{/if}}\n            </div>\n         </article>\n      {{/each}}\n      {{> pagination}}\n   </div>\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/page-empty.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"page\">\n   {{#page}}\n      <div class=\"content\">\n         <div class=\"hero\">\n            <div class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n               <div class=\"wrapper\">\n                  <h1>{{title}}</h1>\n                </div>\n            </div>\n\n            {{#featuredImage}}\n               {{#if url}}\n                  <figure class=\"hero__image\">                      \n                     <img\n                        src=\"{{url}}\"\n                        {{#if @config.site.responsiveImages}}                           \n                            {{responsiveImageAttributes 'featuredImage' srcset.hero sizes.hero}}\n                        {{/if}}\n                        {{ lazyload \"eager\" }}\n                        height=\"{{height}}\"\n                        width=\"{{width}}\"\n                        alt=\"{{alt}}\">\n                      \n                     {{#checkIfAny caption credits}}\n                        <figcaption>\n                           {{caption}}\n                           {{credits}}\n                        </figcaption>                      \n                     {{/checkIfAny}}                      \n                  </figure>\n               {{/if}}\n            {{/featuredImage}}     \n         </div>\n\n         <div class=\"wrapper content__entry content__entry--nospace\">           \n            {{{text}}}            \n         </div>\n\n         {{#if @config.page.displayChildPages}}\n            {{#if subpages}}\n               <div class=\"subpages\">\n                  <div class=\"wrapper\">\n                     <h2 class=\"h6 subpages__title\">{{ translate 'page.childPages' }}</h2>\n                     <ul class=\"subpages__list\">\n                        {{> subpages-list}}\n                     </ul>\n                  </div>\n               </div>\n            {{/if}}\n         {{/if}}\n      </div>\n\n      {{#if @customHTML.afterPage}}\n         <div class=\"banner banner--after-content\">\n            <div class=\"wrapper\">\n               {{{@customHTML.afterPage}}}\n            </div>\n         </div>\n      {{/if}}\n\n   {{/page}}\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/page.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"page\">\n   {{#page}}\n      <article class=\"content\">\n         <div class=\"hero {{#checkIfNone featuredImage.url}}hero--noimage{{/checkIfNone}}\">\n            <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n               <div class=\"wrapper\">\n                  <h1>{{title}}</h1>\n                   {{#checkIfAny @config.page.displayAuthor @config.page.displayDate}}\n                     <div class=\"feed__meta content__meta {{#checkIf @config.custom.alignHero '==' \"center\" }}content__meta--centered{{/checkIf}} \">\n                        {{#if @config.page.displayAuthor}}\n                           {{#author}}\n                              {{#if avatar}}\n                                 <img \n                                    src=\"{{avatarImage.url}}\" \n                                    {{ lazyload \"eager\" }}\n                                    height=\"{{avatarImage.height}}\"\n                                    width=\"{{avatarImage.width}}\"\n                                    class=\"feed__author-thumb\" \n                                    alt=\"\">\n                              {{/if}}\n                              <a href=\"{{url}}\" class=\"feed__author\">{{name}}</a>\n                           {{/author}}\n                        {{/if}}\n\n                        {{#if @config.page.displayDate}}\n                           <time datetime=\"{{date createdAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                              {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                 {{date createdAt @config.custom.formatDate}}\n                              {{else}}\n                                 {{date createdAt @config.custom.formatDateCustom}}\n                              {{/checkIf}}\n                           </time>\n                        {{/if}}\n                     </div>\n                  {{/checkIfAny}}\n                </div>\n            </header>\n\n            {{#featuredImage}}\n               {{#if url}}\n                  <figure class=\"hero__image\"> \n                     <div class=\"hero__image-wrapper\">                     \n                        <img\n                           src=\"{{url}}\"\n                           {{#if @config.site.responsiveImages}}                           \n                              {{responsiveImageAttributes 'featuredImage' srcset.hero sizes.hero}}\n                           {{/if}}\n                           {{ lazyload \"eager\" }}\n                           height=\"{{height}}\"\n                           width=\"{{width}}\"\n                           alt=\"{{alt}}\">\n                        </div>\n                      \n                     {{#checkIfAny caption credits}}\n                        <figcaption>\n                           {{caption}}\n                           {{credits}}\n                        </figcaption>                      \n                     {{/checkIfAny}}                      \n                  </figure>\n               {{/if}}\n            {{/featuredImage}}     \n         </div>\n\n         <div class=\"entry-wrapper content__entry\">           \n            {{{text}}}            \n         </div>\n\n         {{#checkIfAny @config.page.displayLastUpdatedDate @config.page.displayShareButtons @config.page.displayAuthorBio}}\n            <footer class=\"content__footer\">\n               <div class=\"entry-wrapper\">\n\n                  {{#checkIfAny @config.page.displayLastUpdatedDate @config.page.displayShareButtons}}\n                     <div class=\"content__actions\">\n                        {{#if @config.page.displayLastUpdatedDate}}\n                           {{#if modifiedAt}}\n                              <p class=\"content__updated\">\n                                 {{ translate 'post.lastUpdatedDate' }}\n                                 {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                    {{date modifiedAt @config.custom.formatDate}}\n                                 {{else}}\n                                    {{date modifiedAt @config.custom.formatDateCustom}}\n                                 {{/checkIf}}\n                              </p>\n                           {{/if}}\n                        {{/if}}\n\n                        {{#if @config.page.displayShareButtons}}\n                           <div class=\"content__share\">\n                              <button class=\"btn--icon content__share-button js-content__share-button\">\n                                 <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                                       <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#share\"></use>\n                                 </svg> \n                                 <span>{{ translate 'post.shareIt' }}</span>\n                              </button>\n                              {{#checkIfAll @plugins.socialSharing @plugins.socialSharing.state}}\n                                 <div class=\"content__share-popup js-content__share-popup\">\n                                    {{{@customSocialSharing}}}\n                                 </div>\n                              {{else}}\n                                 <div class=\"content__share-popup js-content__share-popup\">\n                                    {{> share-buttons}}\n                                 </div>\n                              {{/checkIfAll}}   \n                           </div>       \n                        {{/if}}\n                     </div>\n                  {{/checkIfAny}}\n\n                  {{#if @config.page.displayAuthorBio}}\n                     <div class=\"content__bio bio\">\n                        {{#author}}\n                           {{#if avatar}}                          \n                              <img \n                                 src=\"{{avatarImage.url}}\" \n                                 {{ lazyload \"lazy\" }}\n                                 height=\"{{avatarImage.height}}\"\n                                 width=\"{{avatarImage.width}}\" \n                                 class=\"bio__avatar\" \n                                 alt=\"\">                          \n                           {{/if}}\n                           <div>\n                              <h3 class=\"h4 bio__name\">\n                                 <a href=\"{{url}}\" rel=\"author\">{{name}}</a>\n                              </h3>\n                              {{#if description}}\n                                 <div class=\"bio__desc\">\n                                    {{{description}}}\n                                 </div>\n                              {{/if}}\n                           </div>\n                        {{/author}}\n                     </div>\n                  {{/if}}\n               </div>\n            </footer>\n         {{/checkIfAny}}\n      </article>\n\n      {{#if @config.page.displayChildPages}}\n         {{#if subpages}}\n            <div class=\"subpages\">\n               <div class=\"entry-wrapper\">\n                  <h2 class=\"h6 subpages__title\">{{ translate 'page.childPages' }}</h2>\n                  <ul class=\"subpages__list\">\n                     {{> subpages-list}}\n                  </ul>\n               </div>\n            </div>\n         {{/if}}\n      {{/if}}\n\n       {{#if @config.page.displayComments}}\n         <div class=\"content__comments\">\n            <div class=\"entry-wrapper\">\n               {{{@commentsCustomCode}}}\n            </div>\n         </div>\n      {{/if}}\n\n      {{#if @customHTML.afterPage}}\n         <div class=\"banner banner--after-content\">\n            <div class=\"wrapper\">\n               {{{@customHTML.afterPage}}}\n            </div>\n         </div>\n      {{/if}}\n\n   {{/page}}\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/fonts.hbs",
    "content": "{{#checkIf @config.custom.fontBody '!==' \"system-ui\"}}\n  <link rel=\"preload\" href=\"{{@website.assetsUrl}}/dynamic/fonts/{{@config.custom.fontBody}}/{{@config.custom.fontBody}}.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\n  {{#if @config.custom.fontBodyItalic}}\n    <link rel=\"preload\" href=\"{{@website.assetsUrl}}/dynamic/fonts/{{@config.custom.fontBody}}/{{@config.custom.fontBody}}-italic.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\n  {{/if}}\n{{/checkIf}}\n\n{{#checkIf @config.custom.fontHeadings '!==' \"system-ui\"}}\n  {{#checkIf @config.custom.fontHeadings '!==' @config.custom.fontBody}}\n    <link rel=\"preload\" href=\"{{@website.assetsUrl}}/dynamic/fonts/{{@config.custom.fontHeadings}}/{{@config.custom.fontHeadings}}.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\n    {{#if @config.custom.fontHeadingsItalic}}\n      <link rel=\"preload\" href=\"{{@website.assetsUrl}}/dynamic/fonts/{{@config.custom.fontHeadings}}/{{@config.custom.fontHeadings}}-italic.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\n    {{/if}}\n  {{else}}\n    {{#unless @config.custom.fontBodyItalic}}\n      {{#if @config.custom.fontHeadingsItalic}}\n        <link rel=\"preload\" href=\"{{@website.assetsUrl}}/dynamic/fonts/{{@config.custom.fontHeadings}}/{{@config.custom.fontHeadings}}-italic.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>\n      {{/if}}\n    {{/unless}}\n  {{/checkIf}}\n{{/checkIf}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/footer.hbs",
    "content": "<footer class=\"footer {{#if @config.post.displayRelatedPosts}}{{#checkIf @config.custom.relatedPostsNumber '>' \"0\"}}\nfooter--glued{{/checkIf}}{{/if}}\">\n    <div class=\"wrapper\">\n         {{#if menus.footerMenu}}\n            <nav class=\"footer__nav\">\n                {{> simple-menu menus.footerMenu}}\n            </nav>\n        {{/if}}\n        \n        {{#if @config.custom.copyrightText}}\n            <div class=\"footer__copyright\">\n                {{{@config.custom.copyrightText}}}\n            </div>\n        {{/if}}\n\n        {{#if @config.custom.socialButtons}}\n            <div class=\"footer__social\">\n                {{#if @config.custom.socialFacebook}}\n                    <a href=\"{{@config.custom.socialFacebook}}\" aria-label=\"{{ translate 'partials.footer.followFacebook' }}\">\n                        <svg>\n                            <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#facebook\"/>\n                        </svg>\n                    </a>\n                {{/if}}\n                {{#if @config.custom.socialTwitter}}\n                    <a href=\"{{@config.custom.socialTwitter}}\" aria-label=\"{{ translate 'partials.footer.followX' }}\">\n                        <svg>\n                            <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#twitter\"/>\n                        </svg>\n                    </a>\n                {{/if}}\n                {{#if @config.custom.socialInstagram}}\n                    <a href=\"{{@config.custom.socialInstagram}}\" aria-label=\"{{ translate 'partials.footer.followInstagram' }}\">\n                        <svg>\n                            <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#instagram\"/>\n                        </svg>\n                    </a>\n                {{/if}}\n                {{#if @config.custom.socialLinkedin}}\n                    <a href=\"{{@config.custom.socialLinkedin}}\" aria-label=\"{{ translate 'partials.footer.followLinkedIn' }}\">\n                        <svg>\n                            <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#linkedin\"/>\n                        </svg>\n                    </a>\n                {{/if}}\n                {{#if @config.custom.socialVimeo}}\n                    <a href=\"{{@config.custom.socialVimeo}}\" aria-label=\"{{ translate 'partials.footer.followVimeo' }}\">\n                        <svg>\n                            <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#vimeo\"/>\n                        </svg>\n                    </a>\n                {{/if}}\n                {{#if @config.custom.socialPinterest}}\n                    <a href=\"{{@config.custom.socialPinterest}}\" aria-label=\"{{ translate 'partials.footer.followPinterest' }}\">\n                        <svg>\n                            <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#pinterest\"/>\n                        </svg>\n                    </a>\n                {{/if}}\n                {{#if @config.custom.socialYoutube}}\n                    <a href=\"{{@config.custom.socialYoutube}}\" aria-label=\"{{ translate 'partials.footer.followYoutube' }}\">\n                        <svg>\n                            <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#youtube\"/>\n                        </svg>\n                    </a>\n                {{/if}}\n            </div>\n        {{/if}}\n        \n        {{#if @config.custom.backToTopButton}}\n            <button id=\"backToTop\" class=\"footer__bttop\" aria-label=\"{{ translate 'partials.footer.backToTop' }}\" title=\"{{ translate 'partials.footer.backToTop' }}\">\n                <svg width=\"20\" height=\"20\">\n                    <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#toparrow\"/>\n                </svg>\n            </button>\n        {{/if}}\n    </div>\n</footer>\n\n<script defer src=\"{{js \"scripts.min.js\"}}\"></script>\n\n<script>window.publiiThemeMenuConfig={mobileMenuMode:'{{@config.custom.mobilemenu}}',animationSpeed:300,submenuWidth: {{#checkIf @config.custom.submenu '==' \"auto\" }}'auto'{{else}}{{@config.custom.submenuWidth}}{{/checkIf}},doubleClickTime:500,mobileMenuExpandableSubmenus:{{@config.custom.mobilemenuExpandableSubmenus}},relatedContainerForOverlayMenuSelector:'.top'};</script>\n\n{{#if @config.site.mediaLazyLoad}}\n    <script>        \n        var images = document.querySelectorAll('img[loading]');\n        for (var i = 0; i < images.length; i++) {\n            if (images[i].complete) {\n                images[i].classList.add('is-loaded');\n            } else {\n                images[i].addEventListener('load', function () {\n                    this.classList.add('is-loaded');\n                }, false);\n            }\n        }\n    </script>\n{{/if}}\n\n{{#if @renderer.previewMode}}\n<script defer src=\"{{js \"svg-map.js\"}}\"></script>\n<script defer src=\"{{js \"svg-fix.js\"}}\"></script>\n{{/if}}\n{{{@footerCustomCode}}}\n{{{ publiiFooter }}}\n</body>\n</html>\n"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/head.hbs",
    "content": "<!DOCTYPE html>\n<html lang=\"{{ @website.language }}\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n        {{#if title}}\n            <title>{{title}}</title>\n        {{else}}\n            <title>{{@website.name}}</title>\n        {{/if}}\n        {{metaDescription}}\n        {{metaRobots}} \n        {{{publiiHead}}}\n        {{canonicalLink}}\n        {{feedLink}}\n        {{socialMetaTags}}\n\t\t{{#if @config.custom.faviconUpload}}\n\t        <link rel=\"shortcut icon\" href=\"{{@config.custom.faviconUpload}}\" type=\"{{@config.custom.faviconExtension}}\" />\n        {{/if}}\n        {{#if @pagination}}\n            {{#if @pagination.previousPage}}\n                <link rel=\"prev\" href=\"{{@pagination.previousPageUrl }}\">\n            {{/if}}\n            {{#if @pagination.nextPage}}\n                <link rel=\"next\" href=\"{{@pagination.nextPageUrl }}\">\n            {{/if}}\n        {{/if}}\n        {{> fonts}}\n        <link rel=\"stylesheet\" href=\"{{css \"style.css\" }}\">\n        {{jsonLD}}\n        <noscript>\n            <style>\n                img[loading] {\n                    opacity: 1;\n                }\n            </style>\n        </noscript>\t\n        {{{@headCustomCode}}}\n    </head>\n    <body class=\"{{#is \"index\"}}home-template{{/is}}{{#is \"index-pagination\"}} pagination-template{{/is}}{{#is \"blogindex\"}}blogindex-template{{/is}}{{#is \"tag\"}}tag-template{{/is}}{{#is \"tag-pagination\"}} pagination-template{{/is}}{{#is \"tags\"}}tags-template{{/is}}{{#is \"page\"}}page-template{{/is}}{{#is \"post\"}}post-template{{/is}}{{#is \"author\"}}author-template{{/is}}{{#is \"author-pagination\"}} pagination-template{{/is}}{{#is \"404\"}}error-template{{/is}}{{#is \"search\"}}search-template{{/is}}\">\n        {{{@bodyCustomCode}}}"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/menu.hbs",
    "content": "{{#unless level}}\n   <nav class=\"navbar js-navbar\">\n      <button\n         class=\"navbar__toggle js-toggle\"\n         aria-label=\"{{ translate 'partials.menu.label' }}\"\n         aria-haspopup=\"true\"\n         aria-expanded=\"false\">\n         <span class=\"navbar__toggle-box\">\n            <span class=\"navbar__toggle-inner\">{{ translate 'partials.menu.label' }}</span>\n         </span>\n      </button>\n   {{/unless}}\n    <ul{{#if level}} class=\"navbar__submenu level-{{level}}\" aria-hidden=\"true\" {{else}} class=\"navbar__menu\"{{/if}}>\n        {{#each items}}\n             <li{{menuItemClasses}}>\n            {{#if link}}\n               <a\n                  href=\"{{menuUrl}}\"\n                  {{#if title}}\n                     title=\"{{title}}\"\n                  {{/if}}\n                  {{#if target}}\n                     target=\"{{target}}\"\n                  {{/if}}\n                  {{#if rel}}\n                     rel=\"{{rel}}\"\n                  {{/if}}\n                  {{#if items}} \n                     aria-haspopup=\"true\"\n                  {{/if}}>\n                  {{label}}\n              </a>\n            {{else}}\n               <span \n                  class=\"is-separator\"\n                  {{#if title}}\n                     title=\"{{title}}\"\n                  {{/if}} \n                  {{#if items}} \n                     aria-haspopup=\"true\"\n                  {{/if}}>\n                  {{label}}\n               </span>\n            {{/if}}\n\n            {{#if items}}\n               {{> menu}}\n            {{/if}}\n         </li>\n        {{/each}}\n    </ul>\n   {{#unless level}}\n   </nav>\n{{/unless}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/navbar.hbs",
    "content": "<header class=\"top js-header\">\n   <a class=\"logo\" href=\"{{@website.url}}\">\n      {{#if @website.logo}}\n         <img src=\"{{@website.logo}}\" alt=\"{{@website.name}}\" width=\"{{@website.logoSize.width}}\" height=\"{{@website.logoSize.height}}\">\n      {{else}}\n         {{@website.name}}\n      {{/if}}\n   </a>\n   {{#if menus.mainMenu}}\n      {{> menu menus.mainMenu}}\n   {{/if}}\n   {{#if @config.custom.searchFeature}}\n      <div class=\"search\">\n         <div class=\"search__overlay js-search-overlay\">\n            <div class=\"wrapper search__overlay-inner\">\n               {{{@customSearchInput}}} \n            </div>\n         </div>\n         <button class=\"search__btn btn--icon js-search-btn\" aria-label=\"{{ translate 'search.search' }}\">\n            <svg height=\"18\" width=\"18\" role=\"presentation\" focusable=\"false\">\n               <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#search\"/>\n            </svg>\n         </button>\n      </div>\n   {{/if}}\n</header>\n"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/pagination.hbs",
    "content": "{{#if @pagination}}\n   <nav class=\"{{#is \"index,tag,author\"}}wrapper{{/is}} pagination {{@website.postsOrdering}}\">\n      {{#checkIf @website.postsOrdering '==' \"desc\"}}\n         {{#if @pagination.nextPage}}\n            {{#checkIf @website.postsOrdering '==' \"desc\"}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.nextPageUrl}}\" class=\"btn btn--icon\">\n                     <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-prev\"/>\n                     </svg> \n                     <span>{{ translate 'partials.pagination.prev' }}</span>\n                  </a>\n               </div>\n            {{else}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.nextPageUrl}}\" class=\"btn btn--icon\">\n                     <span>{{ translate 'partials.pagination.next' }}</span> \n                     <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-next\"/>\n                     </svg> \n                  </a>  \n               </div>\n            {{/checkIf}}\n         {{/if}}\n         {{#if @pagination.previousPage}}\n            {{#checkIf @website.postsOrdering '==' \"desc\"}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.previousPageUrl}}\" class=\"btn btn--icon\">\n                     <span>{{ translate 'partials.pagination.next' }}</span> \n                     <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-next\"/>\n                     </svg>\n                  </a> \n               </div>\n            {{else}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.previousPageUrl}}\" class=\"btn btn--icon\">\n                        <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-prev\"/>\n                     </svg>\n                     <span>{{ translate 'partials.pagination.prev' }}</span>\n                  </a>\n               </div>\n            {{/checkIf}}\n         {{/if}}\n      {{else}}\n         {{#if @pagination.previousPage}}\n            {{#checkIf @website.postsOrdering '==' \"desc\"}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.previousPageUrl}}\" class=\"btn btn--icon\">\n                     <span>{{ translate 'partials.pagination.next' }}</span> \n                     <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-next\"/>\n                     </svg>\n                  </a> \n               </div>\n            {{else}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.previousPageUrl}}\" class=\"btn btn--icon\">\n                     <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-prev\"/>\n                     </svg> \n                     <span>{{ translate 'partials.pagination.prev' }}</span>\n                  </a>\n               </div>\n            {{/checkIf}}\n         {{/if}}\n         {{#if @pagination.nextPage}}\n            {{#checkIf @website.postsOrdering '==' \"desc\"}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.nextPageUrl}}\" class=\"btn btn--icon\">\n                     <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-prev\"/>\n                     </svg>  \n                     <span>{{ translate 'partials.pagination.prev' }}</span>\n                  </a>\n               </div>\n            {{else}}\n               <div class=\"pagination__item\">\n                  <a href=\"{{@pagination.nextPageUrl}}\" class=\"btn btn--icon\">\n                     <span>{{ translate 'partials.pagination.next' }}</span> \n                     <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                        <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#arrow-next\"/>\n                     </svg>\n                  </a> \n               </div>\n            {{/checkIf}}\n         {{/if}}\n      {{/checkIf}}\n   </nav>\n{{/if}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/share-buttons.hbs",
    "content": "{{#if @config.custom.shareFacebook}}\n   <a\n      href=\"https://www.facebook.com/sharer/sharer.php?u={{encodeUrlFragment url}}\"\n      class=\"js-share facebook\"\n      rel=\"nofollow noopener noreferrer\">\n      <svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\">\n         <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#facebook\"/>\n      </svg>\n      <span>{{ translate 'partials.shareButtons.shareWithFacebook' }}</span>\n   </a>\n{{/if}}\n\n{{#if @config.custom.shareTwitter}}\n   <a\n      href=\"https://twitter.com/intent/tweet?url={{encodeUrlFragment url}}&amp;via={{#if @config.custom.shareTwitterName}}{{encodeUrlFragment @config.custom.shareTwitterName}}{{else}}{{encodeUrlFragment @website.name}}{{/if}}&amp;text={{encodeUrlFragment title}}\"\n      class=\"js-share twitter\"\n      rel=\"nofollow noopener noreferrer\">\n      <svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\">\n         <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#twitter\"/>\n      </svg>\n      <span>{{ translate 'partials.shareButtons.shareWithTwitter' }}</span>\n   </a>\n{{/if}}\n\n{{#if @config.custom.sharePinterest}}\n   <a\n      href=\"https://pinterest.com/pin/create/button/?url={{encodeUrlFragment url}}&amp;media={{encodeUrlFragment featuredImage.url}}&amp;description={{encodeUrlFragment title}}\"\n      class=\"js-share pinterest\"\n      rel=\"nofollow noopener noreferrer\">\n      <svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\">\n         <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#pinterest\"/>\n      </svg>\n      <span>{{ translate 'partials.shareButtons.shareWithPinterest' }}</span>\n   </a>\n{{/if}}\n\n{{#if @config.custom.shareMix}}\n   <a\n      href=\"https://mix.com/add?url={{encodeUrlFragment url}}\"\n      class=\"js-share mix\"\n      rel=\"nofollow noopener noreferrer\">\n      <svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\">\n         <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#mix\"/>\n      </svg>\n      <span>{{ translate 'partials.shareButtons.shareWithMix' }}</span>\n   </a>\n{{/if}}\n\n{{#if @config.custom.shareLinkedin}}\n   <a\n      href=\"https://www.linkedin.com/sharing/share-offsite/?url={{encodeUrlFragment url}}\"\n      class=\"js-share linkedin\"\n      rel=\"nofollow noopener noreferrer\">\n      <svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\">\n         <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#linkedin\"/>\n      </svg>\n      <span>{{ translate 'partials.shareButtons.shareWithLinkedIn' }}</span>\n   </a>\n{{/if}}\n\n{{#if @config.custom.shareBuffer}}\n   <a\n      href=\"https://buffer.com/add?text={{encodeUrlFragment title}}&amp;url={{encodeUrlFragment url}}\"\n      class=\"js-share buffer\"\n      rel=\"nofollow noopener noreferrer\">\n      <svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\">\n         <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#buffer\"/>\n      </svg>\n      <span>{{ translate 'partials.shareButtons.shareWithBuffer' }}</span>\n   </a>\n{{/if}}\n\n{{#if @config.custom.shareWhatsApp}}\n   <a\n      href=\"https://api.whatsapp.com/send?text={{encodeUrlFragment title}} {{encodeUrlFragment url}}\" \n      class=\"js-share whatsapp\"\n      rel=\"nofollow noopener noreferrer\">\n      <svg class=\"icon\" aria-hidden=\"true\" focusable=\"false\">\n         <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#whatsapp\"/>\n      </svg>\n      <span>{{ translate 'partials.shareButtons.shareWithWhatsApp' }}</span>\n   </a>\n{{/if}}"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/simple-menu.hbs",
    "content": "<ul>\n   {{#each items}}\n      <li{{menuItemClasses}}>\n         {{#if link}}\n            <a href=\"{{menuUrl}}\" class=\"al\" {{#if title}} title=\"{{title}}\" {{/if}}{{#if target}} target=\"{{target}}\"{{/if}}{{#if rel}} rel=\"{{rel}}\"{{/if}}>\n               {{label}}\n            </a>\n            {{else}}\n            <span{{#if title}} title=\"{{title}}\"{{/if}}>\n               {{label}}\n            </span>\n         {{/if}}\n         {{#if items}}\n            <ul>\n               {{> simple-menu}}\n            </ul>\n         {{/if}}\n      </li>\n   {{/each}}\n</ul>"
  },
  {
    "path": "app/default-files/default-themes/simple/partials/subpages-list.hbs",
    "content": "{{#each subpages}}\n   {{#getPage @this}}\n      <li>\n         <a href=\"{{url}}\">{{title}}</a>\n         {{#if subpages}}\n            <ul>\n               {{> subpages-list}}\n            </ul>\n         {{/if}}\n      </li>\n   {{/getPage}}\n{{/each}}\n\n"
  },
  {
    "path": "app/default-files/default-themes/simple/post.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"post\">\n   {{#post}}\n      <article class=\"content\">\n         <div class=\"hero {{#checkIfNone featuredImage.url}}hero--noimage{{/checkIfNone}}\">\n            <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n               <div class=\"wrapper\">\n                  <h1>{{title}}</h1>\n                   {{#checkIfAny @config.post.displayAuthor @config.post.displayDate}}\n                     <div class=\"feed__meta content__meta {{#checkIf @config.custom.alignHero '==' \"center\" }}content__meta--centered{{/checkIf}} \">\n                        {{#if @config.post.displayAuthor}}\n                           {{#author}}\n                              {{#if avatar}}\n                                 <img \n                                    src=\"{{avatarImage.url}}\" \n                                    {{ lazyload \"eager\" }}\n                                    height=\"{{avatarImage.height}}\"\n                                    width=\"{{avatarImage.width}}\"\n                                    class=\"feed__author-thumb\" \n                                    alt=\"\">\n                              {{/if}}\n                              <a href=\"{{url}}\" class=\"feed__author\">{{name}}</a>\n                           {{/author}}\n                        {{/if}}\n\n                        {{#if @config.post.displayDate}}\n                           <time datetime=\"{{date createdAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                              {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                 {{date createdAt @config.custom.formatDate}}\n                              {{else}}\n                                 {{date createdAt @config.custom.formatDateCustom}}\n                              {{/checkIf}}\n                           </time>\n                        {{/if}}\n                     </div>\n                  {{/checkIfAny}}\n                </div>\n            </header>\n\n            {{#featuredImage}}\n               {{#if url}}\n                  <figure class=\"hero__image\">  \n                     <div class=\"hero__image-wrapper\">                    \n                        <img\n                           src=\"{{url}}\"\n                           {{#if @config.site.responsiveImages}}                           \n                              {{responsiveImageAttributes 'featuredImage' srcset.hero sizes.hero}}\n                           {{/if}}\n                           {{ lazyload \"eager\" }}\n                           height=\"{{height}}\"\n                           width=\"{{width}}\"\n                           alt=\"{{alt}}\">\n                        </div>\n                      \n                     {{#checkIfAny caption credits}}\n                        <figcaption>\n                           {{caption}}\n                           {{credits}}\n                        </figcaption>                      \n                     {{/checkIfAny}}                      \n                  </figure>\n               {{/if}}\n            {{/featuredImage}}     \n         </div>\n\n         <div class=\"entry-wrapper content__entry\">           \n            {{{text}}}            \n         </div>\n\n         {{#checkIfAny @config.post.displayLastUpdatedDate @config.post.displayTags @config.post.displayShareButtons @config.post.displayAuthorBio @config.post.displayPostNavigation}}\n            <footer class=\"content__footer\">\n               <div class=\"entry-wrapper\">\n                  {{#if @config.post.displayLastUpdatedDate}}\n                     {{#if modifiedAt}}\n                        <p class=\"content__updated\">\n                           {{ translate 'post.lastUpdatedDate' }}\n                           {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                              {{date modifiedAt @config.custom.formatDate}}\n                           {{else}}\n                              {{date modifiedAt @config.custom.formatDateCustom}}\n                           {{/checkIf}}\n                        </p>\n                     {{/if}}\n                  {{/if}}\n\n                  {{#checkIfAny @config.post.displayTags @config.post.displayShareButtons}}\n                     <div class=\"content__actions\">\n                        {{#if @config.post.displayTags}}\n                           {{#if tags}}\n                              <ul class=\"content__tag\">\n                                 {{#each tags}}\n                                    <li>\n                                       <a href=\"{{url}}\">{{name}}</a>\n                                    </li>\n                                 {{/each}}\n                              </ul>\n                           {{/if}}\n                        {{/if}}\n\n                        {{#if @config.post.displayShareButtons}}\n                           <div class=\"content__share\">\n                              <button class=\"btn--icon content__share-button js-content__share-button\">\n                                 <svg width=\"20\" height=\"20\" aria-hidden=\"true\">\n                                       <use xlink:href=\"{{@website.assetsUrl}}/svg/svg-map.svg#share\"></use>\n                                 </svg> \n                                 <span>{{ translate 'post.shareIt' }}</span>\n                              </button>\n                              {{#checkIfAll @plugins.socialSharing @plugins.socialSharing.state}}\n                                 <div class=\"content__share-popup js-content__share-popup\">\n                                    {{{@customSocialSharing}}}\n                                 </div>\n                              {{else}}\n                                 <div class=\"content__share-popup js-content__share-popup\">\n                                    {{> share-buttons}}\n                                 </div>\n                              {{/checkIfAll}}   \n                           </div>       \n                        {{/if}}\n                     </div>\n                  {{/checkIfAny}}\n\n                  {{#if @config.post.displayAuthorBio}}\n                     <div class=\"content__bio bio\">\n                        {{#author}}\n                           {{#if avatar}}                          \n                              <img \n                                 src=\"{{avatarImage.url}}\" \n                                 {{ lazyload \"lazy\" }}\n                                 height=\"{{avatarImage.height}}\"\n                                 width=\"{{avatarImage.width}}\" \n                                 class=\"bio__avatar\" \n                                 alt=\"\">                          \n                           {{/if}}\n                           <div>\n                              <h3 class=\"h4 bio__name\">\n                                 <a href=\"{{url}}\" rel=\"author\">{{name}}</a>\n                              </h3>\n                              {{#if description}}\n                                 <div class=\"bio__desc\">\n                                    {{{description}}}\n                                 </div>\n                              {{/if}}\n                           </div>\n                        {{/author}}\n                     </div>\n                  {{/if}}\n               </div>\n\n               {{#if @config.post.displayPostNavigation}}\n                  {{#checkIfAny ../previousPost ../nextPost}}\n                     <nav class=\"content__nav\">\n                        <div class=\"wrapper\">\n                           <div class=\"content__nav-inner\">\n                              {{#../previousPost}}\n                                 <div class=\"content__nav-prev\">\n                                    <a href=\"{{url}}\" class=\"content__nav-link\" rel=\"prev\">\n                                       {{#featuredImage}}\n                                          {{#if url}} \n                                             <figure class=\"content__nav-image\">\n                                                <img \n                                                   {{#if @config.site.responsiveImages}}\n                                                      src=\"{{urlXs}}\"\n                                                   {{else}}\n                                                      src=\"{{url}}\"\n                                                   {{/if}}\n                                                   class=\"lazyload\"\n                                                   {{ lazyload \"lazy\" }} \n                                                   alt=\"{{alt}}\" \n                                                   height=\"{{height}}\" \n                                                   width=\"{{height}}\">\n                                              </figure>                    \n                                          {{/if}}\n                                       {{/featuredImage}}\n                                       <div>\n                                          <span>{{ translate 'post.previousPost' }}</span>\n                                          {{title}}\n                                       </div>\n                                    </a>\n                                 </div>\n                              {{/../previousPost}}\n                              {{#../nextPost}}\n                                 <div class=\"content__nav-next\">\n                                    <a href=\"{{url}}\" class=\"content__nav-link\" rel=\"next\">\n                                       <div>\n                                          <span>{{ translate 'post.nextPost' }}</span>\n                                          {{title}}\n                                       </div>\n                                       {{#featuredImage}}\n                                          {{#if url}} \n                                             <figure class=\"content__nav-image\">\n                                                <img \n                                                   {{#if @config.site.responsiveImages}}\n                                                      src=\"{{urlXs}}\"\n                                                   {{else}}\n                                                      src=\"{{url}}\"\n                                                   {{/if}}\n                                                   class=\"lazyload\"\n                                                   {{ lazyload \"lazy\" }} \n                                                   alt=\"{{alt}}\" \n                                                   height=\"{{height}}\" \n                                                   width=\"{{height}}\">\n                                              </figure>                    \n                                          {{/if}}\n                                       {{/featuredImage}}\n                                    </a>\n                                 </div>\n                              {{/../nextPost}}\n                           </div>\n                        </div>\n                     </nav>\n                  {{/checkIfAny}}\n               {{/if}}\n            </footer>\n         {{/checkIfAny}}\n      </article>\n\n       {{#if @config.post.displayComments}}\n         <div class=\"content__comments\">\n            <div class=\"entry-wrapper\">\n               {{{@commentsCustomCode}}}\n            </div>\n         </div>\n      {{/if}}\n\n      {{#if @config.post.displayRelatedPosts}}\n         {{#if ../relatedPosts}}\n            <div class=\"content__related related\">\n               <div class=\"wrapper\">\n                  <h2 class=\"h4 related__title\">\n                     {{ translate 'post.relatedPosts' }}\n                  </h2>\n                  {{#each ../relatedPosts}}\n                     <article class=\"feed__item {{#checkIf @config.custom.alignFeed '==' \"center\" }}feed__item--centered{{/checkIf}}\">\n                        {{#if @config.custom.feedFeaturedImage}}\n                           {{#featuredImage}}\n                              {{#if url}}\n                                 <figure class=\"feed__image related__image\">\n                                    <img\n                                       src=\"{{url}}\"\n                                       {{#if @config.site.responsiveImages}}\n                                          {{responsiveImageAttributes 'featuredImage' srcset.feed sizes.feed}}\n                                       {{/if}}\n                                       {{ lazyload \"lazy\" }}\n                                       height=\"{{height}}\"\n                                       width=\"{{width}}\"\n                                       alt=\"{{alt}}\">\n                                 </figure>\n                              {{/if}}\n                           {{/featuredImage}}\n                        {{/if}}\n                        <div class=\"feed__content\">\n                           <header>\n                              {{#checkIfAny @config.custom.feedAvatar @config.custom.feedAuthor @config.custom.feedDate}}\n                                 <div class=\"feed__meta\">\n                                    {{#author}}\n                                       {{#if @config.custom.feedAvatar}}\n                                          {{#if avatar}}\n                                             {{#if @config.custom.feedAuthor}}\n                                                <img\n                                                   src=\"{{avatarImage.url}}\" \n                                                   {{ lazyload \"lazy\" }}\n                                                   height=\"{{avatarImage.height}}\"\n                                                   width=\"{{avatarImage.width}}\"      \n                                                   class=\"feed__author-thumb\"\n                                                   alt=\"\">\n                                             {{else}}\n                                                <a href=\"{{url}}\" class=\"feed__author-link\">\n                                                   <img\n                                                      src=\"{{avatarImage.url}}\" \n                                                      {{ lazyload \"lazy\" }}\n                                                      height=\"{{avatarImage.height}}\"\n                                                      width=\"{{avatarImage.width}}\"      \n                                                      class=\"feed__author-thumb\"\n                                                      alt=\"{{avatarImage.alt}}\">\n                                                </a>\n                                             {{/if}}\n                                          {{/if}}\n                                       {{/if}}\n                                       {{#if @config.custom.feedAuthor}}\n                                          <a href=\"{{url}}\" class=\"feed__author\">{{name}}</a>\n                                       {{/if}}\n                                    {{/author}}\n                                    {{#if @config.custom.feedDate}}\n                                       {{#checkIf @config.custom.feedDateType '==' \"published\" }}\n                                          <time datetime=\"{{date createdAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                             {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                                {{date createdAt @config.custom.formatDate}}\n                                             {{else}}\n                                                {{date createdAt @config.custom.formatDateCustom}}\n                                             {{/checkIf}}\n                                          </time>\n                                       {{/checkIf}}\n                                       {{#checkIf @config.custom.feedDateType '==' \"modified\" }}\n                                          <time datetime=\"{{date modifiedAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                             {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                                {{date modifiedAt @config.custom.formatDate}}\n                                             {{else}}\n                                                {{date modifiedAt @config.custom.formatDateCustom}}\n                                             {{/checkIf}}\n                                          </time>\n                                       {{/checkIf}}\n                                    {{/if}}\n                                 </div>\n                              {{/checkIfAny}}\n                              <h3 class=\"feed__title\">\n                                 <a href=\"{{url}}\">\n                                    {{title}}\n                                 </a>\n                              </h3>\n                           </header>               \n                           {{#if hasCustomExcerpt}}\n                              {{{ excerpt }}}\n                           {{else}}\n                              <p>{{{ excerpt }}}</p>\n                           {{/if}}\n                           {{#if @config.custom.feedtReadMore}}\n                              <a href=\"{{url}}\" class=\"readmore feed__readmore\">\n                                 {{ translate 'post.readMore' }}</a>\n                           {{/if}}\n                        </div>\n                     </article>\n                  {{/each}}\n               </div>\n            </div>\n         {{/if}}\n      {{/if}}\n\n      {{#if @customHTML.afterPost}}\n         <div class=\"banner banner--after-content\">\n            <div class=\"wrapper\">\n               {{{@customHTML.afterPost}}}\n            </div>\n         </div>\n      {{/if}}\n\n   {{/post}}\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/posts.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"posts\">\n   <div class=\"hero hero--noimage\">\n      <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n         <div class=\"wrapper\">\n            <h1>\n               {{ translate 'postPrefix.title' }} \n            </h1>\n            <p class=\"page__desc\">\n               {{ translate \"postPrefix.description\" }}\n            </p>\n         </div>\n      </header>\n   </div>\n   <div class=\"wrapper feed\">\n      {{#each posts}}\n         <article class=\"feed__item {{#checkIf @config.custom.alignFeed '==' \"center\" }}feed__item--centered{{/checkIf}}\">\n            {{#if @config.custom.feedFeaturedImage}}\n               {{#featuredImage}}\n                  {{#if url}}\n                     <figure class=\"feed__image\">\n                        <img\n                           src=\"{{url}}\"\n                           {{#if @config.site.responsiveImages}}\n                              {{responsiveImageAttributes 'featuredImage' srcset.feed sizes.feed}}\n                           {{/if}}\n                           {{ lazyload \"lazy\" }}\n                           height=\"{{height}}\"\n                           width=\"{{width}}\"\n                           alt=\"{{alt}}\">\n                     </figure>\n                  {{/if}}\n               {{/featuredImage}}\n            {{/if}}\n            <div class=\"feed__content\">\n               <header>\n                  {{#checkIfAny @config.custom.feedAvatar @config.custom.feedAuthor @config.custom.feedDate}}\n                     <div class=\"feed__meta\">\n                        {{#author}}\n                           {{#if @config.custom.feedAvatar}}\n                              {{#if avatar}}\n                                 {{#if @config.custom.feedAuthor}}\n                                    <img\n                                       src=\"{{avatarImage.url}}\" \n                                       {{ lazyload \"lazy\" }}\n                                       height=\"{{avatarImage.height}}\"\n                                       width=\"{{avatarImage.width}}\"      \n                                       class=\"feed__author-thumb\"\n                                       alt=\"\">\n                                 {{else}}\n                                    <a href=\"{{url}}\" class=\"feed__author-link\">\n                                       <img\n                                          src=\"{{avatarImage.url}}\" \n                                          {{ lazyload \"lazy\" }}\n                                          height=\"{{avatarImage.height}}\"\n                                          width=\"{{avatarImage.width}}\"      \n                                          class=\"feed__author-thumb\"\n                                          alt=\"{{avatarImage.alt}}\">\n                                    </a>\n                                 {{/if}}\n                              {{/if}}\n                           {{/if}}\n                           {{#if @config.custom.feedAuthor}}\n                              <a href=\"{{url}}\" class=\"feed__author\">{{name}}</a>\n                           {{/if}}\n                        {{/author}}\n\n                        {{#if @config.custom.feedDate}}\n                           {{#checkIf @config.custom.feedDateType '==' \"published\" }}\n                              <time datetime=\"{{date createdAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                 {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                    {{date createdAt @config.custom.formatDate}}\n                                 {{else}}\n                                    {{date createdAt @config.custom.formatDateCustom}}\n                                 {{/checkIf}}\n                              </time>\n                           {{/checkIf}}\n                           {{#checkIf @config.custom.feedDateType '==' \"modified\" }}\n                              <time datetime=\"{{date modifiedAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                 {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                    {{date modifiedAt @config.custom.formatDate}}\n                                 {{else}}\n                                    {{date modifiedAt @config.custom.formatDateCustom}}\n                                 {{/checkIf}}\n                              </time>\n                           {{/checkIf}}\n                        {{/if}}\n                     </div>\n                  {{/checkIfAny}}\n                  <h2 class=\"feed__title\">\n                     <a href=\"{{url}}\">\n                        {{title}}\n                     </a>\n                  </h2>\n               </header>               \n               {{#if hasCustomExcerpt}}\n                  {{{ excerpt }}}\n               {{else}}\n                  <p>{{{ excerpt }}}</p>\n               {{/if}}\n               {{#if @config.custom.feedtReadMore}}\n                  <a href=\"{{url}}\" class=\"readmore feed__readmore\">\n                     {{ translate 'post.readMore' }}</a>\n               {{/if}}\n            </div>\n         </article>\n      {{/each}}\n      {{> pagination}}\n   </div>\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/search.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"page page--search\">\n   <div class=\"content search-page\">\n      <div class=\"hero {{#checkIfNone featuredImage.url}}hero--noimage{{/checkIfNone}}\">\n         <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n            <div class=\"wrapper\">\n               <h1>\n                  {{ translate 'search.title' }}\n               </h1>               \n            </div>\n         </header>\n      </div>\n      <div class=\"entry-wrapper content__entry\">\n         {{{@customSearchContent}}}  \n      </div>\n   </div>\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/simple.lang.json",
    "content": "{\n    \"partials\": {\n\n        \"menu\": {\n            \"label\": \"Menu\"\n        },\n        \n        \"pagination\": {\n            \"prev\": \"Previous\",\n            \"next\": \"Next\"\n        },\n        \n        \"shareButtons\": {\n            \"shareWithFacebook\": \"Facebook\",\n            \"shareWithTwitter\": \"Twitter\",\n            \"shareWithPinterest\": \"Pinterest\",\n            \"shareWithMix\": \"Mix\",\n            \"shareWithLinkedIn\": \"LinkedIn\",\n            \"shareWithBuffer\": \"Buffer\",\n            \"shareWithWhatsApp\": \"WhatsApp\"\n        },\n        \n        \"footer\": {\n            \"backToTop\": \"Back to top\",\n            \"followFacebook\": \"Facebook\",\n            \"followX\": \"X\",\n            \"followInstagram\": \"Instagram\",\n            \"followLinkedIn\": \"LinkedIn\",\n            \"followVimeo\": \"Vimeo\",\n            \"followPinterest\": \"Pinterest\",\n            \"followYoutube\": \"Youtube\"\n        }\n    },\n    \n    \"post\": {\n        \"publishedBy\": \"By\",\n        \"relatedPosts\": \"You should also read:\",\n        \"readMore\": \"Continue reading...\",\n        \"lastUpdatedDate\": \"This article was updated on\",\n        \"comments\": \"Comments\",\n        \"previousPost\": \"Previous\",\n        \"nextPost\": \"Next\",\n        \"shareIt\": \"Share It\"\n    },\n\n    \"postPrefix\": {\n        \"title\": \"Insights and Updates\",\n        \"description\": \"Discover our latest posts, insights, and updates on what we do and what we care about.\"\n    },\n\n    \"page\": {\n        \"childPages\": \"Child pages\"\n    },\n    \n    \"tags\": {\n        \"tagsPageTitle\": \"Tags\",\n        \"description\": \"Collection of all  %s tags\",\n        \"featuringPosts\": \"Featuring %s posts\"\n    },\n\n    \"author\": {\n        \"visitWebsite\": \"Visit website\"\n    },\n\n    \"error\": {\n        \"404\": \"404\",\n        \"message\": \"The page you were looking for appears to have been moved, deleted or does not exist. You could go back to where you were or head straight to our home page.\",\n        \"button\": \"Go home\"\n    },\n    \n    \"search\": {\n        \"placeholder\": \"search...\",\n        \"search\": \"Search\",\n        \"close\": \"Close\",\n        \"title\": \"Search\",\n        \"submit\": \"Submit\"\n    }\n}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/tag.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"page page--tag\">\n   {{#tag}}\n      <div class=\"hero {{#checkIfAll tagViewConfig.displayFeaturedImage featuredImage.url}}\n   {{else}}hero--noimage{{/checkIfAll}}\">\n         <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n            <div class=\"wrapper\">           \n               <h1>\n                  {{name}}\n                  {{#if tagViewConfig.displayPostCounter}}<sup>({{postsNumber}})</sup>{{/if}}\n               </h1>\n               {{#if tagViewConfig.displayDescription}} \n                  {{#if description}}\n                     <div class=\"page__desc\">{{{description}}}</div>\n                  {{/if}}  \n               {{/if}}         \n            </div>\n         </header>\n\n         {{#if tagViewConfig.displayFeaturedImage}}\n            {{#featuredImage}}\n               {{#if url}}\n                  <figure class=\"hero__image\"> \n                     <div class=\"hero__image-wrapper\">                     \n                        <img\n                           src=\"{{url}}\"\n                           {{#if @config.site.responsiveImages}}                           \n                                 {{responsiveImageAttributes 'tagImage' srcset sizes}}\n                           {{/if}}\n                           {{ lazyload \"eager\" }}\n                           height=\"{{height}}\"\n                           width=\"{{width}}\"\n                           alt=\"{{alt}}\">\n                        </div>\n                        \n                     {{#checkIfAny caption credits}}\n                        <figcaption>\n                           {{caption}}\n                           {{credits}}\n                        </figcaption>                      \n                     {{/checkIfAny}}                      \n                  </figure>\n               {{/if}}\n            {{/featuredImage}}\n         {{/if}}\n      </div>\n\n      {{#if tagViewConfig.displayPostList}} \n         <div class=\"wrapper feed\">\n            {{#each ../posts}}\n               <article class=\"feed__item {{#checkIf @config.custom.alignFeed '==' \"center\" }}feed__item--centered{{/checkIf}}\">\n                  {{#if @config.custom.feedFeaturedImage}}\n                     {{#featuredImage}}\n                        {{#if url}}\n                           <figure class=\"feed__image\">\n                              <img\n                                 src=\"{{url}}\"\n                                 {{#if @config.site.responsiveImages}}\n                                    {{responsiveImageAttributes 'featuredImage' srcset.feed sizes.feed}}\n                                 {{/if}}\n                                 {{ lazyload \"lazy\" }}\n                                 height=\"{{height}}\"\n                                 width=\"{{width}}\"\n                                 alt=\"{{alt}}\">\n                           </figure>\n                        {{/if}}\n                     {{/featuredImage}}\n                  {{/if}}\n                  <div class=\"feed__content\">\n                     <header>\n                        {{#checkIfAny @config.custom.feedAvatar @config.custom.feedAuthor @config.custom.feedDate}}\n                           <div class=\"feed__meta\">\n                              {{#author}}\n                                 {{#if @config.custom.feedAvatar}}\n                                    {{#if avatar}}\n                                       {{#if @config.custom.feedAuthor}}\n                                          <img\n                                             src=\"{{avatarImage.url}}\" \n                                             {{ lazyload \"lazy\" }}\n                                             height=\"{{avatarImage.height}}\"\n                                             width=\"{{avatarImage.width}}\"      \n                                             class=\"feed__author-thumb\"\n                                             alt=\"\">\n                                       {{else}}\n                                          <a href=\"{{url}}\" class=\"feed__author-link\">\n                                             <img\n                                                src=\"{{avatarImage.url}}\" \n                                                {{ lazyload \"lazy\" }}\n                                                height=\"{{avatarImage.height}}\"\n                                                width=\"{{avatarImage.width}}\"      \n                                                class=\"feed__author-thumb\"\n                                                alt=\"{{avatarImage.alt}}\">\n                                          </a>\n                                       {{/if}}\n                                    {{/if}}\n                                 {{/if}}\n                                 {{#if @config.custom.feedAuthor}}\n                                    <a href=\"{{url}}\" class=\"feed__author\">{{name}}</a>\n                                 {{/if}}\n                              {{/author}}\n                              {{#if @config.custom.feedDate}}\n                                 {{#checkIf @config.custom.feedDateType '==' \"published\" }}\n                                    <time datetime=\"{{date createdAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                       {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                          {{date createdAt @config.custom.formatDate}}\n                                       {{else}}\n                                          {{date createdAt @config.custom.formatDateCustom}}\n                                       {{/checkIf}}\n                                    </time>\n                                 {{/checkIf}}\n                                 {{#checkIf @config.custom.feedDateType '==' \"modified\" }}\n                                    <time datetime=\"{{date modifiedAt 'YYYY-MM-DDTHH:mm'}}\" class=\"feed__date\">\n                                       {{#checkIf @config.custom.formatDate '!=' 'custom'}}\n                                          {{date modifiedAt @config.custom.formatDate}}\n                                       {{else}}\n                                          {{date modifiedAt @config.custom.formatDateCustom}}\n                                       {{/checkIf}}\n                                    </time>\n                                 {{/checkIf}}\n                              {{/if}}\n                           </div>\n                        {{/checkIfAny}}\n                        <h2 class=\"feed__title\">\n                           <a href=\"{{url}}\">\n                              {{title}}\n                           </a>\n                        </h2>\n                     </header>               \n                     {{#if hasCustomExcerpt}}\n                        {{{ excerpt }}}\n                     {{else}}\n                        <p>{{{ excerpt }}}</p>\n                     {{/if}}\n                     {{#if @config.custom.feedtReadMore}}\n                        <a href=\"{{url}}\" class=\"readmore feed__readmore\">\n                           {{ translate 'post.readMore' }}</a>\n                     {{/if}}\n                  </div>\n               </article>\n            {{/each}}\n            {{> pagination}}\n         </div>\n      {{/if}}\n   {{/tag}}\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/tags.hbs",
    "content": "{{> head}}\n{{> navbar}}\n<main class=\"page page--tags\">\n   <div class=\"hero {{#checkIfNone featuredImage.url}}hero--noimage{{/checkIfNone}}\">\n      <header class=\"hero__content {{#checkIf @config.custom.alignHero '==' \"center\" }}hero__content--centered{{/checkIf}}\">\n         <div class=\"wrapper\">\n            <h1>\n               {{ translate 'tags.tagsPageTitle' }} \n            </h1>\n            <p class=\"page__desc\">\n               {{ translate \"tags.description\" tagsNumber }}\n            </p>\n         </div>\n      </header>\n   </div>\n   <div class=\"wrapper\">\n      <ul class=\"feed feed--grid\">\n         {{#each tags}}\n            <li class=\"feed__item\">\n               {{#featuredImage}}\n                  {{#if url}}\n                     <figure class=\"feed__image\">\n                        <img\n                           {{#if @config.site.responsiveImages}}\n                              src=\"{{urlXs}}\"\n                           {{else}}\n                              src=\"{{url}}\"\n                           {{/if}}\n                           {{ lazyload \"lazy\" }}\n                           height=\"{{height}}\"\n                           width=\"{{width}}\"\n                           alt=\"{{alt}}\">\n                     </figure>\n                  {{/if}}\n               {{/featuredImage}}\n\n               <div class=\"feed__content\">\n                  <h2 class=\"h3\">\n                     <a href=\"{{url}}\">\n                        {{name}}\n                     </a>\n                  </h2>\n                  <p>\n                     {{ translate \"tags.featuringPosts\" postsNumber }}\n                  </p>\n               </div>\n            </li>\n         {{/each}}\n      </ul>\n   </div>\n</main>\n{{> footer}}\n"
  },
  {
    "path": "app/default-files/default-themes/simple/theme-variables.js",
    "content": "/*\n * Custom function used to generate the output of the theme variables\n */\n\nvar generateThemeVariables = function (params) {\nlet fontParams = {\n      'system-ui': {\n      name: 'SystemUI',\n      family: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"',\n      hasItalic: false\n    },\n    'albertsans': {\n      name: 'Albert Sans',\n      family: '\\'Albert Sans\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'aleo': {\n      name: 'Aleo',\n      family: '\\'Aleo\\', serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'andadapro': {\n      name: 'Andada Pro',\n      family: '\\'Andada Pro\\', serif',\n      weight: '400 840',\n      hasItalic: true\n    },\n    'antonio': {\n      name: 'Antonio',\n      family: '\\'Antonio\\', sans-serif',\n      weight: '100 700',\n      hasItalic: false\n    },\n    'archivonarrow': {\n      name: 'Archivo Narrow',\n      family: '\\'Archivo Narrow\\', sans-serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'asap': {\n      name: 'Asap',\n      family: '\\'Asap\\', sans-serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'assistant': {\n      name: 'Assistant',\n      family: '\\'Assistant\\', sans-serif',\n      weight: '200 800',\n      hasItalic: false\n    },\n    'adventpro': {\n      name: 'Advent Pro',\n      family: '\\'Advent Pro\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'besley': {\n      name: 'Besley',\n      family: '\\'Besley\\', serif',\n      weight: '400 900',\n      hasItalic: true\n    },\n    'bitter': {\n      name: 'Bitter',\n      family: '\\'Bitter\\', serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'bitcount': {\n      name: 'Bitcount',\n      family: '\\'Bitcount\\', system-ui',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'bodonimoda': {\n      name: 'Bodoni Moda',\n      family: '\\'Bodoni Moda\\', serif',\n      weight: '400 900',\n      hasItalic: true\n    },\n    'brygada1918': {\n      name: 'Brygada 1918',\n      family: '\\'Brygada 1918\\', serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'cabin': {\n      name: 'Cabin',\n      family: '\\'Cabin\\', sans-serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'cairo': {\n      name: 'Cairo',\n      family: '\\'Cairo\\', sans-serif',\n      weight: '200 1000',\n      hasItalic: false\n    },\n    'cinzel': {\n      name: 'Cinzel',\n      family: '\\'Cinzel\\', serif',\n      weight: '400 900',\n      hasItalic: false\n    },\n    'comfortaa': {\n      name: 'Comfortaa',\n      family: '\\'Comfortaa\\', cursive',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'comme': {\n      name: 'Comme',\n      family: '\\'Comme\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'dancingscript': {\n      name: 'Dancing Script',\n      family: '\\'Dancing Script\\', cursive',\n      weight: '400 700',\n      hasItalic: false\n    },\n    'danfo': {\n      name: 'Danfo',\n      family: '\\'Danfo\\', serif',\n      weight: '400 400',\n      hasItalic: false\n    },\n    'dmsans': {\n      name: 'DM Sans',\n      family: '\\'DM Sans\\', sans-serif',\n      weight: '100 1000',\n      hasItalic: true\n    },\n    'domine': {\n      name: 'Domine',\n      family: '\\'Domine\\', serif',\n      weight: '400 700',\n      hasItalic: false\n    },\n    'dosis': {\n      name: 'Dosis',\n      family: '\\'Dosis\\', sans-serif',\n      weight: '200 800',\n      hasItalic: false\n    },\n    'doto': {\n      name: 'Doto',\n      family: '\\'Doto\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'dynapuff': {\n      name: 'DynaPuff',\n      family: '\\'DynaPuff\\', system-ui',\n      weight: '400 700',\n      hasItalic: false\n    },\n    'exo': {\n      name: 'Exo',\n      family: '\\'Exo\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'familjengrotesk': {\n      name: 'Familjen Grotesk',\n      family: '\\'Familjen Grotesk\\', sans-serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'faustina': {\n      name: 'Faustina',\n      family: '\\'Faustina\\', serif',\n      weight: '300 800',\n      hasItalic: true\n    },\n    'figtree': {\n      name: 'Figtree',\n      family: '\\'Figtree\\', sans-serif',\n      weight: '300 900',\n      hasItalic: true\n    },\n    'finlandica': {\n      name: 'Finlandica',\n      family: '\\'Finlandica\\', sans-serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'frankruhllibre': {\n      name: 'Frank Ruhl Libre',\n      family: '\\'Frank Ruhl Libre\\', serif',\n      weight: '300 900',\n      hasItalic: false\n    },\n    'fredoka': {\n      name: 'Fredoka',\n      family: '\\'Fredoka\\', sans-serif',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'funneldisplay': {\n      name: 'Funnel Display',\n      family: '\\'Funnel Display\\', sans-serif',\n      weight: '300 800',\n      hasItalic: false\n    },\n    'gantari': {\n      name: 'Gantari',\n      family: '\\'Gantari\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'geistmono': {\n      name: 'Geist Mono',\n      family: '\\'Geist Mono\\', monospace',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'glory': {\n      name: 'Glory',\n      family: '\\'Glory\\', sans-serif',\n      weight: '100 800',\n      hasItalic: true\n    },\n    'gluten': {\n      name: 'Gluten',\n      family: '\\'Gluten\\', cursive',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'googlesanscode': {\n      name: 'Google Sans Code',\n      family: '\\'Google Sans Code\\', monospace',\n      weight: '300 800',\n      hasItalic: true\n    },\n    'grenzegotisch': {\n      name: 'Grenze Gotisch',\n      family: '\\'Grenze Gotisch\\', serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'handjet': {\n      name: 'Handjet',\n      family: '\\'Handjet\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'heebo': {\n      name: 'Heebo',\n      family: '\\'Heebo\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'hostgrotesk': {\n      name: 'Host Grotesk',\n      family: '\\'Host Grotesk\\', sans-serif',\n      weight: '300 800',\n      hasItalic: true\n    },\n    'imbue': {\n      name: 'Imbue',\n      family: '\\'Imbue\\', serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'inclusivesans': {\n      name: 'Inclusive Sans',\n      family: '\\'Inclusive Sans\\', sans-serif',\n      weight: '300 700',\n      hasItalic: true\n    },\n    'instrumentsans': {\n      name: 'Instrument Sans',\n      family: '\\'Instrument Sans\\', serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'jetbrainsmono': {\n      name: 'JetBrains Mono',\n      family: '\\'JetBrains Mono\\', monospace',\n      weight: '100 800',\n      hasItalic: true\n    },\n    'jura': {\n      name: 'Jura',\n      family: '\\'Jura\\', sans-serif',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'kalnia': {\n      name: 'Kalnia',\n      family: '\\'Kalnia\\', serif',\n      weight: '100 700',\n      hasItalic: false\n    },\n    'karla': {\n      name: 'Karla',\n      family: '\\'Karla\\', sans-serif',\n      weight: '200 800',\n      hasItalic: true\n    },\n    'kreon': {\n      name: 'Kreon',\n      family: '\\'Kreon\\', serif',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'kumbhsans': {\n      name: 'Kumbh Sans',\n      family: '\\'Kumbh Sans\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'labrada': {\n      name: 'Labrada',\n      family: '\\'Labrada\\', serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'leaguespartan': {\n      name: 'League Spartan',\n      family: '\\'League Spartan\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'lemonada': {\n      name: 'Lemonada',\n      family: '\\'Lemonada\\', cursive',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'lexend': {\n      name: 'Lexend',\n      family: '\\'Lexend\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'lexenddeca': {\n      name: 'Lexend Deca',\n      family: '\\'Lexend Deca\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'librefranklin': {\n      name: 'Libre Franklin',\n      family: '\\'Libre Franklin\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'lora': {\n      name: 'Lora',\n      family: '\\'Lora\\', serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'manuale': {\n      name: 'Manuale',\n      family: '\\'Manuale\\', serif',\n      weight: '300 800',\n      hasItalic: true\n    },\n    'manrope': {\n      name: 'Manrope',\n      family: '\\'Manrope\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'mavenpro': {\n      name: 'Maven Pro',\n      family: '\\'Maven Pro\\', sans-serif',\n      weight: '400 900',\n      hasItalic: false\n    },\n    'merriweathersans': {\n      name: 'Merriweather Sans',\n      family: '\\'Merriweather Sans\\', sans-serif',\n      weight: '300 800',\n      hasItalic: true\n    },\n    'montserrat': {\n      name: 'Montserrat',\n      family: '\\'Montserrat\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'mulish': {\n      name: 'Mulish',\n      family: '\\'Mulish\\', sans-serif',\n      weight: '200 1000',\n      hasItalic: true\n    },\n    'nunito': {\n      name: 'Nunito',\n      family: '\\'Nunito\\', sans-serif',\n      weight: '200 1000',\n      hasItalic: true\n    },\n    'orbitron': {\n      name: 'Orbitron',\n      family: '\\'Orbitron\\', sans-serif',\n      weight: '400 900',\n      hasItalic: false\n    },\n    'oswald': {\n      name: 'Oswald',\n      family: '\\'Oswald\\', sans-serif',\n      weight: '200 700',\n      hasItalic: false\n    },\n    'outfit': {\n      name: 'Outfit',\n      family: '\\'Outfit\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'oxanium': {\n      name: 'Oxanium',\n      family: '\\'Oxanium\\', sans-serif',\n      weight: '200 800',\n      hasItalic: false\n    },\n    'parkinsans': {\n      name: 'Parkinsans',\n      family: '\\'Parkinsans\\', sans-serif',\n      weight: '300 800',\n      hasItalic: false\n    },\n    'petrona': {\n      name: 'Petrona',\n      family: '\\'Petrona\\', serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'playfairdisplay': {\n      name: 'Playfair Display',\n      family: '\\'Playfair Display\\', serif',\n      weight: '400 900',\n      hasItalic: true\n    },\n    'playwriteusmodern': {\n      name: 'Playwrite US Modern',\n      family: '\\'Playwrite US Modern\\', cursive',\n      weight: '100 400',\n      hasItalic: false\n    },\n    'playwriteustrad': {\n      name: 'Playwrite US Traditional',\n      family: '\\'Playwrite US Trad\\', cursive',\n      weight: '100 400',\n      hasItalic: false\n    },\n    'plusjakartasans': {\n      name: 'Plus Jakarta Sans',\n      family: '\\'Plus Jakarta Sans\\', sans-serif',\n      weight: '200 800',\n      hasItalic: true\n    },\n    'pontanosans': {\n      name: 'Pontano Sans',\n      family: '\\'Pontano Sans\\', sans-serif',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'publicsans': {\n      name: 'Public Sans',\n      family: '\\'Public Sans\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'quicksand': {\n      name: 'Quicksand',\n      family: '\\'Quicksand\\', sans-serif',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'radiocanadabig': {\n      name: 'Radio Canada Big',\n      family: '\\'Radio Canada Big\\', sans-serif',\n      weight: '400 700',\n      hasItalic: true\n    },\n    'raleway': {\n      name: 'Raleway',\n      family: '\\'Raleway\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'redhatdisplay': {\n      name: 'Red Hat Display',\n      family: '\\'Red Hat Display\\', sans-serif',\n      weight: '300 900',\n      hasItalic: true\n    },\n    'redhatmono': {\n      name: 'Red Hat Mono',\n      family: '\\'Red Hat Mono\\', monospace',\n      weight: '300 700',\n      hasItalic: true\n    },\n    'redhattext': {\n      name: 'Red Hat Text',\n      family: '\\'Red Hat Text\\', sans-serif',\n      weight: '300 700',\n      hasItalic: true\n    },\n    'redrose': {\n      name: 'Red Rose',\n      family: '\\'Red Rose\\', serif',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'rem': {\n      name: 'REM',\n      family: '\\'REM\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'robotoflex': {\n      name: 'Roboto Flex',\n      family: '\\'Roboto Flex\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'robotoslab': {\n      name: 'Roboto Slab',\n      family: '\\'Roboto Slab\\', serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'rokkitt': {\n      name: 'Rokkitt',\n      family: '\\'Rokkitt\\', serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'rubik': {\n      name: 'Rubik',\n      family: '\\'Rubik\\', sans-serif',\n      weight: '300 900',\n      hasItalic: true\n    },\n    'ruda': {\n      name: 'Ruda',\n      family: '\\'Ruda\\', sans-serif',\n      weight: '400 900',\n      hasItalic: false\n    },\n    'smoochsans': {\n      name: 'Smooch Sans',\n      family: '\\'Smooch Sans\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'sora': {\n      name: 'Sora',\n      family: '\\'Sora\\', sans-serif',\n      weight: '100 800',\n      hasItalic: false\n    },\n    'sourcecodepro': {\n      name: 'Source Code Pro',\n      family: '\\'Source Code Pro\\', monospace',\n      weight: '200 900',\n      hasItalic: true\n    },\n    'spartan': {\n      name: 'Spartan',\n      family: '\\'Spartan\\', sans-serif',\n      weight: '100 900',\n      hasItalic: false\n    },\n    'sticknobills': {\n      name: 'Stick No Bills',\n      family: '\\'Stick No Bills\\', sans-serif',\n      weight: '200 800',\n      hasItalic: false\n    },\n    'susemono': {\n      name: 'SUSE Mono',\n      family: '\\'SUSE Mono\\', sans-serif',\n      weight: '100 800',\n      hasItalic: true\n    },\n    'teachers': {\n      name: 'Teachers',\n      family: '\\'Teachers\\', sans-serif',\n      weight: '400 800',\n      hasItalic: true\n    },\n    'tektur': {\n      name: 'Tektur',\n      family: '\\'Tektur\\', sans-serif',\n      weight: '400 900',\n      hasItalic: false\n    },\n    'tourney': {\n      name: 'Tourney',\n      family: '\\'Tourney\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'urbanist': {\n      name: 'Urbanist',\n      family: '\\'Urbanist\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'varta': {\n      name: 'Varta',\n      family: '\\'Varta\\', sans-serif',\n      weight: '300 700',\n      hasItalic: false\n    },\n    'victormono': {\n      name: 'Victor Mono',\n      family: '\\'Victor Mono\\', monospace',\n      weight: '100 700',\n      hasItalic: true\n    },\n    'wixmadefortext': {\n      name: 'Wix Madefor Text',\n      family: '\\'Wix Madefor Text\\', sans-serif',\n      weight: '400 800',\n      hasItalic: true\n    },\n    'workbench': {\n      name: 'Workbench',\n      family: '\\'Workbench\\', sans-serif',\n      weight: '400 400',\n      hasItalic: false\n    },\n    'worksans': {\n      name: 'Work Sans',\n      family: '\\'Work Sans\\', sans-serif',\n      weight: '100 900',\n      hasItalic: true\n    },\n    'yanonekaffeesatz': {\n      name: 'Yanone Kaffeesatz',\n      family: '\\'Yanone Kaffeesatz\\', sans-serif',\n      weight: '200 700',\n      hasItalic: false\n    },\n    'yrsa': {\n      name: 'Yrsa',\n      family: '\\'Yrsa\\', serif',\n      weight: '300 700',\n      hasItalic: true\n    },\n    'zalandosans': {\n      name: 'Zalando Sans',\n      family: '\\'Zalando Sans\\', sans-serif',\n      weight: '200 900',\n      hasItalic: true\n    },\n    'zalandosansexpanded': {\n      name: 'Zalando Sans Expanded',\n      family: '\\'Zalando Sans Expanded\\', sans-serif',\n      weight: '200 900',\n      hasItalic: true\n    }\n  };\n\n  let fontBodyName = fontParams[params.fontBody]?.name;\n  let fontBodyFamily = fontParams[params.fontBody]?.family;\n  let fontBodyWeight = fontParams[params.fontBody]?.weight;\n  let fontBodyHasItalic = fontParams[params.fontBody]?.hasItalic;\n\n  let fontHeadingsName = fontParams[params.fontHeadings]?.name;\n  let fontHeadingsFamily = fontParams[params.fontHeadings]?.family;\n  let fontHeadingsWeight = fontParams[params.fontHeadings]?.weight;\n  let fontHeadingsHasItalic = fontParams[params.fontHeadings]?.hasItalic;\n\n  let output = '';\n  let loadedFonts = new Set();\n\n  const addFontFace = (key, name, weight, hasItalic) => {\n    if (!loadedFonts.has(name)) {\n      output += `             \n        @font-face {\n          font-family: '${name}';\n          src: url('../dynamic/fonts/${key}/${key}.woff2') format('woff2');\n          font-weight: ${weight};\n          font-display: swap;\n          font-style: normal;\n        }\n      `;\n      loadedFonts.add(name);\n    }\n    if (hasItalic && !loadedFonts.has(`${name}-italic`)) {\n      output += `             \n        @font-face {\n          font-family: '${name}';\n          src: url('../dynamic/fonts/${key}/${key}-italic.woff2') format('woff2');\n          font-weight: ${weight}; \n          font-display: swap;\n          font-style: italic;\n        }\n      `;\n      loadedFonts.add(`${name}-italic`);\n    }\n  };\n\n  if (params.fontBody !== 'system-ui') {\n    addFontFace(params.fontBody, fontBodyName, fontBodyWeight, params.fontBodyItalic && fontBodyHasItalic);\n  }\n\n  if (params.fontHeadings !== 'system-ui') {\n    addFontFace(params.fontHeadings, fontHeadingsName, fontHeadingsWeight, params.fontHeadingsItalic && fontHeadingsHasItalic);\n  }\n\n  if (params.fontMenu === 'system-ui') {\n    params.fontMenu = fontParams['system-ui'].family;\n  }\n\n  if (params.fontLogo === 'system-ui') {\n    params.fontLogo = fontParams['system-ui'].family;\n  }\n\n  // Fluid base font-size\n  const minScreen = 20; // rem\n  const maxScreen = 90; // rem\n  const screenRange = maxScreen - minScreen;\n  const minFontSize = params.minFontSize;\n  const maxFontSize = params.maxFontSize;\n  const fontSizeRange = maxFontSize - minFontSize;\n  const fontSizeValue = `clamp(${minFontSize}rem, ${minFontSize}rem + (${fontSizeRange} * ((100vw - ${minScreen}rem) / ${screenRange})), ${maxFontSize}rem)`;\n\n\n  output += `    \n    :root {\n      --page-margin:        ${params.pageMargin};\n      --page-width:         ${params.pageWidth};\n      --entry-width:        ${params.entryWidth}; \n      --navbar-height:      4.4rem; \n      --border-radius:      ${params.borderRadius}px;\n      --baseline:           ${params.baseline};\n      --gallery-gap:        ${params.galleryItemGap}; \n      --body-font:          ${fontBodyFamily};\n      --heading-font:       ${fontHeadingsFamily};\n      --logo-font:          ${params.fontLogo};\n      --menu-font:          ${params.fontMenu};\n      --font-size:          ${fontSizeValue};\n      --font-weight-normal: ${params.fontBodyWeight}; \n      --font-weight-bold:   ${params.fontBoldWeight}; \n      --line-height:        ${params.lineHeight};\n      --letter-spacing:     ${params.letterSpacing}em;  \n      --headings-weight:    ${params.fontHeadignsWeight};\n      --headings-transform: ${params.fontHeadingsTransform};\n      --headings-style:     ${params.fontHeadingsStyle};\n      --headings-letter-spacing: ${params.fontHeadingsletterSpacing}em;\n      --headings-line-height: ${params.fontHeadingsLineHeight};\n      --hero-height:        ${params.heightHero};\n      --feed-image-size:    ${params.feedFeaturedImageSize}rem;\n                          \n    `;\n\n  if (params.colorScheme !== 'dark') {\n    output += ` \n        --white:              #FFFFFF;\n        --black:              #17181E;\n        --helper:             #FFFFFF;\n        --dark:               #17181E;\n        --gray:               #57585a;\n        --light:              #CACBCF;\n        --lighter:            #F3F3F3;\n        --page-bg:            #FFFFFF; \n        --color:              ${params.primaryColor};   \n        --text-color:         #17181E;\n        --headings-color:     #17181E;\n        --link-color:         #17181E;\n        --link-color-hover:   ${params.primaryColor};    \n        --nav-link-color:     #17181E;\n        --nav-link-color-hover: #17181E;  \n        --logo-color:         #17181E;   \n        --highlight-color:    #FFC700;\n        --info-color:         #67B1F3;\n        --success-color:      #00A563;\n        --warning-color:      #EE4E4E;\n    `;\n  }\n\n  if (params.colorScheme === 'dark') {\n    output += ` \n        --white:              #FFFFFF;\n        --black:              #1e1e1e;\n        --helper:             #1e1e1e;\n        --dark:               #CECBCB;\n        --gray:               #9D9D9D;\n        --light:              #373737;\n        --lighter:            #1e1e1e;\n        --page-bg:            #181818; \n        --color:              ${params.primaryDarkColor};   \n        --text-color:         #BFBFBF;\n        --headings-color:     #EEEDED;\n        --link-color:         #EEEDED;\n        --link-color-hover:   ${params.primaryDarkColor};    \n        --nav-link-color:     rgba(255,255,255,1);\n        --nav-link-color-hover: rgba(255,255,255,.7);  \n        --logo-color:         #FFFFFF;   \n        --highlight-color:    #F6DC90;\n        --info-color:         #5B9ED5;\n        --success-color:      #54A468;\n        --warning-color:      #FB6762;\n    `;\n  }\n\n  output += `\n  }`;\n\n  output += ` \n      @media all and (min-width: 56.25em) {\n        :root {\n          --navbar-height: ${params.navbarHeight};\n        }\n      } \n  `;\n\n  if (params.colorScheme === 'auto') {\n    output += ` \n      @media (prefers-color-scheme: dark) {\n        :root {                \n          --white:              #FFFFFF;\n          --black:              #1e1e1e;\n          --helper:             #1e1e1e;\n          --dark:               #CECBCB;\n          --gray:               #9D9D9D;\n          --light:              #373737;\n          --lighter:            #1e1e1e;\n          --page-bg:            #181818; \n          --color:              ${params.primaryDarkColor};   \n          --text-color:         #BFBFBF;\n          --headings-color:     #EEEDED;\n          --link-color:         #EEEDED;\n          --link-color-hover:   ${params.primaryDarkColor};    \n          --nav-link-color:     rgba(255,255,255,1);\n          --nav-link-color-hover: rgba(255,255,255,.7);  \n          --logo-color:         #FFFFFF;   \n          --highlight-color:    #F6DC90;\n          --info-color:         #5B9ED5;\n          --success-color:      #54A468;\n          --warning-color:      #FB6762;\n        }        \n      }\n    `;\n  }\n\n  return output;\n}\n\nmodule.exports = generateThemeVariables;"
  },
  {
    "path": "app/default-files/default-themes/simple/visual-override.js",
    "content": "/*\n * Custom function used to generate the output of the override.css file\n */\n\nvar generateOverride = function (params) {\n    let output = '';\n\n    if (params.primaryColor !== '#D73A42') {\n        output += `\n      \n        input[type=checkbox]:checked + label:before{\n               background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 8'%3e%3cpolygon points='9.53 0 4.4 5.09 1.47 2.18 0 3.64 2.93 6.54 4.4 8 5.87 6.54 11 1.46 9.53 0' fill='${params.primaryColor.replace('#', '%23')}'/%3e%3c/svg%3e\");\n        }\n\n        input[type=radio]:checked + label:before {\n               background-image: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3ccircle cx='4' cy='4' r='4' fill='${params.primaryColor.replace('#', '%23')}'/%3e%3c/svg%3e\");\n        \n\n        }`;\n    }\n\n    if (params.submenu === 'custom') {\n        output += `\n        .navbar .navbar__submenu {\n              white-space: wrap;\n               width: ${params.submenuWidth}px;     \n        }\n        .navbar .has-submenu .has-submenu:active > .navbar__submenu,\n        .navbar .has-submenu .has-submenu:focus > .navbar__submenu,\n        .navbar .has-submenu .has-submenu:hover > .navbar__submenu {\n               left: ${params.submenuWidth}px;  \n        }\n        .navbar .has-submenu .has-submenu:active > .navbar__submenu.is-right-submenu,\n        .navbar .has-submenu .has-submenu:focus > .navbar__submenu.is-right-submenu,\n        .navbar .has-submenu .has-submenu:hover > .navbar__submenu.is-right-submenu {\n               left: -${params.submenuWidth}px; \n        }`;\n    }   \n\n\n    if(params.lazyLoadEffect === 'fadein') {\n        output += ` \n         img[loading] {\n               opacity: 0;\n         }\n\n         img.is-loaded {\n               opacity: 1;\n               transition: opacity 1s cubic-bezier(0.215, 0.61, 0.355, 1); \n         }`;    \t \n    } \n        \n  \n    return output;\n}\n\nmodule.exports = generateOverride;\n"
  },
  {
    "path": "app/default-files/gdpr-assets/gdpr.css",
    "content": ":root {\n  --pcb-width: 42em;\n  --pcb-popup-height: 36em;\n  --pcb-font-base-size: 17px;\n  --pcb-font-weight-normal: 400;\n  --pcb-font-weight-bold: 700;\n  --pcb-border-radius: 4px;\n  --pcb-badge-border-radius: 100%;\n  --pcb-link-transition: all 0.12s linear;\n  --pcb-bg: #ffffff;\n  --pcb-overlay: rgba(12, 13, 16, .8);\n  --pcb-text-color: #343435;\n  --pcb-headings-color: #283149;\n  --pcb-border-color: #e1e1e1;\n  --pcb-bg-light: #f6f6f9;\n  --pcb-btn-primary-bg: #e7e8ea;\n  --pcb-btn-primary-bg-hover: #cbcdd2;\n  --pcb-btn-primary-text: #283149;\n  --pcb-btn-primary-text-hover: #283149;\n  --pcb-btn-secondary-bg: #1089ff;\n  --pcb-btn-secondary-bg-hover: #0079f2;\n  --pcb-btn-secondary-text: #ffffff;\n  --pcb-btn-secondary-text-hover: #ffffff;\n  --pcb-btn-link: #283149;\n  --pcb-btn-link-hover: #0079f2;\n  --pcb-badge-bg: #1089ff;\n  --pcb-badge-bg-hover: #0079f2;\n  --pcb-badge-color: #fff;\n  --pcb-badge-color-hover: #fff;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --pcb-bg: #191a1f;\n    --pcb-overlay: rgba(10, 10, 12, .8);\n    --pcb-text-color: #a5a7b7;\n    --pcb-headings-color: #d9d9e0;\n    --pcb-border-color: #393a47;\n    --pcb-bg-light: #f6f6f9;\n    --pcb-btn-primary-bg: #32343e;\n    --pcb-btn-primary-bg-hover: #3c3f4b;\n    --pcb-btn-primary-text: #d9d9e0;\n    --pcb-btn-primary-text-hover: #ffffff;\n    --pcb-btn-secondary-bg: #1089ff;\n    --pcb-btn-secondary-bg-hover: #0079f2;\n    --pcb-btn-secondary-text: #ffffff;\n    --pcb-btn-secondary-text-hover: #ffffff;\n    --pcb-btn-link: #d9d9e0;\n    --pcb-btn-link-hover: #42a0ff;\n    --pcb-badge-bg: #1089ff;\n    --pcb-badge-bg-hover: #0079f2;\n    --pcb-badge-color: #fff;\n    --pcb-badge-color-hover: #fff;\n  }\n}\n.pcb {\n  color: var(--pcb-text-color);\n  font-weight: 400;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  scroll-behavior: smooth;\n  text-rendering: optimizeLegibility;\n  position: relative;\n  z-index: 999999;\n}\n.pcb *, .pcb::after, .pcb::before {\n  -webkit-animation: none;\n          animation: none;\n  background: 0;\n  border: none;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  -webkit-box-sizing: border-box;\n          box-sizing: border-box;\n  color: inherit;\n  float: none;\n  font-style: normal;\n  font-variant: normal;\n  font-weight: inherit;\n  font-family: inherit;\n  line-height: 1;\n  font-size: 1em;\n  margin: 0;\n  opacity: unset;\n  padding: 0;\n  position: unset;\n  text-transform: none;\n  letter-spacing: unset;\n  visibility: unset;\n  -webkit-transform: none;\n          transform: none;\n  -webkit-transition: none;\n  transition: none;\n  text-decoration: none;\n  text-align: left;\n}\n.pcb__banner {\n  background: var(--pcb-bg);\n  -webkit-box-shadow: 0 0.625em 1.875em rgba(2, 2, 3, 0.28);\n          box-shadow: 0 0.625em 1.875em rgba(2, 2, 3, 0.28);\n  border-radius: calc(1.5 * var(--pcb-border-radius));\n  bottom: 1.25em;\n  font-size: var(--pcb-font-base-size);\n  left: 1em;\n  max-width: var(--pcb-width);\n  margin: 0 auto;\n  overflow: hidden;\n  padding: 1.5em 2em;\n  opacity: 0;\n  position: fixed;\n  right: 1em;\n  text-align: center;\n  -webkit-transform: translateY(1.5em);\n          transform: translateY(1.5em);\n  -webkit-transition: all 0.25s ease-out;\n  transition: all 0.25s ease-out;\n  visibility: hidden;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n      -ms-user-select: none;\n          user-select: none;\n  z-index: 1;\n}\n.pcb__banner--left {\n  margin-left: 0;\n}\n.pcb__banner--right {\n  margin-left: auto;\n  margin-right: 0;\n}\n.pcb__banner--bar {\n  max-width: 100%;\n  border-radius: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  padding: 2.5em 2em;\n}\n@media all and (min-width: 37.5em) {\n  .pcb__banner--bar {\n    font-size: 17px;\n  }\n}\n.pcb__banner.is-visible {\n  opacity: 1;\n  -webkit-transform: translateY(0);\n          transform: translateY(0);\n  visibility: visible;\n}\n.pcb__inner {\n  max-width: var(--pcb-width);\n  margin: 0 auto;\n}\n.pcb__title {\n  color: var(--pcb-headings-color);\n  font-size: 1.05em;\n  font-variation-settings: \"wght\" var(--pcb-font-weight-bold);\n  font-weight: var(--pcb-font-weight-bold);\n  margin-bottom: 0.75em;\n}\n.pcb__txt {\n  font-size: 0.85em;\n  font-variation-settings: \"wght\" var(--pcb-font-weight-normal);\n  font-weight: var(--pcb-font-weight-normal);\n  line-height: 1.5;\n}\n.pcb__txt a {\n  color: var(--pcb-btn-link);\n  font-variation-settings: \"wght\" var(--pcb-font-weight-normal);\n  font-weight: var(--pcb-font-weight-normal);\n  -webkit-transition: var(--pcb-link-transition);\n  transition: var(--pcb-link-transition);\n  text-decoration: underline;\n  text-decoration-thickness: 1px;\n  text-underline-offset: 0.2em;\n  -webkit-text-decoration-skip: ink;\n          text-decoration-skip-ink: auto;\n}\n.pcb__txt a:active, .pcb__txt a:hover, .pcb__txt a:focus {\n  color: var(--pcb-btn-link-hover);\n}\n.pcb__buttons {\n  display: flex;\n  flex-wrap: wrap;\n  margin-top: 0.5em;\n}\n@media all and (min-width: 30em) and (max-width: 37.4375em) {\n  .pcb__buttons {\n    justify-content: space-between;\n  }\n}\n@media all and (min-width: 37.5em) {\n  .pcb__buttons {\n    margin-top: 1.25em;\n  }\n}\n.pcb__btn {\n  background: var(--pcb-btn-primary-bg);\n  border: none;\n  border-radius: var(--pcb-border-radius);\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  cursor: pointer;\n  display: inline-block;\n  color: var(--pcb-btn-primary-text);\n  font-variation-settings: \"wght\" var(--pcb-font-weight-bold);\n  font-weight: var(--pcb-font-weight-bold);\n  font-size: 0.825em;\n  flex: 1 1 100%;\n  height: initial;\n  padding: 1em 1.5em;\n  text-align: center;\n  -webkit-transition: var(--pcb-link-transition);\n  transition: var(--pcb-link-transition);\n  -webkit-transform: none;\n          transform: none;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n      -ms-user-select: none;\n          user-select: none;\n}\n@media all and (min-width: 37.5em) {\n  .pcb__btn {\n    flex: 0 10 auto;\n  }\n}\n.pcb__btn:active, .pcb__btn:hover, .pcb__btn:focus {\n  background: var(--pcb-btn-primary-bg-hover);\n  border: none;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  color: var(--pcb-btn-primary-text-hover);\n  -webkit-transform: none;\n          transform: none;\n}\n.pcb__btn--solid {\n  background: var(--pcb-btn-secondary-bg);\n  color: var(--pcb-btn-secondary-text);\n}\n.pcb__btn--solid:active, .pcb__btn--solid:hover, .pcb__btn--solid:focus {\n  background: var(--pcb-btn-secondary-bg-hover);\n  color: var(--pcb-btn-secondary-text-hover);\n}\n.pcb__btn--link {\n  background: none;\n  color: var(--pcb-btn-link);\n  margin-right: auto;\n  padding-left: 0;\n  padding-right: 0;\n  text-align: left;\n  text-decoration: underline;\n  text-decoration-thickness: 1px;\n  text-underline-offset: 0.2em;\n  -webkit-text-decoration-skip: ink;\n          text-decoration-skip-ink: auto;\n}\n@media all and (max-width: 37.4375em) {\n  .pcb__btn--link {\n    margin-bottom: 0.25em;\n  }\n}\n.pcb__btn--link:active, .pcb__btn--link:hover, .pcb__btn--link:focus {\n  background: none;\n  color: var(--pcb-btn-link-hover);\n}\n.pcb__btn + .pcb__btn {\n  margin: 0.25em 0;\n}\n@media all and (min-width: 37.5em) {\n  .pcb__btn + .pcb__btn {\n    margin: 0 0 0 0.5em;\n  }\n}\n.pcb__overlay {\n  background: var(--pcb-overlay);\n  bottom: 0;\n  display: none;\n  left: 0;\n  position: fixed;\n  right: 0;\n  top: 0;\n  visibility: hidden;\n  z-index: 999999999;\n}\n.pcb__overlay.is-visible {\n  display: block;\n  opacity: 1;\n  visibility: visible;\n}\n.pcb__popup {\n  display: table;\n  height: 100%;\n  left: 0;\n  opacity: 0;\n  pointer-events: none;\n  position: fixed;\n  right: 0;\n  top: 0;\n  width: 100%;\n  visibility: hidden;\n  z-index: 9999999999;\n  -webkit-transition: all 0.25s ease-out;\n  transition: all 0.25s ease-out;\n}\n@media all and (min-width: 37.5em) {\n  .pcb__popup {\n    left: 1em;\n    right: 1em;\n    -webkit-transform: translateY(1.5em);\n            transform: translateY(1.5em);\n    width: calc(100% - 2em);\n  }\n}\n.pcb__popup.is-visible {\n  visibility: visible;\n  opacity: 1;\n  -webkit-transform: translateY(0);\n          transform: translateY(0);\n}\n.pcb__popup__wrapper {\n  background: var(--pcb-bg);\n  font-size: var(--pcb-font-base-size);\n  height: 100%;\n  max-height: 100%;\n  max-width: 100%;\n  margin: 0 auto;\n  overflow: hidden;\n  padding: 1.5em 2em;\n  pointer-events: auto;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n      -ms-user-select: none;\n          user-select: none;\n  width: 100%;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  -webkit-transform: translate(-50%, -50%);\n          transform: translate(-50%, -50%);\n}\n@media all and (min-width: 37.5em) {\n  .pcb__popup__wrapper {\n    border-radius: calc(1.5 * var(--pcb-border-radius));\n    -webkit-box-shadow: 0 0.625em 1.875em rgba(2, 2, 3, 0.28);\n            box-shadow: 0 0.625em 1.875em rgba(2, 2, 3, 0.28);\n    max-height: var(--pcb-popup-height);\n    max-width: var(--pcb-width);\n  }\n}\n.pcb__popup__inner {\n  padding-top: 2.5em;\n  padding-bottom: 10em;\n  position: relative;\n  height: 100%;\n}\n@media all and (min-width: 37.5em) {\n  .pcb__popup__inner {\n    padding-bottom: 2.5em;\n  }\n}\n.pcb__popup__heading {\n  border-bottom: 1px solid var(--pcb-border-color);\n  left: 0;\n  height: 2.5em;\n  padding-bottom: 2em;\n  position: absolute;\n  right: 0;\n  top: 0;\n}\n.pcb__popup__close {\n  background: none;\n  border: none;\n  border-radius: 0;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  cursor: pointer;\n  height: 1.3em;\n  padding: 0.5em;\n  position: absolute;\n  right: 0;\n  top: 0;\n  width: 1.3em;\n}\n@media all and (min-width: 37.5em) {\n  .pcb__popup__close {\n    display: none;\n    visibility: hidden;\n  }\n}\n.pcb__popup__close::before, .pcb__popup__close::after {\n  background-color: var(--pcb-btn-link);\n  content: \" \";\n  display: block;\n  height: 1.3em;\n  left: 0.65em;\n  position: absolute;\n  -webkit-transform: rotate(45deg);\n          transform: rotate(45deg);\n  -webkit-transition: var(--pcb-link-transition);\n  transition: var(--pcb-link-transition);\n  top: 0;\n  width: 1px;\n}\n.pcb__popup__close::after {\n  -webkit-transform: rotate(-45deg);\n          transform: rotate(-45deg);\n}\n.pcb__popup__close:hover {\n  background: none;\n  border: none;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  -webkit-transform: inherit;\n          transform: inherit;\n}\n.pcb__popup__close:hover::before, .pcb__popup__close:hover::after {\n  background-color: var(--pcb-btn-link-hover);\n}\n.pcb__popup__content {\n  display: block;\n  height: 100%;\n  overflow-y: auto;\n  overflow-x: hidden;\n  width: calc(100% + 1em);\n  margin-right: -1em;\n  padding-right: 1em;\n}\n.pcb__popup__txt {\n  margin-top: 2em;\n}\n.pcb__popup__buttons {\n  background-color: var(--pcb-bg);\n  bottom: 0;\n  left: 0;\n  margin: 0;\n  padding: 1.25em 0 0;\n  position: absolute;\n  right: 0;\n}\n.pcb__popup__buttons::before {\n  content: \"\";\n  border-top: 1px solid var(--pcb-border-color);\n  height: 1px;\n  left: 0;\n  width: 100%;\n  position: absolute;\n  top: 0;\n}\n.pcb__popup__buttons .pcb__btn:nth-child(3) {\n  margin-left: auto;\n}\n.pcb__popup__switch {\n  height: 100%;\n  position: absolute;\n  right: 1em;\n  top: 0;\n}\n.pcb__popup__switch input[type=checkbox] {\n  color: unset;\n  display: inline-block;\n  float: none;\n  height: 0;\n  opacity: unset;\n  position: unset;\n  width: 0;\n  visibility: hidden;\n}\n.pcb__popup__switch input[type=checkbox] + label {\n  background: var(--pcb-btn-primary-bg-hover);\n  border-radius: 100px;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  display: block;\n  cursor: pointer;\n  height: 24px;\n  margin: unset;\n  padding: unset;\n  position: relative;\n  text-indent: -9999px;\n  width: 44px;\n}\n.pcb__popup__switch input[type=checkbox] + label::before {\n  display: none;\n}\n.pcb__popup__switch input[type=checkbox] + label::after {\n  content: \"\";\n  background: var(--pcb-bg);\n  border: none;\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  border-radius: 20px;\n  display: block;\n  left: 2px;\n  height: 20px;\n  -webkit-mask: none;\n          mask: none;\n  position: absolute;\n  width: 20px;\n  top: 2px;\n  -webkit-transition: 0.3s;\n  transition: 0.3s;\n}\n.pcb__popup__switch input:checked + label {\n  background: var(--pcb-btn-secondary-bg);\n}\n.pcb__popup__switch input:checked + label:after {\n  left: calc(100% - 2px);\n  -webkit-transform: translateX(-100%);\n          transform: translateX(-100%);\n}\n.pcb__popup__switch.is-checked label {\n  opacity: 0.5;\n  pointer-events: none;\n}\n.pcb__groups {\n  list-style: none;\n  margin: 2em 0 1em;\n}\n.pcb__group {\n  border: 1px solid var(--pcb-border-color);\n  border-radius: var(--pcb-border-radius);\n  margin-bottom: 0.5em;\n  padding: 0 1em;\n  position: relative;\n}\n.pcb__group__title {\n  font-variation-settings: \"wght\" var(--pcb-font-weight-bold);\n  font-weight: var(--pcb-font-weight-bold);\n  font-size: 0.941em;\n  color: var(--pcb-headings-color);\n  cursor: pointer;\n  display: block;\n  padding: 1.25em 3.5em 1.25em 1.25em;\n  position: relative;\n  -webkit-transition: var(--pcb-link-transition);\n  transition: var(--pcb-link-transition);\n}\n.pcb__group__title:hover {\n  color: var(--pcb-btn-link-hover);\n}\n.pcb__group__title:focus {\n  outline: none;\n}\n.pcb__group__title::marker, .pcb__group__title::-webkit-details-marker {\n  display: none;\n}\n.pcb__group__title::before {\n  content: \"\";\n  border-right: 1px solid;\n  border-bottom: 1px solid;\n  border-color: inherit;\n  left: 0;\n  height: 5px;\n  display: block;\n  position: absolute;\n  -webkit-transform: translate(0, -50%) rotate(-45deg);\n          transform: translate(0, -50%) rotate(-45deg);\n  -webkit-transition: var(--pcb-link-transition);\n  transition: var(--pcb-link-transition);\n  top: 50%;\n  width: 5px;\n}\n.pcb__group__title.no-desc {\n  padding-left: 0;\n  pointer-events: none;\n}\n.pcb__group__title.no-desc::before {\n  content: none;\n}\n.pcb__group__txt {\n  font-size: 0.85em;\n  font-variation-settings: \"wght\" var(--pcb-font-weight-normal);\n  font-weight: var(--pcb-font-weight-normal);\n  line-height: 1.5;\n  margin-top: -0.5em;\n  padding: 0.5em 0 1.25em 1.25em;\n}\n.pcb__group__txt:empty {\n  display: none;\n}\n.pcb__group > details[open] .pcb__group__title::before {\n  -webkit-transform: translate(0, -50%) rotate(45deg);\n          transform: translate(0, -50%) rotate(45deg);\n}\n.pcb__badge {\n  background: var(--pcb-badge-bg);\n  border: none;\n  border-radius: var(--pcb-badge-border-radius);\n  -webkit-box-shadow: none;\n          box-shadow: none;\n  bottom: 1.25em;\n  color: var(--pcb-badge-color);\n  cursor: pointer;\n  display: flex;\n  font-size: initial;\n  height: initial;\n  left: 1.25em;\n  padding: 0.55em;\n  position: fixed;\n  -webkit-transition: var(--pcb-link-transition);\n  transition: var(--pcb-link-transition);\n  -webkit-transform: none;\n          transform: none;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n      -ms-user-select: none;\n          user-select: none;\n  visibility: hidden;\n  width: auto;\n}\n.pcb__badge:active, .pcb__badge:hover, .pcb__badge:focus {\n  background: var(--pcb-badge-bg-hover);\n  border: inherit;\n  -webkit-box-shadow: inherit;\n          box-shadow: inherit;\n  color: var(--pcb-badge-color-hover);\n  -webkit-transform: inherit;\n          transform: inherit;\n}\n.pcb__badge.is-visible {\n  visibility: visible;\n}"
  },
  {
    "path": "app/default-files/gdpr-assets/gdpr.js",
    "content": "(function(win) {\n    if (!document.querySelector('.pcb')) {\n        return;\n    }\n\n    var cbConfig = {\n        behaviour: document.querySelector('.pcb').getAttribute('data-behaviour'),\n        behaviourLink: document.querySelector('.pcb').getAttribute('data-behaviour-link'),\n        revision: document.querySelector('.pcb').getAttribute('data-revision'),\n        configTTL: parseInt(document.querySelector('.pcb').getAttribute('data-config-ttl'), 10),\n        debugMode: document.querySelector('.pcb').getAttribute('data-debug-mode') === 'true',\n        initialState: null,\n        initialLsState: null,\n        previouslyAccepted: []\n    };\n\n    var cbUI = {\n        wrapper: document.querySelector('.pcb'),\n        banner: {\n            element: null,\n            btnAccept: null,\n            btnReject: null,\n            btnConfigure: null\n        },\n        popup: {\n            element: null,\n            btnClose: null,\n            btnSave: null,\n            btnAccept: null,\n            btnReject: null,\n            checkboxes: null,\n        },\n        overlay: null,\n        badge: null,\n        blockedScripts: document.querySelectorAll('script[type^=\"gdpr-blocker/\"]'),\n        triggerLinks: cbConfig.behaviourLink ? document.querySelectorAll('a[href*=\"' + cbConfig.behaviourLink + '\"]') : null\n    };\n\n    function initUI () {\n        // setup banner elements\n        cbUI.banner.element = cbUI.wrapper.querySelector('.pcb__banner');\n        cbUI.banner.btnAccept = cbUI.banner.element.querySelector('.pcb__btn--accept');\n        cbUI.banner.btnReject = cbUI.banner.element.querySelector('.pcb__btn--reject');\n        cbUI.banner.btnConfigure = cbUI.banner.element.querySelector('.pcb__btn--configure');\n\n        // setup popup elements\n        if (cbUI.wrapper.querySelector('.pcb__popup')) {\n            cbUI.popup.element = cbUI.wrapper.querySelector('.pcb__popup');\n            cbUI.popup.btnClose = cbUI.wrapper.querySelector('.pcb__popup__close');\n            cbUI.popup.btnSave = cbUI.popup.element.querySelector('.pcb__btn--save');\n            cbUI.popup.btnAccept = cbUI.popup.element.querySelector('.pcb__btn--accept');\n            cbUI.popup.btnReject = cbUI.popup.element.querySelector('.pcb__btn--reject');\n            cbUI.popup.checkboxes = cbUI.popup.element.querySelector('input[type=\"checkbox\"]');\n            // setup overlay\n            cbUI.overlay = cbUI.wrapper.querySelector('.pcb__overlay');\n        }\n\n        cbUI.badge = cbUI.wrapper.querySelector('.pcb__badge');\n\n        if (cbConfig.behaviour.indexOf('link') > -1) {\n            for (var i = 0; i < cbUI.triggerLinks.length; i++) {\n                cbUI.triggerLinks[i].addEventListener('click', function(e) {\n                    e.preventDefault();\n                    showBannerOrPopup();\n                });\n            }\n        }\n    }\n\n    function initState () {\n        var lsKeyName = getConfigName();\n        var currentConfig = localStorage.getItem(lsKeyName);\n        var configIsFresh = checkIfConfigIsFresh();\n\n        if (!configIsFresh || currentConfig === null) {\n            if (cbConfig.debugMode) {\n                console.log('🍪 Config not found, or configuration expired');\n            }\n\n            if (window.publiiCBGCM) {\n                gtag('consent', 'default', {\n                    'ad_storage': window.publiiCBGCM.defaultState.ad_storage ? 'granted' : 'denied',\n                    'ad_personalization': window.publiiCBGCM.defaultState.ad_personalization ? 'granted' : 'denied',\n                    'ad_user_data': window.publiiCBGCM.defaultState.ad_user_data ? 'granted' : 'denied',\n                    'analytics_storage': window.publiiCBGCM.defaultState.analytics_storage ? 'granted' : 'denied',\n                    'personalization_storage': window.publiiCBGCM.defaultState.personalization_storage ? 'granted' : 'denied',\n                    'functionality_storage': window.publiiCBGCM.defaultState.functionality_storage ? 'granted' : 'denied',\n                    'security_storage': window.publiiCBGCM.defaultState.security_storage ? 'granted' : 'denied'\n                });  \n                \n                if (cbConfig.debugMode) {\n                    console.log('🍪 GCMv2 DEFAULT STATE: ' + JSON.stringify({\n                        'ad_storage': window.publiiCBGCM.defaultState.ad_storage ? 'granted' : 'denied',\n                        'ad_personalization': window.publiiCBGCM.defaultState.ad_personalization ? 'granted' : 'denied',\n                        'ad_user_data': window.publiiCBGCM.defaultState.ad_user_data ? 'granted' : 'denied',\n                        'analytics_storage': window.publiiCBGCM.defaultState.analytics_storage ? 'granted' : 'denied',\n                        'personalization_storage': window.publiiCBGCM.defaultState.personalization_storage ? 'granted' : 'denied',\n                        'functionality_storage': window.publiiCBGCM.defaultState.functionality_storage ? 'granted' : 'denied',\n                        'security_storage': window.publiiCBGCM.defaultState.security_storage ? 'granted' : 'denied'\n                    }));\n                }\n            }\n\n            showBanner();\n        } else if (typeof currentConfig === 'string') {\n            if (cbConfig.debugMode) {\n                console.log('🍪 Config founded');\n            }\n\n            cbConfig.initialLsState = currentConfig.split(',');\n\n            if (window.publiiCBGCM) {\n                gtag('consent', 'default', {\n                    'ad_storage': getDefaultConsentState(currentConfig, 'ad_storage'),\n                    'ad_personalization': getDefaultConsentState(currentConfig, 'ad_personalization'),\n                    'ad_user_data': getDefaultConsentState(currentConfig, 'ad_user_data'),\n                    'analytics_storage': getDefaultConsentState(currentConfig, 'analytics_storage'),\n                    'personalization_storage': getDefaultConsentState(currentConfig, 'personalization_storage'),\n                    'functionality_storage': getDefaultConsentState(currentConfig, 'functionality_storage'),\n                    'security_storage': getDefaultConsentState(currentConfig, 'security_storage')\n                });\n                \n                if (cbConfig.debugMode) {\n                    console.log('🍪 GCMv2 DEFAULT STATE: ' + JSON.stringify({\n                        'ad_storage': getDefaultConsentState(currentConfig, 'ad_storage'),\n                        'ad_personalization': getDefaultConsentState(currentConfig, 'ad_personalization'),\n                        'ad_user_data': getDefaultConsentState(currentConfig, 'ad_user_data'),\n                        'analytics_storage': getDefaultConsentState(currentConfig, 'analytics_storage'),\n                        'personalization_storage': getDefaultConsentState(currentConfig, 'personalization_storage'),\n                        'functionality_storage': getDefaultConsentState(currentConfig, 'functionality_storage'),\n                        'security_storage': getDefaultConsentState(currentConfig, 'security_storage')\n                    }));\n                }\n            }\n\n            showBadge();\n\n            if (cbUI.popup.element) {\n                var allowedGroups = currentConfig.split(',');\n                var checkedCheckboxes = cbUI.popup.element.querySelectorAll('input[type=\"checkbox\"]:checked');\n\n                for (var j = 0; j < checkedCheckboxes.length; j++) {\n                    var name = checkedCheckboxes[j].getAttribute('data-group-name');\n\n                    if (name && name !== '-' && allowedGroups.indexOf(name) === -1) {\n                        checkedCheckboxes[j].checked = false;\n                    }\n                }\n\n                for (var i = 0; i < allowedGroups.length; i++) {\n                    var checkbox = cbUI.popup.element.querySelector('input[type=\"checkbox\"][data-group-name=\"' + allowedGroups[i] + '\"]');\n\n                    if (checkbox) {\n                        checkbox.checked = true;\n                    }\n\n                    allowCookieGroup(allowedGroups[i]);\n                }\n            }\n        }\n\n        setTimeout(function () {\n            cbConfig.initialState = getInitialStateOfConsents();\n        }, 0);\n    }\n\n    function checkIfConfigIsFresh () {\n        var lastConfigSave = localStorage.getItem('publii-gdpr-cookies-config-save-date');\n\n        if (lastConfigSave === null) {\n            return false;\n        }\n\n        lastConfigSave = parseInt(lastConfigSave, 10);\n\n        if (lastConfigSave === 0) {\n            return true;\n        }\n\n        if (+new Date() - lastConfigSave < cbConfig.configTTL * 24 * 60 * 60 * 1000) {\n            return true;\n        }\n\n        return false;\n    }\n\n    function getDefaultConsentState (currentConfig, consentGroup) {\n        let configGroups = currentConfig.split(',');\n\n        for (let i = 0; i < configGroups.length; i++) {\n            let groupName = configGroups[i];\n            let group = window.publiiCBGCM.groups.find(group => group.cookieGroup === groupName);\n\n            if (group && group[consentGroup]) {\n                return 'granted';\n            }\n        }  \n        \n        if (window.publiiCBGCM.defaultState[consentGroup]) {\n            return 'granted'; \n        }\n        \n        return 'denied';\n    }\n\n    function initBannerEvents () {\n        cbUI.banner.btnAccept.addEventListener('click', function (e) {\n            e.preventDefault();\n            acceptAllCookies('banner');\n            showBadge();\n        }, false);\n\n        if (cbUI.banner.btnReject) {\n            cbUI.banner.btnReject.addEventListener('click', function (e) {\n                e.preventDefault();\n                rejectAllCookies();\n                showBadge();\n            }, false);\n        }\n\n        if (cbUI.banner.btnConfigure) {\n            cbUI.banner.btnConfigure.addEventListener('click', function (e) {\n                e.preventDefault();\n                hideBanner();\n                showAdvancedPopup();\n                showBadge();\n            }, false);\n        }\n    }\n\n    function initPopupEvents () {\n        if (!cbUI.popup.element) {\n            return;\n        }\n\n        cbUI.overlay.addEventListener('click', function (e) {\n            hideAdvancedPopup();\n        }, false);\n\n        cbUI.popup.element.addEventListener('click', function (e) {\n            e.stopPropagation();\n        }, false);\n\n        cbUI.popup.btnAccept.addEventListener('click', function (e) {\n            e.preventDefault();\n            acceptAllCookies('popup');\n        }, false);\n\n        cbUI.popup.btnReject.addEventListener('click', function (e) {\n            e.preventDefault();\n            rejectAllCookies();\n        }, false);\n\n        cbUI.popup.btnSave.addEventListener('click', function (e) {\n            e.preventDefault();\n            saveConfiguration();\n        }, false);\n\n        cbUI.popup.btnClose.addEventListener('click', function (e) {\n            e.preventDefault();\n            hideAdvancedPopup();\n        }, false);\n    }\n\n    function initBadgeEvents () {\n        if (!cbUI.badge) {\n            return;\n        }\n\n        cbUI.badge.addEventListener('click', function (e) {\n            showBannerOrPopup();\n        }, false);\n    }\n\n    initUI();\n    initState();\n    initBannerEvents();\n    initPopupEvents();\n    initBadgeEvents();\n\n    /**\n     * API\n     */\n    function addScript (src, inline) {\n        var newScript = document.createElement('script');\n\n        if (src) {\n            newScript.setAttribute('src', src);\n        }\n\n        if (inline) {\n            newScript.text = inline;\n        }\n\n        document.body.appendChild(newScript);\n    }\n\n    function allowCookieGroup (allowedGroup) {\n        var scripts = document.querySelectorAll('script[type=\"gdpr-blocker/' + allowedGroup + '\"]');\n        cbConfig.previouslyAccepted.push(allowedGroup);\n    \n        for (var j = 0; j < scripts.length; j++) {\n            addScript(scripts[j].src, scripts[j].text);\n        }\n\n        var groupEvent = new Event('publii-cookie-banner-unblock-' + allowedGroup);\n        document.body.dispatchEvent(groupEvent);\n        unlockEmbeds(allowedGroup);\n\n        if (cbConfig.debugMode) {\n            console.log('🍪 Allowed group: ' + allowedGroup);\n        }\n\n        if (window.publiiCBGCM && (!cbConfig.initialLsState || cbConfig.initialLsState.indexOf(allowedGroup) === -1)) {\n            let consentResult = {};\n            let group = window.publiiCBGCM.groups.find(group => group.cookieGroup === allowedGroup);\n\n            if (group) {\n                let foundSomeConsents = false;\n\n                Object.keys(group).forEach(key => {\n                    if (key !== 'cookieGroup' && group[key] === true) {\n                        consentResult[key] = 'granted';\n                        foundSomeConsents = true;\n                    }\n                });\n\n                if (foundSomeConsents) {\n                    gtag('consent', 'update', consentResult);   \n\n                    if (cbConfig.debugMode) {\n                        console.log('🍪 GCMv2 UPDATE: ' + JSON.stringify(consentResult));\n                    }\n                }\n            }\n        }\n    }\n\n    function showBannerOrPopup () {\n        if (cbUI.popup.element) {\n            showAdvancedPopup();\n        } else {\n            showBanner();\n        }\n    }\n\n    function showAdvancedPopup () {\n        cbUI.popup.element.classList.add('is-visible');\n        cbUI.overlay.classList.add('is-visible');\n        cbUI.popup.element.setAttribute('aria-hidden', 'false');\n        cbUI.overlay.setAttribute('aria-hidden', 'false');\n    }\n\n    function hideAdvancedPopup () {\n        cbUI.popup.element.classList.remove('is-visible');\n        cbUI.overlay.classList.remove('is-visible');\n        cbUI.popup.element.setAttribute('aria-hidden', 'true');\n        cbUI.overlay.setAttribute('aria-hidden', 'true');\n    }\n\n    function showBanner () {\n        cbUI.banner.element.classList.add('is-visible');\n        cbUI.banner.element.setAttribute('aria-hidden', 'false');\n    }\n\n    function hideBanner () {\n        cbUI.banner.element.classList.remove('is-visible');\n        cbUI.banner.element.setAttribute('aria-hidden', 'true');\n    }\n\n    function showBadge () {\n        if (!cbUI.badge) {\n            return;\n        }\n\n        cbUI.badge.classList.add('is-visible');\n        cbUI.badge.setAttribute('aria-hidden', 'false');\n    }\n\n    function getConfigName () {\n        var lsKeyName = 'publii-gdpr-allowed-cookies';\n\n        if (cbConfig.revision) {\n            lsKeyName = lsKeyName + '-v' + parseInt(cbConfig.revision, 10);\n        }\n\n        return lsKeyName;\n    }\n\n    function storeConfiguration (allowedGroups) {\n        var lsKeyName = getConfigName();\n        var dataToStore = allowedGroups.join(',');\n        localStorage.setItem(lsKeyName, dataToStore);\n\n        if (cbConfig.configTTL === 0) {\n            localStorage.setItem('publii-gdpr-cookies-config-save-date', 0);\n\n            if (cbConfig.debugMode) {\n                console.log('🍪 Store never expiring configuration');\n            }\n        } else {\n            localStorage.setItem('publii-gdpr-cookies-config-save-date', +new Date());\n        }\n    }\n\n    function getInitialStateOfConsents () {\n        if (!cbUI.popup.element) {\n            return [];\n        }\n\n        var checkedGroups = cbUI.popup.element.querySelectorAll('input[type=\"checkbox\"]:checked');\n        var groups = [];\n\n        for (var i = 0; i < checkedGroups.length; i++) {\n            var allowedGroup = checkedGroups[i].getAttribute('data-group-name');\n\n            if (allowedGroup !== '') {\n                groups.push(allowedGroup);\n            }\n        }\n\n        if (cbConfig.debugMode) {\n            console.log('🍪 Initial state: ' + groups.join(', '));\n        }\n\n        return groups;\n    }\n\n    function getCurrentStateOfConsents () {\n        if (!cbUI.popup.element) {\n            return [];\n        }\n\n        var checkedGroups = cbUI.popup.element.querySelectorAll('input[type=\"checkbox\"]:checked');\n        var groups = [];\n\n        for (var i = 0; i < checkedGroups.length; i++) {\n            var allowedGroup = checkedGroups[i].getAttribute('data-group-name');\n\n            if (allowedGroup !== '') {\n                groups.push(allowedGroup);\n            }\n        }\n\n        if (cbConfig.debugMode) {\n            console.log('🍪 State to save: ' + groups.join(', '));\n        }\n\n        return groups;\n    }\n\n    function getAllGroups () {\n        if (!cbUI.popup.element) {\n            return [];\n        }\n\n        var checkedGroups = cbUI.popup.element.querySelectorAll('input[type=\"checkbox\"]');\n        var groups = [];\n\n        for (var i = 0; i < checkedGroups.length; i++) {\n            var allowedGroup = checkedGroups[i].getAttribute('data-group-name');\n\n            if (allowedGroup !== '') {\n                groups.push(allowedGroup);\n            }\n        }\n\n        return groups;\n    }\n\n    function acceptAllCookies (source) {\n        var groupsToAccept = getAllGroups();\n        storeConfiguration(groupsToAccept);\n\n        for (var i = 0; i < groupsToAccept.length; i++) {\n            var group = groupsToAccept[i];\n\n            if (cbConfig.initialState.indexOf(group) > -1 || cbConfig.previouslyAccepted.indexOf(group) > -1) {\n                if (cbConfig.debugMode) {\n                    console.log('🍪 Skip previously activated group: ' + group);\n                }\n\n                continue;\n            }\n\n            allowCookieGroup(group);\n        }\n\n        if (cbUI.popup.element) {\n            var checkboxesToCheck = cbUI.popup.element.querySelectorAll('input[type=\"checkbox\"]');\n\n            for (var j = 0; j < checkboxesToCheck.length; j++) {\n                checkboxesToCheck[j].checked = true;\n            }\n        }\n\n        if (cbConfig.debugMode) {\n            console.log('🍪 Accept all cookies: ', groupsToAccept.join(', '));\n        }\n\n        if (source === 'popup') {\n            hideAdvancedPopup();\n        } else if (source === 'banner') {\n            hideBanner();\n        }\n    }\n\n    function rejectAllCookies () {\n        if (cbConfig.debugMode) {\n            console.log('🍪 Reject all cookies');\n        }\n\n        storeConfiguration([]);\n        setTimeout(function () {\n            window.location.reload();\n        }, 100);\n    }\n\n    function saveConfiguration () {\n        var groupsToAccept = getCurrentStateOfConsents();\n        storeConfiguration(groupsToAccept);\n\n        if (cbConfig.debugMode) {\n            console.log('🍪 Save new config: ', groupsToAccept.join(', '));\n        }\n\n        if (reloadIsNeeded(groupsToAccept)) {\n            setTimeout(function () {\n                window.location.reload();\n            }, 100);\n            return;\n        }\n\n        for (var i = 0; i < groupsToAccept.length; i++) {\n            var group = groupsToAccept[i];\n\n            if (cbConfig.initialState.indexOf(group) > -1 || cbConfig.previouslyAccepted.indexOf(group) > -1) {\n                if (cbConfig.debugMode) {\n                    console.log('🍪 Skip previously activated group: ' + group);\n                }\n\n                continue;\n            }\n\n            allowCookieGroup(group);\n        }\n\n        hideAdvancedPopup();\n    }\n\n    function reloadIsNeeded (groupsToAccept) {\n        // check if user rejected consent for initial groups\n        var initialGroups = cbConfig.initialState;\n        var previouslyAcceptedGroups = cbConfig.previouslyAccepted;\n        var groupsToCheck = initialGroups.concat(previouslyAcceptedGroups);\n\n        for (var i = 0; i < groupsToCheck.length; i++) {\n            var groupToCheck = groupsToCheck[i];\n\n            if (groupToCheck !== '' && groupsToAccept.indexOf(groupToCheck) === -1) {\n                if (cbConfig.debugMode) {\n                    console.log('🍪 Reload is needed due lack of: ', groupToCheck);\n                }\n\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    function unlockEmbeds (cookieGroup) {\n        var iframesToUnlock = document.querySelectorAll('.pec-wrapper[data-consent-group-id=\"' + cookieGroup + '\"]');\n\n        for (var i = 0; i < iframesToUnlock.length; i++) {\n            var iframeWrapper = iframesToUnlock[i];\n            iframeWrapper.querySelector('.pec-overlay').classList.remove('is-active');\n            iframeWrapper.querySelector('.pec-overlay').setAttribute('aria-hidden', 'true');\n            var iframe = iframeWrapper.querySelector('iframe');\n            iframe.setAttribute('src', iframe.getAttribute('data-consent-src'));\n        }\n    }\n\n    win.publiiEmbedConsentGiven = function (cookieGroup) {\n        // it will unlock embeds\n        allowCookieGroup(cookieGroup);\n\n        var checkbox = cbUI.popup.element.querySelector('input[type=\"checkbox\"][data-group-name=\"' + cookieGroup + '\"]');\n\n        if (checkbox) {\n            checkbox.checked = true;\n        }\n\n        var groupsToAccept = getCurrentStateOfConsents();\n        storeConfiguration(groupsToAccept);\n\n        if (cbConfig.debugMode) {\n            console.log('🍪 Save new config: ', groupsToAccept.join(', '));\n        }\n    }\n})(window);\n"
  },
  {
    "path": "app/default-files/gdpr-assets/template.html",
    "content": "<!-- Start main cookie container -->\n<div \n    class=\"pcb\"\n    data-behaviour=\"{{behaviour}}\"\n    data-behaviour-link=\"{{behaviourLink}}\"\n    data-revision=\"{{cookieSettingsRevision}}\"\n    data-config-ttl=\"{{cookieSettingsTTL}}\"\n    data-debug-mode=\"{{debugMode}}\">\n    <!-- Start Banner -->\n    <div \n        role=\"dialog\" \n        aria-modal=\"true\" \n        aria-hidden=\"true\" \n        aria-labelledby=\"pcb-title\" \n        aria-describedby=\"pcb-txt\" \n        class=\"pcb__banner {{bannerPositionCssClass}}\">\n        <div class=\"pcb__inner\">\n            <div \n                id=\"pcb-title\" \n                role=\"heading\" \n                aria-level=\"2\" \n                class=\"pcb__title\">\n                {{popupTitlePrimary}}\n            </div>\n            <div \n                id=\"pcb-txt\" \n                class=\"pcb__txt\">\n                {{popupDesc}}\n                {{privacyPolicyLink}}\n            </div>\n          \n            <div class=\"pcb__buttons\">\n                {{#allowAdvancedConfiguration}}\n                <button \n                    type=\"button\" \n                    class=\"pcb__btn pcb__btn--link pcb__btn--configure\" \n                    aria-haspopup=\"dialog\">\n                    {{advancedConfigurationLinkLabel}}\n                </button>\n                {{/allowAdvancedConfiguration}}\n                {{#showRejectButton}}\n                <button \n                    type=\"button\" \n                    class=\"pcb__btn pcb__btn--reject\">\n                    {{rejectButtonLabel}}\n                </button>\n                {{/showRejectButton}}\n                <button \n                    type=\"button\" \n                    class=\"pcb__btn pcb__btn--solid pcb__btn--accept\">\n                    {{saveButtonLabel}}\n                </button>\n            </div>\n        </div>\n    </div>\n    <!-- End of Banner -->\n\n    {{#allowAdvancedConfiguration}}\n    <!-- Start Popup -->\n    <div \n        class=\"pcb__popup\" \n        role=\"dialog\" \n        aria-modal=\"true\" \n        aria-hidden=\"true\" \n        aria-labelledby=\"pcb-popup-title\">\n        <div class=\"pcb__popup__wrapper\">\n            <div class=\"pcb__inner pcb__popup__inner\">\n                <div class=\"pcb__popup__heading\">\n                    <div \n                        id=\"pcb-popup-title\" \n                        role=\"heading\" \n                        aria-level=\"2\"\n                        class=\"pcb__title\">\n                        {{advancedConfigurationTitle}}\n                    </div>\n                    <button class=\"pcb__popup__close\" aria-label=\"Close\"></button>\n                </div>\n             \n                <div class=\"pcb__popup__content\">\n                    <div class=\"pcb__txt pcb__popup__txt\">\n                        {{advancedConfigurationDescription}}\n\n                        {{#advancedConfigurationShowDescriptionLink}}\n                        {{privacyPolicyLink}}\n                        {{/advancedConfigurationShowDescriptionLink}}\n                    </div>\n                \n                    <ul class=\"pcb__groups\">\n                        {{cbGroups}}\n                    </ul>\n                </div>\n\n                <div class=\"pcb__buttons pcb__popup__buttons\">\n                    <button type=\"button\" class=\"pcb__btn pcb__btn--solid pcb__btn--accept\">{{advancedConfigurationAcceptButtonLabel}}</button>\n                    <button type=\"button\" class=\"pcb__btn pcb__btn--reject\">{{advancedConfigurationRejectButtonLabel}}</button>\n                    <button type=\"button\" class=\"pcb__btn pcb__btn--save\">{{advancedConfigurationSaveButtonLabel}}</button>\n                </div>\n            </div>\n        </div>\n    </div>\n    <!-- End of Popup -->\n    <!-- Overlay -->\n    <div class=\"pcb__overlay\" aria-hidden=\"true\"></div>\n    <!-- End of Overlay -->\n    {{/allowAdvancedConfiguration}}\n\n    {{#showBadge}}\n    <!-- Badge -->\n    <button \n        class=\"pcb__badge\" \n        aria-label=\"{{badgeLabel}}\"\n        aria-hidden=\"true\">\n        <svg \n            xmlns=\"http://www.w3.org/2000/svg\" \n            aria-hidden=\"true\" \n            focusable=\"false\" \n            width=\"40\" \n            height=\"40\" \n            viewBox=\"0 0 23 23\" \n            fill=\"currentColor\">\n            <path d=\"M21.41 12.71c-.08-.01-.15 0-.22 0h-.03c-.03 0-.05 0-.08.01-.07 0-.13.01-.19.04-.52.21-1.44.19-2.02-.22-.44-.31-.65-.83-.62-1.53a.758.758 0 0 0-.27-.61.73.73 0 0 0-.65-.14c-1.98.51-3.49.23-4.26-.78-.82-1.08-.73-2.89.24-4.49.14-.23.14-.52 0-.75a.756.756 0 0 0-.67-.36c-.64.03-1.11-.1-1.31-.35-.19-.26-.13-.71-.01-1.29.04-.18.06-.38.03-.59-.05-.4-.4-.7-.81-.66C5.1 1.54 1 6.04 1 11.48 1 17.28 5.75 22 11.6 22c5.02 0 9.39-3.54 10.39-8.42.08-.4-.18-.78-.58-.87Zm-9.81 7.82c-5.03 0-9.12-4.06-9.12-9.06 0-4.34 3.05-8 7.25-8.86-.08.7.05 1.33.42 1.81.24.32.66.67 1.38.84-.76 1.86-.65 3.78.36 5.11.61.81 2.03 2 4.95 1.51.18.96.71 1.54 1.18 1.87.62.43 1.38.62 2.1.62.05 0 .09 0 .13-.01-1.23 3.64-4.7 6.18-8.64 6.18ZM13 17c0 .55-.45 1-1 1s-1-.45-1-1 .45-1 1-1 1 .45 1 1Zm5.29-12.3a.99.99 0 0 1-.29-.71c0-.55.45-.99 1-.99a1 1 0 0 1 .71.3c.19.19.29.44.29.71 0 .55-.45.99-1 .99a1 1 0 0 1-.71-.3ZM9 13.5c0 .83-.67 1.5-1.5 1.5S6 14.33 6 13.5 6.67 12 7.5 12s1.5.67 1.5 1.5Zm3.25.81a.744.744 0 0 1-.06-1.05c.28-.32.75-.34 1.05-.06.31.28.33.75.05 1.06-.15.16-.35.25-.56.25-.18 0-.36-.06-.5-.19ZM8.68 7.26c.41.37.44 1 .07 1.41-.2.22-.47.33-.75.33a.96.96 0 0 1-.67-.26c-.41-.37-.44-1-.07-1.41.37-.42 1-.45 1.41-.08Zm11.48 1.88c.18-.19.52-.19.7 0 .05.04.09.1.11.16.03.06.04.12.04.19 0 .13-.05.26-.15.35-.09.1-.22.15-.35.15s-.26-.05-.35-.15a.355.355 0 0 1-.11-.16.433.433 0 0 1-.04-.19c0-.13.05-.26.15-.35Zm-4.93-1.86a.75.75 0 1 1 1.059-1.06.75.75 0 0 1-1.059 1.06Z\" />\n        </svg>\n    </button>\n    <!-- End of Badge -->\n    {{/showBadge}}\n</div>\n<!-- End of main cookie container -->\n"
  },
  {
    "path": "app/default-files/theme-files/config.json",
    "content": "{\n    \"postTemplates\": {},\n    \"pageTemplates\": {},\n    \"tagTemplates\": {},\n    \"authorTemplates\": {},\n    \"defaultTemplates\": {\n        \"post\": \"\",\n        \"page\": \"\"\n    },\n    \"menus\": {},\n    \"renderer\": {\n        \"relatedPostsNumber\": 5,\n        \"renderRelatedPosts\": true,\n        \"renderSimilarPosts\": true,\n        \"renderPrevNextPosts\": true,\n        \"createContentStructure\": true,\n        \"includeFeaturedInPosts\": true,\n        \"tagsIncludeFeaturedInPosts\": true,\n        \"authorsIncludeFeaturedInPosts\": true,\n        \"featuredPostsNumber\": 0,\n        \"tagsFeaturedPostsNumber\": 0,\n        \"authorsFeaturedPostsNumber\": 0,\n        \"create404page\": false,\n        \"createSearchPage\": false,\n        \"createAuthorPages\": true,\n        \"createTagPages\": true,\n        \"customHTML\": false,\n        \"includeHandlebarsInHelpers\": false\n    },\n    \"extensions\": {\n        \"postEditorConfigOverride\": false,\n        \"postEditorCustomScript\": false\n    },\n    \"config\": [\n        {\n            \"name\": \"logo\",\n            \"label\": \"Website logo\",\n            \"value\": \"\",\n            \"type\": \"media\",\n            \"upload\": true\n        },\n        {\n            \"name\": \"postsPerPage\",\n            \"label\": \"Posts per page\",\n            \"value\": 5,\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"tagsPostsPerPage\",\n            \"label\": \"Tags posts per page\",\n            \"value\": 5,\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"authorsPostsPerPage\",\n            \"label\": \"Authors posts per page\",\n            \"value\": 5,\n            \"type\": \"number\"\n        },\n        {\n            \"name\": \"excerptLength\",\n            \"label\": \"Excerpt length\",\n            \"value\": 30,\n            \"type\": \"number\"\n        }\n    ],\n    \"customConfig\": [],\n    \"postConfig\": [],\n    \"pageConfig\": [],\n    \"tagConfig\": [],\n    \"authorConfig\": [],\n    \"files\": {\n        \"ignoreAssets\": [\n            \"scss\",\n            \".DS_Store\"\n        ],\n        \"assetsPath\": \"assets\",\n        \"partialsPath\": \"partials\",\n        \"responsiveImages\": {\n            \"galleryImages\": {\n                \"sizes\": \"\",\n                \"dimensions\": {\n                    \"thumbnail\": {\n                        \"width\": 240,\n                        \"height\": 240,\n                        \"crop\": true\n                    }\n                }\n            }\n        }\n    },\n    \"customElements\": []\n}\n"
  },
  {
    "path": "app/default-files/theme-files/feed-json.hbs",
    "content": "{\n    \"version\": \"https://jsonfeed.org/version/1\",\n    \"title\": \"{{siteName}}\",\n    \"description\": \"\",\n    \"home_page_url\": \"{{siteDomain}}\",\n    \"feed_url\": \"{{siteDomain}}/feed.json\",\n    \"user_comment\": \"\",\n    {{#if siteLogo}}\n    \"icon\": \"{{siteLogo}}\",\n    {{/if}}\n    \"author\": {\n        {{#siteAuthor}}\n        \"name\": \"{{name}}\"\n        {{/siteAuthor}}\n    },\n    \"items\": [\n        {{#each posts}}\n        {\n            \"id\": \"{{url}}\",\n            \"url\": \"{{url}}\",\n            \"title\": \"{{title}}\",\n            {{#if excerpt}}\n            \"summary\": {{{jsonify excerpt}}},\n            {{/if}}\n            {{#if text}}\n            \"content_html\": {{{jsonify text}}},\n            {{/if}}\n            {{#if thumbnail}}\n            \"image\": \"{{thumbnail.url}}\",\n            {{/if}}\n            \"author\": {\n                {{#author}}\n                \"name\": \"{{name}}\"\n                {{/author}}\n            },\n            \"tags\": [\n                {{#each categories}}\n                   \"{{name}}\"{{#unless @last}},{{/unless}}\n                {{/each}}\n            ],\n            \"date_published\": \"{{date createdAt \"YYYY-MM-DDTHH:mm:ssZ\"}}\",\n            \"date_modified\": \"{{date modifiedAt \"YYYY-MM-DDTHH:mm:ssZ\"}}\"\n        }{{#unless @last}},{{/unless}}\n        {{/each}}\n    ]\n}\n"
  },
  {
    "path": "app/default-files/theme-files/feed-xml.hbs",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\" xmlns:media=\"http://search.yahoo.com/mrss/\">\n    <title>{{siteName}}</title>\n    <link href=\"{{siteDomain}}/feed.xml\" rel=\"self\" />\n    <link href=\"{{siteDomain}}\" />\n    <updated>{{date siteLastUpdate \"YYYY-MM-DDTHH:mm:ssZ\"}}</updated>\n    <author>\n        {{#siteAuthor}}\n        <name>{{name}}</name>\n        {{/siteAuthor}}\n    </author>\n    <id>{{siteDomain}}</id>\n\n    {{#each posts}}\n    <entry>\n        <title>{{title}}</title>\n        <author>\n            {{#author}}\n            <name>{{name}}</name>\n            {{/author}}\n        </author>\n        <link href=\"{{url}}\"/>\n        <id>{{url}}</id>\n        {{#if thumbnail}}\n        <media:content url=\"{{thumbnail.url}}\" medium=\"image\" />\n        {{/if}}\n        {{#each categories}}\n            <category term=\"{{name}}\"/>\n        {{/each}}\n\n        {{#checkIf ../updatedDateType \"==\" \"createdAt\"}}\n        <updated>{{date createdAt \"YYYY-MM-DDTHH:mm:ssZ\"}}</updated>\n        {{/checkIf}}\n        {{#checkIf ../updatedDateType \"==\" \"modifiedAt\"}}\n        <updated>{{date modifiedAt \"YYYY-MM-DDTHH:mm:ssZ\"}}</updated>\n        {{/checkIf}}\n        {{#if excerpt}}\n            <summary type=\"html\">\n                <![CDATA[\n                    {{#if thumbnail}}\n                        <img src=\"{{thumbnail.url}}\" alt=\"{{thumbnail.alt}}\" />\n                    {{/if}}\n                    {{{excerpt}}}\n                ]]>\n            </summary>\n        {{else}}\n            <summary></summary>\n        {{/if}}\n        {{#if text}}\n        <content type=\"html\">\n            <![CDATA[\n                {{#if thumbnail}}\n                    <p><img src=\"{{thumbnail.url}}\" class=\"type:primaryImage\" alt=\"{{thumbnail.alt}}\" /></p>\n                {{/if}}\n                {{{text}}}\n            ]]>\n        </content>\n        {{else}}\n        <content></content>\n        {{/if}}\n    </entry>\n    {{/each}}\n</feed>\n"
  },
  {
    "path": "app/default-files/theme-files/menu.hbs",
    "content": "{{#unless level}}\n<nav>\n{{/unless}}\n    <ul{{#if level}} class=\"submenu submenu-level-{{level}}\"{{else}} class=\"menu menu-level-1\"{{/if}}>\n        {{#each items}}\n            <li{{menuItemClasses}}>\n            {{#if link}}\n            <a href=\"{{menuUrl}}\"{{#if title}} title=\"{{title}}\"{{/if}}{{#if target}} target=\"{{target}}\"{{/if}}>{{label}}</a>\n            {{else}}\n            <span{{#if title}} title=\"{{title}}\"{{/if}}>{{label}}</span>\n            {{/if}}\n\n            {{#if items}}\n                {{> menu}}\n            {{/if}}\n        </li>\n        {{/each}}\n    </ul>\n{{#unless level}}\n</nav>\n{{/unless}}\n"
  },
  {
    "path": "app/default-files/theme-files/pagination.hbs",
    "content": "{{#if @pagination}}\n    <ul>\n        {{#if @pagination.previousPage}}\n            <li>\n                <a href=\"{{@pagination.previousPageUrl}}\">PREV</a>\n            </li>\n        {{/if}}\n\n        {{#each @pagination.pages}}\n            <li>\n                {{#isCurrentPage @pagination.currentPage this}}\n                    <strong>\n                        {{this}}\n                    </strong>\n                {{else}}\n                    <a href=\"{{pageUrl @pagination.context this}}\">\n                        {{this}}\n                    </a>\n                {{/isCurrentPage}}\n            </li>\n        {{/each}}\n\n        {{#if @pagination.nextPage}}\n            <li>\n                <a href=\"{{@pagination.nextPageUrl}}\">NEXT</a>\n            </li>\n        {{/if}}\n    </ul>\n\n    <small>\n        Page {{@pagination.currentPage}} of {{@pagination.totalPages}}\n    </small>\n{{/if}}\n"
  },
  {
    "path": "app/default-files/theme-files/sitemap.xsl",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xsl:stylesheet version=\"2.0\"\n    xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\n    xmlns:sitemap=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n    xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"\n    xmlns:html=\"http://www.w3.org/TR/REC-html40\">\n    <xsl:output version=\"1.0\" method=\"html\" encoding=\"UTF-8\" />\n    <xsl:template match=\"/\">\n        <html xmlns=\"http://www.w3.org/1999/xhtml\">\n            <head>\n                <title>Sitemap XML</title>\n                <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n                <style type=\"text/css\">\n                    html,\n                    body {\n                        color: #333;\n                        font-family: Arial, sans-serif;\n                        font-size: 14px;\n                        line-height: 1.4;\n                    }\n\n                    a {\n                        color: #000;\n                        text-decoration: none;\n                    }\n\n                    a:hover {\n                        text-decoration: underline;\n                    }\n\n                    .wrapper {\n                        margin: 20px auto;\n                        width: 1240px;\n                    }\n\n                    .header {\n                        font-size: 32px;\n                        font-weight: normal;\n                    }\n\n                    .info {\n                        margin: 25px 0;\n                    }\n\n                    .list {\n                        width: 100%;\n                    }\n\n                    .list,\n                    .list td,\n                    .list th {\n                        border: none;\n                    }\n\n                    .list td,\n                    .list th {\n                        padding: 5px;\n                    }\n\n                    .list thead th {\n                        background: #eee;\n                    }\n\n                    .list tr:nth-child(even) td {\n                        background-color: #f0f0f0;\n                    }\n                </style>\n            </head>\n\t\t    <body>\n                <div class=\"wrapper\">\n                    <h1 class=\"header\">Sitemap XML</h1>\n\n                    <div class=\"info\">This sitemap contains <xsl:value-of select=\"count(sitemap:urlset/sitemap:url)\"/> URLs.</div>\n                    <table class=\"list\" border=\"none\" cellspacing=\"0\" cellpadding=\"3\">\n                        <thead>\n                            <tr>\n                                <th width=\"70%\">URL</th>\n                                <th width=\"10%\">Images</th>\n                                <th width=\"20%\">Last Modified</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <xsl:for-each select=\"sitemap:urlset/sitemap:url\">\n                            <tr>\n                                <td>\n                                    <xsl:variable name=\"url\"><xsl:value-of select=\"sitemap:loc\"/></xsl:variable>\n                                    <a href=\"{$url}\"><xsl:value-of select=\"sitemap:loc\"/></a>\n                                </td>\n                                <td style=\"text-align: center;\">\n                                    <xsl:value-of select=\"count(image:image)\"/>\n                                </td>\n                                <td style=\"text-align: center; font-family: monospace;\">\n                                    <xsl:value-of select=\"concat( substring( sitemap:lastmod, 0, 11), concat(' ', substring( sitemap:lastmod, 12, 14 ) ) )\"/>\n                                </td>\n                            </tr>\n                            </xsl:for-each>\n                        </tbody>\n                    </table>\n                </div>\n            </body>\n\t\t</html>\n    </xsl:template>\n</xsl:stylesheet>\n"
  },
  {
    "path": "app/default-files/vendor/prism.js",
    "content": "/* PrismJS 1.29.0\nhttps://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+apacheconf+aspnet+bash+basic+batch+bbcode+c+csharp+cpp+cfscript+dart+docker+elixir+elm+gdscript+git+glsl+go+graphql+haml+handlebars+haskell+http+ini+java+js-extras+json+jsonp+kotlin+latex+less+lisp+lua+makefile+markdown+markup-templating+matlab+nasm+nginx+objectivec+pascal+perl+php+powershell+pug+python+r+jsx+regex+ruby+rust+sass+scss+scala+scheme+sql+swift+twig+typescript+vbnet+visual-basic+yaml */\nvar _self=\"undefined\"!=typeof window?window:\"undefined\"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\\s)lang(?:uage)?-([\\w-]+)(?=\\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/\\u00a0/g,\" \")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,\"__id\",{value:++t}),e.__id},clone:function e(n,t){var r,i;switch(t=t||{},a.util.type(n)){case\"Object\":if(i=a.util.objId(n),t[i])return t[i];for(var l in r={},t[i]=r,n)n.hasOwnProperty(l)&&(r[l]=e(n[l],t));return r;case\"Array\":return i=a.util.objId(n),t[i]?t[i]:(r=[],t[i]=r,n.forEach((function(n,a){r[a]=e(n,t)})),r);default:return n}},getLanguage:function(e){for(;e;){var t=n.exec(e.className);if(t)return t[1].toLowerCase();e=e.parentElement}return\"none\"},setLanguage:function(e,t){e.className=e.className.replace(RegExp(n,\"gi\"),\"\"),e.classList.add(\"language-\"+t)},currentScript:function(){if(\"undefined\"==typeof document)return null;if(\"currentScript\"in document)return document.currentScript;try{throw new Error}catch(r){var e=(/at [^(\\r\\n]*\\((.*):[^:]+:[^:]+\\)$/i.exec(r.stack)||[])[1];if(e){var n=document.getElementsByTagName(\"script\");for(var t in n)if(n[t].src==e)return n[t]}return null}},isActive:function(e,n,t){for(var r=\"no-\"+n;e;){var a=e.classList;if(a.contains(n))return!0;if(a.contains(r))return!1;e=e.parentElement}return!!t}},languages:{plain:r,plaintext:r,text:r,txt:r,extend:function(e,n){var t=a.util.clone(a.languages[e]);for(var r in n)t[r]=n[r];return t},insertBefore:function(e,n,t,r){var i=(r=r||a.languages)[e],l={};for(var o in i)if(i.hasOwnProperty(o)){if(o==n)for(var s in t)t.hasOwnProperty(s)&&(l[s]=t[s]);t.hasOwnProperty(o)||(l[o]=i[o])}var u=r[e];return r[e]=l,a.languages.DFS(a.languages,(function(n,t){t===u&&n!=e&&(this[n]=l)})),l},DFS:function e(n,t,r,i){i=i||{};var l=a.util.objId;for(var o in n)if(n.hasOwnProperty(o)){t.call(n,o,n[o],r||o);var s=n[o],u=a.util.type(s);\"Object\"!==u||i[l(s)]?\"Array\"!==u||i[l(s)]||(i[l(s)]=!0,e(s,t,o,i)):(i[l(s)]=!0,e(s,t,null,i))}}},plugins:{},highlightAll:function(e,n){a.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,t){var r={callback:t,container:e,selector:'code[class*=\"language-\"], [class*=\"language-\"] code, code[class*=\"lang-\"], [class*=\"lang-\"] code'};a.hooks.run(\"before-highlightall\",r),r.elements=Array.prototype.slice.apply(r.container.querySelectorAll(r.selector)),a.hooks.run(\"before-all-elements-highlight\",r);for(var i,l=0;i=r.elements[l++];)a.highlightElement(i,!0===n,r.callback)},highlightElement:function(n,t,r){var i=a.util.getLanguage(n),l=a.languages[i];a.util.setLanguage(n,i);var o=n.parentElement;o&&\"pre\"===o.nodeName.toLowerCase()&&a.util.setLanguage(o,i);var s={element:n,language:i,grammar:l,code:n.textContent};function u(e){s.highlightedCode=e,a.hooks.run(\"before-insert\",s),s.element.innerHTML=s.highlightedCode,a.hooks.run(\"after-highlight\",s),a.hooks.run(\"complete\",s),r&&r.call(s.element)}if(a.hooks.run(\"before-sanity-check\",s),(o=s.element.parentElement)&&\"pre\"===o.nodeName.toLowerCase()&&!o.hasAttribute(\"tabindex\")&&o.setAttribute(\"tabindex\",\"0\"),!s.code)return a.hooks.run(\"complete\",s),void(r&&r.call(s.element));if(a.hooks.run(\"before-highlight\",s),s.grammar)if(t&&e.Worker){var c=new Worker(a.filename);c.onmessage=function(e){u(e.data)},c.postMessage(JSON.stringify({language:s.language,code:s.code,immediateClose:!0}))}else u(a.highlight(s.code,s.grammar,s.language));else u(a.util.encode(s.code))},highlight:function(e,n,t){var r={code:e,grammar:n,language:t};if(a.hooks.run(\"before-tokenize\",r),!r.grammar)throw new Error('The language \"'+r.language+'\" has no grammar.');return r.tokens=a.tokenize(r.code,r.grammar),a.hooks.run(\"after-tokenize\",r),i.stringify(a.util.encode(r.tokens),r.language)},tokenize:function(e,n){var t=n.rest;if(t){for(var r in t)n[r]=t[r];delete n.rest}var a=new s;return u(a,a.head,e),o(e,a,n,a.head,0),function(e){for(var n=[],t=e.head.next;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=a.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=a.hooks.all[e];if(t&&t.length)for(var r,i=0;r=t[i++];)r(n)}},Token:i};function i(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||\"\").length}function l(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function o(e,n,t,r,s,g){for(var f in t)if(t.hasOwnProperty(f)&&t[f]){var h=t[f];h=Array.isArray(h)?h:[h];for(var d=0;d<h.length;++d){if(g&&g.cause==f+\",\"+d)return;var v=h[d],p=v.inside,m=!!v.lookbehind,y=!!v.greedy,k=v.alias;if(y&&!v.pattern.global){var x=v.pattern.toString().match(/[imsuy]*$/)[0];v.pattern=RegExp(v.pattern.source,x+\"g\")}for(var b=v.pattern||v,w=r.next,A=s;w!==n.tail&&!(g&&A>=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(j<O||\"string\"==typeof C.value);C=C.next)L++,j+=C.value.length;L--,E=e.slice(A,j),P.index-=A}else if(!(P=l(b,0,E,m)))continue;S=P.index;var N=P[0],_=E.slice(0,S),M=E.slice(S+N.length),W=A+E.length;g&&W>g.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+\",\"+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a<t&&r!==e.tail;a++)r=r.next;n.next=r,r.prev=n,e.length-=a}if(e.Prism=a,i.stringify=function e(n,t){if(\"string\"==typeof n)return n;if(Array.isArray(n)){var r=\"\";return n.forEach((function(n){r+=e(n,t)})),r}var i={type:n.type,content:e(n.content,t),tag:\"span\",classes:[\"token\",n.type],attributes:{},language:t},l=n.alias;l&&(Array.isArray(l)?Array.prototype.push.apply(i.classes,l):i.classes.push(l)),a.hooks.run(\"wrap\",i);var o=\"\";for(var s in i.attributes)o+=\" \"+s+'=\"'+(i.attributes[s]||\"\").replace(/\"/g,\"&quot;\")+'\"';return\"<\"+i.tag+' class=\"'+i.classes.join(\" \")+'\"'+o+\">\"+i.content+\"</\"+i.tag+\">\"},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener(\"message\",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute(\"data-manual\")&&(a.manual=!0)),!a.manual){var h=document.readyState;\"loading\"===h||\"interactive\"===h&&g&&g.defer?document.addEventListener(\"DOMContentLoaded\",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);\"undefined\"!=typeof module&&module.exports&&(module.exports=Prism),\"undefined\"!=typeof global&&(global.Prism=Prism);\nPrism.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\\s\\S])*?-->/,greedy:!0},prolog:{pattern:/<\\?[\\s\\S]+?\\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>\"'[\\]]|\"[^\"]*\"|'[^']*')+(?:\\[(?:[^<\"'\\]]|\"[^\"]*\"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\\]\\s*)?>/i,greedy:!0,inside:{\"internal-subset\":{pattern:/(^[^\\[]*\\[)[\\s\\S]+(?=\\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/\"[^\"]*\"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\\]]/,\"doctype-tag\":/^DOCTYPE/i,name:/[^\\s<>'\"]+/}},cdata:{pattern:/<!\\[CDATA\\[[\\s\\S]*?\\]\\]>/i,greedy:!0},tag:{pattern:/<\\/?(?!\\d)[^\\s>\\/=$<%]+(?:\\s(?:\\s*[^\\s>\\/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))|(?=[\\s/>])))+)?\\s*\\/?>/,greedy:!0,inside:{tag:{pattern:/^<\\/?[^\\s>\\/]+/,inside:{punctuation:/^<\\/?/,namespace:/^[^\\s>\\/:]+:/}},\"special-attr\":[],\"attr-value\":{pattern:/=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:\"attr-equals\"},{pattern:/^(\\s*)[\"']|[\"']$/,lookbehind:!0}]}},punctuation:/\\/?>/,\"attr-name\":{pattern:/[^\\s>\\/]+/,inside:{namespace:/^[^\\s>\\/:]+:/}}}},entity:[{pattern:/&[\\da-z]{1,8};/i,alias:\"named-entity\"},/&#x?[\\da-f]{1,8};/i]},Prism.languages.markup.tag.inside[\"attr-value\"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside[\"internal-subset\"].inside=Prism.languages.markup,Prism.hooks.add(\"wrap\",(function(a){\"entity\"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,\"&\"))})),Object.defineProperty(Prism.languages.markup.tag,\"addInlined\",{value:function(a,e){var s={};s[\"language-\"+e]={pattern:/(^<!\\[CDATA\\[)[\\s\\S]+?(?=\\]\\]>$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^<!\\[CDATA\\[|\\]\\]>$/i;var t={\"included-cdata\":{pattern:/<!\\[CDATA\\[[\\s\\S]*?\\]\\]>/i,inside:s}};t[\"language-\"+e]={pattern:/[\\s\\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp(\"(<__[^>]*>)(?:<!\\\\[CDATA\\\\[(?:[^\\\\]]|\\\\](?!\\\\]>))*\\\\]\\\\]>|(?!<!\\\\[CDATA\\\\[)[^])*?(?=</__>)\".replace(/__/g,(function(){return a})),\"i\"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore(\"markup\",\"cdata\",n)}}),Object.defineProperty(Prism.languages.markup.tag,\"addAttribute\",{value:function(a,e){Prism.languages.markup.tag.inside[\"special-attr\"].push({pattern:RegExp(\"(^|[\\\"'\\\\s])(?:\"+a+\")\\\\s*=\\\\s*(?:\\\"[^\\\"]*\\\"|'[^']*'|[^\\\\s'\\\">=]+(?=[\\\\s>]))\",\"i\"),lookbehind:!0,inside:{\"attr-name\":/^[^\\s=]+/,\"attr-value\":{pattern:/=[\\s\\S]+/,inside:{value:{pattern:/(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2$)/,lookbehind:!0,alias:[e,\"language-\"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:\"attr-equals\"},/\"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend(\"markup\",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml;\n!function(s){var e=/(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')/;s.languages.css={comment:/\\/\\*[\\s\\S]*?\\*\\//,atrule:{pattern:RegExp(\"@[\\\\w-](?:[^;{\\\\s\\\"']|\\\\s+(?!\\\\s)|\"+e.source+\")*?(?:;|(?=\\\\s*\\\\{))\"),inside:{rule:/^@[\\w-]+/,\"selector-function-argument\":{pattern:/(\\bselector\\s*\\(\\s*(?![\\s)]))(?:[^()\\s]|\\s+(?![\\s)])|\\((?:[^()]|\\([^()]*\\))*\\))+(?=\\s*\\))/,lookbehind:!0,alias:\"selector\"},keyword:{pattern:/(^|[^\\w-])(?:and|not|only|or)(?![\\w-])/,lookbehind:!0}}},url:{pattern:RegExp(\"\\\\burl\\\\((?:\"+e.source+\"|(?:[^\\\\\\\\\\r\\n()\\\"']|\\\\\\\\[^])*)\\\\)\",\"i\"),greedy:!0,inside:{function:/^url/i,punctuation:/^\\(|\\)$/,string:{pattern:RegExp(\"^\"+e.source+\"$\"),alias:\"url\"}}},selector:{pattern:RegExp(\"(^|[{}\\\\s])[^{}\\\\s](?:[^{};\\\"'\\\\s]|\\\\s+(?![\\\\s{])|\"+e.source+\")*(?=\\\\s*\\\\{)\"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\\w\\xA0-\\uFFFF])(?!\\s)[-_a-z\\xA0-\\uFFFF](?:(?!\\s)[-\\w\\xA0-\\uFFFF])*(?=\\s*:)/i,lookbehind:!0},important:/!important\\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined(\"style\",\"css\"),t.tag.addAttribute(\"style\",\"css\"))}(Prism);\nPrism.languages.clike={comment:[{pattern:/(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\\\:])\\/\\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0},\"class-name\":{pattern:/(\\b(?:class|extends|implements|instanceof|interface|new|trait)\\s+|\\bcatch\\s+\\()[\\w.\\\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\\\]/}},keyword:/\\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\\b/,boolean:/\\b(?:false|true)\\b/,function:/\\b\\w+(?=\\()/,number:/\\b0x[\\da-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\\+\\+?|&&?|\\|\\|?|[?*/~^%]/,punctuation:/[{}[\\];(),.:]/};\nPrism.languages.javascript=Prism.languages.extend(\"clike\",{\"class-name\":[Prism.languages.clike[\"class-name\"],{pattern:/(^|[^$\\w\\xA0-\\uFFFF])(?!\\s)[_$A-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\\})\\s*)catch\\b/,lookbehind:!0},{pattern:/(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[$\\w\\xA0-\\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|$))|for|from(?=\\s*(?:['\"]|$))|function|(?:get|set)(?=\\s*(?:[#\\[$\\w\\xA0-\\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b/,lookbehind:!0}],function:/#?(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()/,number:{pattern:RegExp(\"(^|[^\\\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\\\dA-Fa-f]+(?:_[\\\\dA-Fa-f]+)*n?|\\\\d+(?:_\\\\d+)*n|(?:\\\\d+(?:_\\\\d+)*(?:\\\\.(?:\\\\d+(?:_\\\\d+)*)?)?|\\\\.\\\\d+(?:_\\\\d+)*)(?:[Ee][+-]?\\\\d+(?:_\\\\d+)*)?)(?![\\\\w$])\"),lookbehind:!0},operator:/--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]/}),Prism.languages.javascript[\"class-name\"][0].pattern=/(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+/,Prism.languages.insertBefore(\"javascript\",\"keyword\",{regex:{pattern:RegExp(\"((?:^|[^$\\\\w\\\\xA0-\\\\uFFFF.\\\"'\\\\])\\\\s]|\\\\b(?:return|yield))\\\\s*)/(?:(?:\\\\[(?:[^\\\\]\\\\\\\\\\r\\n]|\\\\\\\\.)*\\\\]|\\\\\\\\.|[^/\\\\\\\\\\\\[\\r\\n])+/[dgimyus]{0,7}|(?:\\\\[(?:[^[\\\\]\\\\\\\\\\r\\n]|\\\\\\\\.|\\\\[(?:[^[\\\\]\\\\\\\\\\r\\n]|\\\\\\\\.|\\\\[(?:[^[\\\\]\\\\\\\\\\r\\n]|\\\\\\\\.)*\\\\])*\\\\])*\\\\]|\\\\\\\\.|[^/\\\\\\\\\\\\[\\r\\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\\\s|/\\\\*(?:[^*]|\\\\*(?!/))*\\\\*/)*(?:$|[\\r\\n,.;:})\\\\]]|//))\"),lookbehind:!0,greedy:!0,inside:{\"regex-source\":{pattern:/^(\\/)[\\s\\S]+(?=\\/[a-z]*$)/,lookbehind:!0,alias:\"language-regex\",inside:Prism.languages.regex},\"regex-delimiter\":/^\\/|\\/$/,\"regex-flags\":/^[a-z]+$/}},\"function-variable\":{pattern:/#?(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*)\\s*=>))/,alias:\"function\"},parameter:[{pattern:/(function(?:\\s+(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\\w\\xA0-\\uFFFF])(?!\\s)[_$a-z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?=\\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\\b[A-Z](?:[A-Z_]|\\dx?)*\\b/}),Prism.languages.insertBefore(\"javascript\",\"string\",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:\"comment\"},\"template-string\":{pattern:/`(?:\\\\[\\s\\S]|\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\$\\{)[^\\\\`])*`/,greedy:!0,inside:{\"template-punctuation\":{pattern:/^`|`$/,alias:\"string\"},interpolation:{pattern:/((?:^|[^\\\\])(?:\\\\{2})*)\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}/,lookbehind:!0,inside:{\"interpolation-punctuation\":{pattern:/^\\$\\{|\\}$/,alias:\"punctuation\"},rest:Prism.languages.javascript}},string:/[\\s\\S]+/}},\"string-property\":{pattern:/((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)/m,lookbehind:!0,greedy:!0,alias:\"property\"}}),Prism.languages.insertBefore(\"javascript\",\"operator\",{\"literal-property\":{pattern:/((?:^|[,{])[ \\t]*)(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?=\\s*:)/m,lookbehind:!0,alias:\"property\"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined(\"script\",\"javascript\"),Prism.languages.markup.tag.addAttribute(\"on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)\",\"javascript\")),Prism.languages.js=Prism.languages.javascript;\nPrism.languages.apacheconf={comment:/#.*/,\"directive-inline\":{pattern:/(^[\\t ]*)\\b(?:AcceptFilter|AcceptPathInfo|AccessFileName|Action|Add(?:Alt|AltByEncoding|AltByType|Charset|DefaultCharset|Description|Encoding|Handler|Icon|IconByEncoding|IconByType|InputFilter|Language|ModuleInfo|OutputFilter|OutputFilterByType|Type)|Alias|AliasMatch|Allow(?:CONNECT|EncodedSlashes|Methods|Override|OverrideList)?|Anonymous(?:_LogEmail|_MustGiveEmail|_NoUserID|_VerifyEmail)?|AsyncRequestWorkerFactor|Auth(?:BasicAuthoritative|BasicFake|BasicProvider|BasicUseDigestAlgorithm|DBDUserPWQuery|DBDUserRealmQuery|DBMGroupFile|DBMType|DBMUserFile|Digest(?:Algorithm|Domain|NonceLifetime|Provider|Qop|ShmemSize)|Form(?:Authoritative|Body|DisableNoStore|FakeBasicAuth|Location|LoginRequiredLocation|LoginSuccessLocation|LogoutLocation|Method|Mimetype|Password|Provider|SitePassphrase|Size|Username)|GroupFile|LDAP(?:AuthorizePrefix|BindAuthoritative|BindDN|BindPassword|CharsetConfig|CompareAsUser|CompareDNOnServer|DereferenceAliases|GroupAttribute|GroupAttributeIsDN|InitialBindAsUser|InitialBindPattern|MaxSubGroupDepth|RemoteUserAttribute|RemoteUserIsDN|SearchAsUser|SubGroupAttribute|SubGroupClass|Url)|Merging|Name|nCache(?:Context|Enable|ProvideFor|SOCache|Timeout)|nzFcgiCheckAuthnProvider|nzFcgiDefineProvider|Type|UserFile|zDBDLoginToReferer|zDBDQuery|zDBDRedirectQuery|zDBMType|zSendForbiddenOnFailure)|BalancerGrowth|BalancerInherit|BalancerMember|BalancerPersist|BrowserMatch|BrowserMatchNoCase|BufferedLogs|BufferSize|Cache(?:DefaultExpire|DetailHeader|DirLength|DirLevels|Disable|Enable|File|Header|IgnoreCacheControl|IgnoreHeaders|IgnoreNoLastMod|IgnoreQueryString|IgnoreURLSessionIdentifiers|KeyBaseURL|LastModifiedFactor|Lock|LockMaxAge|LockPath|MaxExpire|MaxFileSize|MinExpire|MinFileSize|NegotiatedDocs|QuickHandler|ReadSize|ReadTime|Root|Socache(?:MaxSize|MaxTime|MinTime|ReadSize|ReadTime)?|StaleOnError|StoreExpired|StoreNoStore|StorePrivate)|CGIDScriptTimeout|CGIMapExtension|CharsetDefault|CharsetOptions|CharsetSourceEnc|CheckCaseOnly|CheckSpelling|ChrootDir|ContentDigest|CookieDomain|CookieExpires|CookieName|CookieStyle|CookieTracking|CoreDumpDirectory|CustomLog|Dav|DavDepthInfinity|DavGenericLockDB|DavLockDB|DavMinTimeout|DBDExptime|DBDInitSQL|DBDKeep|DBDMax|DBDMin|DBDParams|DBDPersist|DBDPrepareSQL|DBDriver|DefaultIcon|DefaultLanguage|DefaultRuntimeDir|DefaultType|Define|Deflate(?:BufferSize|CompressionLevel|FilterNote|InflateLimitRequestBody|InflateRatio(?:Burst|Limit)|MemLevel|WindowSize)|Deny|DirectoryCheckHandler|DirectoryIndex|DirectoryIndexRedirect|DirectorySlash|DocumentRoot|DTracePrivileges|DumpIOInput|DumpIOOutput|EnableExceptionHook|EnableMMAP|EnableSendfile|Error|ErrorDocument|ErrorLog|ErrorLogFormat|Example|ExpiresActive|ExpiresByType|ExpiresDefault|ExtendedStatus|ExtFilterDefine|ExtFilterOptions|FallbackResource|FileETag|FilterChain|FilterDeclare|FilterProtocol|FilterProvider|FilterTrace|ForceLanguagePriority|ForceType|ForensicLog|GprofDir|GracefulShutdownTimeout|Group|Header|HeaderName|Heartbeat(?:Address|Listen|MaxServers|Storage)|HostnameLookups|IdentityCheck|IdentityCheckTimeout|ImapBase|ImapDefault|ImapMenu|Include|IncludeOptional|Index(?:HeadInsert|Ignore|IgnoreReset|Options|OrderDefault|StyleSheet)|InputSed|ISAPI(?:AppendLogToErrors|AppendLogToQuery|CacheFile|FakeAsync|LogNotSupported|ReadAheadBuffer)|KeepAlive|KeepAliveTimeout|KeptBodySize|LanguagePriority|LDAP(?:CacheEntries|CacheTTL|ConnectionPoolTTL|ConnectionTimeout|LibraryDebug|OpCacheEntries|OpCacheTTL|ReferralHopLimit|Referrals|Retries|RetryDelay|SharedCacheFile|SharedCacheSize|Timeout|TrustedClientCert|TrustedGlobalCert|TrustedMode|VerifyServerCert)|Limit(?:InternalRecursion|Request(?:Body|Fields|FieldSize|Line)|XMLRequestBody)|Listen|ListenBackLog|LoadFile|LoadModule|LogFormat|LogLevel|LogMessage|LuaAuthzProvider|LuaCodeCache|Lua(?:Hook(?:AccessChecker|AuthChecker|CheckUserID|Fixups|InsertFilter|Log|MapToStorage|TranslateName|TypeChecker)|Inherit|InputFilter|MapHandler|OutputFilter|PackageCPath|PackagePath|QuickHandler|Root|Scope)|Max(?:ConnectionsPerChild|KeepAliveRequests|MemFree|RangeOverlaps|RangeReversals|Ranges|RequestWorkers|SpareServers|SpareThreads|Threads)|MergeTrailers|MetaDir|MetaFiles|MetaSuffix|MimeMagicFile|MinSpareServers|MinSpareThreads|MMapFile|ModemStandard|ModMimeUsePathInfo|MultiviewsMatch|Mutex|NameVirtualHost|NoProxy|NWSSLTrustedCerts|NWSSLUpgradeable|Options|Order|OutputSed|PassEnv|PidFile|PrivilegesMode|Protocol|ProtocolEcho|Proxy(?:AddHeaders|BadHeader|Block|Domain|ErrorOverride|ExpressDBMFile|ExpressDBMType|ExpressEnable|FtpDirCharset|FtpEscapeWildcards|FtpListOnWildcard|HTML(?:BufSize|CharsetOut|DocType|Enable|Events|Extended|Fixups|Interp|Links|Meta|StripComments|URLMap)|IOBufferSize|MaxForwards|Pass(?:Inherit|InterpolateEnv|Match|Reverse|ReverseCookieDomain|ReverseCookiePath)?|PreserveHost|ReceiveBufferSize|Remote|RemoteMatch|Requests|SCGIInternalRedirect|SCGISendfile|Set|SourceAddress|Status|Timeout|Via)|ReadmeName|ReceiveBufferSize|Redirect|RedirectMatch|RedirectPermanent|RedirectTemp|ReflectorHeader|RemoteIP(?:Header|InternalProxy|InternalProxyList|ProxiesHeader|TrustedProxy|TrustedProxyList)|RemoveCharset|RemoveEncoding|RemoveHandler|RemoveInputFilter|RemoveLanguage|RemoveOutputFilter|RemoveType|RequestHeader|RequestReadTimeout|Require|Rewrite(?:Base|Cond|Engine|Map|Options|Rule)|RLimitCPU|RLimitMEM|RLimitNPROC|Satisfy|ScoreBoardFile|Script(?:Alias|AliasMatch|InterpreterSource|Log|LogBuffer|LogLength|Sock)?|SecureListen|SeeRequestTail|SendBufferSize|Server(?:Admin|Alias|Limit|Name|Path|Root|Signature|Tokens)|Session(?:Cookie(?:Name|Name2|Remove)|Crypto(?:Cipher|Driver|Passphrase|PassphraseFile)|DBD(?:CookieName|CookieName2|CookieRemove|DeleteLabel|InsertLabel|PerUser|SelectLabel|UpdateLabel)|Env|Exclude|Header|Include|MaxAge)?|SetEnv|SetEnvIf|SetEnvIfExpr|SetEnvIfNoCase|SetHandler|SetInputFilter|SetOutputFilter|SSIEndTag|SSIErrorMsg|SSIETag|SSILastModified|SSILegacyExprParser|SSIStartTag|SSITimeFormat|SSIUndefinedEcho|SSL(?:CACertificateFile|CACertificatePath|CADNRequestFile|CADNRequestPath|CARevocationCheck|CARevocationFile|CARevocationPath|CertificateChainFile|CertificateFile|CertificateKeyFile|CipherSuite|Compression|CryptoDevice|Engine|FIPS|HonorCipherOrder|InsecureRenegotiation|OCSP(?:DefaultResponder|Enable|OverrideResponder|ResponderTimeout|ResponseMaxAge|ResponseTimeSkew|UseRequestNonce)|OpenSSLConfCmd|Options|PassPhraseDialog|Protocol|Proxy(?:CACertificateFile|CACertificatePath|CARevocation(?:Check|File|Path)|CheckPeer(?:CN|Expire|Name)|CipherSuite|Engine|MachineCertificate(?:ChainFile|File|Path)|Protocol|Verify|VerifyDepth)|RandomSeed|RenegBufferSize|Require|RequireSSL|Session(?:Cache|CacheTimeout|TicketKeyFile|Tickets)|SRPUnknownUserSeed|SRPVerifierFile|Stapling(?:Cache|ErrorCacheTimeout|FakeTryLater|ForceURL|ResponderTimeout|ResponseMaxAge|ResponseTimeSkew|ReturnResponderErrors|StandardCacheTimeout)|StrictSNIVHostCheck|UserName|UseStapling|VerifyClient|VerifyDepth)|StartServers|StartThreads|Substitute|Suexec|SuexecUserGroup|ThreadLimit|ThreadsPerChild|ThreadStackSize|TimeOut|TraceEnable|TransferLog|TypesConfig|UnDefine|UndefMacro|UnsetEnv|Use|UseCanonicalName|UseCanonicalPhysicalPort|User|UserDir|VHostCGIMode|VHostCGIPrivs|VHostGroup|VHostPrivs|VHostSecure|VHostUser|Virtual(?:DocumentRoot|ScriptAlias)(?:IP)?|WatchdogInterval|XBitHack|xml2EncAlias|xml2EncDefault|xml2StartParse)\\b/im,lookbehind:!0,alias:\"property\"},\"directive-block\":{pattern:/<\\/?\\b(?:Auth[nz]ProviderAlias|Directory|DirectoryMatch|Else|ElseIf|Files|FilesMatch|If|IfDefine|IfModule|IfVersion|Limit|LimitExcept|Location|LocationMatch|Macro|Proxy|Require(?:All|Any|None)|VirtualHost)\\b.*>/i,inside:{\"directive-block\":{pattern:/^<\\/?\\w+/,inside:{punctuation:/^<\\/?/},alias:\"tag\"},\"directive-block-parameter\":{pattern:/.*[^>]/,inside:{punctuation:/:/,string:{pattern:/(\"|').*\\1/,inside:{variable:/[$%]\\{?(?:\\w\\.?[-+:]?)+\\}?/}}},alias:\"attr-value\"},punctuation:/>/},alias:\"tag\"},\"directive-flags\":{pattern:/\\[(?:[\\w=],?)+\\]/,alias:\"keyword\"},string:{pattern:/(\"|').*\\1/,inside:{variable:/[$%]\\{?(?:\\w\\.?[-+:]?)+\\}?/}},variable:/[$%]\\{?(?:\\w\\.?[-+:]?)+\\}?/,regex:/\\^?.*\\$|\\^.*\\$?/};\n!function(e){function n(e,n){return e.replace(/<<(\\d+)>>/g,(function(e,s){return\"(?:\"+n[+s]+\")\"}))}function s(e,s,a){return RegExp(n(e,s),a||\"\")}function a(e,n){for(var s=0;s<n;s++)e=e.replace(/<<self>>/g,(function(){return\"(?:\"+e+\")\"}));return e.replace(/<<self>>/g,\"[^\\\\s\\\\S]\")}var t=\"bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void\",r=\"class enum interface record struct\",i=\"add alias and ascending async await by descending from(?=\\\\s*(?:\\\\w|$)) get global group into init(?=\\\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\\\s*{)\",o=\"abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield\";function l(e){return\"\\\\b(?:\"+e.trim().replace(/ /g,\"|\")+\")\\\\b\"}var d=l(r),p=RegExp(l(t+\" \"+r+\" \"+i+\" \"+o)),c=l(r+\" \"+i+\" \"+o),u=l(t+\" \"+r+\" \"+o),g=a(\"<(?:[^<>;=+\\\\-*/%&|^]|<<self>>)*>\",2),b=a(\"\\\\((?:[^()]|<<self>>)*\\\\)\",2),h=\"@?\\\\b[A-Za-z_]\\\\w*\\\\b\",f=n(\"<<0>>(?:\\\\s*<<1>>)?\",[h,g]),m=n(\"(?!<<0>>)<<1>>(?:\\\\s*\\\\.\\\\s*<<1>>)*\",[c,f]),k=\"\\\\[\\\\s*(?:,\\\\s*)*\\\\]\",y=n(\"<<0>>(?:\\\\s*(?:\\\\?\\\\s*)?<<1>>)*(?:\\\\s*\\\\?)?\",[m,k]),w=n(\"[^,()<>[\\\\];=+\\\\-*/%&|^]|<<0>>|<<1>>|<<2>>\",[g,b,k]),v=n(\"\\\\(<<0>>+(?:,<<0>>+)+\\\\)\",[w]),x=n(\"(?:<<0>>|<<1>>)(?:\\\\s*(?:\\\\?\\\\s*)?<<2>>)*(?:\\\\s*\\\\?)?\",[v,m,k]),$={keyword:p,punctuation:/[<>()?,.:[\\]]/},_=\"'(?:[^\\r\\n'\\\\\\\\]|\\\\\\\\.|\\\\\\\\[Uux][\\\\da-fA-F]{1,8})'\",B='\"(?:\\\\\\\\.|[^\\\\\\\\\"\\r\\n])*\"';e.languages.csharp=e.languages.extend(\"clike\",{string:[{pattern:s(\"(^|[^$\\\\\\\\])<<0>>\",['@\"(?:\"\"|\\\\\\\\[^]|[^\\\\\\\\\"])*\"(?!\")']),lookbehind:!0,greedy:!0},{pattern:s(\"(^|[^@$\\\\\\\\])<<0>>\",[B]),lookbehind:!0,greedy:!0}],\"class-name\":[{pattern:s(\"(\\\\busing\\\\s+static\\\\s+)<<0>>(?=\\\\s*;)\",[m]),lookbehind:!0,inside:$},{pattern:s(\"(\\\\busing\\\\s+<<0>>\\\\s*=\\\\s*)<<1>>(?=\\\\s*;)\",[h,x]),lookbehind:!0,inside:$},{pattern:s(\"(\\\\busing\\\\s+)<<0>>(?=\\\\s*=)\",[h]),lookbehind:!0},{pattern:s(\"(\\\\b<<0>>\\\\s+)<<1>>\",[d,f]),lookbehind:!0,inside:$},{pattern:s(\"(\\\\bcatch\\\\s*\\\\(\\\\s*)<<0>>\",[m]),lookbehind:!0,inside:$},{pattern:s(\"(\\\\bwhere\\\\s+)<<0>>\",[h]),lookbehind:!0},{pattern:s(\"(\\\\b(?:is(?:\\\\s+not)?|as)\\\\s+)<<0>>\",[y]),lookbehind:!0,inside:$},{pattern:s(\"\\\\b<<0>>(?=\\\\s+(?!<<1>>|with\\\\s*\\\\{)<<2>>(?:\\\\s*[=,;:{)\\\\]]|\\\\s+(?:in|when)\\\\b))\",[x,u,h]),inside:$}],keyword:p,number:/(?:\\b0(?:x[\\da-f_]*[\\da-f]|b[01_]*[01])|(?:\\B\\.\\d+(?:_+\\d+)*|\\b\\d+(?:_+\\d+)*(?:\\.\\d+(?:_+\\d+)*)?)(?:e[-+]?\\d+(?:_+\\d+)*)?)(?:[dflmu]|lu|ul)?\\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\\1|~|\\?\\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\\?\\.?|::|[{}[\\];(),.:]/}),e.languages.insertBefore(\"csharp\",\"number\",{range:{pattern:/\\.\\./,alias:\"operator\"}}),e.languages.insertBefore(\"csharp\",\"punctuation\",{\"named-parameter\":{pattern:s(\"([(,]\\\\s*)<<0>>(?=\\\\s*:)\",[h]),lookbehind:!0,alias:\"punctuation\"}}),e.languages.insertBefore(\"csharp\",\"class-name\",{namespace:{pattern:s(\"(\\\\b(?:namespace|using)\\\\s+)<<0>>(?:\\\\s*\\\\.\\\\s*<<0>>)*(?=\\\\s*[;{])\",[h]),lookbehind:!0,inside:{punctuation:/\\./}},\"type-expression\":{pattern:s(\"(\\\\b(?:default|sizeof|typeof)\\\\s*\\\\(\\\\s*(?!\\\\s))(?:[^()\\\\s]|\\\\s(?!\\\\s)|<<0>>)*(?=\\\\s*\\\\))\",[b]),lookbehind:!0,alias:\"class-name\",inside:$},\"return-type\":{pattern:s(\"<<0>>(?=\\\\s+(?:<<1>>\\\\s*(?:=>|[({]|\\\\.\\\\s*this\\\\s*\\\\[)|this\\\\s*\\\\[))\",[x,m]),inside:$,alias:\"class-name\"},\"constructor-invocation\":{pattern:s(\"(\\\\bnew\\\\s+)<<0>>(?=\\\\s*[[({])\",[x]),lookbehind:!0,inside:$,alias:\"class-name\"},\"generic-method\":{pattern:s(\"<<0>>\\\\s*<<1>>(?=\\\\s*\\\\()\",[h,g]),inside:{function:s(\"^<<0>>\",[h]),generic:{pattern:RegExp(g),alias:\"class-name\",inside:$}}},\"type-list\":{pattern:s(\"\\\\b((?:<<0>>\\\\s+<<1>>|record\\\\s+<<1>>\\\\s*<<5>>|where\\\\s+<<2>>)\\\\s*:\\\\s*)(?:<<3>>|<<4>>|<<1>>\\\\s*<<5>>|<<6>>)(?:\\\\s*,\\\\s*(?:<<3>>|<<4>>|<<6>>))*(?=\\\\s*(?:where|[{;]|=>|$))\",[d,f,h,x,p.source,b,\"\\\\bnew\\\\s*\\\\(\\\\s*\\\\)\"]),lookbehind:!0,inside:{\"record-arguments\":{pattern:s(\"(^(?!new\\\\s*\\\\()<<0>>\\\\s*)<<1>>\",[f,b]),lookbehind:!0,greedy:!0,inside:e.languages.csharp},keyword:p,\"class-name\":{pattern:RegExp(x),greedy:!0,inside:$},punctuation:/[,()]/}},preprocessor:{pattern:/(^[\\t ]*)#.*/m,lookbehind:!0,alias:\"property\",inside:{directive:{pattern:/(#)\\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\\b/,lookbehind:!0,alias:\"keyword\"}}}});var E=B+\"|\"+_,R=n(\"/(?![*/])|//[^\\r\\n]*[\\r\\n]|/\\\\*(?:[^*]|\\\\*(?!/))*\\\\*/|<<0>>\",[E]),z=a(n(\"[^\\\"'/()]|<<0>>|\\\\(<<self>>*\\\\)\",[R]),2),S=\"\\\\b(?:assembly|event|field|method|module|param|property|return|type)\\\\b\",j=n(\"<<0>>(?:\\\\s*\\\\(<<1>>*\\\\))?\",[m,z]);e.languages.insertBefore(\"csharp\",\"class-name\",{attribute:{pattern:s(\"((?:^|[^\\\\s\\\\w>)?])\\\\s*\\\\[\\\\s*)(?:<<0>>\\\\s*:\\\\s*)?<<1>>(?:\\\\s*,\\\\s*<<1>>)*(?=\\\\s*\\\\])\",[S,j]),lookbehind:!0,greedy:!0,inside:{target:{pattern:s(\"^<<0>>(?=\\\\s*:)\",[S]),alias:\"keyword\"},\"attribute-arguments\":{pattern:s(\"\\\\(<<0>>*\\\\)\",[z]),inside:e.languages.csharp},\"class-name\":{pattern:RegExp(m),inside:{punctuation:/\\./}},punctuation:/[:,]/}}});var A=\":[^}\\r\\n]+\",F=a(n(\"[^\\\"'/()]|<<0>>|\\\\(<<self>>*\\\\)\",[R]),2),P=n(\"\\\\{(?!\\\\{)(?:(?![}:])<<0>>)*<<1>>?\\\\}\",[F,A]),U=a(n(\"[^\\\"'/()]|/(?!\\\\*)|/\\\\*(?:[^*]|\\\\*(?!/))*\\\\*/|<<0>>|\\\\(<<self>>*\\\\)\",[E]),2),Z=n(\"\\\\{(?!\\\\{)(?:(?![}:])<<0>>)*<<1>>?\\\\}\",[U,A]);function q(n,a){return{interpolation:{pattern:s(\"((?:^|[^{])(?:\\\\{\\\\{)*)<<0>>\",[n]),lookbehind:!0,inside:{\"format-string\":{pattern:s(\"(^\\\\{(?:(?![}:])<<0>>)*)<<1>>(?=\\\\}$)\",[a,A]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\\{|\\}$/,expression:{pattern:/[\\s\\S]+/,alias:\"language-csharp\",inside:e.languages.csharp}}},string:/[\\s\\S]+/}}e.languages.insertBefore(\"csharp\",\"string\",{\"interpolation-string\":[{pattern:s('(^|[^\\\\\\\\])(?:\\\\$@|@\\\\$)\"(?:\"\"|\\\\\\\\[^]|\\\\{\\\\{|<<0>>|[^\\\\\\\\{\"])*\"',[P]),lookbehind:!0,greedy:!0,inside:q(P,F)},{pattern:s('(^|[^@\\\\\\\\])\\\\$\"(?:\\\\\\\\.|\\\\{\\\\{|<<0>>|[^\\\\\\\\\"{])*\"',[Z]),lookbehind:!0,greedy:!0,inside:q(Z,U)}],char:{pattern:RegExp(_),greedy:!0}}),e.languages.dotnet=e.languages.cs=e.languages.csharp}(Prism);\nPrism.languages.aspnet=Prism.languages.extend(\"markup\",{\"page-directive\":{pattern:/<%\\s*@.*%>/,alias:\"tag\",inside:{\"page-directive\":{pattern:/<%\\s*@\\s*(?:Assembly|Control|Implements|Import|Master(?:Type)?|OutputCache|Page|PreviousPageType|Reference|Register)?|%>/i,alias:\"tag\"},rest:Prism.languages.markup.tag.inside}},directive:{pattern:/<%.*%>/,alias:\"tag\",inside:{directive:{pattern:/<%\\s*?[$=%#:]{0,2}|%>/,alias:\"tag\"},rest:Prism.languages.csharp}}}),Prism.languages.aspnet.tag.pattern=/<(?!%)\\/?[^\\s>\\/]+(?:\\s+[^\\s>\\/=]+(?:=(?:(\"|')(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])*\\1|[^\\s'\">=]+))?)*\\s*\\/?>/,Prism.languages.insertBefore(\"inside\",\"punctuation\",{directive:Prism.languages.aspnet.directive},Prism.languages.aspnet.tag.inside[\"attr-value\"]),Prism.languages.insertBefore(\"aspnet\",\"comment\",{\"asp-comment\":{pattern:/<%--[\\s\\S]*?--%>/,alias:[\"asp\",\"comment\"]}}),Prism.languages.insertBefore(\"aspnet\",Prism.languages.javascript?\"script\":\"tag\",{\"asp-script\":{pattern:/(<script(?=.*runat=['\"]?server\\b)[^>]*>)[\\s\\S]*?(?=<\\/script>)/i,lookbehind:!0,alias:[\"asp\",\"script\"],inside:Prism.languages.csharp||{}}});\n!function(e){var t=\"\\\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\\\b\",a={pattern:/(^([\"']?)\\w+\\2)[ \\t]+\\S.*/,lookbehind:!0,alias:\"punctuation\",inside:null},n={bash:a,environment:{pattern:RegExp(\"\\\\$\"+t),alias:\"constant\"},variable:[{pattern:/\\$?\\(\\([\\s\\S]+?\\)\\)/,greedy:!0,inside:{variable:[{pattern:/(^\\$\\(\\([\\s\\S]+)\\)\\)/,lookbehind:!0},/^\\$\\(\\(/],number:/\\b0x[\\dA-Fa-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[Ee]-?\\d+)?/,operator:/--|\\+\\+|\\*\\*=?|<<=?|>>=?|&&|\\|\\||[=!+\\-*/%<>^&|]=?|[?~:]/,punctuation:/\\(\\(?|\\)\\)?|,|;/}},{pattern:/\\$\\((?:\\([^)]+\\)|[^()])+\\)|`[^`]+`/,greedy:!0,inside:{variable:/^\\$\\(|^`|\\)$|`$/}},{pattern:/\\$\\{[^}]+\\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\\/]|##?|%%?|\\^\\^?|,,?/,punctuation:/[\\[\\]]/,environment:{pattern:RegExp(\"(\\\\{)\"+t),lookbehind:!0,alias:\"constant\"}}},/\\$(?:\\w+|[#?*!@$])/],entity:/\\\\(?:[abceEfnrtv\\\\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\\s*\\/.*/,alias:\"important\"},comment:{pattern:/(^|[^\"{\\\\$])#.*/,lookbehind:!0},\"function-name\":[{pattern:/(\\bfunction\\s+)[\\w-]+(?=(?:\\s*\\(?:\\s*\\))?\\s*\\{)/,lookbehind:!0,alias:\"function\"},{pattern:/\\b[\\w-]+(?=\\s*\\(\\s*\\)\\s*\\{)/,alias:\"function\"}],\"for-or-select\":{pattern:/(\\b(?:for|select)\\s+)\\w+(?=\\s+in\\s)/,alias:\"variable\",lookbehind:!0},\"assign-left\":{pattern:/(^|[\\s;|&]|[<>]\\()\\w+(?:\\.\\w+)*(?=\\+?=)/,inside:{environment:{pattern:RegExp(\"(^|[\\\\s;|&]|[<>]\\\\()\"+t),lookbehind:!0,alias:\"constant\"}},alias:\"variable\",lookbehind:!0},parameter:{pattern:/(^|\\s)-{1,2}(?:\\w+:[+-]?)?\\w+(?:\\.\\w+)*(?=[=\\s]|$)/,alias:\"variable\",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\\s*)(\\w+)\\s[\\s\\S]*?(?:\\r?\\n|\\r)\\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\\s*)([\"'])(\\w+)\\2\\s[\\s\\S]*?(?:\\r?\\n|\\r)\\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\\\](?:\\\\\\\\)*)\"(?:\\\\[\\s\\S]|\\$\\([^)]+\\)|\\$(?!\\()|`[^`]+`|[^\"\\\\`$])*\"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\\$'(?:[^'\\\\]|\\\\[\\s\\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp(\"\\\\$?\"+t),alias:\"constant\"},variable:n.variable,function:{pattern:/(^|[\\s;|&]|[<>]\\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\\s;|&]|[<>]\\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\\s;|&]|[<>]\\()(?:\\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\\s;|&])/,lookbehind:!0,alias:\"class-name\"},boolean:{pattern:/(^|[\\s;|&]|[<>]\\()(?:false|true)(?=$|[)\\s;|&])/,lookbehind:!0},\"file-descriptor\":{pattern:/\\B&\\d\\b/,alias:\"important\"},operator:{pattern:/\\d?<>|>\\||\\+=|=[=~]?|!=?|<<[<-]?|[&\\d]?>>|\\d[<>]&?|[<>][&=]?|&[>&]?|\\|[&|]?/,inside:{\"file-descriptor\":{pattern:/^\\d/,alias:\"important\"}}},punctuation:/\\$?\\(\\(?|\\)\\)?|\\.\\.|[{}[\\];\\\\]/,number:{pattern:/(^|\\s)(?:[1-9]\\d*|0)(?:[.,]\\d+)?\\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=[\"comment\",\"function-name\",\"for-or-select\",\"assign-left\",\"parameter\",\"string\",\"environment\",\"function\",\"keyword\",\"builtin\",\"boolean\",\"file-descriptor\",\"operator\",\"punctuation\",\"number\"],o=n.variable[1].inside,i=0;i<s.length;i++)o[s[i]]=e.languages.bash[s[i]];e.languages.sh=e.languages.bash,e.languages.shell=e.languages.bash}(Prism);\nPrism.languages.basic={comment:{pattern:/(?:!|REM\\b).+/i,inside:{keyword:/^REM/i}},string:{pattern:/\"(?:\"\"|[!#$%&'()*,\\/:;<=>?^\\w +\\-.])*\"/,greedy:!0},number:/(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:E[+-]?\\d+)?/i,keyword:/\\b(?:AS|BEEP|BLOAD|BSAVE|CALL(?: ABSOLUTE)?|CASE|CHAIN|CHDIR|CLEAR|CLOSE|CLS|COM|COMMON|CONST|DATA|DECLARE|DEF(?: FN| SEG|DBL|INT|LNG|SNG|STR)|DIM|DO|DOUBLE|ELSE|ELSEIF|END|ENVIRON|ERASE|ERROR|EXIT|FIELD|FILES|FOR|FUNCTION|GET|GOSUB|GOTO|IF|INPUT|INTEGER|IOCTL|KEY|KILL|LINE INPUT|LOCATE|LOCK|LONG|LOOP|LSET|MKDIR|NAME|NEXT|OFF|ON(?: COM| ERROR| KEY| TIMER)?|OPEN|OPTION BASE|OUT|POKE|PUT|READ|REDIM|REM|RESTORE|RESUME|RETURN|RMDIR|RSET|RUN|SELECT CASE|SHARED|SHELL|SINGLE|SLEEP|STATIC|STEP|STOP|STRING|SUB|SWAP|SYSTEM|THEN|TIMER|TO|TROFF|TRON|TYPE|UNLOCK|UNTIL|USING|VIEW PRINT|WAIT|WEND|WHILE|WRITE)(?:\\$|\\b)/i,function:/\\b(?:ABS|ACCESS|ACOS|ANGLE|AREA|ARITHMETIC|ARRAY|ASIN|ASK|AT|ATN|BASE|BEGIN|BREAK|CAUSE|CEIL|CHR|CLIP|COLLATE|COLOR|CON|COS|COSH|COT|CSC|DATE|DATUM|DEBUG|DECIMAL|DEF|DEG|DEGREES|DELETE|DET|DEVICE|DISPLAY|DOT|ELAPSED|EPS|ERASABLE|EXLINE|EXP|EXTERNAL|EXTYPE|FILETYPE|FIXED|FP|GO|GRAPH|HANDLER|IDN|IMAGE|IN|INT|INTERNAL|IP|IS|KEYED|LBOUND|LCASE|LEFT|LEN|LENGTH|LET|LINE|LINES|LOG|LOG10|LOG2|LTRIM|MARGIN|MAT|MAX|MAXNUM|MID|MIN|MISSING|MOD|NATIVE|NUL|NUMERIC|OF|OPTION|ORD|ORGANIZATION|OUTIN|OUTPUT|PI|POINT|POINTER|POINTS|POS|PRINT|PROGRAM|PROMPT|RAD|RADIANS|RANDOMIZE|RECORD|RECSIZE|RECTYPE|RELATIVE|REMAINDER|REPEAT|REST|RETRY|REWRITE|RIGHT|RND|ROUND|RTRIM|SAME|SEC|SELECT|SEQUENTIAL|SET|SETTER|SGN|SIN|SINH|SIZE|SKIP|SQR|STANDARD|STATUS|STR|STREAM|STYLE|TAB|TAN|TANH|TEMPLATE|TEXT|THERE|TIME|TIMEOUT|TRACE|TRANSFORM|TRUNCATE|UBOUND|UCASE|USE|VAL|VARIABLE|VIEWPORT|WHEN|WINDOW|WITH|ZER|ZONEWIDTH)(?:\\$|\\b)/i,operator:/<[=>]?|>=?|[+\\-*\\/^=&]|\\b(?:AND|EQV|IMP|NOT|OR|XOR)\\b/i,punctuation:/[,;:()]/};\n!function(e){var r=/%%?[~:\\w]+%?|!\\S+!/,t={pattern:/\\/[a-z?]+(?=[ :]|$):?|-[a-z]\\b|--[a-z-]+\\b/im,alias:\"attr-name\",inside:{punctuation:/:/}},n=/\"(?:[\\\\\"]\"|[^\"])*\"(?!\")/,i=/(?:\\b|-)\\d+\\b/;e.languages.batch={comment:[/^::.*/m,{pattern:/((?:^|[&(])[ \\t]*)rem\\b(?:[^^&)\\r\\n]|\\^(?:\\r\\n|[\\s\\S]))*/im,lookbehind:!0}],label:{pattern:/^:.*/m,alias:\"property\"},command:[{pattern:/((?:^|[&(])[ \\t]*)for(?: \\/[a-z?](?:[ :](?:\"[^\"]*\"|[^\\s\"/]\\S*))?)* \\S+ in \\([^)]+\\) do/im,lookbehind:!0,inside:{keyword:/\\b(?:do|in)\\b|^for\\b/i,string:n,parameter:t,variable:r,number:i,punctuation:/[()',]/}},{pattern:/((?:^|[&(])[ \\t]*)if(?: \\/[a-z?](?:[ :](?:\"[^\"]*\"|[^\\s\"/]\\S*))?)* (?:not )?(?:cmdextversion \\d+|defined \\w+|errorlevel \\d+|exist \\S+|(?:\"[^\"]*\"|(?!\")(?:(?!==)\\S)+)?(?:==| (?:equ|geq|gtr|leq|lss|neq) )(?:\"[^\"]*\"|[^\\s\"]\\S*))/im,lookbehind:!0,inside:{keyword:/\\b(?:cmdextversion|defined|errorlevel|exist|not)\\b|^if\\b/i,string:n,parameter:t,variable:r,number:i,operator:/\\^|==|\\b(?:equ|geq|gtr|leq|lss|neq)\\b/i}},{pattern:/((?:^|[&()])[ \\t]*)else\\b/im,lookbehind:!0,inside:{keyword:/^else\\b/i}},{pattern:/((?:^|[&(])[ \\t]*)set(?: \\/[a-z](?:[ :](?:\"[^\"]*\"|[^\\s\"/]\\S*))?)* (?:[^^&)\\r\\n]|\\^(?:\\r\\n|[\\s\\S]))*/im,lookbehind:!0,inside:{keyword:/^set\\b/i,string:n,parameter:t,variable:[r,/\\w+(?=(?:[*\\/%+\\-&^|]|<<|>>)?=)/],number:i,operator:/[*\\/%+\\-&^|]=?|<<=?|>>=?|[!~_=]/,punctuation:/[()',]/}},{pattern:/((?:^|[&(])[ \\t]*@?)\\w+\\b(?:\"(?:[\\\\\"]\"|[^\"])*\"(?!\")|[^\"^&)\\r\\n]|\\^(?:\\r\\n|[\\s\\S]))*/m,lookbehind:!0,inside:{keyword:/^\\w+\\b/,string:n,parameter:t,label:{pattern:/(^\\s*):\\S+/m,lookbehind:!0,alias:\"property\"},variable:r,number:i,operator:/\\^/}}],operator:/[&@]/,punctuation:/[()']/}}(Prism);\nPrism.languages.bbcode={tag:{pattern:/\\[\\/?[^\\s=\\]]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\"\\]=]+))?(?:\\s+[^\\s=\\]]+\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\"\\]=]+))*\\s*\\]/,inside:{tag:{pattern:/^\\[\\/?[^\\s=\\]]+/,inside:{punctuation:/^\\[\\/?/}},\"attr-value\":{pattern:/=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\"\\]=]+)/,inside:{punctuation:[/^=/,{pattern:/^(\\s*)[\"']|[\"']$/,lookbehind:!0}]}},punctuation:/\\]/,\"attr-name\":/[^\\s=\\]]+/}}},Prism.languages.shortcode=Prism.languages.bbcode;\nPrism.languages.c=Prism.languages.extend(\"clike\",{comment:{pattern:/\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/,greedy:!0},string:{pattern:/\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"/,greedy:!0},\"class-name\":{pattern:/(\\b(?:enum|struct)\\s+(?:__attribute__\\s*\\(\\([\\s\\S]*?\\)\\)\\s*)?)\\w+|\\b[a-z]\\w*_t\\b/,lookbehind:!0},keyword:/\\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\\b/,function:/\\b[a-z_]\\w*(?=\\s*\\()/i,number:/(?:\\b0x(?:[\\da-f]+(?:\\.[\\da-f]*)?|\\.[\\da-f]+)(?:p[+-]?\\d+)?|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\\1|[?:~]|[-+*/%&|^!=<>]=?/}),Prism.languages.insertBefore(\"c\",\"string\",{char:{pattern:/'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'/,greedy:!0}}),Prism.languages.insertBefore(\"c\",\"string\",{macro:{pattern:/(^[\\t ]*)#\\s*[a-z](?:[^\\r\\n\\\\/]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\\\\(?:\\r\\n|[\\s\\S]))*/im,lookbehind:!0,greedy:!0,alias:\"property\",inside:{string:[{pattern:/^(#\\s*include\\s*)<[^>]+>/,lookbehind:!0},Prism.languages.c.string],char:Prism.languages.c.char,comment:Prism.languages.c.comment,\"macro-name\":[{pattern:/(^#\\s*define\\s+)\\w+\\b(?!\\()/i,lookbehind:!0},{pattern:/(^#\\s*define\\s+)\\w+\\b(?=\\()/i,lookbehind:!0,alias:\"function\"}],directive:{pattern:/^(#\\s*)[a-z]+/,lookbehind:!0,alias:\"keyword\"},\"directive-hash\":/^#/,punctuation:/##|\\\\(?=[\\r\\n])/,expression:{pattern:/\\S[\\s\\S]*/,inside:Prism.languages.c}}}}),Prism.languages.insertBefore(\"c\",\"function\",{constant:/\\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\\b/}),delete Prism.languages.c.boolean;\n!function(e){var t=/\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b/,n=\"\\\\b(?!<keyword>)\\\\w+(?:\\\\s*\\\\.\\\\s*\\\\w+)*\\\\b\".replace(/<keyword>/g,(function(){return t.source}));e.languages.cpp=e.languages.extend(\"c\",{\"class-name\":[{pattern:RegExp(\"(\\\\b(?:class|concept|enum|struct|typename)\\\\s+)(?!<keyword>)\\\\w+\".replace(/<keyword>/g,(function(){return t.source}))),lookbehind:!0},/\\b[A-Z]\\w*(?=\\s*::\\s*\\w+\\s*\\()/,/\\b[A-Z_]\\w*(?=\\s*::\\s*~\\w+\\s*\\()/i,/\\b\\w+(?=\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\\s*::\\s*\\w+\\s*\\()/],keyword:t,number:{pattern:/(?:\\b0b[01']+|\\b0x(?:[\\da-f']+(?:\\.[\\da-f']*)?|\\.[\\da-f']+)(?:p[+-]?[\\d']+)?|(?:\\b[\\d']+(?:\\.[\\d']*)?|\\B\\.[\\d']+)(?:e[+-]?[\\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\\+\\+|&&|\\|\\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\\b/,boolean:/\\b(?:false|true)\\b/}),e.languages.insertBefore(\"cpp\",\"string\",{module:{pattern:RegExp('(\\\\b(?:import|module)\\\\s+)(?:\"(?:\\\\\\\\(?:\\r\\n|[^])|[^\"\\\\\\\\\\r\\n])*\"|<[^<>\\r\\n]*>|'+\"<mod-name>(?:\\\\s*:\\\\s*<mod-name>)?|:\\\\s*<mod-name>\".replace(/<mod-name>/g,(function(){return n}))+\")\"),lookbehind:!0,greedy:!0,inside:{string:/^[<\"][\\s\\S]+/,operator:/:/,punctuation:/\\./}},\"raw-string\":{pattern:/R\"([^()\\\\ ]{0,16})\\([\\s\\S]*?\\)\\1\"/,alias:\"string\",greedy:!0}}),e.languages.insertBefore(\"cpp\",\"keyword\",{\"generic-function\":{pattern:/\\b(?!operator\\b)[a-z_]\\w*\\s*<(?:[^<>]|<[^<>]*>)*>(?=\\s*\\()/i,inside:{function:/^\\w+/,generic:{pattern:/<[\\s\\S]+/,alias:\"class-name\",inside:e.languages.cpp}}}}),e.languages.insertBefore(\"cpp\",\"operator\",{\"double-colon\":{pattern:/::/,alias:\"punctuation\"}}),e.languages.insertBefore(\"cpp\",\"class-name\",{\"base-clause\":{pattern:/(\\b(?:class|struct)\\s+\\w+\\s*:\\s*)[^;{}\"'\\s]+(?:\\s+[^;{}\"'\\s]+)*(?=\\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend(\"cpp\",{})}}),e.languages.insertBefore(\"inside\",\"double-colon\",{\"class-name\":/\\b[a-z_]\\w*\\b(?!\\s*::)/i},e.languages.cpp[\"base-clause\"])}(Prism);\nPrism.languages.cfscript=Prism.languages.extend(\"clike\",{comment:[{pattern:/(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|$)/,lookbehind:!0,inside:{annotation:{pattern:/(?:^|[^.])@[\\w\\.]+/,alias:\"punctuation\"}}},{pattern:/(^|[^\\\\:])\\/\\/.*/,lookbehind:!0,greedy:!0}],keyword:/\\b(?:abstract|break|catch|component|continue|default|do|else|extends|final|finally|for|function|if|in|include|package|private|property|public|remote|required|rethrow|return|static|switch|throw|try|var|while|xml)\\b(?!\\s*=)/,operator:[/\\+\\+|--|&&|\\|\\||::|=>|[!=]==|[-+*/%&|^!=<>]=?|\\?(?:\\.|:)?|:/,/\\b(?:and|contains|eq|equal|eqv|gt|gte|imp|is|lt|lte|mod|not|or|xor)\\b/],scope:{pattern:/\\b(?:application|arguments|cgi|client|cookie|local|session|super|this|variables)\\b/,alias:\"global\"},type:{pattern:/\\b(?:any|array|binary|boolean|date|guid|numeric|query|string|struct|uuid|void|xml)\\b/,alias:\"builtin\"}}),Prism.languages.insertBefore(\"cfscript\",\"keyword\",{\"function-variable\":{pattern:/[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*)\\s*=>))/,alias:\"function\"}}),delete Prism.languages.cfscript[\"class-name\"],Prism.languages.cfc=Prism.languages.cfscript;\n!function(e){var a=[/\\b(?:async|sync|yield)\\*/,/\\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\\b/],n=\"(^|[^\\\\w.])(?:[a-z]\\\\w*\\\\s*\\\\.\\\\s*)*(?:[A-Z]\\\\w*\\\\s*\\\\.\\\\s*)*\",s={pattern:RegExp(n+\"[A-Z](?:[\\\\d_A-Z]*[a-z]\\\\w*)?\\\\b\"),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?/,inside:{punctuation:/\\./}}}};e.languages.dart=e.languages.extend(\"clike\",{\"class-name\":[s,{pattern:RegExp(n+\"[A-Z]\\\\w*(?=\\\\s+\\\\w+\\\\s*[;,=()])\"),lookbehind:!0,inside:s.inside}],keyword:a,operator:/\\bis!|\\b(?:as|is)\\b|\\+\\+|--|&&|\\|\\||<<=?|>>=?|~(?:\\/=?)?|[+\\-*\\/%&^|=!<>]=?|\\?/}),e.languages.insertBefore(\"dart\",\"string\",{\"string-literal\":{pattern:/r?(?:(\"\"\"|''')[\\s\\S]*?\\1|([\"'])(?:\\\\.|(?!\\2)[^\\\\\\r\\n])*\\2(?!\\2))/,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^\\\\])(?:\\\\{2})*)\\$(?:\\w+|\\{(?:[^{}]|\\{[^{}]*\\})*\\})/,lookbehind:!0,inside:{punctuation:/^\\$\\{?|\\}$/,expression:{pattern:/[\\s\\S]+/,inside:e.languages.dart}}},string:/[\\s\\S]+/}},string:void 0}),e.languages.insertBefore(\"dart\",\"class-name\",{metadata:{pattern:/@\\w+/,alias:\"function\"}}),e.languages.insertBefore(\"dart\",\"class-name\",{generics:{pattern:/<(?:[\\w\\s,.&?]|<(?:[\\w\\s,.&?]|<(?:[\\w\\s,.&?]|<[\\w\\s,.&?]*>)*>)*>)*>/,inside:{\"class-name\":s,keyword:a,punctuation:/[<>(),.:]/,operator:/[?&|]/}}})}(Prism);\n!function(e){var n=\"(?:[ \\t]+(?![ \\t])(?:<SP_BS>)?|<SP_BS>)\".replace(/<SP_BS>/g,(function(){return\"\\\\\\\\[\\r\\n](?:\\\\s|\\\\\\\\[\\r\\n]|#.*(?!.))*(?![\\\\s#]|\\\\\\\\[\\r\\n])\"})),r=\"\\\"(?:[^\\\"\\\\\\\\\\r\\n]|\\\\\\\\(?:\\r\\n|[^]))*\\\"|'(?:[^'\\\\\\\\\\r\\n]|\\\\\\\\(?:\\r\\n|[^]))*'\",t=\"--[\\\\w-]+=(?:<STR>|(?![\\\"'])(?:[^\\\\s\\\\\\\\]|\\\\\\\\.)+)\".replace(/<STR>/g,(function(){return r})),o={pattern:RegExp(r),greedy:!0},i={pattern:/(^[ \\t]*)#.*/m,lookbehind:!0,greedy:!0};function a(e,r){return e=e.replace(/<OPT>/g,(function(){return t})).replace(/<SP>/g,(function(){return n})),RegExp(e,r)}e.languages.docker={instruction:{pattern:/(^[ \\t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\\s)(?:\\\\.|[^\\r\\n\\\\])*(?:\\\\$(?:\\s|#.*$)*(?![\\s#])(?:\\\\.|[^\\r\\n\\\\])*)*/im,lookbehind:!0,greedy:!0,inside:{options:{pattern:a(\"(^(?:ONBUILD<SP>)?\\\\w+<SP>)<OPT>(?:<SP><OPT>)*\",\"i\"),lookbehind:!0,greedy:!0,inside:{property:{pattern:/(^|\\s)--[\\w-]+/,lookbehind:!0},string:[o,{pattern:/(=)(?![\"'])(?:[^\\s\\\\]|\\\\.)+/,lookbehind:!0}],operator:/\\\\$/m,punctuation:/=/}},keyword:[{pattern:a(\"(^(?:ONBUILD<SP>)?HEALTHCHECK<SP>(?:<OPT><SP>)*)(?:CMD|NONE)\\\\b\",\"i\"),lookbehind:!0,greedy:!0},{pattern:a(\"(^(?:ONBUILD<SP>)?FROM<SP>(?:<OPT><SP>)*(?!--)[^ \\t\\\\\\\\]+<SP>)AS\",\"i\"),lookbehind:!0,greedy:!0},{pattern:a(\"(^ONBUILD<SP>)\\\\w+\",\"i\"),lookbehind:!0,greedy:!0},{pattern:/^\\w+/,greedy:!0}],comment:i,string:o,variable:/\\$(?:\\w+|\\{[^{}\"'\\\\]*\\})/,operator:/\\\\$/m}},comment:i},e.languages.dockerfile=e.languages.docker}(Prism);\nPrism.languages.elixir={doc:{pattern:/@(?:doc|moduledoc)\\s+(?:(\"\"\"|''')[\\s\\S]*?\\1|(\"|')(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2)/,inside:{attribute:/^@\\w+/,string:/['\"][\\s\\S]+/}},comment:{pattern:/#.*/,greedy:!0},regex:{pattern:/~[rR](?:(\"\"\"|''')(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])+\\1|([\\/|\"'])(?:\\\\.|(?!\\2)[^\\\\\\r\\n])+\\2|\\((?:\\\\.|[^\\\\)\\r\\n])+\\)|\\[(?:\\\\.|[^\\\\\\]\\r\\n])+\\]|\\{(?:\\\\.|[^\\\\}\\r\\n])+\\}|<(?:\\\\.|[^\\\\>\\r\\n])+>)[uismxfr]*/,greedy:!0},string:[{pattern:/~[cCsSwW](?:(\"\"\"|''')(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])+\\1|([\\/|\"'])(?:\\\\.|(?!\\2)[^\\\\\\r\\n])+\\2|\\((?:\\\\.|[^\\\\)\\r\\n])+\\)|\\[(?:\\\\.|[^\\\\\\]\\r\\n])+\\]|\\{(?:\\\\.|#\\{[^}]+\\}|#(?!\\{)|[^#\\\\}\\r\\n])+\\}|<(?:\\\\.|[^\\\\>\\r\\n])+>)[csa]?/,greedy:!0,inside:{}},{pattern:/(\"\"\"|''')[\\s\\S]*?\\1/,greedy:!0,inside:{}},{pattern:/(\"|')(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0,inside:{}}],atom:{pattern:/(^|[^:]):\\w+/,lookbehind:!0,alias:\"symbol\"},module:{pattern:/\\b[A-Z]\\w*\\b/,alias:\"class-name\"},\"attr-name\":/\\b\\w+\\??:(?!:)/,argument:{pattern:/(^|[^&])&\\d+/,lookbehind:!0,alias:\"variable\"},attribute:{pattern:/@\\w+/,alias:\"variable\"},function:/\\b[_a-zA-Z]\\w*[?!]?(?:(?=\\s*(?:\\.\\s*)?\\()|(?=\\/\\d))/,number:/\\b(?:0[box][a-f\\d_]+|\\d[\\d_]*)(?:\\.[\\d_]+)?(?:e[+-]?[\\d_]+)?\\b/i,keyword:/\\b(?:after|alias|and|case|catch|cond|def(?:callback|delegate|exception|impl|macro|module|n|np|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|quote|raise|require|rescue|try|unless|unquote|use|when)\\b/,boolean:/\\b(?:false|nil|true)\\b/,operator:[/\\bin\\b|&&?|\\|[|>]?|\\\\\\\\|::|\\.\\.\\.?|\\+\\+?|-[->]?|<[-=>]|>=|!==?|\\B!|=(?:==?|[>~])?|[*\\/^]/,{pattern:/([^<])<(?!<)/,lookbehind:!0},{pattern:/([^>])>(?!>)/,lookbehind:!0}],punctuation:/<<|>>|[.,%\\[\\]{}()]/},Prism.languages.elixir.string.forEach((function(e){e.inside={interpolation:{pattern:/#\\{[^}]+\\}/,inside:{delimiter:{pattern:/^#\\{|\\}$/,alias:\"punctuation\"},rest:Prism.languages.elixir}}}}));\nPrism.languages.elm={comment:/--.*|\\{-[\\s\\S]*?-\\}/,char:{pattern:/'(?:[^\\\\'\\r\\n]|\\\\(?:[abfnrtv\\\\']|\\d+|x[0-9a-fA-F]+|u\\{[0-9a-fA-F]+\\}))'/,greedy:!0},string:[{pattern:/\"\"\"[\\s\\S]*?\"\"\"/,greedy:!0},{pattern:/\"(?:[^\\\\\"\\r\\n]|\\\\.)*\"/,greedy:!0}],\"import-statement\":{pattern:/(^[\\t ]*)import\\s+[A-Z]\\w*(?:\\.[A-Z]\\w*)*(?:\\s+as\\s+(?:[A-Z]\\w*)(?:\\.[A-Z]\\w*)*)?(?:\\s+exposing\\s+)?/m,lookbehind:!0,inside:{keyword:/\\b(?:as|exposing|import)\\b/}},keyword:/\\b(?:alias|as|case|else|exposing|if|in|infixl|infixr|let|module|of|then|type)\\b/,builtin:/\\b(?:abs|acos|always|asin|atan|atan2|ceiling|clamp|compare|cos|curry|degrees|e|flip|floor|fromPolar|identity|isInfinite|isNaN|logBase|max|min|negate|never|not|pi|radians|rem|round|sin|sqrt|tan|toFloat|toPolar|toString|truncate|turns|uncurry|xor)\\b/,number:/\\b(?:\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?|0x[0-9a-f]+)\\b/i,operator:/\\s\\.\\s|[+\\-/*=.$<>:&|^?%#@~!]{2,}|[+\\-/*=$<>:&|^?%#@~!]/,hvariable:/\\b(?:[A-Z]\\w*\\.)*[a-z]\\w*\\b/,constant:/\\b(?:[A-Z]\\w*\\.)*[A-Z]\\w*\\b/,punctuation:/[{}[\\]|(),.:]/};\nPrism.languages.gdscript={comment:/#.*/,string:{pattern:/@?(?:(\"|')(?:(?!\\1)[^\\n\\\\]|\\\\[\\s\\S])*\\1(?!\"|')|\"\"\"(?:[^\\\\]|\\\\[\\s\\S])*?\"\"\")/,greedy:!0},\"class-name\":{pattern:/(^(?:class|class_name|extends)[ \\t]+|^export\\([ \\t]*|\\bas[ \\t]+|(?:\\b(?:const|var)[ \\t]|[,(])[ \\t]*\\w+[ \\t]*:[ \\t]*|->[ \\t]*)[a-zA-Z_]\\w*/m,lookbehind:!0},keyword:/\\b(?:and|as|assert|break|breakpoint|class|class_name|const|continue|elif|else|enum|export|extends|for|func|if|in|is|master|mastersync|match|not|null|onready|or|pass|preload|puppet|puppetsync|remote|remotesync|return|self|setget|signal|static|tool|var|while|yield)\\b/,function:/\\b[a-z_]\\w*(?=[ \\t]*\\()/i,variable:/\\$\\w+/,number:[/\\b0b[01_]+\\b|\\b0x[\\da-fA-F_]+\\b|(?:\\b\\d[\\d_]*(?:\\.[\\d_]*)?|\\B\\.[\\d_]+)(?:e[+-]?[\\d_]+)?\\b/,/\\b(?:INF|NAN|PI|TAU)\\b/],constant:/\\b[A-Z][A-Z_\\d]*\\b/,boolean:/\\b(?:false|true)\\b/,operator:/->|:=|&&|\\|\\||<<|>>|[-+*/%&|!<>=]=?|[~^]/,punctuation:/[.:,;()[\\]{}]/};\nPrism.languages.git={comment:/^#.*/m,deleted:/^[-–].*/m,inserted:/^\\+.*/m,string:/(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/,command:{pattern:/^.*\\$ git .*$/m,inside:{parameter:/\\s--?\\w+/}},coord:/^@@.*@@$/m,\"commit-sha1\":/^commit \\w{40}$/m};\nPrism.languages.glsl=Prism.languages.extend(\"c\",{keyword:/\\b(?:active|asm|atomic_uint|attribute|[ibdu]?vec[234]|bool|break|buffer|case|cast|centroid|class|coherent|common|const|continue|d?mat[234](?:x[234])?|default|discard|do|double|else|enum|extern|external|false|filter|fixed|flat|float|for|fvec[234]|goto|half|highp|hvec[234]|[iu]?sampler2DMS(?:Array)?|[iu]?sampler2DRect|[iu]?samplerBuffer|[iu]?samplerCube|[iu]?samplerCubeArray|[iu]?sampler[123]D|[iu]?sampler[12]DArray|[iu]?image2DMS(?:Array)?|[iu]?image2DRect|[iu]?imageBuffer|[iu]?imageCube|[iu]?imageCubeArray|[iu]?image[123]D|[iu]?image[12]DArray|if|in|inline|inout|input|int|interface|invariant|layout|long|lowp|mediump|namespace|noinline|noperspective|out|output|partition|patch|precise|precision|public|readonly|resource|restrict|return|sample|sampler[12]DArrayShadow|sampler[12]DShadow|sampler2DRectShadow|sampler3DRect|samplerCubeArrayShadow|samplerCubeShadow|shared|short|sizeof|smooth|static|struct|subroutine|superp|switch|template|this|true|typedef|uint|uniform|union|unsigned|using|varying|void|volatile|while|writeonly)\\b/});\nPrism.languages.go=Prism.languages.extend(\"clike\",{string:{pattern:/(^|[^\\\\])\"(?:\\\\.|[^\"\\\\\\r\\n])*\"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\\b/,boolean:/\\b(?:_|false|iota|nil|true)\\b/,number:[/\\b0(?:b[01_]+|o[0-7_]+)i?\\b/i,/\\b0x(?:[a-f\\d_]+(?:\\.[a-f\\d_]*)?|\\.[a-f\\d_]+)(?:p[+-]?\\d+(?:_\\d+)*)?i?(?!\\w)/i,/(?:\\b\\d[\\d_]*(?:\\.[\\d_]*)?|\\B\\.\\d[\\d_]*)(?:e[+-]?[\\d_]+)?i?(?!\\w)/i],operator:/[*\\/%^!=]=?|\\+[=+]?|-[=-]?|\\|[=|]?|&(?:=|&|\\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\\.\\.\\./,builtin:/\\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\\b/}),Prism.languages.insertBefore(\"go\",\"string\",{char:{pattern:/'(?:\\\\.|[^'\\\\\\r\\n]){0,10}'/,greedy:!0}}),delete Prism.languages.go[\"class-name\"];\nPrism.languages.graphql={comment:/#.*/,description:{pattern:/(?:\"\"\"(?:[^\"]|(?!\"\"\")\")*\"\"\"|\"(?:\\\\.|[^\\\\\"\\r\\n])*\")(?=\\s*[a-z_])/i,greedy:!0,alias:\"string\",inside:{\"language-markdown\":{pattern:/(^\"(?:\"\")?)(?!\\1)[\\s\\S]+(?=\\1$)/,lookbehind:!0,inside:Prism.languages.markdown}}},string:{pattern:/\"\"\"(?:[^\"]|(?!\"\"\")\")*\"\"\"|\"(?:\\\\.|[^\\\\\"\\r\\n])*\"/,greedy:!0},number:/(?:\\B-|\\b)\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b/i,boolean:/\\b(?:false|true)\\b/,variable:/\\$[a-z_]\\w*/i,directive:{pattern:/@[a-z_]\\w*/i,alias:\"function\"},\"attr-name\":{pattern:/\\b[a-z_]\\w*(?=\\s*(?:\\((?:[^()\"]|\"(?:\\\\.|[^\\\\\"\\r\\n])*\")*\\))?:)/i,greedy:!0},\"atom-input\":{pattern:/\\b[A-Z]\\w*Input\\b/,alias:\"class-name\"},scalar:/\\b(?:Boolean|Float|ID|Int|String)\\b/,constant:/\\b[A-Z][A-Z_\\d]*\\b/,\"class-name\":{pattern:/(\\b(?:enum|implements|interface|on|scalar|type|union)\\s+|&\\s*|:\\s*|\\[)[A-Z_]\\w*/,lookbehind:!0},fragment:{pattern:/(\\bfragment\\s+|\\.{3}\\s*(?!on\\b))[a-zA-Z_]\\w*/,lookbehind:!0,alias:\"function\"},\"definition-mutation\":{pattern:/(\\bmutation\\s+)[a-zA-Z_]\\w*/,lookbehind:!0,alias:\"function\"},\"definition-query\":{pattern:/(\\bquery\\s+)[a-zA-Z_]\\w*/,lookbehind:!0,alias:\"function\"},keyword:/\\b(?:directive|enum|extend|fragment|implements|input|interface|mutation|on|query|repeatable|scalar|schema|subscription|type|union)\\b/,operator:/[!=|&]|\\.{3}/,\"property-query\":/\\w+(?=\\s*\\()/,object:/\\w+(?=\\s*\\{)/,punctuation:/[!(){}\\[\\]:=,]/,property:/\\w+/},Prism.hooks.add(\"after-tokenize\",(function(n){if(\"graphql\"===n.language)for(var t=n.tokens.filter((function(n){return\"string\"!=typeof n&&\"comment\"!==n.type&&\"scalar\"!==n.type})),e=0;e<t.length;){var a=t[e++];if(\"keyword\"===a.type&&\"mutation\"===a.content){var r=[];if(c([\"definition-mutation\",\"punctuation\"])&&\"(\"===l(1).content){e+=2;var i=f(/^\\($/,/^\\)$/);if(-1===i)continue;for(;e<i;e++){var o=l(0);\"variable\"===o.type&&(b(o,\"variable-input\"),r.push(o.content))}e=i+1}if(c([\"punctuation\",\"property-query\"])&&\"{\"===l(0).content&&(e++,b(l(0),\"property-mutation\"),r.length>0)){var s=f(/^\\{$/,/^\\}$/);if(-1===s)continue;for(var u=e;u<s;u++){var p=t[u];\"variable\"===p.type&&r.indexOf(p.content)>=0&&b(p,\"variable-input\")}}}}function l(n){return t[e+n]}function c(n,t){t=t||0;for(var e=0;e<n.length;e++){var a=l(e+t);if(!a||a.type!==n[e])return!1}return!0}function f(n,a){for(var r=1,i=e;i<t.length;i++){var o=t[i],s=o.content;if(\"punctuation\"===o.type&&\"string\"==typeof s)if(n.test(s))r++;else if(a.test(s)&&0==--r)return i}return-1}function b(n,t){var e=n.alias;e?Array.isArray(e)||(n.alias=e=[e]):n.alias=e=[],e.push(t)}}));\n!function(e){e.languages.ruby=e.languages.extend(\"clike\",{comment:{pattern:/#.*|^=begin\\s[\\s\\S]*?^=end/m,greedy:!0},\"class-name\":{pattern:/(\\b(?:class|module)\\s+|\\bcatch\\s+\\()[\\w.\\\\]+|\\b[A-Z_]\\w*(?=\\s*\\.\\s*new\\b)/,lookbehind:!0,inside:{punctuation:/[.\\\\]/}},keyword:/\\b(?:BEGIN|END|alias|and|begin|break|case|class|def|define_method|defined|do|each|else|elsif|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|private|protected|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\\b/,operator:/\\.{2,3}|&\\.|===|<?=>|[!=]?~|(?:&&|\\|\\||<<|>>|\\*\\*|[+\\-*/%<>!^&|=])=?|[?:]/,punctuation:/[(){}[\\].,;]/}),e.languages.insertBefore(\"ruby\",\"operator\",{\"double-colon\":{pattern:/::/,alias:\"punctuation\"}});var n={pattern:/((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}/,lookbehind:!0,inside:{content:{pattern:/^(#\\{)[\\s\\S]+(?=\\}$)/,lookbehind:!0,inside:e.languages.ruby},delimiter:{pattern:/^#\\{|\\}$/,alias:\"punctuation\"}}};delete e.languages.ruby.function;var t=\"(?:\"+[\"([^a-zA-Z0-9\\\\s{(\\\\[<=])(?:(?!\\\\1)[^\\\\\\\\]|\\\\\\\\[^])*\\\\1\",\"\\\\((?:[^()\\\\\\\\]|\\\\\\\\[^]|\\\\((?:[^()\\\\\\\\]|\\\\\\\\[^])*\\\\))*\\\\)\",\"\\\\{(?:[^{}\\\\\\\\]|\\\\\\\\[^]|\\\\{(?:[^{}\\\\\\\\]|\\\\\\\\[^])*\\\\})*\\\\}\",\"\\\\[(?:[^\\\\[\\\\]\\\\\\\\]|\\\\\\\\[^]|\\\\[(?:[^\\\\[\\\\]\\\\\\\\]|\\\\\\\\[^])*\\\\])*\\\\]\",\"<(?:[^<>\\\\\\\\]|\\\\\\\\[^]|<(?:[^<>\\\\\\\\]|\\\\\\\\[^])*>)*>\"].join(\"|\")+\")\",i='(?:\"(?:\\\\\\\\.|[^\"\\\\\\\\\\r\\n])*\"|(?:\\\\b[a-zA-Z_]\\\\w*|[^\\\\s\\0-\\\\x7F]+)[?!]?|\\\\$.)';e.languages.insertBefore(\"ruby\",\"keyword\",{\"regex-literal\":[{pattern:RegExp(\"%r\"+t+\"[egimnosux]{0,6}\"),greedy:!0,inside:{interpolation:n,regex:/[\\s\\S]+/}},{pattern:/(^|[^/])\\/(?!\\/)(?:\\[[^\\r\\n\\]]+\\]|\\\\.|[^[/\\\\\\r\\n])+\\/[egimnosux]{0,6}(?=\\s*(?:$|[\\r\\n,.;})#]))/,lookbehind:!0,greedy:!0,inside:{interpolation:n,regex:/[\\s\\S]+/}}],variable:/[@$]+[a-zA-Z_]\\w*(?:[?!]|\\b)/,symbol:[{pattern:RegExp(\"(^|[^:]):\"+i),lookbehind:!0,greedy:!0},{pattern:RegExp(\"([\\r\\n{(,][ \\t]*)\"+i+\"(?=:(?!:))\"),lookbehind:!0,greedy:!0}],\"method-definition\":{pattern:/(\\bdef\\s+)\\w+(?:\\s*\\.\\s*\\w+)?/,lookbehind:!0,inside:{function:/\\b\\w+$/,keyword:/^self\\b/,\"class-name\":/^\\w+/,punctuation:/\\./}}}),e.languages.insertBefore(\"ruby\",\"string\",{\"string-literal\":[{pattern:RegExp(\"%[qQiIwWs]?\"+t),greedy:!0,inside:{interpolation:n,string:/[\\s\\S]+/}},{pattern:/(\"|')(?:#\\{[^}]+\\}|#(?!\\{)|\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\#\\r\\n])*\\1/,greedy:!0,inside:{interpolation:n,string:/[\\s\\S]+/}},{pattern:/<<[-~]?([a-z_]\\w*)[\\r\\n](?:.*[\\r\\n])*?[\\t ]*\\1/i,alias:\"heredoc-string\",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?[a-z_]\\w*|\\b[a-z_]\\w*$/i,inside:{symbol:/\\b\\w+/,punctuation:/^<<[-~]?/}},interpolation:n,string:/[\\s\\S]+/}},{pattern:/<<[-~]?'([a-z_]\\w*)'[\\r\\n](?:.*[\\r\\n])*?[\\t ]*\\1/i,alias:\"heredoc-string\",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?'[a-z_]\\w*'|\\b[a-z_]\\w*$/i,inside:{symbol:/\\b\\w+/,punctuation:/^<<[-~]?'|'$/}},string:/[\\s\\S]+/}}],\"command-literal\":[{pattern:RegExp(\"%x\"+t),greedy:!0,inside:{interpolation:n,command:{pattern:/[\\s\\S]+/,alias:\"string\"}}},{pattern:/`(?:#\\{[^}]+\\}|#(?!\\{)|\\\\(?:\\r\\n|[\\s\\S])|[^\\\\`#\\r\\n])*`/,greedy:!0,inside:{interpolation:n,command:{pattern:/[\\s\\S]+/,alias:\"string\"}}}]}),delete e.languages.ruby.string,e.languages.insertBefore(\"ruby\",\"number\",{builtin:/\\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Fixnum|Float|Hash|IO|Integer|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|Stat|String|Struct|Symbol|TMS|Thread|ThreadGroup|Time|TrueClass)\\b/,constant:/\\b[A-Z][A-Z0-9_]*(?:[?!]|\\b)/}),e.languages.rb=e.languages.ruby}(Prism);\n!function(n){n.languages.haml={\"multiline-comment\":{pattern:/((?:^|\\r?\\n|\\r)([\\t ]*))(?:\\/|-#).*(?:(?:\\r?\\n|\\r)\\2[\\t ].+)*/,lookbehind:!0,alias:\"comment\"},\"multiline-code\":[{pattern:/((?:^|\\r?\\n|\\r)([\\t ]*)(?:[~-]|[&!]?=)).*,[\\t ]*(?:(?:\\r?\\n|\\r)\\2[\\t ].*,[\\t ]*)*(?:(?:\\r?\\n|\\r)\\2[\\t ].+)/,lookbehind:!0,inside:n.languages.ruby},{pattern:/((?:^|\\r?\\n|\\r)([\\t ]*)(?:[~-]|[&!]?=)).*\\|[\\t ]*(?:(?:\\r?\\n|\\r)\\2[\\t ].*\\|[\\t ]*)*/,lookbehind:!0,inside:n.languages.ruby}],filter:{pattern:/((?:^|\\r?\\n|\\r)([\\t ]*)):[\\w-]+(?:(?:\\r?\\n|\\r)(?:\\2[\\t ].+|\\s*?(?=\\r?\\n|\\r)))+/,lookbehind:!0,inside:{\"filter-name\":{pattern:/^:[\\w-]+/,alias:\"symbol\"}}},markup:{pattern:/((?:^|\\r?\\n|\\r)[\\t ]*)<.+/,lookbehind:!0,inside:n.languages.markup},doctype:{pattern:/((?:^|\\r?\\n|\\r)[\\t ]*)!!!(?: .+)?/,lookbehind:!0},tag:{pattern:/((?:^|\\r?\\n|\\r)[\\t ]*)[%.#][\\w\\-#.]*[\\w\\-](?:\\([^)]+\\)|\\{(?:\\{[^}]+\\}|[^{}])+\\}|\\[[^\\]]+\\])*[\\/<>]*/,lookbehind:!0,inside:{attributes:[{pattern:/(^|[^#])\\{(?:\\{[^}]+\\}|[^{}])+\\}/,lookbehind:!0,inside:n.languages.ruby},{pattern:/\\([^)]+\\)/,inside:{\"attr-value\":{pattern:/(=\\s*)(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|[^)\\s]+)/,lookbehind:!0},\"attr-name\":/[\\w:-]+(?=\\s*!?=|\\s*[,)])/,punctuation:/[=(),]/}},{pattern:/\\[[^\\]]+\\]/,inside:n.languages.ruby}],punctuation:/[<>]/}},code:{pattern:/((?:^|\\r?\\n|\\r)[\\t ]*(?:[~-]|[&!]?=)).+/,lookbehind:!0,inside:n.languages.ruby},interpolation:{pattern:/#\\{[^}]+\\}/,inside:{delimiter:{pattern:/^#\\{|\\}$/,alias:\"punctuation\"},ruby:{pattern:/[\\s\\S]+/,inside:n.languages.ruby}}},punctuation:{pattern:/((?:^|\\r?\\n|\\r)[\\t ]*)[~=\\-&!]+/,lookbehind:!0}};for(var e=[\"css\",{filter:\"coffee\",language:\"coffeescript\"},\"erb\",\"javascript\",\"less\",\"markdown\",\"ruby\",\"scss\",\"textile\"],t={},r=0,a=e.length;r<a;r++){var i=e[r];i=\"string\"==typeof i?{filter:i,language:i}:i,n.languages[i.language]&&(t[\"filter-\"+i.filter]={pattern:RegExp(\"((?:^|\\\\r?\\\\n|\\\\r)([\\\\t ]*)):{{filter_name}}(?:(?:\\\\r?\\\\n|\\\\r)(?:\\\\2[\\\\t ].+|\\\\s*?(?=\\\\r?\\\\n|\\\\r)))+\".replace(\"{{filter_name}}\",(function(){return i.filter}))),lookbehind:!0,inside:{\"filter-name\":{pattern:/^:[\\w-]+/,alias:\"symbol\"},text:{pattern:/[\\s\\S]+/,alias:[i.language,\"language-\"+i.language],inside:n.languages[i.language]}}})}n.languages.insertBefore(\"haml\",\"filter\",t)}(Prism);\n!function(e){function n(e,n){return\"___\"+e.toUpperCase()+n+\"___\"}Object.defineProperties(e.languages[\"markup-templating\"]={},{buildPlaceholders:{value:function(t,a,r,o){if(t.language===a){var c=t.tokenStack=[];t.code=t.code.replace(r,(function(e){if(\"function\"==typeof o&&!o(e))return e;for(var r,i=c.length;-1!==t.code.indexOf(r=n(a,i));)++i;return c[i]=e,r})),t.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(t,a){if(t.language===a&&t.tokenStack){t.grammar=e.languages[a];var r=0,o=Object.keys(t.tokenStack);!function c(i){for(var u=0;u<i.length&&!(r>=o.length);u++){var g=i[u];if(\"string\"==typeof g||g.content&&\"string\"==typeof g.content){var l=o[r],s=t.tokenStack[l],f=\"string\"==typeof g?g:g.content,p=n(a,l),k=f.indexOf(p);if(k>-1){++r;var m=f.substring(0,k),d=new e.Token(a,e.tokenize(s,t.grammar),\"language-\"+a,s),h=f.substring(k+p.length),v=[];m&&v.push.apply(v,c([m])),v.push(d),h&&v.push.apply(v,c([h])),\"string\"==typeof g?i.splice.apply(i,[u,1].concat(v)):g.content=v}}else g.content&&c(g.content)}return i}(t.tokens)}}}})}(Prism);\n!function(a){a.languages.handlebars={comment:/\\{\\{![\\s\\S]*?\\}\\}/,delimiter:{pattern:/^\\{\\{\\{?|\\}\\}\\}?$/,alias:\"punctuation\"},string:/([\"'])(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/,number:/\\b0x[\\dA-Fa-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[Ee][+-]?\\d+)?/,boolean:/\\b(?:false|true)\\b/,block:{pattern:/^(\\s*(?:~\\s*)?)[#\\/]\\S+?(?=\\s*(?:~\\s*)?$|\\s)/,lookbehind:!0,alias:\"keyword\"},brackets:{pattern:/\\[[^\\]]+\\]/,inside:{punctuation:/\\[|\\]/,variable:/[\\s\\S]+/}},punctuation:/[!\"#%&':()*+,.\\/;<=>@\\[\\\\\\]^`{|}~]/,variable:/[^!\"#%&'()*+,\\/;<=>@\\[\\\\\\]^`{|}~\\s]+/},a.hooks.add(\"before-tokenize\",(function(e){a.languages[\"markup-templating\"].buildPlaceholders(e,\"handlebars\",/\\{\\{\\{[\\s\\S]+?\\}\\}\\}|\\{\\{[\\s\\S]+?\\}\\}/g)})),a.hooks.add(\"after-tokenize\",(function(e){a.languages[\"markup-templating\"].tokenizePlaceholders(e,\"handlebars\")})),a.languages.hbs=a.languages.handlebars,a.languages.mustache=a.languages.handlebars}(Prism);\nPrism.languages.haskell={comment:{pattern:/(^|[^-!#$%*+=?&@|~.:<>^\\\\\\/])(?:--(?:(?=.)[^-!#$%*+=?&@|~.:<>^\\\\\\/].*|$)|\\{-[\\s\\S]*?-\\})/m,lookbehind:!0},char:{pattern:/'(?:[^\\\\']|\\\\(?:[abfnrtv\\\\\"'&]|\\^[A-Z@[\\]^_]|ACK|BEL|BS|CAN|CR|DC1|DC2|DC3|DC4|DEL|DLE|EM|ENQ|EOT|ESC|ETB|ETX|FF|FS|GS|HT|LF|NAK|NUL|RS|SI|SO|SOH|SP|STX|SUB|SYN|US|VT|\\d+|o[0-7]+|x[0-9a-fA-F]+))'/,alias:\"string\"},string:{pattern:/\"(?:[^\\\\\"]|\\\\(?:\\S|\\s+\\\\))*\"/,greedy:!0},keyword:/\\b(?:case|class|data|deriving|do|else|if|in|infixl|infixr|instance|let|module|newtype|of|primitive|then|type|where)\\b/,\"import-statement\":{pattern:/(^[\\t ]*)import\\s+(?:qualified\\s+)?(?:[A-Z][\\w']*)(?:\\.[A-Z][\\w']*)*(?:\\s+as\\s+(?:[A-Z][\\w']*)(?:\\.[A-Z][\\w']*)*)?(?:\\s+hiding\\b)?/m,lookbehind:!0,inside:{keyword:/\\b(?:as|hiding|import|qualified)\\b/,punctuation:/\\./}},builtin:/\\b(?:abs|acos|acosh|all|and|any|appendFile|approxRational|asTypeOf|asin|asinh|atan|atan2|atanh|basicIORun|break|catch|ceiling|chr|compare|concat|concatMap|const|cos|cosh|curry|cycle|decodeFloat|denominator|digitToInt|div|divMod|drop|dropWhile|either|elem|encodeFloat|enumFrom|enumFromThen|enumFromThenTo|enumFromTo|error|even|exp|exponent|fail|filter|flip|floatDigits|floatRadix|floatRange|floor|fmap|foldl|foldl1|foldr|foldr1|fromDouble|fromEnum|fromInt|fromInteger|fromIntegral|fromRational|fst|gcd|getChar|getContents|getLine|group|head|id|inRange|index|init|intToDigit|interact|ioError|isAlpha|isAlphaNum|isAscii|isControl|isDenormalized|isDigit|isHexDigit|isIEEE|isInfinite|isLower|isNaN|isNegativeZero|isOctDigit|isPrint|isSpace|isUpper|iterate|last|lcm|length|lex|lexDigits|lexLitChar|lines|log|logBase|lookup|map|mapM|mapM_|max|maxBound|maximum|maybe|min|minBound|minimum|mod|negate|not|notElem|null|numerator|odd|or|ord|otherwise|pack|pi|pred|primExitWith|print|product|properFraction|putChar|putStr|putStrLn|quot|quotRem|range|rangeSize|read|readDec|readFile|readFloat|readHex|readIO|readInt|readList|readLitChar|readLn|readOct|readParen|readSigned|reads|readsPrec|realToFrac|recip|rem|repeat|replicate|return|reverse|round|scaleFloat|scanl|scanl1|scanr|scanr1|seq|sequence|sequence_|show|showChar|showInt|showList|showLitChar|showParen|showSigned|showString|shows|showsPrec|significand|signum|sin|sinh|snd|sort|span|splitAt|sqrt|subtract|succ|sum|tail|take|takeWhile|tan|tanh|threadToIOResult|toEnum|toInt|toInteger|toLower|toRational|toUpper|truncate|uncurry|undefined|unlines|until|unwords|unzip|unzip3|userError|words|writeFile|zip|zip3|zipWith|zipWith3)\\b/,number:/\\b(?:\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?|0o[0-7]+|0x[0-9a-f]+)\\b/i,operator:[{pattern:/`(?:[A-Z][\\w']*\\.)*[_a-z][\\w']*`/,greedy:!0},{pattern:/(\\s)\\.(?=\\s)/,lookbehind:!0},/[-!#$%*+=?&@|~:<>^\\\\\\/][-!#$%*+=?&@|~.:<>^\\\\\\/]*|\\.[-!#$%*+=?&@|~.:<>^\\\\\\/]+/],hvariable:{pattern:/\\b(?:[A-Z][\\w']*\\.)*[_a-z][\\w']*/,inside:{punctuation:/\\./}},constant:{pattern:/\\b(?:[A-Z][\\w']*\\.)*[A-Z][\\w']*/,inside:{punctuation:/\\./}},punctuation:/[{}[\\];(),.:]/},Prism.languages.hs=Prism.languages.haskell;\n!function(t){function a(t){return RegExp(\"(^(?:\"+t+\"):[ \\t]*(?![ \\t]))[^]+\",\"i\")}t.languages.http={\"request-line\":{pattern:/^(?:CONNECT|DELETE|GET|HEAD|OPTIONS|PATCH|POST|PRI|PUT|SEARCH|TRACE)\\s(?:https?:\\/\\/|\\/)\\S*\\sHTTP\\/[\\d.]+/m,inside:{method:{pattern:/^[A-Z]+\\b/,alias:\"property\"},\"request-target\":{pattern:/^(\\s)(?:https?:\\/\\/|\\/)\\S*(?=\\s)/,lookbehind:!0,alias:\"url\",inside:t.languages.uri},\"http-version\":{pattern:/^(\\s)HTTP\\/[\\d.]+/,lookbehind:!0,alias:\"property\"}}},\"response-status\":{pattern:/^HTTP\\/[\\d.]+ \\d+ .+/m,inside:{\"http-version\":{pattern:/^HTTP\\/[\\d.]+/,alias:\"property\"},\"status-code\":{pattern:/^(\\s)\\d+(?=\\s)/,lookbehind:!0,alias:\"number\"},\"reason-phrase\":{pattern:/^(\\s).+/,lookbehind:!0,alias:\"string\"}}},header:{pattern:/^[\\w-]+:.+(?:(?:\\r\\n?|\\n)[ \\t].+)*/m,inside:{\"header-value\":[{pattern:a(\"Content-Security-Policy\"),lookbehind:!0,alias:[\"csp\",\"languages-csp\"],inside:t.languages.csp},{pattern:a(\"Public-Key-Pins(?:-Report-Only)?\"),lookbehind:!0,alias:[\"hpkp\",\"languages-hpkp\"],inside:t.languages.hpkp},{pattern:a(\"Strict-Transport-Security\"),lookbehind:!0,alias:[\"hsts\",\"languages-hsts\"],inside:t.languages.hsts},{pattern:a(\"[^:]+\"),lookbehind:!0}],\"header-name\":{pattern:/^[^:]+/,alias:\"keyword\"},punctuation:/^:/}}};var e,n=t.languages,s={\"application/javascript\":n.javascript,\"application/json\":n.json||n.javascript,\"application/xml\":n.xml,\"text/xml\":n.xml,\"text/html\":n.html,\"text/css\":n.css,\"text/plain\":n.plain},i={\"application/json\":!0,\"application/xml\":!0};function r(t){var a=t.replace(/^[a-z]+\\//,\"\");return\"(?:\"+t+\"|\\\\w+/(?:[\\\\w.-]+\\\\+)+\"+a+\"(?![+\\\\w.-]))\"}for(var p in s)if(s[p]){e=e||{};var l=i[p]?r(p):p;e[p.replace(/\\//g,\"-\")]={pattern:RegExp(\"(content-type:\\\\s*\"+l+\"(?:(?:\\r\\n?|\\n)[\\\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\\\w-][^]*\",\"i\"),lookbehind:!0,inside:s[p]}}e&&t.languages.insertBefore(\"http\",\"header\",e)}(Prism);\nPrism.languages.ini={comment:{pattern:/(^[ \\f\\t\\v]*)[#;][^\\n\\r]*/m,lookbehind:!0},section:{pattern:/(^[ \\f\\t\\v]*)\\[[^\\n\\r\\]]*\\]?/m,lookbehind:!0,inside:{\"section-name\":{pattern:/(^\\[[ \\f\\t\\v]*)[^ \\f\\t\\v\\]]+(?:[ \\f\\t\\v]+[^ \\f\\t\\v\\]]+)*/,lookbehind:!0,alias:\"selector\"},punctuation:/\\[|\\]/}},key:{pattern:/(^[ \\f\\t\\v]*)[^ \\f\\n\\r\\t\\v=]+(?:[ \\f\\t\\v]+[^ \\f\\n\\r\\t\\v=]+)*(?=[ \\f\\t\\v]*=)/m,lookbehind:!0,alias:\"attr-name\"},value:{pattern:/(=[ \\f\\t\\v]*)[^ \\f\\n\\r\\t\\v]+(?:[ \\f\\t\\v]+[^ \\f\\n\\r\\t\\v]+)*/,lookbehind:!0,alias:\"attr-value\",inside:{\"inner-value\":{pattern:/^(\"|').+(?=\\1$)/,lookbehind:!0}}},punctuation:/=/};\n!function(e){var n=/\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\\s*[(){}[\\]<>=%~.:,;?+\\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b/,t=\"(?:[a-z]\\\\w*\\\\s*\\\\.\\\\s*)*(?:[A-Z]\\\\w*\\\\s*\\\\.\\\\s*)*\",s={pattern:RegExp(\"(^|[^\\\\w.])\"+t+\"[A-Z](?:[\\\\d_A-Z]*[a-z]\\\\w*)?\\\\b\"),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?/,inside:{punctuation:/\\./}},punctuation:/\\./}};e.languages.java=e.languages.extend(\"clike\",{string:{pattern:/(^|[^\\\\])\"(?:\\\\.|[^\"\\\\\\r\\n])*\"/,lookbehind:!0,greedy:!0},\"class-name\":[s,{pattern:RegExp(\"(^|[^\\\\w.])\"+t+\"[A-Z]\\\\w*(?=\\\\s+\\\\w+\\\\s*[;,=()]|\\\\s*(?:\\\\[[\\\\s,]*\\\\]\\\\s*)?::\\\\s*new\\\\b)\"),lookbehind:!0,inside:s.inside},{pattern:RegExp(\"(\\\\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\\\\s+)\"+t+\"[A-Z]\\\\w*\\\\b\"),lookbehind:!0,inside:s.inside}],keyword:n,function:[e.languages.clike.function,{pattern:/(::\\s*)[a-z_]\\w*/,lookbehind:!0}],number:/\\b0b[01][01_]*L?\\b|\\b0x(?:\\.[\\da-f_p+-]+|[\\da-f_]+(?:\\.[\\da-f_p+-]+)?)\\b|(?:\\b\\d[\\d_]*(?:\\.[\\d_]*)?|\\B\\.\\d[\\d_]*)(?:e[+-]?\\d[\\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\\+\\+|&&|\\|\\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0},constant:/\\b[A-Z][A-Z_\\d]+\\b/}),e.languages.insertBefore(\"java\",\"string\",{\"triple-quoted-string\":{pattern:/\"\"\"[ \\t]*[\\r\\n](?:(?:\"|\"\")?(?:\\\\.|[^\"\\\\]))*\"\"\"/,greedy:!0,alias:\"string\"},char:{pattern:/'(?:\\\\.|[^'\\\\\\r\\n]){1,6}'/,greedy:!0}}),e.languages.insertBefore(\"java\",\"class-name\",{annotation:{pattern:/(^|[^.])@\\w+(?:\\s*\\.\\s*\\w+)*/,lookbehind:!0,alias:\"punctuation\"},generics:{pattern:/<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{\"class-name\":s,keyword:n,punctuation:/[<>(),.:]/,operator:/[?&|]/}},import:[{pattern:RegExp(\"(\\\\bimport\\\\s+)\"+t+\"(?:[A-Z]\\\\w*|\\\\*)(?=\\\\s*;)\"),lookbehind:!0,inside:{namespace:s.inside.namespace,punctuation:/\\./,operator:/\\*/,\"class-name\":/\\w+/}},{pattern:RegExp(\"(\\\\bimport\\\\s+static\\\\s+)\"+t+\"(?:\\\\w+|\\\\*)(?=\\\\s*;)\"),lookbehind:!0,alias:\"static\",inside:{namespace:s.inside.namespace,static:/\\b\\w+$/,punctuation:/\\./,operator:/\\*/,\"class-name\":/\\w+/}}],namespace:{pattern:RegExp(\"(\\\\b(?:exports|import(?:\\\\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\\\\s+)(?!<keyword>)[a-z]\\\\w*(?:\\\\.[a-z]\\\\w*)*\\\\.?\".replace(/<keyword>/g,(function(){return n.source}))),lookbehind:!0,inside:{punctuation:/\\./}}})}(Prism);\n!function(a){function e(a,e){return RegExp(a.replace(/<ID>/g,(function(){return\"(?!\\\\s)[_$a-zA-Z\\\\xA0-\\\\uFFFF](?:(?!\\\\s)[$\\\\w\\\\xA0-\\\\uFFFF])*\"})),e)}a.languages.insertBefore(\"javascript\",\"function-variable\",{\"method-variable\":{pattern:RegExp(\"(\\\\.\\\\s*)\"+a.languages.javascript[\"function-variable\"].pattern.source),lookbehind:!0,alias:[\"function-variable\",\"method\",\"function\",\"property-access\"]}}),a.languages.insertBefore(\"javascript\",\"function\",{method:{pattern:RegExp(\"(\\\\.\\\\s*)\"+a.languages.javascript.function.source),lookbehind:!0,alias:[\"function\",\"property-access\"]}}),a.languages.insertBefore(\"javascript\",\"constant\",{\"known-class-name\":[{pattern:/\\b(?:(?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)?Array|ArrayBuffer|BigInt|Boolean|DataView|Date|Error|Function|Intl|JSON|(?:Weak)?(?:Map|Set)|Math|Number|Object|Promise|Proxy|Reflect|RegExp|String|Symbol|WebAssembly)\\b/,alias:\"class-name\"},{pattern:/\\b(?:[A-Z]\\w*)Error\\b/,alias:\"class-name\"}]}),a.languages.insertBefore(\"javascript\",\"keyword\",{imports:{pattern:e(\"(\\\\bimport\\\\b\\\\s*)(?:<ID>(?:\\\\s*,\\\\s*(?:\\\\*\\\\s*as\\\\s+<ID>|\\\\{[^{}]*\\\\}))?|\\\\*\\\\s*as\\\\s+<ID>|\\\\{[^{}]*\\\\})(?=\\\\s*\\\\bfrom\\\\b)\"),lookbehind:!0,inside:a.languages.javascript},exports:{pattern:e(\"(\\\\bexport\\\\b\\\\s*)(?:\\\\*(?:\\\\s*as\\\\s+<ID>)?(?=\\\\s*\\\\bfrom\\\\b)|\\\\{[^{}]*\\\\})\"),lookbehind:!0,inside:a.languages.javascript}}),a.languages.javascript.keyword.unshift({pattern:/\\b(?:as|default|export|from|import)\\b/,alias:\"module\"},{pattern:/\\b(?:await|break|catch|continue|do|else|finally|for|if|return|switch|throw|try|while|yield)\\b/,alias:\"control-flow\"},{pattern:/\\bnull\\b/,alias:[\"null\",\"nil\"]},{pattern:/\\bundefined\\b/,alias:\"nil\"}),a.languages.insertBefore(\"javascript\",\"operator\",{spread:{pattern:/\\.{3}/,alias:\"operator\"},arrow:{pattern:/=>/,alias:\"operator\"}}),a.languages.insertBefore(\"javascript\",\"punctuation\",{\"property-access\":{pattern:e(\"(\\\\.\\\\s*)#?<ID>\"),lookbehind:!0},\"maybe-class-name\":{pattern:/(^|[^$\\w\\xA0-\\uFFFF])[A-Z][$\\w\\xA0-\\uFFFF]+/,lookbehind:!0},dom:{pattern:/\\b(?:document|(?:local|session)Storage|location|navigator|performance|window)\\b/,alias:\"variable\"},console:{pattern:/\\bconsole(?=\\s*\\.)/,alias:\"class-name\"}});for(var t=[\"function\",\"function-variable\",\"method\",\"method-variable\",\"property-access\"],r=0;r<t.length;r++){var n=t[r],s=a.languages.javascript[n];\"RegExp\"===a.util.type(s)&&(s=a.languages.javascript[n]={pattern:s});var o=s.inside||{};s.inside=o,o[\"maybe-class-name\"]=/^[A-Z][\\s\\S]*/}}(Prism);\nPrism.languages.json={property:{pattern:/(^|[^\\\\])\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?=\\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\\\])\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?!\\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\\/\\/.*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/,greedy:!0},number:/-?\\b\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b/i,punctuation:/[{}[\\],]/,operator:/:/,boolean:/\\b(?:false|true)\\b/,null:{pattern:/\\bnull\\b/,alias:\"keyword\"}},Prism.languages.webmanifest=Prism.languages.json;\nPrism.languages.jsonp=Prism.languages.extend(\"json\",{punctuation:/[{}[\\]();,.]/}),Prism.languages.insertBefore(\"jsonp\",\"punctuation\",{function:/(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?=\\s*\\()/});\n!function(n){n.languages.kotlin=n.languages.extend(\"clike\",{keyword:{pattern:/(^|[^.])\\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\\b/,lookbehind:!0},function:[{pattern:/(?:`[^\\r\\n`]+`|\\b\\w+)(?=\\s*\\()/,greedy:!0},{pattern:/(\\.)(?:`[^\\r\\n`]+`|\\w+)(?=\\s*\\{)/,lookbehind:!0,greedy:!0}],number:/\\b(?:0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\\d+(?:_\\d+)*(?:\\.\\d+(?:_\\d+)*)?(?:[eE][+-]?\\d+(?:_\\d+)*)?[fFL]?)\\b/,operator:/\\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\\/*%<>]=?|[?:]:?|\\.\\.|&&|\\|\\||\\b(?:and|inv|or|shl|shr|ushr|xor)\\b/}),delete n.languages.kotlin[\"class-name\"];var e={\"interpolation-punctuation\":{pattern:/^\\$\\{?|\\}$/,alias:\"punctuation\"},expression:{pattern:/[\\s\\S]+/,inside:n.languages.kotlin}};n.languages.insertBefore(\"kotlin\",\"string\",{\"string-literal\":[{pattern:/\"\"\"(?:[^$]|\\$(?:(?!\\{)|\\{[^{}]*\\}))*?\"\"\"/,alias:\"multiline\",inside:{interpolation:{pattern:/\\$(?:[a-z_]\\w*|\\{[^{}]*\\})/i,inside:e},string:/[\\s\\S]+/}},{pattern:/\"(?:[^\"\\\\\\r\\n$]|\\\\.|\\$(?:(?!\\{)|\\{[^{}]*\\}))*\"/,alias:\"singleline\",inside:{interpolation:{pattern:/((?:^|[^\\\\])(?:\\\\{2})*)\\$(?:[a-z_]\\w*|\\{[^{}]*\\})/i,lookbehind:!0,inside:e},string:/[\\s\\S]+/}}],char:{pattern:/'(?:[^'\\\\\\r\\n]|\\\\(?:.|u[a-fA-F0-9]{0,4}))'/,greedy:!0}}),delete n.languages.kotlin.string,n.languages.insertBefore(\"kotlin\",\"keyword\",{annotation:{pattern:/\\B@(?:\\w+:)?(?:[A-Z]\\w*|\\[[^\\]]+\\])/,alias:\"builtin\"}}),n.languages.insertBefore(\"kotlin\",\"function\",{label:{pattern:/\\b\\w+@|@\\w+\\b/,alias:\"symbol\"}}),n.languages.kt=n.languages.kotlin,n.languages.kts=n.languages.kotlin}(Prism);\n!function(a){var e=/\\\\(?:[^a-z()[\\]]|[a-z*]+)/i,n={\"equation-command\":{pattern:e,alias:\"regex\"}};a.languages.latex={comment:/%.*/,cdata:{pattern:/(\\\\begin\\{((?:lstlisting|verbatim)\\*?)\\})[\\s\\S]*?(?=\\\\end\\{\\2\\})/,lookbehind:!0},equation:[{pattern:/\\$\\$(?:\\\\[\\s\\S]|[^\\\\$])+\\$\\$|\\$(?:\\\\[\\s\\S]|[^\\\\$])+\\$|\\\\\\([\\s\\S]*?\\\\\\)|\\\\\\[[\\s\\S]*?\\\\\\]/,inside:n,alias:\"string\"},{pattern:/(\\\\begin\\{((?:align|eqnarray|equation|gather|math|multline)\\*?)\\})[\\s\\S]*?(?=\\\\end\\{\\2\\})/,lookbehind:!0,inside:n,alias:\"string\"}],keyword:{pattern:/(\\\\(?:begin|cite|documentclass|end|label|ref|usepackage)(?:\\[[^\\]]+\\])?\\{)[^}]+(?=\\})/,lookbehind:!0},url:{pattern:/(\\\\url\\{)[^}]+(?=\\})/,lookbehind:!0},headline:{pattern:/(\\\\(?:chapter|frametitle|paragraph|part|section|subparagraph|subsection|subsubparagraph|subsubsection|subsubsubparagraph)\\*?(?:\\[[^\\]]+\\])?\\{)[^}]+(?=\\})/,lookbehind:!0,alias:\"class-name\"},function:{pattern:e,alias:\"selector\"},punctuation:/[[\\]{}&]/},a.languages.tex=a.languages.latex,a.languages.context=a.languages.latex}(Prism);\nPrism.languages.less=Prism.languages.extend(\"css\",{comment:[/\\/\\*[\\s\\S]*?\\*\\//,{pattern:/(^|[^\\\\])\\/\\/.*/,lookbehind:!0}],atrule:{pattern:/@[\\w-](?:\\((?:[^(){}]|\\([^(){}]*\\))*\\)|[^(){};\\s]|\\s+(?!\\s))*?(?=\\s*\\{)/,inside:{punctuation:/[:()]/}},selector:{pattern:/(?:@\\{[\\w-]+\\}|[^{};\\s@])(?:@\\{[\\w-]+\\}|\\((?:[^(){}]|\\([^(){}]*\\))*\\)|[^(){};@\\s]|\\s+(?!\\s))*?(?=\\s*\\{)/,inside:{variable:/@+[\\w-]+/}},property:/(?:@\\{[\\w-]+\\}|[\\w-])+(?:\\+_?)?(?=\\s*:)/,operator:/[+\\-*\\/]/}),Prism.languages.insertBefore(\"less\",\"property\",{variable:[{pattern:/@[\\w-]+\\s*:/,inside:{punctuation:/:/}},/@@?[\\w-]+/],\"mixin-usage\":{pattern:/([{;]\\s*)[.#](?!\\d)[\\w-].*?(?=[(;])/,lookbehind:!0,alias:\"function\"}});\n!function(e){function n(e){return RegExp(\"(\\\\()(?:\"+e+\")(?=[\\\\s\\\\)])\")}function a(e){return RegExp(\"([\\\\s([])(?:\"+e+\")(?=[\\\\s)])\")}var t=\"(?!\\\\d)[-+*/~!@$%^=<>{}\\\\w]+\",r=\"(\\\\()\",i=\"(?:[^()]|\\\\((?:[^()]|\\\\((?:[^()]|\\\\((?:[^()]|\\\\((?:[^()]|\\\\([^()]*\\\\))*\\\\))*\\\\))*\\\\))*\\\\))*\",s={heading:{pattern:/;;;.*/,alias:[\"comment\",\"title\"]},comment:/;.*/,string:{pattern:/\"(?:[^\"\\\\]|\\\\.)*\"/,greedy:!0,inside:{argument:/[-A-Z]+(?=[.,\\s])/,symbol:RegExp(\"`\"+t+\"'\")}},\"quoted-symbol\":{pattern:RegExp(\"#?'\"+t),alias:[\"variable\",\"symbol\"]},\"lisp-property\":{pattern:RegExp(\":\"+t),alias:\"property\"},splice:{pattern:RegExp(\",@?\"+t),alias:[\"symbol\",\"variable\"]},keyword:[{pattern:RegExp(\"(\\\\()(?:and|(?:cl-)?letf|cl-loop|cond|cons|error|if|(?:lexical-)?let\\\\*?|message|not|null|or|provide|require|setq|unless|use-package|when|while)(?=\\\\s)\"),lookbehind:!0},{pattern:RegExp(\"(\\\\()(?:append|by|collect|concat|do|finally|for|in|return)(?=\\\\s)\"),lookbehind:!0}],declare:{pattern:n(\"declare\"),lookbehind:!0,alias:\"keyword\"},interactive:{pattern:n(\"interactive\"),lookbehind:!0,alias:\"keyword\"},boolean:{pattern:a(\"nil|t\"),lookbehind:!0},number:{pattern:a(\"[-+]?\\\\d+(?:\\\\.\\\\d*)?\"),lookbehind:!0},defvar:{pattern:RegExp(\"(\\\\()def(?:const|custom|group|var)\\\\s+\"+t),lookbehind:!0,inside:{keyword:/^def[a-z]+/,variable:RegExp(t)}},defun:{pattern:RegExp(\"(\\\\()(?:cl-)?(?:defmacro|defun\\\\*?)\\\\s+\"+t+\"\\\\s+\\\\(\"+i+\"\\\\)\"),lookbehind:!0,greedy:!0,inside:{keyword:/^(?:cl-)?def\\S+/,arguments:null,function:{pattern:RegExp(\"(^\\\\s)\"+t),lookbehind:!0},punctuation:/[()]/}},lambda:{pattern:RegExp(\"(\\\\()lambda\\\\s+\\\\(\\\\s*(?:&?\"+t+\"(?:\\\\s+&?\"+t+\")*\\\\s*)?\\\\)\"),lookbehind:!0,greedy:!0,inside:{keyword:/^lambda/,arguments:null,punctuation:/[()]/}},car:{pattern:RegExp(r+t),lookbehind:!0},punctuation:[/(?:['`,]?\\(|[)\\[\\]])/,{pattern:/(\\s)\\.(?=\\s)/,lookbehind:!0}]},l={\"lisp-marker\":RegExp(\"&(?!\\\\d)[-+*/~!@$%^=<>{}\\\\w]+\"),varform:{pattern:RegExp(\"\\\\(\"+t+\"\\\\s+(?=\\\\S)\"+i+\"\\\\)\"),inside:s},argument:{pattern:RegExp(\"(^|[\\\\s(])\"+t),lookbehind:!0,alias:\"variable\"},rest:s},o=\"\\\\S+(?:\\\\s+\\\\S+)*\",p={pattern:RegExp(r+i+\"(?=\\\\))\"),lookbehind:!0,inside:{\"rest-vars\":{pattern:RegExp(\"&(?:body|rest)\\\\s+\"+o),inside:l},\"other-marker-vars\":{pattern:RegExp(\"&(?:aux|optional)\\\\s+\"+o),inside:l},keys:{pattern:RegExp(\"&key\\\\s+\"+o+\"(?:\\\\s+&allow-other-keys)?\"),inside:l},argument:{pattern:RegExp(t),alias:\"variable\"},punctuation:/[()]/}};s.lambda.inside.arguments=p,s.defun.inside.arguments=e.util.clone(p),s.defun.inside.arguments.inside.sublist=p,e.languages.lisp=s,e.languages.elisp=s,e.languages.emacs=s,e.languages[\"emacs-lisp\"]=s}(Prism);\nPrism.languages.lua={comment:/^#!.+|--(?:\\[(=*)\\[[\\s\\S]*?\\]\\1\\]|.*)/m,string:{pattern:/([\"'])(?:(?!\\1)[^\\\\\\r\\n]|\\\\z(?:\\r\\n|\\s)|\\\\(?:\\r\\n|[^z]))*\\1|\\[(=*)\\[[\\s\\S]*?\\]\\2\\]/,greedy:!0},number:/\\b0x[a-f\\d]+(?:\\.[a-f\\d]*)?(?:p[+-]?\\d+)?\\b|\\b\\d+(?:\\.\\B|(?:\\.\\d*)?(?:e[+-]?\\d+)?\\b)|\\B\\.\\d+(?:e[+-]?\\d+)?\\b/i,keyword:/\\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\\b/,function:/(?!\\d)\\w+(?=\\s*(?:[({]))/,operator:[/[-+*%^&|#]|\\/\\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\\.\\.(?!\\.)/,lookbehind:!0}],punctuation:/[\\[\\](){},;]|\\.+|:+/};\nPrism.languages.makefile={comment:{pattern:/(^|[^\\\\])#(?:\\\\(?:\\r\\n|[\\s\\S])|[^\\\\\\r\\n])*/,lookbehind:!0},string:{pattern:/([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0},\"builtin-target\":{pattern:/\\.[A-Z][^:#=\\s]+(?=\\s*:(?!=))/,alias:\"builtin\"},target:{pattern:/^(?:[^:=\\s]|[ \\t]+(?![\\s:]))+(?=\\s*:(?!=))/m,alias:\"symbol\",inside:{variable:/\\$+(?:(?!\\$)[^(){}:#=\\s]+|(?=[({]))/}},variable:/\\$+(?:(?!\\$)[^(){}:#=\\s]+|\\([@*%<^+?][DF]\\)|(?=[({]))/,keyword:/-include\\b|\\b(?:define|else|endef|endif|export|ifn?def|ifn?eq|include|override|private|sinclude|undefine|unexport|vpath)\\b/,function:{pattern:/(\\()(?:abspath|addsuffix|and|basename|call|dir|error|eval|file|filter(?:-out)?|findstring|firstword|flavor|foreach|guile|if|info|join|lastword|load|notdir|or|origin|patsubst|realpath|shell|sort|strip|subst|suffix|value|warning|wildcard|word(?:list|s)?)(?=[ \\t])/,lookbehind:!0},operator:/(?:::|[?:+!])?=|[|@]/,punctuation:/[:;(){}]/};\n!function(n){function e(n){return n=n.replace(/<inner>/g,(function(){return\"(?:\\\\\\\\.|[^\\\\\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))\"})),RegExp(\"((?:^|[^\\\\\\\\])(?:\\\\\\\\{2})*)(?:\"+n+\")\")}var t=\"(?:\\\\\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\\\\\|\\r\\n`])+\",a=\"\\\\|?__(?:\\\\|__)+\\\\|?(?:(?:\\n|\\r\\n?)|(?![^]))\".replace(/__/g,(function(){return t})),i=\"\\\\|?[ \\t]*:?-{3,}:?[ \\t]*(?:\\\\|[ \\t]*:?-{3,}:?[ \\t]*)+\\\\|?(?:\\n|\\r\\n?)\";n.languages.markdown=n.languages.extend(\"markup\",{}),n.languages.insertBefore(\"markdown\",\"prolog\",{\"front-matter-block\":{pattern:/(^(?:\\s*[\\r\\n])?)---(?!.)[\\s\\S]*?[\\r\\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,\"front-matter\":{pattern:/\\S+(?:\\s+\\S+)*/,alias:[\"yaml\",\"language-yaml\"],inside:n.languages.yaml}}},blockquote:{pattern:/^>(?:[\\t ]*>)*/m,alias:\"punctuation\"},table:{pattern:RegExp(\"^\"+a+i+\"(?:\"+a+\")*\",\"m\"),inside:{\"table-data-rows\":{pattern:RegExp(\"^(\"+a+i+\")(?:\"+a+\")*$\"),lookbehind:!0,inside:{\"table-data\":{pattern:RegExp(t),inside:n.languages.markdown},punctuation:/\\|/}},\"table-line\":{pattern:RegExp(\"^(\"+a+\")\"+i+\"$\"),lookbehind:!0,inside:{punctuation:/\\||:?-{3,}:?/}},\"table-header-row\":{pattern:RegExp(\"^\"+a+\"$\"),inside:{\"table-header\":{pattern:RegExp(t),alias:\"important\",inside:n.languages.markdown},punctuation:/\\|/}}}},code:[{pattern:/((?:^|\\n)[ \\t]*\\n|(?:^|\\r\\n?)[ \\t]*\\r\\n?)(?: {4}|\\t).+(?:(?:\\n|\\r\\n?)(?: {4}|\\t).+)*/,lookbehind:!0,alias:\"keyword\"},{pattern:/^```[\\s\\S]*?^```$/m,greedy:!0,inside:{\"code-block\":{pattern:/^(```.*(?:\\n|\\r\\n?))[\\s\\S]+?(?=(?:\\n|\\r\\n?)^```$)/m,lookbehind:!0},\"code-language\":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\\S.*(?:\\n|\\r\\n?)(?:==+|--+)(?=[ \\t]*$)/m,alias:\"important\",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\\s*)#.+/m,lookbehind:!0,alias:\"important\",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\\s*)([*-])(?:[\\t ]*\\2){2,}(?=\\s*$)/m,lookbehind:!0,alias:\"punctuation\"},list:{pattern:/(^\\s*)(?:[*+-]|\\d+\\.)(?=[\\t ].)/m,lookbehind:!0,alias:\"punctuation\"},\"url-reference\":{pattern:/!?\\[[^\\]]+\\]:[\\t ]+(?:\\S+|<(?:\\\\.|[^>\\\\])+>)(?:[\\t ]+(?:\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|\\((?:\\\\.|[^)\\\\])*\\)))?/,inside:{variable:{pattern:/^(!?\\[)[^\\]]+/,lookbehind:!0},string:/(?:\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|\\((?:\\\\.|[^)\\\\])*\\))$/,punctuation:/^[\\[\\]!:]|[<>]/},alias:\"url\"},bold:{pattern:e(\"\\\\b__(?:(?!_)<inner>|_(?:(?!_)<inner>)+_)+__\\\\b|\\\\*\\\\*(?:(?!\\\\*)<inner>|\\\\*(?:(?!\\\\*)<inner>)+\\\\*)+\\\\*\\\\*\"),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\\s\\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\\*\\*|__/}},italic:{pattern:e(\"\\\\b_(?:(?!_)<inner>|__(?:(?!_)<inner>)+__)+_\\\\b|\\\\*(?:(?!\\\\*)<inner>|\\\\*\\\\*(?:(?!\\\\*)<inner>)+\\\\*\\\\*)+\\\\*\"),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\\s\\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:e(\"(~~?)(?:(?!~)<inner>)+\\\\2\"),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\\s\\S]+(?=\\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},\"code-snippet\":{pattern:/(^|[^\\\\`])(?:``[^`\\r\\n]+(?:`[^`\\r\\n]+)*``(?!`)|`[^`\\r\\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:[\"code\",\"keyword\"]},url:{pattern:e('!?\\\\[(?:(?!\\\\])<inner>)+\\\\](?:\\\\([^\\\\s)]+(?:[\\t ]+\"(?:\\\\\\\\.|[^\"\\\\\\\\])*\")?\\\\)|[ \\t]?\\\\[(?:(?!\\\\])<inner>)+\\\\])'),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\\[)[^\\]]+(?=\\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\\][ \\t]?\\[)[^\\]]+(?=\\]$)/,lookbehind:!0},url:{pattern:/(^\\]\\()[^\\s)]+/,lookbehind:!0},string:{pattern:/(^[ \\t]+)\"(?:\\\\.|[^\"\\\\])*\"(?=\\)$)/,lookbehind:!0}}}}),[\"url\",\"bold\",\"italic\",\"strike\"].forEach((function(e){[\"url\",\"bold\",\"italic\",\"strike\",\"code-snippet\"].forEach((function(t){e!==t&&(n.languages.markdown[e].inside.content.inside[t]=n.languages.markdown[t])}))})),n.hooks.add(\"after-tokenize\",(function(n){\"markdown\"!==n.language&&\"md\"!==n.language||function n(e){if(e&&\"string\"!=typeof e)for(var t=0,a=e.length;t<a;t++){var i=e[t];if(\"code\"===i.type){var r=i.content[1],o=i.content[3];if(r&&o&&\"code-language\"===r.type&&\"code-block\"===o.type&&\"string\"==typeof r.content){var l=r.content.replace(/\\b#/g,\"sharp\").replace(/\\b\\+\\+/g,\"pp\"),s=\"language-\"+(l=(/[a-z][\\w-]*/i.exec(l)||[\"\"])[0].toLowerCase());o.alias?\"string\"==typeof o.alias?o.alias=[o.alias,s]:o.alias.push(s):o.alias=[s]}}else n(i.content)}}(n.tokens)})),n.hooks.add(\"wrap\",(function(e){if(\"code-block\"===e.type){for(var t=\"\",a=0,i=e.classes.length;a<i;a++){var s=e.classes[a],d=/language-(.+)/.exec(s);if(d){t=d[1];break}}var p=n.languages[t];if(p)e.content=n.highlight(e.content.replace(r,\"\").replace(/&(\\w{1,8}|#x?[\\da-f]{1,8});/gi,(function(n,e){var t;return\"#\"===(e=e.toLowerCase())[0]?(t=\"x\"===e[1]?parseInt(e.slice(2),16):Number(e.slice(1)),l(t)):o[e]||n})),p,t);else if(t&&\"none\"!==t&&n.plugins.autoloader){var u=\"md-\"+(new Date).valueOf()+\"-\"+Math.floor(1e16*Math.random());e.attributes.id=u,n.plugins.autoloader.loadLanguages(t,(function(){var e=document.getElementById(u);e&&(e.innerHTML=n.highlight(e.textContent,n.languages[t],t))}))}}}));var r=RegExp(n.languages.markup.tag.pattern.source,\"gi\"),o={amp:\"&\",lt:\"<\",gt:\">\",quot:'\"'},l=String.fromCodePoint||String.fromCharCode;n.languages.md=n.languages.markdown}(Prism);\nPrism.languages.matlab={comment:[/%\\{[\\s\\S]*?\\}%/,/%.+/],string:{pattern:/\\B'(?:''|[^'\\r\\n])*'/,greedy:!0},number:/(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?(?:[ij])?|\\b[ij]\\b/,keyword:/\\b(?:NaN|break|case|catch|continue|else|elseif|end|for|function|if|inf|otherwise|parfor|pause|pi|return|switch|try|while)\\b/,function:/\\b(?!\\d)\\w+(?=\\s*\\()/,operator:/\\.?[*^\\/\\\\']|[+\\-:@]|[<>=~]=?|&&?|\\|\\|?/,punctuation:/\\.{3}|[.,;\\[\\](){}!]/};\nPrism.languages.nasm={comment:/;.*$/m,string:/([\"'`])(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/,label:{pattern:/(^\\s*)[A-Za-z._?$][\\w.?$@~#]*:/m,lookbehind:!0,alias:\"function\"},keyword:[/\\[?BITS (?:16|32|64)\\]?/,{pattern:/(^\\s*)section\\s*[a-z.]+:?/im,lookbehind:!0},/(?:extern|global)[^;\\r\\n]*/i,/(?:CPU|DEFAULT|FLOAT).*$/m],register:{pattern:/\\b(?:st\\d|[xyz]mm\\d\\d?|[cdt]r\\d|r\\d\\d?[bwd]?|[er]?[abcd]x|[abcd][hl]|[er]?(?:bp|di|si|sp)|[cdefgs]s)\\b/i,alias:\"variable\"},number:/(?:\\b|(?=\\$))(?:0[hx](?:\\.[\\da-f]+|[\\da-f]+(?:\\.[\\da-f]+)?)(?:p[+-]?\\d+)?|\\d[\\da-f]+[hx]|\\$\\d[\\da-f]*|0[oq][0-7]+|[0-7]+[oq]|0[by][01]+|[01]+[by]|0[dt]\\d+|(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:\\.?e[+-]?\\d+)?[dt]?)\\b/i,operator:/[\\[\\]*+\\-\\/%<>=&|$!]/};\n!function(e){var n=/\\$(?:\\w[a-z\\d]*(?:_[^\\x00-\\x1F\\s\"'\\\\()$]*)?|\\{[^}\\s\"'\\\\]+\\})/i;e.languages.nginx={comment:{pattern:/(^|[\\s{};])#.*/,lookbehind:!0,greedy:!0},directive:{pattern:/(^|\\s)\\w(?:[^;{}\"'\\\\\\s]|\\\\.|\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|\\s+(?:#.*(?!.)|(?![#\\s])))*?(?=\\s*[;{])/,lookbehind:!0,greedy:!0,inside:{string:{pattern:/((?:^|[^\\\\])(?:\\\\\\\\)*)(?:\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*')/,lookbehind:!0,greedy:!0,inside:{escape:{pattern:/\\\\[\"'\\\\nrt]/,alias:\"entity\"},variable:n}},comment:{pattern:/(\\s)#.*/,lookbehind:!0,greedy:!0},keyword:{pattern:/^\\S+/,greedy:!0},boolean:{pattern:/(\\s)(?:off|on)(?!\\S)/,lookbehind:!0},number:{pattern:/(\\s)\\d+[a-z]*(?!\\S)/i,lookbehind:!0},variable:n}},punctuation:/[{};]/}}(Prism);\nPrism.languages.objectivec=Prism.languages.extend(\"c\",{string:{pattern:/@?\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"/,greedy:!0},keyword:/\\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\\b/,operator:/-[->]?|\\+\\+?|!=?|<<?=?|>>?=?|==?|&&?|\\|\\|?|[~^%?*\\/@]/}),delete Prism.languages.objectivec[\"class-name\"],Prism.languages.objc=Prism.languages.objectivec;\nPrism.languages.pascal={directive:{pattern:/\\{\\$[\\s\\S]*?\\}/,greedy:!0,alias:[\"marco\",\"property\"]},comment:{pattern:/\\(\\*[\\s\\S]*?\\*\\)|\\{[\\s\\S]*?\\}|\\/\\/.*/,greedy:!0},string:{pattern:/(?:'(?:''|[^'\\r\\n])*'(?!')|#[&$%]?[a-f\\d]+)+|\\^[a-z]/i,greedy:!0},asm:{pattern:/(\\basm\\b)[\\s\\S]+?(?=\\bend\\s*[;[])/i,lookbehind:!0,greedy:!0,inside:null},keyword:[{pattern:/(^|[^&])\\b(?:absolute|array|asm|begin|case|const|constructor|destructor|do|downto|else|end|file|for|function|goto|if|implementation|inherited|inline|interface|label|nil|object|of|operator|packed|procedure|program|record|reintroduce|repeat|self|set|string|then|to|type|unit|until|uses|var|while|with)\\b/i,lookbehind:!0},{pattern:/(^|[^&])\\b(?:dispose|exit|false|new|true)\\b/i,lookbehind:!0},{pattern:/(^|[^&])\\b(?:class|dispinterface|except|exports|finalization|finally|initialization|inline|library|on|out|packed|property|raise|resourcestring|threadvar|try)\\b/i,lookbehind:!0},{pattern:/(^|[^&])\\b(?:absolute|abstract|alias|assembler|bitpacked|break|cdecl|continue|cppdecl|cvar|default|deprecated|dynamic|enumerator|experimental|export|external|far|far16|forward|generic|helper|implements|index|interrupt|iochecks|local|message|name|near|nodefault|noreturn|nostackframe|oldfpccall|otherwise|overload|override|pascal|platform|private|protected|public|published|read|register|reintroduce|result|safecall|saveregisters|softfloat|specialize|static|stdcall|stored|strict|unaligned|unimplemented|varargs|virtual|write)\\b/i,lookbehind:!0}],number:[/(?:[&%]\\d+|\\$[a-f\\d]+)/i,/\\b\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?/i],operator:[/\\.\\.|\\*\\*|:=|<[<=>]?|>[>=]?|[+\\-*\\/]=?|[@^=]/,{pattern:/(^|[^&])\\b(?:and|as|div|exclude|in|include|is|mod|not|or|shl|shr|xor)\\b/,lookbehind:!0}],punctuation:/\\(\\.|\\.\\)|[()\\[\\]:;,.]/},Prism.languages.pascal.asm.inside=Prism.languages.extend(\"pascal\",{asm:void 0,keyword:void 0,operator:void 0}),Prism.languages.objectpascal=Prism.languages.pascal;\n!function(e){var n=\"(?:\\\\((?:[^()\\\\\\\\]|\\\\\\\\[^])*\\\\)|\\\\{(?:[^{}\\\\\\\\]|\\\\\\\\[^])*\\\\}|\\\\[(?:[^[\\\\]\\\\\\\\]|\\\\\\\\[^])*\\\\]|<(?:[^<>\\\\\\\\]|\\\\\\\\[^])*>)\";e.languages.perl={comment:[{pattern:/(^\\s*)=\\w[\\s\\S]*?=cut.*/m,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\\\$])#.*/,lookbehind:!0,greedy:!0}],string:[{pattern:RegExp(\"\\\\b(?:q|qq|qw|qx)(?![a-zA-Z0-9])\\\\s*(?:\"+[\"([^a-zA-Z0-9\\\\s{(\\\\[<])(?:(?!\\\\1)[^\\\\\\\\]|\\\\\\\\[^])*\\\\1\",\"([a-zA-Z0-9])(?:(?!\\\\2)[^\\\\\\\\]|\\\\\\\\[^])*\\\\2\",n].join(\"|\")+\")\"),greedy:!0},{pattern:/(\"|`)(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1/,greedy:!0},{pattern:/'(?:[^'\\\\\\r\\n]|\\\\.)*'/,greedy:!0}],regex:[{pattern:RegExp(\"\\\\b(?:m|qr)(?![a-zA-Z0-9])\\\\s*(?:\"+[\"([^a-zA-Z0-9\\\\s{(\\\\[<])(?:(?!\\\\1)[^\\\\\\\\]|\\\\\\\\[^])*\\\\1\",\"([a-zA-Z0-9])(?:(?!\\\\2)[^\\\\\\\\]|\\\\\\\\[^])*\\\\2\",n].join(\"|\")+\")[msixpodualngc]*\"),greedy:!0},{pattern:RegExp(\"(^|[^-])\\\\b(?:s|tr|y)(?![a-zA-Z0-9])\\\\s*(?:\"+[\"([^a-zA-Z0-9\\\\s{(\\\\[<])(?:(?!\\\\2)[^\\\\\\\\]|\\\\\\\\[^])*\\\\2(?:(?!\\\\2)[^\\\\\\\\]|\\\\\\\\[^])*\\\\2\",\"([a-zA-Z0-9])(?:(?!\\\\3)[^\\\\\\\\]|\\\\\\\\[^])*\\\\3(?:(?!\\\\3)[^\\\\\\\\]|\\\\\\\\[^])*\\\\3\",n+\"\\\\s*\"+n].join(\"|\")+\")[msixpodualngcer]*\"),lookbehind:!0,greedy:!0},{pattern:/\\/(?:[^\\/\\\\\\r\\n]|\\\\.)*\\/[msixpodualngc]*(?=\\s*(?:$|[\\r\\n,.;})&|\\-+*~<>!?^]|(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|x|xor)\\b))/,greedy:!0}],variable:[/[&*$@%]\\{\\^[A-Z]+\\}/,/[&*$@%]\\^[A-Z_]/,/[&*$@%]#?(?=\\{)/,/[&*$@%]#?(?:(?:::)*'?(?!\\d)[\\w$]+(?![\\w$]))+(?:::)*/,/[&*$@%]\\d+/,/(?!%=)[$@%][!\"#$%&'()*+,\\-.\\/:;<=>?@[\\\\\\]^_`{|}~]/],filehandle:{pattern:/<(?![<=])\\S*?>|\\b_\\b/,alias:\"symbol\"},\"v-string\":{pattern:/v\\d+(?:\\.\\d+)*|\\d+(?:\\.\\d+){2,}/,alias:\"string\"},function:{pattern:/(\\bsub[ \\t]+)\\w+/,lookbehind:!0},keyword:/\\b(?:any|break|continue|default|delete|die|do|else|elsif|eval|for|foreach|given|goto|if|last|local|my|next|our|package|print|redo|require|return|say|state|sub|switch|undef|unless|until|use|when|while)\\b/,number:/\\b(?:0x[\\dA-Fa-f](?:_?[\\dA-Fa-f])*|0b[01](?:_?[01])*|(?:(?:\\d(?:_?\\d)*)?\\.)?\\d(?:_?\\d)*(?:[Ee][+-]?\\d+)?)\\b/,operator:/-[rwxoRWXOezsfdlpSbctugkTBMAC]\\b|\\+[+=]?|-[-=>]?|\\*\\*?=?|\\/\\/?=?|=[=~>]?|~[~=]?|\\|\\|?=?|&&?=?|<(?:=>?|<=?)?|>>?=?|![~=]?|[%^]=?|\\.(?:=|\\.\\.?)?|[\\\\?]|\\bx(?:=|\\b)|\\b(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|xor)\\b/,punctuation:/[{}[\\];(),:]/}}(Prism);\n!function(e){var a=/\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*|#(?!\\[).*/,t=[{pattern:/\\b(?:false|true)\\b/i,alias:\"boolean\"},{pattern:/(::\\s*)\\b[a-z_]\\w*\\b(?!\\s*\\()/i,greedy:!0,lookbehind:!0},{pattern:/(\\b(?:case|const)\\s+)\\b[a-z_]\\w*(?=\\s*[;=])/i,greedy:!0,lookbehind:!0},/\\b(?:null)\\b/i,/\\b[A-Z_][A-Z0-9_]*\\b(?!\\s*\\()/],i=/\\b0b[01]+(?:_[01]+)*\\b|\\b0o[0-7]+(?:_[0-7]+)*\\b|\\b0x[\\da-f]+(?:_[\\da-f]+)*\\b|(?:\\b\\d+(?:_\\d+)*\\.?(?:\\d+(?:_\\d+)*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?/i,n=/<?=>|\\?\\?=?|\\.{3}|\\??->|[!=]=?=?|::|\\*\\*=?|--|\\+\\+|&&|\\|\\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,s=/[{}\\[\\](),:;]/;e.languages.php={delimiter:{pattern:/\\?>$|^<\\?(?:php(?=\\s)|=)?/i,alias:\"important\"},comment:a,variable:/\\$+(?:\\w+\\b|(?=\\{))/,package:{pattern:/(namespace\\s+|use\\s+(?:function\\s+)?)(?:\\\\?\\b[a-z_]\\w*)+\\b(?!\\\\)/i,lookbehind:!0,inside:{punctuation:/\\\\/}},\"class-name-definition\":{pattern:/(\\b(?:class|enum|interface|trait)\\s+)\\b[a-z_]\\w*(?!\\\\)\\b/i,lookbehind:!0,alias:\"class-name\"},\"function-definition\":{pattern:/(\\bfunction\\s+)[a-z_]\\w*(?=\\s*\\()/i,lookbehind:!0,alias:\"function\"},keyword:[{pattern:/(\\(\\s*)\\b(?:array|bool|boolean|float|int|integer|object|string)\\b(?=\\s*\\))/i,alias:\"type-casting\",greedy:!0,lookbehind:!0},{pattern:/([(,?]\\s*)\\b(?:array(?!\\s*\\()|bool|callable|(?:false|null)(?=\\s*\\|)|float|int|iterable|mixed|object|self|static|string)\\b(?=\\s*\\$)/i,alias:\"type-hint\",greedy:!0,lookbehind:!0},{pattern:/(\\)\\s*:\\s*(?:\\?\\s*)?)\\b(?:array(?!\\s*\\()|bool|callable|(?:false|null)(?=\\s*\\|)|float|int|iterable|mixed|never|object|self|static|string|void)\\b/i,alias:\"return-type\",greedy:!0,lookbehind:!0},{pattern:/\\b(?:array(?!\\s*\\()|bool|float|int|iterable|mixed|object|string|void)\\b/i,alias:\"type-declaration\",greedy:!0},{pattern:/(\\|\\s*)(?:false|null)\\b|\\b(?:false|null)(?=\\s*\\|)/i,alias:\"type-declaration\",greedy:!0,lookbehind:!0},{pattern:/\\b(?:parent|self|static)(?=\\s*::)/i,alias:\"static-context\",greedy:!0},{pattern:/(\\byield\\s+)from\\b/i,lookbehind:!0},/\\bclass\\b/i,{pattern:/((?:^|[^\\s>:]|(?:^|[^-])>|(?:^|[^:]):)\\s*)\\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\\b/i,lookbehind:!0}],\"argument-name\":{pattern:/([(,]\\s*)\\b[a-z_]\\w*(?=\\s*:(?!:))/i,lookbehind:!0},\"class-name\":[{pattern:/(\\b(?:extends|implements|instanceof|new(?!\\s+self|\\s+static))\\s+|\\bcatch\\s*\\()\\b[a-z_]\\w*(?!\\\\)\\b/i,greedy:!0,lookbehind:!0},{pattern:/(\\|\\s*)\\b[a-z_]\\w*(?!\\\\)\\b/i,greedy:!0,lookbehind:!0},{pattern:/\\b[a-z_]\\w*(?!\\\\)\\b(?=\\s*\\|)/i,greedy:!0},{pattern:/(\\|\\s*)(?:\\\\?\\b[a-z_]\\w*)+\\b/i,alias:\"class-name-fully-qualified\",greedy:!0,lookbehind:!0,inside:{punctuation:/\\\\/}},{pattern:/(?:\\\\?\\b[a-z_]\\w*)+\\b(?=\\s*\\|)/i,alias:\"class-name-fully-qualified\",greedy:!0,inside:{punctuation:/\\\\/}},{pattern:/(\\b(?:extends|implements|instanceof|new(?!\\s+self\\b|\\s+static\\b))\\s+|\\bcatch\\s*\\()(?:\\\\?\\b[a-z_]\\w*)+\\b(?!\\\\)/i,alias:\"class-name-fully-qualified\",greedy:!0,lookbehind:!0,inside:{punctuation:/\\\\/}},{pattern:/\\b[a-z_]\\w*(?=\\s*\\$)/i,alias:\"type-declaration\",greedy:!0},{pattern:/(?:\\\\?\\b[a-z_]\\w*)+(?=\\s*\\$)/i,alias:[\"class-name-fully-qualified\",\"type-declaration\"],greedy:!0,inside:{punctuation:/\\\\/}},{pattern:/\\b[a-z_]\\w*(?=\\s*::)/i,alias:\"static-context\",greedy:!0},{pattern:/(?:\\\\?\\b[a-z_]\\w*)+(?=\\s*::)/i,alias:[\"class-name-fully-qualified\",\"static-context\"],greedy:!0,inside:{punctuation:/\\\\/}},{pattern:/([(,?]\\s*)[a-z_]\\w*(?=\\s*\\$)/i,alias:\"type-hint\",greedy:!0,lookbehind:!0},{pattern:/([(,?]\\s*)(?:\\\\?\\b[a-z_]\\w*)+(?=\\s*\\$)/i,alias:[\"class-name-fully-qualified\",\"type-hint\"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\\\/}},{pattern:/(\\)\\s*:\\s*(?:\\?\\s*)?)\\b[a-z_]\\w*(?!\\\\)\\b/i,alias:\"return-type\",greedy:!0,lookbehind:!0},{pattern:/(\\)\\s*:\\s*(?:\\?\\s*)?)(?:\\\\?\\b[a-z_]\\w*)+\\b(?!\\\\)/i,alias:[\"class-name-fully-qualified\",\"return-type\"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\\\/}}],constant:t,function:{pattern:/(^|[^\\\\\\w])\\\\?[a-z_](?:[\\w\\\\]*\\w)?(?=\\s*\\()/i,lookbehind:!0,inside:{punctuation:/\\\\/}},property:{pattern:/(->\\s*)\\w+/,lookbehind:!0},number:i,operator:n,punctuation:s};var l={pattern:/\\{\\$(?:\\{(?:\\{[^{}]+\\}|[^{}]+)\\}|[^{}])+\\}|(^|[^\\\\{])\\$+(?:\\w+(?:\\[[^\\r\\n\\[\\]]+\\]|->\\w+)?)/,lookbehind:!0,inside:e.languages.php},r=[{pattern:/<<<'([^']+)'[\\r\\n](?:.*[\\r\\n])*?\\1;/,alias:\"nowdoc-string\",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\\w*;$/i,alias:\"symbol\",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:\"([^\"]+)\"[\\r\\n](?:.*[\\r\\n])*?\\1;|([a-z_]\\w*)[\\r\\n](?:.*[\\r\\n])*?\\2;)/i,alias:\"heredoc-string\",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:\"[^\"]+\"|[a-z_]\\w*)|[a-z_]\\w*;$/i,alias:\"symbol\",inside:{punctuation:/^<<<\"?|[\";]$/}},interpolation:l}},{pattern:/`(?:\\\\[\\s\\S]|[^\\\\`])*`/,alias:\"backtick-quoted-string\",greedy:!0},{pattern:/'(?:\\\\[\\s\\S]|[^\\\\'])*'/,alias:\"single-quoted-string\",greedy:!0},{pattern:/\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"/,alias:\"double-quoted-string\",greedy:!0,inside:{interpolation:l}}];e.languages.insertBefore(\"php\",\"variable\",{string:r,attribute:{pattern:/#\\[(?:[^\"'\\/#]|\\/(?![*/])|\\/\\/.*$|#(?!\\[).*$|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|'(?:\\\\[\\s\\S]|[^\\\\'])*')+\\](?=\\s*[a-z$#])/im,greedy:!0,inside:{\"attribute-content\":{pattern:/^(#\\[)[\\s\\S]+(?=\\]$)/,lookbehind:!0,inside:{comment:a,string:r,\"attribute-class-name\":[{pattern:/([^:]|^)\\b[a-z_]\\w*(?!\\\\)\\b/i,alias:\"class-name\",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\\\?\\b[a-z_]\\w*)+/i,alias:[\"class-name\",\"class-name-fully-qualified\"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\\\/}}],constant:t,number:i,operator:n,punctuation:s}},delimiter:{pattern:/^#\\[|\\]$/,alias:\"punctuation\"}}}}),e.hooks.add(\"before-tokenize\",(function(a){/<\\?/.test(a.code)&&e.languages[\"markup-templating\"].buildPlaceholders(a,\"php\",/<\\?(?:[^\"'/#]|\\/(?![*/])|(\"|')(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])*\\1|(?:\\/\\/|#(?!\\[))(?:[^?\\n\\r]|\\?(?!>))*(?=$|\\?>|[\\r\\n])|#\\[|\\/\\*(?:[^*]|\\*(?!\\/))*(?:\\*\\/|$))*?(?:\\?>|$)/g)})),e.hooks.add(\"after-tokenize\",(function(a){e.languages[\"markup-templating\"].tokenizePlaceholders(a,\"php\")}))}(Prism);\n!function(e){var i=e.languages.powershell={comment:[{pattern:/(^|[^`])<#[\\s\\S]*?#>/,lookbehind:!0},{pattern:/(^|[^`])#.*/,lookbehind:!0}],string:[{pattern:/\"(?:`[\\s\\S]|[^`\"])*\"/,greedy:!0,inside:null},{pattern:/'(?:[^']|'')*'/,greedy:!0}],namespace:/\\[[a-z](?:\\[(?:\\[[^\\]]*\\]|[^\\[\\]])*\\]|[^\\[\\]])*\\]/i,boolean:/\\$(?:false|true)\\b/i,variable:/\\$\\w+\\b/,function:[/\\b(?:Add|Approve|Assert|Backup|Block|Checkpoint|Clear|Close|Compare|Complete|Compress|Confirm|Connect|Convert|ConvertFrom|ConvertTo|Copy|Debug|Deny|Disable|Disconnect|Dismount|Edit|Enable|Enter|Exit|Expand|Export|Find|ForEach|Format|Get|Grant|Group|Hide|Import|Initialize|Install|Invoke|Join|Limit|Lock|Measure|Merge|Move|New|Open|Optimize|Out|Ping|Pop|Protect|Publish|Push|Read|Receive|Redo|Register|Remove|Rename|Repair|Request|Reset|Resize|Resolve|Restart|Restore|Resume|Revoke|Save|Search|Select|Send|Set|Show|Skip|Sort|Split|Start|Step|Stop|Submit|Suspend|Switch|Sync|Tee|Test|Trace|Unblock|Undo|Uninstall|Unlock|Unprotect|Unpublish|Unregister|Update|Use|Wait|Watch|Where|Write)-[a-z]+\\b/i,/\\b(?:ac|cat|chdir|clc|cli|clp|clv|compare|copy|cp|cpi|cpp|cvpa|dbp|del|diff|dir|ebp|echo|epal|epcsv|epsn|erase|fc|fl|ft|fw|gal|gbp|gc|gci|gcs|gdr|gi|gl|gm|gp|gps|group|gsv|gu|gv|gwmi|iex|ii|ipal|ipcsv|ipsn|irm|iwmi|iwr|kill|lp|ls|measure|mi|mount|move|mp|mv|nal|ndr|ni|nv|ogv|popd|ps|pushd|pwd|rbp|rd|rdr|ren|ri|rm|rmdir|rni|rnp|rp|rv|rvpa|rwmi|sal|saps|sasv|sbp|sc|select|set|shcm|si|sl|sleep|sls|sort|sp|spps|spsv|start|sv|swmi|tee|trcm|type|write)\\b/i],keyword:/\\b(?:Begin|Break|Catch|Class|Continue|Data|Define|Do|DynamicParam|Else|ElseIf|End|Exit|Filter|Finally|For|ForEach|From|Function|If|InlineScript|Parallel|Param|Process|Return|Sequence|Switch|Throw|Trap|Try|Until|Using|Var|While|Workflow)\\b/i,operator:{pattern:/(^|\\W)(?:!|-(?:b?(?:and|x?or)|as|(?:Not)?(?:Contains|In|Like|Match)|eq|ge|gt|is(?:Not)?|Join|le|lt|ne|not|Replace|sh[lr])\\b|-[-=]?|\\+[+=]?|[*\\/%]=?)/i,lookbehind:!0},punctuation:/[|{}[\\];(),.]/};i.string[0].inside={function:{pattern:/(^|[^`])\\$\\((?:\\$\\([^\\r\\n()]*\\)|(?!\\$\\()[^\\r\\n)])*\\)/,lookbehind:!0,inside:i},boolean:i.boolean,variable:i.variable}}(Prism);\n!function(e){e.languages.pug={comment:{pattern:/(^([\\t ]*))\\/\\/.*(?:(?:\\r?\\n|\\r)\\2[\\t ].+)*/m,lookbehind:!0},\"multiline-script\":{pattern:/(^([\\t ]*)script\\b.*\\.[\\t ]*)(?:(?:\\r?\\n|\\r(?!\\n))(?:\\2[\\t ].+|\\s*?(?=\\r?\\n|\\r)))+/m,lookbehind:!0,inside:e.languages.javascript},filter:{pattern:/(^([\\t ]*)):.+(?:(?:\\r?\\n|\\r(?!\\n))(?:\\2[\\t ].+|\\s*?(?=\\r?\\n|\\r)))+/m,lookbehind:!0,inside:{\"filter-name\":{pattern:/^:[\\w-]+/,alias:\"variable\"},text:/\\S[\\s\\S]*/}},\"multiline-plain-text\":{pattern:/(^([\\t ]*)[\\w\\-#.]+\\.[\\t ]*)(?:(?:\\r?\\n|\\r(?!\\n))(?:\\2[\\t ].+|\\s*?(?=\\r?\\n|\\r)))+/m,lookbehind:!0},markup:{pattern:/(^[\\t ]*)<.+/m,lookbehind:!0,inside:e.languages.markup},doctype:{pattern:/((?:^|\\n)[\\t ]*)doctype(?: .+)?/,lookbehind:!0},\"flow-control\":{pattern:/(^[\\t ]*)(?:case|default|each|else|if|unless|when|while)\\b(?: .+)?/m,lookbehind:!0,inside:{each:{pattern:/^each .+? in\\b/,inside:{keyword:/\\b(?:each|in)\\b/,punctuation:/,/}},branch:{pattern:/^(?:case|default|else|if|unless|when|while)\\b/,alias:\"keyword\"},rest:e.languages.javascript}},keyword:{pattern:/(^[\\t ]*)(?:append|block|extends|include|prepend)\\b.+/m,lookbehind:!0},mixin:[{pattern:/(^[\\t ]*)mixin .+/m,lookbehind:!0,inside:{keyword:/^mixin/,function:/\\w+(?=\\s*\\(|\\s*$)/,punctuation:/[(),.]/}},{pattern:/(^[\\t ]*)\\+.+/m,lookbehind:!0,inside:{name:{pattern:/^\\+\\w+/,alias:\"function\"},rest:e.languages.javascript}}],script:{pattern:/(^[\\t ]*script(?:(?:&[^(]+)?\\([^)]+\\))*[\\t ]).+/m,lookbehind:!0,inside:e.languages.javascript},\"plain-text\":{pattern:/(^[\\t ]*(?!-)[\\w\\-#.]*[\\w\\-](?:(?:&[^(]+)?\\([^)]+\\))*\\/?[\\t ]).+/m,lookbehind:!0},tag:{pattern:/(^[\\t ]*)(?!-)[\\w\\-#.]*[\\w\\-](?:(?:&[^(]+)?\\([^)]+\\))*\\/?:?/m,lookbehind:!0,inside:{attributes:[{pattern:/&[^(]+\\([^)]+\\)/,inside:e.languages.javascript},{pattern:/\\([^)]+\\)/,inside:{\"attr-value\":{pattern:/(=\\s*(?!\\s))(?:\\{[^}]*\\}|[^,)\\r\\n]+)/,lookbehind:!0,inside:e.languages.javascript},\"attr-name\":/[\\w-]+(?=\\s*!?=|\\s*[,)])/,punctuation:/[!=(),]+/}}],punctuation:/:/,\"attr-id\":/#[\\w\\-]+/,\"attr-class\":/\\.[\\w\\-]+/}},code:[{pattern:/(^[\\t ]*(?:-|!?=)).+/m,lookbehind:!0,inside:e.languages.javascript}],punctuation:/[.\\-!=|]+/};for(var t=[{filter:\"atpl\",language:\"twig\"},{filter:\"coffee\",language:\"coffeescript\"},\"ejs\",\"handlebars\",\"less\",\"livescript\",\"markdown\",{filter:\"sass\",language:\"scss\"},\"stylus\"],n={},a=0,i=t.length;a<i;a++){var r=t[a];r=\"string\"==typeof r?{filter:r,language:r}:r,e.languages[r.language]&&(n[\"filter-\"+r.filter]={pattern:RegExp(\"(^([\\t ]*)):<filter_name>(?:(?:\\r?\\n|\\r(?!\\n))(?:\\\\2[\\t ].+|\\\\s*?(?=\\r?\\n|\\r)))+\".replace(\"<filter_name>\",(function(){return r.filter})),\"m\"),lookbehind:!0,inside:{\"filter-name\":{pattern:/^:[\\w-]+/,alias:\"variable\"},text:{pattern:/\\S[\\s\\S]*/,alias:[r.language,\"language-\"+r.language],inside:e.languages[r.language]}}})}e.languages.insertBefore(\"pug\",\"filter\",n)}(Prism);\nPrism.languages.python={comment:{pattern:/(^|[^\\\\])#.*/,lookbehind:!0,greedy:!0},\"string-interpolation\":{pattern:/(?:f|fr|rf)(?:(\"\"\"|''')[\\s\\S]*?\\1|(\"|')(?:\\\\.|(?!\\2)[^\\\\\\r\\n])*\\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\\{\\{)*)\\{(?!\\{)(?:[^{}]|\\{(?!\\{)(?:[^{}]|\\{(?!\\{)(?:[^{}])+\\})+\\})+\\}/,lookbehind:!0,inside:{\"format-spec\":{pattern:/(:)[^:(){}]+(?=\\}$)/,lookbehind:!0},\"conversion-option\":{pattern:/![sra](?=[:}]$)/,alias:\"punctuation\"},rest:null}},string:/[\\s\\S]+/}},\"triple-quoted-string\":{pattern:/(?:[rub]|br|rb)?(\"\"\"|''')[\\s\\S]*?\\1/i,greedy:!0,alias:\"string\"},string:{pattern:/(?:[rub]|br|rb)?(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/i,greedy:!0},function:{pattern:/((?:^|\\s)def[ \\t]+)[a-zA-Z_]\\w*(?=\\s*\\()/g,lookbehind:!0},\"class-name\":{pattern:/(\\bclass\\s+)\\w+/i,lookbehind:!0},decorator:{pattern:/(^[\\t ]*)@\\w+(?:\\.\\w+)*/m,lookbehind:!0,alias:[\"annotation\",\"punctuation\"],inside:{punctuation:/\\./}},keyword:/\\b(?:_(?=\\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\\b/,builtin:/\\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\\b/,boolean:/\\b(?:False|None|True)\\b/,number:/\\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\\b|(?:\\b\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\B\\.\\d+(?:_\\d+)*)(?:e[+-]?\\d+(?:_\\d+)*)?j?(?!\\w)/i,operator:/[-+%=]=?|!=|:=|\\*\\*?=?|\\/\\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\\];(),.:]/},Prism.languages.python[\"string-interpolation\"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python;\nPrism.languages.r={comment:/#.*/,string:{pattern:/(['\"])(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0},\"percent-operator\":{pattern:/%[^%\\s]*%/,alias:\"operator\"},boolean:/\\b(?:FALSE|TRUE)\\b/,ellipsis:/\\.\\.(?:\\.|\\d+)/,number:[/\\b(?:Inf|NaN)\\b/,/(?:\\b0x[\\dA-Fa-f]+(?:\\.\\d*)?|\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[EePp][+-]?\\d+)?[iL]?/],keyword:/\\b(?:NA|NA_character_|NA_complex_|NA_integer_|NA_real_|NULL|break|else|for|function|if|in|next|repeat|while)\\b/,operator:/->?>?|<(?:=|<?-)?|[>=!]=?|::?|&&?|\\|\\|?|[+*\\/^$@~]/,punctuation:/[(){}\\[\\],;]/};\n!function(t){var n=t.util.clone(t.languages.javascript),e=\"(?:\\\\{<S>*\\\\.{3}(?:[^{}]|<BRACES>)*\\\\})\";function a(t,n){return t=t.replace(/<S>/g,(function(){return\"(?:\\\\s|//.*(?!.)|/\\\\*(?:[^*]|\\\\*(?!/))\\\\*/)\"})).replace(/<BRACES>/g,(function(){return\"(?:\\\\{(?:\\\\{(?:\\\\{[^{}]*\\\\}|[^{}])*\\\\}|[^{}])*\\\\})\"})).replace(/<SPREAD>/g,(function(){return e})),RegExp(t,n)}e=a(e).source,t.languages.jsx=t.languages.extend(\"markup\",n),t.languages.jsx.tag.pattern=a(\"</?(?:[\\\\w.:-]+(?:<S>+(?:[\\\\w.:$-]+(?:=(?:\\\"(?:\\\\\\\\[^]|[^\\\\\\\\\\\"])*\\\"|'(?:\\\\\\\\[^]|[^\\\\\\\\'])*'|[^\\\\s{'\\\"/>=]+|<BRACES>))?|<SPREAD>))*<S>*/?)?>\"),t.languages.jsx.tag.inside.tag.pattern=/^<\\/?[^\\s>\\/]*/,t.languages.jsx.tag.inside[\"attr-value\"].pattern=/=(?!\\{)(?:\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|'(?:\\\\[\\s\\S]|[^\\\\'])*'|[^\\s'\">]+)/,t.languages.jsx.tag.inside.tag.inside[\"class-name\"]=/^[A-Z]\\w*(?:\\.[A-Z]\\w*)*$/,t.languages.jsx.tag.inside.comment=n.comment,t.languages.insertBefore(\"inside\",\"attr-name\",{spread:{pattern:a(\"<SPREAD>\"),inside:t.languages.jsx}},t.languages.jsx.tag),t.languages.insertBefore(\"inside\",\"special-attr\",{script:{pattern:a(\"=<BRACES>\"),alias:\"language-javascript\",inside:{\"script-punctuation\":{pattern:/^=(?=\\{)/,alias:\"punctuation\"},rest:t.languages.jsx}}},t.languages.jsx.tag);var s=function(t){return t?\"string\"==typeof t?t:\"string\"==typeof t.content?t.content:t.content.map(s).join(\"\"):\"\"},g=function(n){for(var e=[],a=0;a<n.length;a++){var o=n[a],i=!1;if(\"string\"!=typeof o&&(\"tag\"===o.type&&o.content[0]&&\"tag\"===o.content[0].type?\"</\"===o.content[0].content[0].content?e.length>0&&e[e.length-1].tagName===s(o.content[0].content[1])&&e.pop():\"/>\"===o.content[o.content.length-1].content||e.push({tagName:s(o.content[0].content[1]),openedBraces:0}):e.length>0&&\"punctuation\"===o.type&&\"{\"===o.content?e[e.length-1].openedBraces++:e.length>0&&e[e.length-1].openedBraces>0&&\"punctuation\"===o.type&&\"}\"===o.content?e[e.length-1].openedBraces--:i=!0),(i||\"string\"==typeof o)&&e.length>0&&0===e[e.length-1].openedBraces){var r=s(o);a<n.length-1&&(\"string\"==typeof n[a+1]||\"plain-text\"===n[a+1].type)&&(r+=s(n[a+1]),n.splice(a+1,1)),a>0&&(\"string\"==typeof n[a-1]||\"plain-text\"===n[a-1].type)&&(r=s(n[a-1])+r,n.splice(a-1,1),a--),n[a]=new t.Token(\"plain-text\",r,null,r)}o.content&&\"string\"!=typeof o.content&&g(o.content)}};t.hooks.add(\"after-tokenize\",(function(t){\"jsx\"!==t.language&&\"tsx\"!==t.language||g(t.tokens)}))}(Prism);\n!function(a){var e={pattern:/\\\\[\\\\(){}[\\]^$+*?|.]/,alias:\"escape\"},n=/\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)/,t=\"(?:[^\\\\\\\\-]|\"+n.source+\")\",s=RegExp(t+\"-\"+t),i={pattern:/(<|')[^<>']+(?=[>']$)/,lookbehind:!0,alias:\"variable\"};a.languages.regex={\"char-class\":{pattern:/((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]/,lookbehind:!0,inside:{\"char-class-negation\":{pattern:/(^\\[)\\^/,lookbehind:!0,alias:\"operator\"},\"char-class-punctuation\":{pattern:/^\\[|\\]$/,alias:\"punctuation\"},range:{pattern:s,inside:{escape:n,\"range-punctuation\":{pattern:/-/,alias:\"operator\"}}},\"special-escape\":e,\"char-set\":{pattern:/\\\\[wsd]|\\\\p\\{[^{}]+\\}/i,alias:\"class-name\"},escape:n}},\"special-escape\":e,\"char-set\":{pattern:/\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}/i,alias:\"class-name\"},backreference:[{pattern:/\\\\(?![123][0-7]{2})[1-9]/,alias:\"keyword\"},{pattern:/\\\\k<[^<>']+>/,alias:\"keyword\",inside:{\"group-name\":i}}],anchor:{pattern:/[$^]|\\\\[ABbGZz]/,alias:\"function\"},escape:n,group:[{pattern:/\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]|<?[=!]|[idmnsuxU]+(?:-[idmnsuxU]+)?:?))?/,alias:\"punctuation\",inside:{\"group-name\":i}},{pattern:/\\)/,alias:\"punctuation\"}],quantifier:{pattern:/(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?/,alias:\"number\"},alternation:{pattern:/\\|/,alias:\"keyword\"}}}(Prism);\n!function(e){for(var a=\"/\\\\*(?:[^*/]|\\\\*(?!/)|/(?!\\\\*)|<self>)*\\\\*/\",t=0;t<2;t++)a=a.replace(/<self>/g,(function(){return a}));a=a.replace(/<self>/g,(function(){return\"[^\\\\s\\\\S]\"})),e.languages.rust={comment:[{pattern:RegExp(\"(^|[^\\\\\\\\])\"+a),lookbehind:!0,greedy:!0},{pattern:/(^|[^\\\\:])\\/\\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/b?\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|b?r(#*)\"(?:[^\"]|\"(?!\\1))*\"\\1/,greedy:!0},char:{pattern:/b?'(?:\\\\(?:x[0-7][\\da-fA-F]|u\\{(?:[\\da-fA-F]_*){1,6}\\}|.)|[^\\\\\\r\\n\\t'])'/,greedy:!0},attribute:{pattern:/#!?\\[(?:[^\\[\\]\"]|\"(?:\\\\[\\s\\S]|[^\\\\\"])*\")*\\]/,greedy:!0,alias:\"attr-name\",inside:{string:null}},\"closure-params\":{pattern:/([=(,:]\\s*|\\bmove\\s*)\\|[^|]*\\||\\|[^|]*\\|(?=\\s*(?:\\{|->))/,lookbehind:!0,greedy:!0,inside:{\"closure-punctuation\":{pattern:/^\\||\\|$/,alias:\"punctuation\"},rest:null}},\"lifetime-annotation\":{pattern:/'\\w+/,alias:\"symbol\"},\"fragment-specifier\":{pattern:/(\\$\\w+:)[a-z]+/,lookbehind:!0,alias:\"punctuation\"},variable:/\\$\\w+/,\"function-definition\":{pattern:/(\\bfn\\s+)\\w+/,lookbehind:!0,alias:\"function\"},\"type-definition\":{pattern:/(\\b(?:enum|struct|trait|type|union)\\s+)\\w+/,lookbehind:!0,alias:\"class-name\"},\"module-declaration\":[{pattern:/(\\b(?:crate|mod)\\s+)[a-z][a-z_\\d]*/,lookbehind:!0,alias:\"namespace\"},{pattern:/(\\b(?:crate|self|super)\\s*)::\\s*[a-z][a-z_\\d]*\\b(?:\\s*::(?:\\s*[a-z][a-z_\\d]*\\s*::)*)?/,lookbehind:!0,alias:\"namespace\",inside:{punctuation:/::/}}],keyword:[/\\b(?:Self|abstract|as|async|await|become|box|break|const|continue|crate|do|dyn|else|enum|extern|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|override|priv|pub|ref|return|self|static|struct|super|trait|try|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\\b/,/\\b(?:bool|char|f(?:32|64)|[ui](?:8|16|32|64|128|size)|str)\\b/],function:/\\b[a-z_]\\w*(?=\\s*(?:::\\s*<|\\())/,macro:{pattern:/\\b\\w+!/,alias:\"property\"},constant:/\\b[A-Z_][A-Z_\\d]+\\b/,\"class-name\":/\\b[A-Z]\\w*\\b/,namespace:{pattern:/(?:\\b[a-z][a-z_\\d]*\\s*::\\s*)*\\b[a-z][a-z_\\d]*\\s*::(?!\\s*<)/,inside:{punctuation:/::/}},number:/\\b(?:0x[\\dA-Fa-f](?:_?[\\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:(?:\\d(?:_?\\d)*)?\\.)?\\d(?:_?\\d)*(?:[Ee][+-]?\\d+)?)(?:_?(?:f32|f64|[iu](?:8|16|32|64|size)?))?\\b/,boolean:/\\b(?:false|true)\\b/,punctuation:/->|\\.\\.=|\\.{1,3}|::|[{}[\\];(),:]/,operator:/[-+*\\/%!^]=?|=[=>]?|&[&=]?|\\|[|=]?|<<?=?|>>?=?|[@?]/},e.languages.rust[\"closure-params\"].inside.rest=e.languages.rust,e.languages.rust.attribute.inside.string=e.languages.rust.string}(Prism);\n!function(e){e.languages.sass=e.languages.extend(\"css\",{comment:{pattern:/^([ \\t]*)\\/[\\/*].*(?:(?:\\r?\\n|\\r)\\1[ \\t].+)*/m,lookbehind:!0,greedy:!0}}),e.languages.insertBefore(\"sass\",\"atrule\",{\"atrule-line\":{pattern:/^(?:[ \\t]*)[@+=].+/m,greedy:!0,inside:{atrule:/(?:@[\\w-]+|[+=])/}}}),delete e.languages.sass.atrule;var r=/\\$[-\\w]+|#\\{\\$[-\\w]+\\}/,t=[/[+*\\/%]|[=!]=|<=?|>=?|\\b(?:and|not|or)\\b/,{pattern:/(\\s)-(?=\\s)/,lookbehind:!0}];e.languages.insertBefore(\"sass\",\"property\",{\"variable-line\":{pattern:/^[ \\t]*\\$.+/m,greedy:!0,inside:{punctuation:/:/,variable:r,operator:t}},\"property-line\":{pattern:/^[ \\t]*(?:[^:\\s]+ *:.*|:[^:\\s].*)/m,greedy:!0,inside:{property:[/[^:\\s]+(?=\\s*:)/,{pattern:/(:)[^:\\s]+/,lookbehind:!0}],punctuation:/:/,variable:r,operator:t,important:e.languages.sass.important}}}),delete e.languages.sass.property,delete e.languages.sass.important,e.languages.insertBefore(\"sass\",\"punctuation\",{selector:{pattern:/^([ \\t]*)\\S(?:,[^,\\r\\n]+|[^,\\r\\n]*)(?:,[^,\\r\\n]+)*(?:,(?:\\r?\\n|\\r)\\1[ \\t]+\\S(?:,[^,\\r\\n]+|[^,\\r\\n]*)(?:,[^,\\r\\n]+)*)*/m,lookbehind:!0,greedy:!0}})}(Prism);\nPrism.languages.scss=Prism.languages.extend(\"css\",{comment:{pattern:/(^|[^\\\\])(?:\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*)/,lookbehind:!0},atrule:{pattern:/@[\\w-](?:\\([^()]+\\)|[^()\\s]|\\s+(?!\\s))*?(?=\\s+[{;])/,inside:{rule:/@[\\w-]+/}},url:/(?:[-a-z]+-)?url(?=\\()/i,selector:{pattern:/(?=\\S)[^@;{}()]?(?:[^@;{}()\\s]|\\s+(?!\\s)|#\\{\\$[-\\w]+\\})+(?=\\s*\\{(?:\\}|\\s|[^}][^:{}]*[:{][^}]))/,inside:{parent:{pattern:/&/,alias:\"important\"},placeholder:/%[-\\w]+/,variable:/\\$[-\\w]+|#\\{\\$[-\\w]+\\}/}},property:{pattern:/(?:[-\\w]|\\$[-\\w]|#\\{\\$[-\\w]+\\})+(?=\\s*:)/,inside:{variable:/\\$[-\\w]+|#\\{\\$[-\\w]+\\}/}}}),Prism.languages.insertBefore(\"scss\",\"atrule\",{keyword:[/@(?:content|debug|each|else(?: if)?|extend|for|forward|function|if|import|include|mixin|return|use|warn|while)\\b/i,{pattern:/( )(?:from|through)(?= )/,lookbehind:!0}]}),Prism.languages.insertBefore(\"scss\",\"important\",{variable:/\\$[-\\w]+|#\\{\\$[-\\w]+\\}/}),Prism.languages.insertBefore(\"scss\",\"function\",{\"module-modifier\":{pattern:/\\b(?:as|hide|show|with)\\b/i,alias:\"keyword\"},placeholder:{pattern:/%[-\\w]+/,alias:\"selector\"},statement:{pattern:/\\B!(?:default|optional)\\b/i,alias:\"keyword\"},boolean:/\\b(?:false|true)\\b/,null:{pattern:/\\bnull\\b/,alias:\"keyword\"},operator:{pattern:/(\\s)(?:[-+*\\/%]|[=!]=|<=?|>=?|and|not|or)(?=\\s)/,lookbehind:!0}}),Prism.languages.scss.atrule.inside.rest=Prism.languages.scss;\nPrism.languages.scala=Prism.languages.extend(\"java\",{\"triple-quoted-string\":{pattern:/\"\"\"[\\s\\S]*?\"\"\"/,greedy:!0,alias:\"string\"},string:{pattern:/(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0},keyword:/<-|=>|\\b(?:abstract|case|catch|class|def|derives|do|else|enum|extends|extension|final|finally|for|forSome|given|if|implicit|import|infix|inline|lazy|match|new|null|object|opaque|open|override|package|private|protected|return|sealed|self|super|this|throw|trait|transparent|try|type|using|val|var|while|with|yield)\\b/,number:/\\b0x(?:[\\da-f]*\\.)?[\\da-f]+|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e\\d+)?[dfl]?/i,builtin:/\\b(?:Any|AnyRef|AnyVal|Boolean|Byte|Char|Double|Float|Int|Long|Nothing|Short|String|Unit)\\b/,symbol:/'[^\\d\\s\\\\]\\w*/}),Prism.languages.insertBefore(\"scala\",\"triple-quoted-string\",{\"string-interpolation\":{pattern:/\\b[a-z]\\w*(?:\"\"\"(?:[^$]|\\$(?:[^{]|\\{(?:[^{}]|\\{[^{}]*\\})*\\}))*?\"\"\"|\"(?:[^$\"\\r\\n]|\\$(?:[^{]|\\{(?:[^{}]|\\{[^{}]*\\})*\\}))*\")/i,greedy:!0,inside:{id:{pattern:/^\\w+/,greedy:!0,alias:\"function\"},escape:{pattern:/\\\\\\$\"|\\$[$\"]/,greedy:!0,alias:\"symbol\"},interpolation:{pattern:/\\$(?:\\w+|\\{(?:[^{}]|\\{[^{}]*\\})*\\})/,greedy:!0,inside:{punctuation:/^\\$\\{?|\\}$/,expression:{pattern:/[\\s\\S]+/,inside:Prism.languages.scala}}},string:/[\\s\\S]+/}}}),delete Prism.languages.scala[\"class-name\"],delete Prism.languages.scala.function,delete Prism.languages.scala.constant;\n!function(e){e.languages.scheme={comment:/;.*|#;\\s*(?:\\((?:[^()]|\\([^()]*\\))*\\)|\\[(?:[^\\[\\]]|\\[[^\\[\\]]*\\])*\\])|#\\|(?:[^#|]|#(?!\\|)|\\|(?!#)|#\\|(?:[^#|]|#(?!\\|)|\\|(?!#))*\\|#)*\\|#/,string:{pattern:/\"(?:[^\"\\\\]|\\\\.)*\"/,greedy:!0},symbol:{pattern:/'[^()\\[\\]#'\\s]+/,greedy:!0},char:{pattern:/#\\\\(?:[ux][a-fA-F\\d]+\\b|[-a-zA-Z]+\\b|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|\\S)/,greedy:!0},\"lambda-parameter\":[{pattern:/((?:^|[^'`#])[(\\[]lambda\\s+)(?:[^|()\\[\\]'\\s]+|\\|(?:[^\\\\|]|\\\\.)*\\|)/,lookbehind:!0},{pattern:/((?:^|[^'`#])[(\\[]lambda\\s+[(\\[])[^()\\[\\]']+/,lookbehind:!0}],keyword:{pattern:/((?:^|[^'`#])[(\\[])(?:begin|case(?:-lambda)?|cond(?:-expand)?|define(?:-library|-macro|-record-type|-syntax|-values)?|defmacro|delay(?:-force)?|do|else|except|export|guard|if|import|include(?:-ci|-library-declarations)?|lambda|let(?:rec)?(?:-syntax|-values|\\*)?|let\\*-values|only|parameterize|prefix|(?:quasi-?)?quote|rename|set!|syntax-(?:case|rules)|unless|unquote(?:-splicing)?|when)(?=[()\\[\\]\\s]|$)/,lookbehind:!0},builtin:{pattern:/((?:^|[^'`#])[(\\[])(?:abs|and|append|apply|assoc|ass[qv]|binary-port\\?|boolean=?\\?|bytevector(?:-append|-copy|-copy!|-length|-u8-ref|-u8-set!|\\?)?|caar|cadr|call-with-(?:current-continuation|port|values)|call\\/cc|car|cdar|cddr|cdr|ceiling|char(?:->integer|-ready\\?|\\?|<\\?|<=\\?|=\\?|>\\?|>=\\?)|close-(?:input-port|output-port|port)|complex\\?|cons|current-(?:error|input|output)-port|denominator|dynamic-wind|eof-object\\??|eq\\?|equal\\?|eqv\\?|error|error-object(?:-irritants|-message|\\?)|eval|even\\?|exact(?:-integer-sqrt|-integer\\?|\\?)?|expt|features|file-error\\?|floor(?:-quotient|-remainder|\\/)?|flush-output-port|for-each|gcd|get-output-(?:bytevector|string)|inexact\\??|input-port(?:-open\\?|\\?)|integer(?:->char|\\?)|lcm|length|list(?:->string|->vector|-copy|-ref|-set!|-tail|\\?)?|make-(?:bytevector|list|parameter|string|vector)|map|max|member|memq|memv|min|modulo|negative\\?|newline|not|null\\?|number(?:->string|\\?)|numerator|odd\\?|open-(?:input|output)-(?:bytevector|string)|or|output-port(?:-open\\?|\\?)|pair\\?|peek-char|peek-u8|port\\?|positive\\?|procedure\\?|quotient|raise|raise-continuable|rational\\?|rationalize|read-(?:bytevector|bytevector!|char|error\\?|line|string|u8)|real\\?|remainder|reverse|round|set-c[ad]r!|square|string(?:->list|->number|->symbol|->utf8|->vector|-append|-copy|-copy!|-fill!|-for-each|-length|-map|-ref|-set!|\\?|<\\?|<=\\?|=\\?|>\\?|>=\\?)?|substring|symbol(?:->string|\\?|=\\?)|syntax-error|textual-port\\?|truncate(?:-quotient|-remainder|\\/)?|u8-ready\\?|utf8->string|values|vector(?:->list|->string|-append|-copy|-copy!|-fill!|-for-each|-length|-map|-ref|-set!|\\?)?|with-exception-handler|write-(?:bytevector|char|string|u8)|zero\\?)(?=[()\\[\\]\\s]|$)/,lookbehind:!0},operator:{pattern:/((?:^|[^'`#])[(\\[])(?:[-+*%/]|[<>]=?|=>?)(?=[()\\[\\]\\s]|$)/,lookbehind:!0},number:{pattern:RegExp(function(e){for(var r in e)e[r]=e[r].replace(/<[\\w\\s]+>/g,(function(r){return\"(?:\"+e[r].trim()+\")\"}));return e[r]}({\"<ureal dec>\":\"\\\\d+(?:/\\\\d+)|(?:\\\\d+(?:\\\\.\\\\d*)?|\\\\.\\\\d+)(?:[esfdl][+-]?\\\\d+)?\",\"<real dec>\":\"[+-]?<ureal dec>|[+-](?:inf|nan)\\\\.0\",\"<imaginary dec>\":\"[+-](?:<ureal dec>|(?:inf|nan)\\\\.0)?i\",\"<complex dec>\":\"<real dec>(?:@<real dec>|<imaginary dec>)?|<imaginary dec>\",\"<num dec>\":\"(?:#d(?:#[ei])?|#[ei](?:#d)?)?<complex dec>\",\"<ureal box>\":\"[0-9a-f]+(?:/[0-9a-f]+)?\",\"<real box>\":\"[+-]?<ureal box>|[+-](?:inf|nan)\\\\.0\",\"<imaginary box>\":\"[+-](?:<ureal box>|(?:inf|nan)\\\\.0)?i\",\"<complex box>\":\"<real box>(?:@<real box>|<imaginary box>)?|<imaginary box>\",\"<num box>\":\"#[box](?:#[ei])?|(?:#[ei])?#[box]<complex box>\",\"<number>\":\"(^|[()\\\\[\\\\]\\\\s])(?:<num dec>|<num box>)(?=[()\\\\[\\\\]\\\\s]|$)\"}),\"i\"),lookbehind:!0},boolean:{pattern:/(^|[()\\[\\]\\s])#(?:[ft]|false|true)(?=[()\\[\\]\\s]|$)/,lookbehind:!0},function:{pattern:/((?:^|[^'`#])[(\\[])(?:[^|()\\[\\]'\\s]+|\\|(?:[^\\\\|]|\\\\.)*\\|)(?=[()\\[\\]\\s]|$)/,lookbehind:!0},identifier:{pattern:/(^|[()\\[\\]\\s])\\|(?:[^\\\\|]|\\\\.)*\\|(?=[()\\[\\]\\s]|$)/,lookbehind:!0,greedy:!0},punctuation:/[()\\[\\]']/}}(Prism);\nPrism.languages.sql={comment:{pattern:/(^|[^\\\\])(?:\\/\\*[\\s\\S]*?\\*\\/|(?:--|\\/\\/|#).*)/,lookbehind:!0},variable:[{pattern:/@([\"'`])(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])+\\1/,greedy:!0},/@[\\w.$]+/],string:{pattern:/(^|[^@\\\\])(\"|')(?:\\\\[\\s\\S]|(?!\\2)[^\\\\]|\\2\\2)*\\2/,greedy:!0,lookbehind:!0},identifier:{pattern:/(^|[^@\\\\])`(?:\\\\[\\s\\S]|[^`\\\\]|``)*`/,greedy:!0,lookbehind:!0,inside:{punctuation:/^`|`$/}},function:/\\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\\s*\\()/i,keyword:/\\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\\b/i,boolean:/\\b(?:FALSE|NULL|TRUE)\\b/i,number:/\\b0x[\\da-f]+\\b|\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+\\b/i,operator:/[-+*\\/=%^~]|&&?|\\|\\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\\b/i,punctuation:/[;[\\]()`,.]/};\nPrism.languages.swift={comment:{pattern:/(^|[^\\\\:])(?:\\/\\/.*|\\/\\*(?:[^/*]|\\/(?!\\*)|\\*(?!\\/)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*\\*\\/)/,lookbehind:!0,greedy:!0},\"string-literal\":[{pattern:RegExp('(^|[^\"#])(?:\"(?:\\\\\\\\(?:\\\\((?:[^()]|\\\\([^()]*\\\\))*\\\\)|\\r\\n|[^(])|[^\\\\\\\\\\r\\n\"])*\"|\"\"\"(?:\\\\\\\\(?:\\\\((?:[^()]|\\\\([^()]*\\\\))*\\\\)|[^(])|[^\\\\\\\\\"]|\"(?!\"\"))*\"\"\")(?![\"#])'),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\\\\()(?:[^()]|\\([^()]*\\))*(?=\\))/,lookbehind:!0,inside:null},\"interpolation-punctuation\":{pattern:/^\\)|\\\\\\($/,alias:\"punctuation\"},punctuation:/\\\\(?=[\\r\\n])/,string:/[\\s\\S]+/}},{pattern:RegExp('(^|[^\"#])(#+)(?:\"(?:\\\\\\\\(?:#+\\\\((?:[^()]|\\\\([^()]*\\\\))*\\\\)|\\r\\n|[^#])|[^\\\\\\\\\\r\\n])*?\"|\"\"\"(?:\\\\\\\\(?:#+\\\\((?:[^()]|\\\\([^()]*\\\\))*\\\\)|[^#])|[^\\\\\\\\])*?\"\"\")\\\\2'),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\\#+\\()(?:[^()]|\\([^()]*\\))*(?=\\))/,lookbehind:!0,inside:null},\"interpolation-punctuation\":{pattern:/^\\)|\\\\#+\\($/,alias:\"punctuation\"},string:/[\\s\\S]+/}}],directive:{pattern:RegExp(\"#(?:(?:elseif|if)\\\\b(?:[ \\t]*(?:![ \\t]*)?(?:\\\\b\\\\w+\\\\b(?:[ \\t]*\\\\((?:[^()]|\\\\([^()]*\\\\))*\\\\))?|\\\\((?:[^()]|\\\\([^()]*\\\\))*\\\\))(?:[ \\t]*(?:&&|\\\\|\\\\|))?)+|(?:else|endif)\\\\b)\"),alias:\"property\",inside:{\"directive-name\":/^#\\w+/,boolean:/\\b(?:false|true)\\b/,number:/\\b\\d+(?:\\.\\d+)*\\b/,operator:/!|&&|\\|\\||[<>]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\\b/,alias:\"constant\"},\"other-directive\":{pattern:/#\\w+\\b/,alias:\"property\"},attribute:{pattern:/@\\w+/,alias:\"atrule\"},\"function-definition\":{pattern:/(\\bfunc\\s+)\\w+/,lookbehind:!0,alias:\"function\"},label:{pattern:/\\b(break|continue)\\s+\\w+|\\b[a-zA-Z_]\\w*(?=\\s*:\\s*(?:for|repeat|while)\\b)/,lookbehind:!0,alias:\"important\"},keyword:/\\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\\b/,boolean:/\\b(?:false|true)\\b/,nil:{pattern:/\\bnil\\b/,alias:\"constant\"},\"short-argument\":/\\$\\d+\\b/,omit:{pattern:/\\b_\\b/,alias:\"keyword\"},number:/\\b(?:[\\d_]+(?:\\.[\\de_]+)?|0x[a-f0-9_]+(?:\\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b/i,\"class-name\":/\\b[A-Z](?:[A-Z_\\d]*[a-z]\\w*)?\\b/,function:/\\b[a-z_]\\w*(?=\\s*\\()/i,constant:/\\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\\b/,operator:/[-+*/%=!<>&|^~?]+|\\.[.\\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\\]();,.:\\\\]/},Prism.languages.swift[\"string-literal\"].forEach((function(e){e.inside.interpolation.inside=Prism.languages.swift}));\nPrism.languages.twig={comment:/^\\{#[\\s\\S]*?#\\}$/,\"tag-name\":{pattern:/(^\\{%-?\\s*)\\w+/,lookbehind:!0,alias:\"keyword\"},delimiter:{pattern:/^\\{[{%]-?|-?[%}]\\}$/,alias:\"punctuation\"},string:{pattern:/(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/,inside:{punctuation:/^['\"]|['\"]$/}},keyword:/\\b(?:even|if|odd)\\b/,boolean:/\\b(?:false|null|true)\\b/,number:/\\b0x[\\dA-Fa-f]+|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[Ee][-+]?\\d+)?/,operator:[{pattern:/(\\s)(?:and|b-and|b-or|b-xor|ends with|in|is|matches|not|or|same as|starts with)(?=\\s)/,lookbehind:!0},/[=<>]=?|!=|\\*\\*?|\\/\\/?|\\?:?|[-+~%|]/],punctuation:/[()\\[\\]{}:.,]/},Prism.hooks.add(\"before-tokenize\",(function(e){\"twig\"===e.language&&Prism.languages[\"markup-templating\"].buildPlaceholders(e,\"twig\",/\\{(?:#[\\s\\S]*?#|%[\\s\\S]*?%|\\{[\\s\\S]*?\\})\\}/g)})),Prism.hooks.add(\"after-tokenize\",(function(e){Prism.languages[\"markup-templating\"].tokenizePlaceholders(e,\"twig\")}));\n!function(e){e.languages.typescript=e.languages.extend(\"javascript\",{\"class-name\":{pattern:/(\\b(?:class|extends|implements|instanceof|interface|new|type)\\s+)(?!keyof\\b)(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*(?:\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\\b/}),e.languages.typescript.keyword.push(/\\b(?:abstract|declare|is|keyof|readonly|require)\\b/,/\\b(?:asserts|infer|interface|module|namespace|type)\\b(?=\\s*(?:[{_$a-zA-Z\\xA0-\\uFFFF]|$))/,/\\btype\\b(?=\\s*(?:[\\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript[\"literal-property\"];var s=e.languages.extend(\"typescript\",{});delete s[\"class-name\"],e.languages.typescript[\"class-name\"].inside=s,e.languages.insertBefore(\"typescript\",\"function\",{decorator:{pattern:/@[$\\w\\xA0-\\uFFFF]+/,inside:{at:{pattern:/^@/,alias:\"operator\"},function:/^[\\s\\S]+/}},\"generic-function\":{pattern:/#?(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\\s*\\()/,greedy:!0,inside:{function:/^#?(?!\\s)[_$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[$\\w\\xA0-\\uFFFF])*/,generic:{pattern:/<[\\s\\S]+/,alias:\"class-name\",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism);\nPrism.languages.vbnet=Prism.languages.extend(\"basic\",{comment:[{pattern:/(?:!|REM\\b).+/i,inside:{keyword:/^REM/i}},{pattern:/(^|[^\\\\:])'.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(^|[^\"])\"(?:\"\"|[^\"])*\"(?!\")/,lookbehind:!0,greedy:!0},keyword:/(?:\\b(?:ADDHANDLER|ADDRESSOF|ALIAS|AND|ANDALSO|AS|BEEP|BLOAD|BOOLEAN|BSAVE|BYREF|BYTE|BYVAL|CALL(?: ABSOLUTE)?|CASE|CATCH|CBOOL|CBYTE|CCHAR|CDATE|CDBL|CDEC|CHAIN|CHAR|CHDIR|CINT|CLASS|CLEAR|CLNG|CLOSE|CLS|COBJ|COM|COMMON|CONST|CONTINUE|CSBYTE|CSHORT|CSNG|CSTR|CTYPE|CUINT|CULNG|CUSHORT|DATA|DATE|DECIMAL|DECLARE|DEF(?: FN| SEG|DBL|INT|LNG|SNG|STR)|DEFAULT|DELEGATE|DIM|DIRECTCAST|DO|DOUBLE|ELSE|ELSEIF|END|ENUM|ENVIRON|ERASE|ERROR|EVENT|EXIT|FALSE|FIELD|FILES|FINALLY|FOR(?: EACH)?|FRIEND|FUNCTION|GET|GETTYPE|GETXMLNAMESPACE|GLOBAL|GOSUB|GOTO|HANDLES|IF|IMPLEMENTS|IMPORTS|IN|INHERITS|INPUT|INTEGER|INTERFACE|IOCTL|IS|ISNOT|KEY|KILL|LET|LIB|LIKE|LINE INPUT|LOCATE|LOCK|LONG|LOOP|LSET|ME|MKDIR|MOD|MODULE|MUSTINHERIT|MUSTOVERRIDE|MYBASE|MYCLASS|NAME|NAMESPACE|NARROWING|NEW|NEXT|NOT|NOTHING|NOTINHERITABLE|NOTOVERRIDABLE|OBJECT|OF|OFF|ON(?: COM| ERROR| KEY| TIMER)?|OPEN|OPERATOR|OPTION(?: BASE)?|OPTIONAL|OR|ORELSE|OUT|OVERLOADS|OVERRIDABLE|OVERRIDES|PARAMARRAY|PARTIAL|POKE|PRIVATE|PROPERTY|PROTECTED|PUBLIC|PUT|RAISEEVENT|READ|READONLY|REDIM|REM|REMOVEHANDLER|RESTORE|RESUME|RETURN|RMDIR|RSET|RUN|SBYTE|SELECT(?: CASE)?|SET|SHADOWS|SHARED|SHELL|SHORT|SINGLE|SLEEP|STATIC|STEP|STOP|STRING|STRUCTURE|SUB|SWAP|SYNCLOCK|SYSTEM|THEN|THROW|TIMER|TO|TROFF|TRON|TRUE|TRY|TRYCAST|TYPE|TYPEOF|UINTEGER|ULONG|UNLOCK|UNTIL|USHORT|USING|VIEW PRINT|WAIT|WEND|WHEN|WHILE|WIDENING|WITH|WITHEVENTS|WRITE|WRITEONLY|XOR)|\\B(?:#CONST|#ELSE|#ELSEIF|#END|#IF))(?:\\$|\\b)/i,punctuation:/[,;:(){}]/});\nPrism.languages[\"visual-basic\"]={comment:{pattern:/(?:['‘’]|REM\\b)(?:[^\\r\\n_]|_(?:\\r\\n?|\\n)?)*/i,inside:{keyword:/^REM/i}},directive:{pattern:/#(?:Const|Else|ElseIf|End|ExternalChecksum|ExternalSource|If|Region)(?:\\b_[ \\t]*(?:\\r\\n?|\\n)|.)+/i,alias:\"property\",greedy:!0},string:{pattern:/\\$?[\"“”](?:[\"“”]{2}|[^\"“”])*[\"“”]C?/i,greedy:!0},date:{pattern:/#[ \\t]*(?:\\d+([/-])\\d+\\1\\d+(?:[ \\t]+(?:\\d+[ \\t]*(?:AM|PM)|\\d+:\\d+(?::\\d+)?(?:[ \\t]*(?:AM|PM))?))?|\\d+[ \\t]*(?:AM|PM)|\\d+:\\d+(?::\\d+)?(?:[ \\t]*(?:AM|PM))?)[ \\t]*#/i,alias:\"number\"},number:/(?:(?:\\b\\d+(?:\\.\\d+)?|\\.\\d+)(?:E[+-]?\\d+)?|&[HO][\\dA-F]+)(?:[FRD]|U?[ILS])?/i,boolean:/\\b(?:False|Nothing|True)\\b/i,keyword:/\\b(?:AddHandler|AddressOf|Alias|And(?:Also)?|As|Boolean|ByRef|Byte|ByVal|Call|Case|Catch|C(?:Bool|Byte|Char|Date|Dbl|Dec|Int|Lng|Obj|SByte|Short|Sng|Str|Type|UInt|ULng|UShort)|Char|Class|Const|Continue|Currency|Date|Decimal|Declare|Default|Delegate|Dim|DirectCast|Do|Double|Each|Else(?:If)?|End(?:If)?|Enum|Erase|Error|Event|Exit|Finally|For|Friend|Function|Get(?:Type|XMLNamespace)?|Global|GoSub|GoTo|Handles|If|Implements|Imports|In|Inherits|Integer|Interface|Is|IsNot|Let|Lib|Like|Long|Loop|Me|Mod|Module|Must(?:Inherit|Override)|My(?:Base|Class)|Namespace|Narrowing|New|Next|Not(?:Inheritable|Overridable)?|Object|Of|On|Operator|Option(?:al)?|Or(?:Else)?|Out|Overloads|Overridable|Overrides|ParamArray|Partial|Private|Property|Protected|Public|RaiseEvent|ReadOnly|ReDim|RemoveHandler|Resume|Return|SByte|Select|Set|Shadows|Shared|short|Single|Static|Step|Stop|String|Structure|Sub|SyncLock|Then|Throw|To|Try|TryCast|Type|TypeOf|U(?:Integer|Long|Short)|Until|Using|Variant|Wend|When|While|Widening|With(?:Events)?|WriteOnly|Xor)\\b/i,operator:/[+\\-*/\\\\^<=>&#@$%!]|\\b_(?=[ \\t]*[\\r\\n])/,punctuation:/[{}().,:?]/},Prism.languages.vb=Prism.languages[\"visual-basic\"],Prism.languages.vba=Prism.languages[\"visual-basic\"];\n!function(e){var n=/[*&][^\\s[\\]{},]+/,r=/!(?:<[\\w\\-%#;/?:@&=+$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+$.~*'()]+)?/,t=\"(?:\"+r.source+\"(?:[ \\t]+\"+n.source+\")?|\"+n.source+\"(?:[ \\t]+\"+r.source+\")?)\",a=\"(?:[^\\\\s\\\\x00-\\\\x08\\\\x0e-\\\\x1f!\\\"#%&'*,\\\\-:>?@[\\\\]`{|}\\\\x7f-\\\\x84\\\\x86-\\\\x9f\\\\ud800-\\\\udfff\\\\ufffe\\\\uffff]|[?:-]<PLAIN>)(?:[ \\t]*(?:(?![#:])<PLAIN>|:<PLAIN>))*\".replace(/<PLAIN>/g,(function(){return\"[^\\\\s\\\\x00-\\\\x08\\\\x0e-\\\\x1f,[\\\\]{}\\\\x7f-\\\\x84\\\\x86-\\\\x9f\\\\ud800-\\\\udfff\\\\ufffe\\\\uffff]\"})),d=\"\\\"(?:[^\\\"\\\\\\\\\\r\\n]|\\\\\\\\.)*\\\"|'(?:[^'\\\\\\\\\\r\\n]|\\\\\\\\.)*'\";function o(e,n){n=(n||\"\").replace(/m/g,\"\")+\"m\";var r=\"([:\\\\-,[{]\\\\s*(?:\\\\s<<prop>>[ \\t]+)?)(?:<<value>>)(?=[ \\t]*(?:$|,|\\\\]|\\\\}|(?:[\\r\\n]\\\\s*)?#))\".replace(/<<prop>>/g,(function(){return t})).replace(/<<value>>/g,(function(){return e}));return RegExp(r,n)}e.languages.yaml={scalar:{pattern:RegExp(\"([\\\\-:]\\\\s*(?:\\\\s<<prop>>[ \\t]+)?[|>])[ \\t]*(?:((?:\\r?\\n|\\r)[ \\t]+)\\\\S[^\\r\\n]*(?:\\\\2[^\\r\\n]+)*)\".replace(/<<prop>>/g,(function(){return t}))),lookbehind:!0,alias:\"string\"},comment:/#.*/,key:{pattern:RegExp(\"((?:^|[:\\\\-,[{\\r\\n?])[ \\t]*(?:<<prop>>[ \\t]+)?)<<key>>(?=\\\\s*:\\\\s)\".replace(/<<prop>>/g,(function(){return t})).replace(/<<key>>/g,(function(){return\"(?:\"+a+\"|\"+d+\")\"}))),lookbehind:!0,greedy:!0,alias:\"atrule\"},directive:{pattern:/(^[ \\t]*)%.+/m,lookbehind:!0,alias:\"important\"},datetime:{pattern:o(\"\\\\d{4}-\\\\d\\\\d?-\\\\d\\\\d?(?:[tT]|[ \\t]+)\\\\d\\\\d?:\\\\d{2}:\\\\d{2}(?:\\\\.\\\\d*)?(?:[ \\t]*(?:Z|[-+]\\\\d\\\\d?(?::\\\\d{2})?))?|\\\\d{4}-\\\\d{2}-\\\\d{2}|\\\\d\\\\d?:\\\\d{2}(?::\\\\d{2}(?:\\\\.\\\\d*)?)?\"),lookbehind:!0,alias:\"number\"},boolean:{pattern:o(\"false|true\",\"i\"),lookbehind:!0,alias:\"important\"},null:{pattern:o(\"null|~\",\"i\"),lookbehind:!0,alias:\"important\"},string:{pattern:o(d),lookbehind:!0,greedy:!0},number:{pattern:o(\"[+-]?(?:0x[\\\\da-f]+|0o[0-7]+|(?:\\\\d+(?:\\\\.\\\\d*)?|\\\\.\\\\d+)(?:e[+-]?\\\\d+)?|\\\\.inf|\\\\.nan)\",\"i\"),lookbehind:!0},tag:r,important:n,punctuation:/---|[:[\\]{}\\-,|>?]|\\.\\.\\./},e.languages.yml=e.languages.yaml}(Prism);\n"
  },
  {
    "path": "app/dist/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Publii</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>\n    <script src=\"./vendor/jquery/jquery.min.js\"></script>\n    <script src=\"./vendor/tinymce/tinymce.min.js\"></script>\n    <script>if (window.module) module = window.module;</script>\n    <script>if (typeof global === 'undefined') window.global = window;</script>\n    <script src=\"./build.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "app/license.txt",
    "content": "GNU GENERAL PUBLIC LICENSE\n   Version 3, 29 June 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\n        Preamble\n\nThe GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\nWhen we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nTo protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\nFor example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\nDevelopers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\nFor the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\nSome devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\nFinally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\n   TERMS AND CONDITIONS\n\n0. Definitions.\n\n\"This License\" refers to version 3 of the GNU General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\na) The work must carry prominent notices stating that you modified\nit, and giving a relevant date.\n\nb) The work must carry prominent notices stating that it is\nreleased under this License and any conditions added under section\n7.  This requirement modifies the requirement in section 4 to\n\"keep intact all notices\".\n\nc) You must license the entire work, as a whole, under this\nLicense to anyone who comes into possession of a copy.  This\nLicense will therefore apply, along with any applicable section 7\nadditional terms, to the whole of the work, and all its parts,\nregardless of how they are packaged.  This License gives no\npermission to license the work in any other way, but it does not\ninvalidate such permission if you have separately received it.\n\nd) If the work has interactive user interfaces, each must display\nAppropriate Legal Notices; however, if the Program has interactive\ninterfaces that do not display Appropriate Legal Notices, your\nwork need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n6. Conveying Non-Source Forms.\n\nYou may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\na) Convey the object code in, or embodied in, a physical product\n(including a physical distribution medium), accompanied by the\nCorresponding Source fixed on a durable physical medium\ncustomarily used for software interchange.\n\nb) Convey the object code in, or embodied in, a physical product\n(including a physical distribution medium), accompanied by a\nwritten offer, valid for at least three years and valid for as\nlong as you offer spare parts or customer support for that product\nmodel, to give anyone who possesses the object code either (1) a\ncopy of the Corresponding Source for all the software in the\nproduct that is covered by this License, on a durable physical\nmedium customarily used for software interchange, for a price no\nmore than your reasonable cost of physically performing this\nconveying of source, or (2) access to copy the\nCorresponding Source from a network server at no charge.\n\nc) Convey individual copies of the object code with a copy of the\nwritten offer to provide the Corresponding Source.  This\nalternative is allowed only occasionally and noncommercially, and\nonly if you received the object code with such an offer, in accord\nwith subsection 6b.\n\nd) Convey the object code by offering access from a designated\nplace (gratis or for a charge), and offer equivalent access to the\nCorresponding Source in the same way through the same place at no\nfurther charge.  You need not require recipients to copy the\nCorresponding Source along with the object code.  If the place to\ncopy the object code is a network server, the Corresponding Source\nmay be on a different server (operated by you or a third party)\nthat supports equivalent copying facilities, provided you maintain\nclear directions next to the object code saying where to find the\nCorresponding Source.  Regardless of what server hosts the\nCorresponding Source, you remain obligated to ensure that it is\navailable for as long as needed to satisfy these requirements.\n\ne) Convey the object code using peer-to-peer transmission, provided\nyou inform other peers where the object code and Corresponding\nSource of the work are being offered to the general public at no\ncharge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\na) Disclaiming warranty or limiting liability differently from the\nterms of sections 15 and 16 of this License; or\n\nb) Requiring preservation of specified reasonable legal notices or\nauthor attributions in that material or in the Appropriate Legal\nNotices displayed by works containing it; or\n\nc) Prohibiting misrepresentation of the origin of that material, or\nrequiring that modified versions of such material be marked in\nreasonable ways as different from the original version; or\n\nd) Limiting the use for publicity purposes of names of licensors or\nauthors of the material; or\n\ne) Declining to grant rights under trademark law for use of some\ntrade names, trademarks, or service marks; or\n\nf) Requiring indemnification of licensors and authors of that\nmaterial by anyone who conveys the material (or modified versions of\nit) with contractual assumptions of liability to the recipient, for\nany liability that these contractual assumptions directly impose on\nthose licensors and authors.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n13. Use with the GNU Affero General Public License.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n14. Revised Versions of this License.\n\nThe Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n END OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n<one line to give the program's name and a brief idea of what it does.>\nCopyright (C) <year>  <name of author>\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n<program>  Copyright (C) <year>  <name of author>\nThis program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\nThis is free software, and you are welcome to redistribute it\nunder certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\nYou should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\nThe GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "app/licenses/agent-base/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/all-licenses.json",
    "content": "{\"aws-crypto/crc32@5.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-crypto-helpers\",\"publisher\":\"AWS Crypto Tools Team\",\"email\":\"aws-cryptools@amazon.com\",\"url\":\"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\"path\":\"/node_modules/@aws-crypto/crc32\",\"licenseFile\":\"/node_modules/@aws-crypto/crc32/LICENSE\"},\"aws-crypto/crc32c@5.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-crypto-helpers\",\"publisher\":\"AWS Crypto Tools Team\",\"email\":\"aws-cryptools@amazon.com\",\"url\":\"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\"path\":\"/node_modules/@aws-crypto/crc32c\",\"licenseFile\":\"/node_modules/@aws-crypto/crc32c/LICENSE\"},\"aws-crypto/sha1-browser@5.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-crypto-helpers\",\"publisher\":\"AWS Crypto Tools Team\",\"email\":\"aws-cryptools@amazon.com\",\"url\":\"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\"path\":\"/node_modules/@aws-crypto/sha1-browser\",\"licenseFile\":\"/node_modules/@aws-crypto/sha1-browser/LICENSE\"},\"aws-crypto/sha256-browser@5.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-crypto-helpers\",\"publisher\":\"AWS Crypto Tools Team\",\"email\":\"aws-cryptools@amazon.com\",\"url\":\"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\"path\":\"/node_modules/@aws-crypto/sha256-browser\",\"licenseFile\":\"/node_modules/@aws-crypto/sha256-browser/LICENSE\"},\"aws-crypto/sha256-js@5.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-crypto-helpers\",\"publisher\":\"AWS Crypto Tools Team\",\"email\":\"aws-cryptools@amazon.com\",\"url\":\"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\"path\":\"/node_modules/@aws-crypto/sha256-js\",\"licenseFile\":\"/node_modules/@aws-crypto/sha256-js/LICENSE\"},\"aws-crypto/supports-web-crypto@5.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-crypto-helpers\",\"publisher\":\"AWS Crypto Tools Team\",\"email\":\"aws-cryptools@amazon.com\",\"url\":\"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\"path\":\"/node_modules/@aws-crypto/supports-web-crypto\",\"licenseFile\":\"/node_modules/@aws-crypto/supports-web-crypto/LICENSE\"},\"aws-crypto/util@5.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-crypto-helpers\",\"publisher\":\"AWS Crypto Tools Team\",\"email\":\"aws-cryptools@amazon.com\",\"url\":\"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\"path\":\"/node_modules/@aws-crypto/util\",\"licenseFile\":\"/node_modules/@aws-crypto/util/LICENSE\"},\"aws-sdk/client-s3@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/client-s3\",\"licenseFile\":\"/node_modules/@aws-sdk/client-s3/LICENSE\"},\"aws-sdk/client-sso-oidc@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/client-sso-oidc\",\"licenseFile\":\"/node_modules/@aws-sdk/client-sso-oidc/LICENSE\"},\"aws-sdk/client-sso@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/client-sso\",\"licenseFile\":\"/node_modules/@aws-sdk/client-sso/LICENSE\"},\"aws-sdk/client-sts@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/client-sts\",\"licenseFile\":\"/node_modules/@aws-sdk/client-sts/LICENSE\"},\"aws-sdk/core@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/core\",\"licenseFile\":\"/node_modules/@aws-sdk/core/README.md\"},\"aws-sdk/credential-provider-env@3.620.1\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/credential-provider-env\",\"licenseFile\":\"/node_modules/@aws-sdk/credential-provider-env/LICENSE\"},\"aws-sdk/credential-provider-http@3.622.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/credential-provider-http\",\"licenseFile\":\"/node_modules/@aws-sdk/credential-provider-http/README.md\"},\"aws-sdk/credential-provider-ini@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/credential-provider-ini\",\"licenseFile\":\"/node_modules/@aws-sdk/credential-provider-ini/LICENSE\"},\"aws-sdk/credential-provider-node@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/credential-provider-node\",\"licenseFile\":\"/node_modules/@aws-sdk/credential-provider-node/LICENSE\"},\"aws-sdk/credential-provider-process@3.620.1\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/credential-provider-process\",\"licenseFile\":\"/node_modules/@aws-sdk/credential-provider-process/LICENSE\"},\"aws-sdk/credential-provider-sso@3.623.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/credential-provider-sso\",\"licenseFile\":\"/node_modules/@aws-sdk/credential-provider-sso/LICENSE\"},\"aws-sdk/credential-provider-web-identity@3.621.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/credential-provider-web-identity\",\"licenseFile\":\"/node_modules/@aws-sdk/credential-provider-web-identity/LICENSE\"},\"aws-sdk/middleware-bucket-endpoint@3.620.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-bucket-endpoint\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-bucket-endpoint/LICENSE\"},\"aws-sdk/middleware-expect-continue@3.620.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-expect-continue\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-expect-continue/LICENSE\"},\"aws-sdk/middleware-flexible-checksums@3.620.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-flexible-checksums\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-flexible-checksums/LICENSE\"},\"aws-sdk/middleware-host-header@3.620.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-host-header\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-host-header/LICENSE\"},\"aws-sdk/middleware-location-constraint@3.609.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-location-constraint\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-location-constraint/LICENSE\"},\"aws-sdk/middleware-logger@3.609.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-logger\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-logger/LICENSE\"},\"aws-sdk/middleware-recursion-detection@3.620.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-recursion-detection\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-recursion-detection/LICENSE\"},\"aws-sdk/middleware-sdk-s3@3.622.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-sdk-s3\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-sdk-s3/LICENSE\"},\"aws-sdk/middleware-signing@3.620.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-signing\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-signing/LICENSE\"},\"aws-sdk/middleware-ssec@3.609.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-ssec\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-ssec/LICENSE\"},\"aws-sdk/middleware-user-agent@3.620.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/middleware-user-agent\",\"licenseFile\":\"/node_modules/@aws-sdk/middleware-user-agent/LICENSE\"},\"aws-sdk/region-config-resolver@3.614.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/region-config-resolver\",\"licenseFile\":\"/node_modules/@aws-sdk/region-config-resolver/LICENSE\"},\"aws-sdk/signature-v4-multi-region@3.622.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/signature-v4-multi-region\",\"licenseFile\":\"/node_modules/@aws-sdk/signature-v4-multi-region/LICENSE\"},\"aws-sdk/token-providers@3.614.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/token-providers\",\"licenseFile\":\"/node_modules/@aws-sdk/token-providers/LICENSE\"},\"aws-sdk/types@3.609.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/types\",\"licenseFile\":\"/node_modules/@aws-sdk/types/LICENSE\"},\"aws-sdk/util-arn-parser@3.568.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/util-arn-parser\",\"licenseFile\":\"/node_modules/@aws-sdk/util-arn-parser/LICENSE\"},\"aws-sdk/util-endpoints@3.614.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/util-endpoints\",\"licenseFile\":\"/node_modules/@aws-sdk/util-endpoints/LICENSE\"},\"aws-sdk/util-locate-window@3.568.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/util-locate-window\",\"licenseFile\":\"/node_modules/@aws-sdk/util-locate-window/LICENSE\"},\"aws-sdk/util-user-agent-browser@3.609.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/util-user-agent-browser\",\"licenseFile\":\"/node_modules/@aws-sdk/util-user-agent-browser/LICENSE\"},\"aws-sdk/util-user-agent-node@3.614.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/util-user-agent-node\",\"licenseFile\":\"/node_modules/@aws-sdk/util-user-agent-node/LICENSE\"},\"aws-sdk/xml-builder@3.609.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/aws/aws-sdk-js-v3\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-sdk/xml-builder\",\"licenseFile\":\"/node_modules/@aws-sdk/xml-builder/LICENSE\"},\"gitbeaker/core@35.8.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jdalrymple/gitbeaker\",\"publisher\":\"Justin Dalrymple\",\"path\":\"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/core\",\"licenseFile\":\"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/core/README.md\"},\"gitbeaker/node@35.8.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jdalrymple/gitbeaker\",\"publisher\":\"Justin Dalrymple\",\"path\":\"/node_modules/@gitbeaker/node\",\"licenseFile\":\"/node_modules/@gitbeaker/node/README.md\"},\"gitbeaker/requester-utils@35.8.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jdalrymple/gitbeaker\",\"publisher\":\"Justin Dalrymple\",\"path\":\"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/requester-utils\",\"licenseFile\":\"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/requester-utils/README.md\"},\"google-cloud/paginator@3.0.7\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/nodejs-paginator\",\"publisher\":\"Google Inc.\",\"path\":\"/node_modules/@google-cloud/paginator\",\"licenseFile\":\"/node_modules/@google-cloud/paginator/LICENSE\"},\"google-cloud/projectify@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/nodejs-projectify\",\"publisher\":\"Google Inc.\",\"path\":\"/node_modules/@google-cloud/projectify\",\"licenseFile\":\"/node_modules/@google-cloud/projectify/LICENSE\"},\"google-cloud/promisify@3.0.1\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/nodejs-promisify\",\"publisher\":\"Google Inc.\",\"path\":\"/node_modules/@google-cloud/promisify\",\"licenseFile\":\"/node_modules/@google-cloud/promisify/LICENSE\"},\"google-cloud/storage@6.11.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/nodejs-storage\",\"publisher\":\"Google Inc.\",\"path\":\"/node_modules/@google-cloud/storage\",\"licenseFile\":\"/node_modules/@google-cloud/storage/LICENSE\"},\"img/sharp-darwin-arm64@0.34.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/lovell/sharp\",\"publisher\":\"Lovell Fuller\",\"email\":\"npm@lovell.info\",\"path\":\"/node_modules/@img/sharp-darwin-arm64\",\"licenseFile\":\"/node_modules/@img/sharp-darwin-arm64/LICENSE\"},\"img/sharp-libvips-darwin-arm64@1.2.0\":{\"licenses\":\"LGPL-3.0-or-later\",\"repository\":\"https://github.com/lovell/sharp-libvips\",\"publisher\":\"Lovell Fuller\",\"email\":\"npm@lovell.info\",\"path\":\"/node_modules/@img/sharp-libvips-darwin-arm64\",\"licenseFile\":\"/node_modules/@img/sharp-libvips-darwin-arm64/README.md\"},\"isaacs/cliui@8.0.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/yargs/cliui\",\"publisher\":\"Ben Coe\",\"email\":\"ben@npmjs.com\",\"path\":\"/node_modules/@isaacs/cliui\",\"licenseFile\":\"/node_modules/@isaacs/cliui/LICENSE.txt\"},\"jimp/bmp@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/bmp\",\"licenseFile\":\"/node_modules/@jimp/bmp/LICENSE\"},\"jimp/core@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"publisher\":\"Oliver Moran\",\"email\":\"oliver.moran@gmail.com\",\"path\":\"/node_modules/@jimp/core\",\"licenseFile\":\"/node_modules/@jimp/core/LICENSE\"},\"jimp/custom@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/custom\",\"licenseFile\":\"/node_modules/@jimp/custom/LICENSE\"},\"jimp/gif@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/gif\",\"licenseFile\":\"/node_modules/@jimp/gif/LICENSE\"},\"jimp/jpeg@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/jpeg\",\"licenseFile\":\"/node_modules/@jimp/jpeg/LICENSE\"},\"jimp/plugin-blit@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-blit\",\"licenseFile\":\"/node_modules/@jimp/plugin-blit/LICENSE\"},\"jimp/plugin-blur@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-blur\",\"licenseFile\":\"/node_modules/@jimp/plugin-blur/LICENSE\"},\"jimp/plugin-circle@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-circle\",\"licenseFile\":\"/node_modules/@jimp/plugin-circle/LICENSE\"},\"jimp/plugin-color@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-color\",\"licenseFile\":\"/node_modules/@jimp/plugin-color/LICENSE\"},\"jimp/plugin-contain@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-contain\",\"licenseFile\":\"/node_modules/@jimp/plugin-contain/LICENSE\"},\"jimp/plugin-cover@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-cover\",\"licenseFile\":\"/node_modules/@jimp/plugin-cover/LICENSE\"},\"jimp/plugin-crop@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-crop\",\"licenseFile\":\"/node_modules/@jimp/plugin-crop/LICENSE\"},\"jimp/plugin-displace@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-displace\",\"licenseFile\":\"/node_modules/@jimp/plugin-displace/LICENSE\"},\"jimp/plugin-dither@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-dither\",\"licenseFile\":\"/node_modules/@jimp/plugin-dither/LICENSE\"},\"jimp/plugin-fisheye@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-fisheye\",\"licenseFile\":\"/node_modules/@jimp/plugin-fisheye/LICENSE\"},\"jimp/plugin-flip@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-flip\",\"licenseFile\":\"/node_modules/@jimp/plugin-flip/LICENSE\"},\"jimp/plugin-gaussian@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-gaussian\",\"licenseFile\":\"/node_modules/@jimp/plugin-gaussian/LICENSE\"},\"jimp/plugin-invert@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-invert\",\"licenseFile\":\"/node_modules/@jimp/plugin-invert/LICENSE\"},\"jimp/plugin-mask@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-mask\",\"licenseFile\":\"/node_modules/@jimp/plugin-mask/LICENSE\"},\"jimp/plugin-normalize@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-normalize\",\"licenseFile\":\"/node_modules/@jimp/plugin-normalize/LICENSE\"},\"jimp/plugin-print@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-print\",\"licenseFile\":\"/node_modules/@jimp/plugin-print/LICENSE\"},\"jimp/plugin-resize@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-resize\",\"licenseFile\":\"/node_modules/@jimp/plugin-resize/LICENSE\"},\"jimp/plugin-rotate@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-rotate\",\"licenseFile\":\"/node_modules/@jimp/plugin-rotate/LICENSE\"},\"jimp/plugin-scale@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-scale\",\"licenseFile\":\"/node_modules/@jimp/plugin-scale/LICENSE\"},\"jimp/plugin-shadow@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-shadow\",\"licenseFile\":\"/node_modules/@jimp/plugin-shadow/LICENSE\"},\"jimp/plugin-threshold@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugin-threshold\",\"licenseFile\":\"/node_modules/@jimp/plugin-threshold/LICENSE\"},\"jimp/plugins@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/plugins\",\"licenseFile\":\"/node_modules/@jimp/plugins/LICENSE\"},\"jimp/png@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/png\",\"licenseFile\":\"/node_modules/@jimp/png/LICENSE\"},\"jimp/tiff@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/tiff\",\"licenseFile\":\"/node_modules/@jimp/tiff/LICENSE\"},\"jimp/types@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/types\",\"licenseFile\":\"/node_modules/@jimp/types/LICENSE\"},\"jimp/utils@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"path\":\"/node_modules/@jimp/utils\",\"licenseFile\":\"/node_modules/@jimp/utils/LICENSE\"},\"octokit/auth-token@6.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/auth-token.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/@octokit/auth-token\",\"licenseFile\":\"/node_modules/@octokit/auth-token/LICENSE\"},\"octokit/core@7.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/core.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/@octokit/core\",\"licenseFile\":\"/node_modules/@octokit/core/LICENSE\"},\"octokit/endpoint@11.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/endpoint.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/@octokit/endpoint\",\"licenseFile\":\"/node_modules/@octokit/endpoint/LICENSE\"},\"octokit/graphql@9.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/graphql.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/@octokit/graphql\",\"licenseFile\":\"/node_modules/@octokit/graphql/LICENSE\"},\"octokit/openapi-types@25.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/openapi-types.ts\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://twitter.com/gr2m\",\"path\":\"/node_modules/@octokit/openapi-types\",\"licenseFile\":\"/node_modules/@octokit/openapi-types/LICENSE\"},\"octokit/plugin-paginate-rest@13.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/plugin-paginate-rest.js\",\"path\":\"/node_modules/@octokit/plugin-paginate-rest\",\"licenseFile\":\"/node_modules/@octokit/plugin-paginate-rest/LICENSE\"},\"octokit/plugin-request-log@6.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/plugin-request-log.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://twitter.com/gr2m\",\"path\":\"/node_modules/@octokit/plugin-request-log\",\"licenseFile\":\"/node_modules/@octokit/plugin-request-log/LICENSE\"},\"octokit/plugin-rest-endpoint-methods@16.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/plugin-rest-endpoint-methods.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://twitter.com/gr2m\",\"path\":\"/node_modules/@octokit/plugin-rest-endpoint-methods\",\"licenseFile\":\"/node_modules/@octokit/plugin-rest-endpoint-methods/LICENSE\"},\"octokit/request-error@7.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/request-error.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/@octokit/request-error\",\"licenseFile\":\"/node_modules/@octokit/request-error/LICENSE\"},\"octokit/request@10.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/request.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/@octokit/request\",\"licenseFile\":\"/node_modules/@octokit/request/LICENSE\"},\"octokit/rest@22.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/rest.js\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/@octokit/rest\",\"licenseFile\":\"/node_modules/@octokit/rest/LICENSE\"},\"octokit/types@14.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/octokit/types.ts\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://twitter.com/gr2m\",\"path\":\"/node_modules/@octokit/types\",\"licenseFile\":\"/node_modules/@octokit/types/LICENSE\"},\"one-ini/wasm@0.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/one-ini/core\",\"path\":\"/node_modules/@one-ini/wasm\",\"licenseFile\":\"/node_modules/@one-ini/wasm/LICENSE\"},\"pkgjs/parseargs@0.11.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/pkgjs/parseargs\",\"path\":\"/node_modules/@pkgjs/parseargs\",\"licenseFile\":\"/node_modules/@pkgjs/parseargs/LICENSE\"},\"sindresorhus/is@4.6.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/is\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/@sindresorhus/is\",\"licenseFile\":\"/node_modules/@sindresorhus/is/license\"},\"smithy/abort-controller@3.1.1\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/abort-controller\",\"licenseFile\":\"/node_modules/@smithy/abort-controller/LICENSE\"},\"smithy/chunked-blob-reader-native@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/chunked-blob-reader-native\",\"licenseFile\":\"/node_modules/@smithy/chunked-blob-reader-native/LICENSE\"},\"smithy/chunked-blob-reader@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/chunked-blob-reader\",\"licenseFile\":\"/node_modules/@smithy/chunked-blob-reader/LICENSE\"},\"smithy/config-resolver@3.0.5\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/config-resolver\",\"licenseFile\":\"/node_modules/@smithy/config-resolver/LICENSE\"},\"smithy/core@2.3.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS Smithy Team\",\"url\":\"https://smithy.io\",\"path\":\"/node_modules/@smithy/core\",\"licenseFile\":\"/node_modules/@smithy/core/LICENSE\"},\"smithy/credential-provider-imds@3.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/credential-provider-imds\",\"licenseFile\":\"/node_modules/@smithy/credential-provider-imds/LICENSE\"},\"smithy/eventstream-codec@3.1.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/eventstream-codec\",\"licenseFile\":\"/node_modules/@smithy/eventstream-codec/LICENSE\"},\"smithy/eventstream-serde-browser@3.0.5\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/eventstream-serde-browser\",\"licenseFile\":\"/node_modules/@smithy/eventstream-serde-browser/LICENSE\"},\"smithy/eventstream-serde-config-resolver@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/eventstream-serde-config-resolver\",\"licenseFile\":\"/node_modules/@smithy/eventstream-serde-config-resolver/LICENSE\"},\"smithy/eventstream-serde-node@3.0.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/eventstream-serde-node\",\"licenseFile\":\"/node_modules/@smithy/eventstream-serde-node/LICENSE\"},\"smithy/eventstream-serde-universal@3.0.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/eventstream-serde-universal\",\"licenseFile\":\"/node_modules/@smithy/eventstream-serde-universal/LICENSE\"},\"smithy/fetch-http-handler@3.2.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/fetch-http-handler\",\"licenseFile\":\"/node_modules/@smithy/fetch-http-handler/LICENSE\"},\"smithy/hash-blob-browser@3.1.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/hash-blob-browser\",\"licenseFile\":\"/node_modules/@smithy/hash-blob-browser/LICENSE\"},\"smithy/hash-node@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/hash-node\",\"licenseFile\":\"/node_modules/@smithy/hash-node/LICENSE\"},\"smithy/hash-stream-node@3.1.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/hash-stream-node\",\"licenseFile\":\"/node_modules/@smithy/hash-stream-node/LICENSE\"},\"smithy/invalid-dependency@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/invalid-dependency\",\"licenseFile\":\"/node_modules/@smithy/invalid-dependency/LICENSE\"},\"smithy/is-array-buffer@2.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer\",\"licenseFile\":\"/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer/LICENSE\"},\"smithy/is-array-buffer@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/is-array-buffer\",\"licenseFile\":\"/node_modules/@smithy/is-array-buffer/LICENSE\"},\"smithy/md5-js@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/md5-js\",\"licenseFile\":\"/node_modules/@smithy/md5-js/LICENSE\"},\"smithy/middleware-content-length@3.0.5\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/middleware-content-length\",\"licenseFile\":\"/node_modules/@smithy/middleware-content-length/LICENSE\"},\"smithy/middleware-endpoint@3.1.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/middleware-endpoint\",\"licenseFile\":\"/node_modules/@smithy/middleware-endpoint/LICENSE\"},\"smithy/middleware-retry@3.0.14\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/middleware-retry\",\"licenseFile\":\"/node_modules/@smithy/middleware-retry/LICENSE\"},\"smithy/middleware-serde@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/middleware-serde\",\"licenseFile\":\"/node_modules/@smithy/middleware-serde/LICENSE\"},\"smithy/middleware-stack@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/middleware-stack\",\"licenseFile\":\"/node_modules/@smithy/middleware-stack/LICENSE\"},\"smithy/node-config-provider@3.1.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/node-config-provider\",\"licenseFile\":\"/node_modules/@smithy/node-config-provider/LICENSE\"},\"smithy/node-http-handler@3.1.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/node-http-handler\",\"licenseFile\":\"/node_modules/@smithy/node-http-handler/LICENSE\"},\"smithy/property-provider@3.1.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/property-provider\",\"licenseFile\":\"/node_modules/@smithy/property-provider/LICENSE\"},\"smithy/protocol-http@4.1.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS Smithy Team\",\"url\":\"https://smithy.io\",\"path\":\"/node_modules/@smithy/protocol-http\",\"licenseFile\":\"/node_modules/@smithy/protocol-http/LICENSE\"},\"smithy/querystring-builder@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/querystring-builder\",\"licenseFile\":\"/node_modules/@smithy/querystring-builder/LICENSE\"},\"smithy/querystring-parser@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/querystring-parser\",\"licenseFile\":\"/node_modules/@smithy/querystring-parser/LICENSE\"},\"smithy/service-error-classification@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/service-error-classification\",\"licenseFile\":\"/node_modules/@smithy/service-error-classification/LICENSE\"},\"smithy/shared-ini-file-loader@3.1.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/shared-ini-file-loader\",\"licenseFile\":\"/node_modules/@smithy/shared-ini-file-loader/LICENSE\"},\"smithy/signature-v4@4.1.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/signature-v4\",\"licenseFile\":\"/node_modules/@smithy/signature-v4/LICENSE\"},\"smithy/smithy-client@3.1.12\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/smithy-client\",\"licenseFile\":\"/node_modules/@smithy/smithy-client/LICENSE\"},\"smithy/types@3.3.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS Smithy Team\",\"url\":\"https://smithy.io\",\"path\":\"/node_modules/@smithy/types\",\"licenseFile\":\"/node_modules/@smithy/types/LICENSE\"},\"smithy/url-parser@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/url-parser\",\"licenseFile\":\"/node_modules/@smithy/url-parser/LICENSE\"},\"smithy/util-base64@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-base64\",\"licenseFile\":\"/node_modules/@smithy/util-base64/LICENSE\"},\"smithy/util-body-length-browser@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-body-length-browser\",\"licenseFile\":\"/node_modules/@smithy/util-body-length-browser/LICENSE\"},\"smithy/util-body-length-node@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-body-length-node\",\"licenseFile\":\"/node_modules/@smithy/util-body-length-node/LICENSE\"},\"smithy/util-buffer-from@2.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from\",\"licenseFile\":\"/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from/LICENSE\"},\"smithy/util-buffer-from@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-buffer-from\",\"licenseFile\":\"/node_modules/@smithy/util-buffer-from/LICENSE\"},\"smithy/util-config-provider@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-config-provider\",\"licenseFile\":\"/node_modules/@smithy/util-config-provider/LICENSE\"},\"smithy/util-defaults-mode-browser@3.0.14\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-defaults-mode-browser\",\"licenseFile\":\"/node_modules/@smithy/util-defaults-mode-browser/LICENSE\"},\"smithy/util-defaults-mode-node@3.0.14\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-defaults-mode-node\",\"licenseFile\":\"/node_modules/@smithy/util-defaults-mode-node/LICENSE\"},\"smithy/util-endpoints@2.0.5\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-endpoints\",\"licenseFile\":\"/node_modules/@smithy/util-endpoints/LICENSE\"},\"smithy/util-hex-encoding@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-hex-encoding\",\"licenseFile\":\"/node_modules/@smithy/util-hex-encoding/LICENSE\"},\"smithy/util-middleware@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-middleware\",\"licenseFile\":\"/node_modules/@smithy/util-middleware/LICENSE\"},\"smithy/util-retry@3.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-retry\",\"licenseFile\":\"/node_modules/@smithy/util-retry/LICENSE\"},\"smithy/util-stream@3.1.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-stream\",\"licenseFile\":\"/node_modules/@smithy/util-stream/LICENSE\"},\"smithy/util-uri-escape@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-uri-escape\",\"licenseFile\":\"/node_modules/@smithy/util-uri-escape/LICENSE\"},\"smithy/util-utf8@2.3.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8\",\"licenseFile\":\"/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8/LICENSE\"},\"smithy/util-utf8@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-utf8\",\"licenseFile\":\"/node_modules/@smithy/util-utf8/LICENSE\"},\"smithy/util-waiter@3.1.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/awslabs/smithy-typescript\",\"publisher\":\"AWS SDK for JavaScript Team\",\"url\":\"https://aws.amazon.com/javascript/\",\"path\":\"/node_modules/@smithy/util-waiter\",\"licenseFile\":\"/node_modules/@smithy/util-waiter/LICENSE\"},\"szmarczak/http-timer@4.0.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/szmarczak/http-timer\",\"publisher\":\"Szymon Marczak\",\"path\":\"/node_modules/@szmarczak/http-timer\",\"licenseFile\":\"/node_modules/@szmarczak/http-timer/LICENSE\"},\"tokenizer/token@0.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Borewit/tokenizer-token\",\"publisher\":\"Borewit\",\"url\":\"https://github.com/Borewit\",\"path\":\"/node_modules/@tokenizer/token\",\"licenseFile\":\"/node_modules/@tokenizer/token/README.md\"},\"tootallnate/once@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/TooTallNate/once\",\"publisher\":\"Nathan Rajlich\",\"email\":\"nathan@tootallnate.net\",\"url\":\"http://n8.io/\",\"path\":\"/node_modules/@tootallnate/once\",\"licenseFile\":\"/node_modules/@tootallnate/once/LICENSE\"},\"types/cacheable-request@6.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/cacheable-request\",\"licenseFile\":\"/node_modules/@types/cacheable-request/LICENSE\"},\"types/codemirror@5.60.15\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/codemirror\",\"licenseFile\":\"/node_modules/@types/codemirror/LICENSE\"},\"types/estree@1.0.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/estree\",\"licenseFile\":\"/node_modules/@types/estree/LICENSE\"},\"types/http-cache-semantics@4.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/http-cache-semantics\",\"licenseFile\":\"/node_modules/@types/http-cache-semantics/LICENSE\"},\"types/keyv@3.1.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/keyv\",\"licenseFile\":\"/node_modules/@types/keyv/LICENSE\"},\"types/marked@4.3.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/marked\",\"licenseFile\":\"/node_modules/@types/marked/LICENSE\"},\"types/node@16.9.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/image-q/node_modules/@types/node\",\"licenseFile\":\"/node_modules/image-q/node_modules/@types/node/LICENSE\"},\"types/node@22.17.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/node\",\"licenseFile\":\"/node_modules/@types/node/LICENSE\"},\"types/responselike@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/responselike\",\"licenseFile\":\"/node_modules/@types/responselike/LICENSE\"},\"types/tern@0.23.9\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/tern\",\"licenseFile\":\"/node_modules/@types/tern/LICENSE\"},\"types/trusted-types@2.0.7\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/DefinitelyTyped/DefinitelyTyped\",\"path\":\"/node_modules/@types/trusted-types\",\"licenseFile\":\"/node_modules/@types/trusted-types/LICENSE\"},\"abbrev@2.0.0\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/npm/abbrev-js\",\"publisher\":\"GitHub Inc.\",\"path\":\"/node_modules/abbrev\",\"licenseFile\":\"/node_modules/abbrev/LICENSE\"},\"abort-controller@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mysticatea/abort-controller\",\"publisher\":\"Toru Nagashima\",\"url\":\"https://github.com/mysticatea\",\"path\":\"/node_modules/abort-controller\",\"licenseFile\":\"/node_modules/abort-controller/LICENSE\"},\"adm-zip@0.5.10\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/cthackers/adm-zip\",\"publisher\":\"Nasca Iacob\",\"email\":\"sy@another-d-mention.ro\",\"url\":\"https://github.com/cthackers\",\"path\":\"/node_modules/adm-zip\",\"licenseFile\":\"/node_modules/adm-zip/LICENSE\"},\"agent-base@6.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/TooTallNate/node-agent-base\",\"publisher\":\"Nathan Rajlich\",\"email\":\"nathan@tootallnate.net\",\"url\":\"http://n8.io/\",\"path\":\"/node_modules/agent-base\",\"licenseFile\":\"/node_modules/agent-base/README.md\"},\"ansi-diff-stream@1.2.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/ansi-diff-stream\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/ansi-diff-stream\",\"licenseFile\":\"/node_modules/ansi-diff-stream/LICENSE\"},\"ansi-regex@2.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/ansi-regex\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/ansi-regex\",\"licenseFile\":\"/node_modules/ansi-regex/license\"},\"ansi-regex@3.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/ansi-regex\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/strip-ansi/node_modules/ansi-regex\",\"licenseFile\":\"/node_modules/strip-ansi/node_modules/ansi-regex/license\"},\"ansi-regex@5.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/ansi-regex\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/string-width-cjs/node_modules/ansi-regex\",\"licenseFile\":\"/node_modules/string-width-cjs/node_modules/ansi-regex/license\"},\"ansi-regex@6.2.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/ansi-regex\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/@isaacs/cliui/node_modules/ansi-regex\",\"licenseFile\":\"/node_modules/@isaacs/cliui/node_modules/ansi-regex/license\"},\"ansi-styles@4.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/ansi-styles\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/ansi-styles\",\"licenseFile\":\"/node_modules/ansi-styles/license\"},\"ansi-styles@6.2.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/ansi-styles\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/@isaacs/cliui/node_modules/ansi-styles\",\"licenseFile\":\"/node_modules/@isaacs/cliui/node_modules/ansi-styles/license\"},\"any-base@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/HarasimowiczKamil/any-base\",\"publisher\":\"Kamil Harasimowicz\",\"email\":\"mifczu@gmail.com\",\"path\":\"/node_modules/any-base\",\"licenseFile\":\"/node_modules/any-base/LICENSE\"},\"archiver-utils@2.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/archiverjs/archiver-utils\",\"publisher\":\"Chris Talkington\",\"url\":\"http://christalkington.com/\",\"path\":\"/node_modules/archiver-utils\",\"licenseFile\":\"/node_modules/archiver-utils/LICENSE\"},\"archiver-utils@3.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/archiverjs/archiver-utils\",\"publisher\":\"Chris Talkington\",\"url\":\"http://christalkington.com/\",\"path\":\"/node_modules/zip-stream/node_modules/archiver-utils\",\"licenseFile\":\"/node_modules/zip-stream/node_modules/archiver-utils/LICENSE\"},\"archiver@5.3.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/archiverjs/node-archiver\",\"publisher\":\"Chris Talkington\",\"url\":\"http://christalkington.com/\",\"path\":\"/node_modules/archiver\",\"licenseFile\":\"/node_modules/archiver/LICENSE\"},\"array-find-index@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/array-find-index\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/array-find-index\",\"licenseFile\":\"/node_modules/array-find-index/license\"},\"arrify@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/arrify\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/arrify\",\"licenseFile\":\"/node_modules/arrify/license\"},\"asn1@0.2.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/joyent/node-asn1\",\"publisher\":\"Joyent\",\"url\":\"joyent.com\",\"path\":\"/node_modules/asn1\",\"licenseFile\":\"/node_modules/asn1/LICENSE\"},\"async-lock@1.4.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/rogierschouten/async-lock\",\"publisher\":\"Rogier Schouten\",\"email\":\"github@workingcode.ninja\",\"url\":\"https://github.com/rogierschouten/\",\"path\":\"/node_modules/async-lock\",\"licenseFile\":\"/node_modules/async-lock/LICENSE\"},\"async-retry@1.3.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/vercel/async-retry\",\"path\":\"/node_modules/async-retry\",\"licenseFile\":\"/node_modules/async-retry/LICENSE.md\"},\"async@3.2.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/caolan/async\",\"publisher\":\"Caolan McMahon\",\"path\":\"/node_modules/async\",\"licenseFile\":\"/node_modules/async/LICENSE\"},\"asynckit@0.4.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/alexindigo/asynckit\",\"publisher\":\"Alex Indigo\",\"email\":\"iam@alexindigo.com\",\"path\":\"/node_modules/asynckit\",\"licenseFile\":\"/node_modules/asynckit/LICENSE\"},\"available-typed-arrays@1.0.7\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/available-typed-arrays\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/available-typed-arrays\",\"licenseFile\":\"/node_modules/available-typed-arrays/LICENSE\"},\"b4a@1.6.6\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/b4a\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/b4a\",\"licenseFile\":\"/node_modules/b4a/LICENSE\"},\"balanced-match@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/juliangruber/balanced-match\",\"publisher\":\"Julian Gruber\",\"email\":\"mail@juliangruber.com\",\"url\":\"http://juliangruber.com\",\"path\":\"/node_modules/balanced-match\",\"licenseFile\":\"/node_modules/balanced-match/LICENSE.md\"},\"bare-events@2.6.1\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/bare-events\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/bare-events\",\"licenseFile\":\"/node_modules/bare-events/LICENSE\"},\"bare-fs@4.4.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/bare-fs\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/bare-fs\",\"licenseFile\":\"/node_modules/bare-fs/LICENSE\"},\"bare-os@3.6.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/bare-os\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/bare-os\",\"licenseFile\":\"/node_modules/bare-os/LICENSE\"},\"bare-path@3.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/bare-path\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/bare-path\",\"licenseFile\":\"/node_modules/bare-path/LICENSE\",\"noticeFile\":\"/node_modules/bare-path/NOTICE\"},\"bare-stream@2.7.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/bare-stream\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/bare-stream\",\"licenseFile\":\"/node_modules/bare-stream/LICENSE\"},\"bare-url@2.2.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/bare-url\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/bare-url\",\"licenseFile\":\"/node_modules/bare-url/LICENSE\"},\"base64-js@1.5.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/beatgammit/base64-js\",\"publisher\":\"T. Jameson Little\",\"email\":\"t.jameson.little@gmail.com\",\"path\":\"/node_modules/base64-js\",\"licenseFile\":\"/node_modules/base64-js/LICENSE\"},\"basic-ftp@5.0.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/patrickjuchli/basic-ftp\",\"publisher\":\"Patrick Juchli\",\"email\":\"patrickjuchli@gmail.com\",\"path\":\"/node_modules/basic-ftp\",\"licenseFile\":\"/node_modules/basic-ftp/LICENSE.txt\"},\"bcrypt-pbkdf@1.0.2\":{\"licenses\":\"BSD-3-Clause\",\"repository\":\"https://github.com/joyent/node-bcrypt-pbkdf\",\"path\":\"/node_modules/bcrypt-pbkdf\",\"licenseFile\":\"/node_modules/bcrypt-pbkdf/LICENSE\"},\"before-after-hook@4.0.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/gr2m/before-after-hook\",\"publisher\":\"Gregor Martynus\",\"path\":\"/node_modules/before-after-hook\",\"licenseFile\":\"/node_modules/before-after-hook/LICENSE\"},\"better-sqlite3@12.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/WiseLibs/better-sqlite3\",\"publisher\":\"Joshua Wise\",\"email\":\"joshuathomaswise@gmail.com\",\"path\":\"/node_modules/better-sqlite3\",\"licenseFile\":\"/node_modules/better-sqlite3/LICENSE\"},\"bignumber.js@9.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/MikeMcl/bignumber.js\",\"publisher\":\"Michael Mclaughlin\",\"email\":\"M8ch88l@gmail.com\",\"path\":\"/node_modules/bignumber.js\",\"licenseFile\":\"/node_modules/bignumber.js/LICENCE.md\"},\"bindings@1.5.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/TooTallNate/node-bindings\",\"publisher\":\"Nathan Rajlich\",\"email\":\"nathan@tootallnate.net\",\"url\":\"http://tootallnate.net\",\"path\":\"/node_modules/bindings\",\"licenseFile\":\"/node_modules/bindings/LICENSE.md\"},\"bl@4.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/rvagg/bl\",\"path\":\"/node_modules/bl\",\"licenseFile\":\"/node_modules/bl/LICENSE.md\"},\"bmp-js@0.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/shaozilee/bmp-js\",\"publisher\":\"shaozilee\",\"email\":\"shaozilee@gmail.com\",\"path\":\"/node_modules/bmp-js\",\"licenseFile\":\"/node_modules/bmp-js/LICENSE\"},\"bowser@2.11.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lancedikson/bowser\",\"publisher\":\"Dustin Diaz\",\"email\":\"dustin@dustindiaz.com\",\"url\":\"http://dustindiaz.com\",\"path\":\"/node_modules/bowser\",\"licenseFile\":\"/node_modules/bowser/LICENSE\"},\"brace-expansion@1.1.11\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/juliangruber/brace-expansion\",\"publisher\":\"Julian Gruber\",\"email\":\"mail@juliangruber.com\",\"url\":\"http://juliangruber.com\",\"path\":\"/node_modules/brace-expansion\",\"licenseFile\":\"/node_modules/brace-expansion/LICENSE\"},\"brace-expansion@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/juliangruber/brace-expansion\",\"publisher\":\"Julian Gruber\",\"email\":\"mail@juliangruber.com\",\"url\":\"http://juliangruber.com\",\"path\":\"/node_modules/readdir-glob/node_modules/brace-expansion\",\"licenseFile\":\"/node_modules/readdir-glob/node_modules/brace-expansion/LICENSE\"},\"brace-expansion@2.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/juliangruber/brace-expansion\",\"publisher\":\"Julian Gruber\",\"email\":\"mail@juliangruber.com\",\"url\":\"http://juliangruber.com\",\"path\":\"/node_modules/editorconfig/node_modules/brace-expansion\",\"licenseFile\":\"/node_modules/editorconfig/node_modules/brace-expansion/LICENSE\"},\"buffer-crc32@0.2.13\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/brianloveswords/buffer-crc32\",\"publisher\":\"Brian J. Brennan\",\"email\":\"brianloveswords@gmail.com\",\"path\":\"/node_modules/buffer-crc32\",\"licenseFile\":\"/node_modules/buffer-crc32/LICENSE\"},\"buffer-equal-constant-time@1.0.1\":{\"licenses\":\"BSD-3-Clause\",\"repository\":\"https://github.com/goinstant/buffer-equal-constant-time\",\"publisher\":\"GoInstant Inc., a salesforce.com company\",\"path\":\"/node_modules/buffer-equal-constant-time\",\"licenseFile\":\"/node_modules/buffer-equal-constant-time/LICENSE.txt\"},\"buffer-equal@0.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/substack/node-buffer-equal\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/buffer-equal\",\"licenseFile\":\"/node_modules/buffer-equal/README.markdown\"},\"buffer-from@1.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/LinusU/buffer-from\",\"path\":\"/node_modules/buffer-from\",\"licenseFile\":\"/node_modules/buffer-from/LICENSE\"},\"buffer@5.7.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/feross/buffer\",\"publisher\":\"Feross Aboukhadijeh\",\"email\":\"feross@feross.org\",\"url\":\"https://feross.org\",\"path\":\"/node_modules/buffer\",\"licenseFile\":\"/node_modules/buffer/LICENSE\"},\"buffer@6.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/feross/buffer\",\"publisher\":\"Feross Aboukhadijeh\",\"email\":\"feross@feross.org\",\"url\":\"https://feross.org\",\"path\":\"/node_modules/readable-web-to-node-stream/node_modules/buffer\",\"licenseFile\":\"/node_modules/readable-web-to-node-stream/node_modules/buffer/LICENSE\"},\"cacheable-lookup@5.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/szmarczak/cacheable-lookup\",\"publisher\":\"Szymon Marczak\",\"path\":\"/node_modules/cacheable-lookup\",\"licenseFile\":\"/node_modules/cacheable-lookup/LICENSE\"},\"cacheable-request@7.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lukechilds/cacheable-request\",\"publisher\":\"Luke Childs\",\"email\":\"lukechilds123@gmail.com\",\"url\":\"http://lukechilds.co.uk\",\"path\":\"/node_modules/cacheable-request\",\"licenseFile\":\"/node_modules/cacheable-request/LICENSE\"},\"call-bind-apply-helpers@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/call-bind-apply-helpers\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/call-bind-apply-helpers\",\"licenseFile\":\"/node_modules/call-bind-apply-helpers/LICENSE\"},\"call-bind@1.0.8\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/call-bind\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/call-bind\",\"licenseFile\":\"/node_modules/call-bind/LICENSE\"},\"call-bound@1.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/call-bound\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/call-bound\",\"licenseFile\":\"/node_modules/call-bound/LICENSE\"},\"camel-case@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/blakeembrey/camel-case\",\"publisher\":\"Blake Embrey\",\"email\":\"hello@blakeembrey.com\",\"url\":\"http://blakeembrey.me\",\"path\":\"/node_modules/camel-case\",\"licenseFile\":\"/node_modules/camel-case/LICENSE\"},\"camelcase-keys@2.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/camelcase-keys\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"http://sindresorhus.com\",\"path\":\"/node_modules/camelcase-keys\",\"licenseFile\":\"/node_modules/camelcase-keys/license\"},\"camelcase@2.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/camelcase\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"http://sindresorhus.com\",\"path\":\"/node_modules/camelcase\",\"licenseFile\":\"/node_modules/camelcase/license\"},\"centra@2.7.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ethanent/centra\",\"publisher\":\"Ethan Davis\",\"path\":\"/node_modules/centra\",\"licenseFile\":\"/node_modules/centra/LICENSE\"},\"chownr@1.1.4\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/chownr\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/chownr\",\"licenseFile\":\"/node_modules/chownr/LICENSE\"},\"clean-css@4.2.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jakubpawlowicz/clean-css\",\"publisher\":\"Jakub Pawlowicz\",\"email\":\"contact@jakubpawlowicz.com\",\"url\":\"http://twitter.com/jakubpawlowicz\",\"path\":\"/node_modules/html-minifier/node_modules/clean-css\",\"licenseFile\":\"/node_modules/html-minifier/node_modules/clean-css/LICENSE\"},\"clean-css@5.3.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/clean-css/clean-css\",\"publisher\":\"Jakub Pawlowicz\",\"email\":\"contact@jakubpawlowicz.com\",\"path\":\"/node_modules/clean-css\",\"licenseFile\":\"/node_modules/clean-css/LICENSE\"},\"clean-git-ref@2.0.1\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/TheSavior/clean-git-ref\",\"publisher\":\"Eli White\",\"email\":\"github@eli-white.com\",\"path\":\"/node_modules/clean-git-ref\",\"licenseFile\":\"/node_modules/clean-git-ref/README.md\"},\"cliui@8.0.1\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/yargs/cliui\",\"publisher\":\"Ben Coe\",\"email\":\"ben@npmjs.com\",\"path\":\"/node_modules/cliui\",\"licenseFile\":\"/node_modules/cliui/LICENSE.txt\"},\"clone-response@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/clone-response\",\"publisher\":\"Luke Childs\",\"email\":\"lukechilds123@gmail.com\",\"url\":\"http://lukechilds.co.uk\",\"path\":\"/node_modules/clone-response\",\"licenseFile\":\"/node_modules/clone-response/LICENSE\"},\"codemirror-advanceddialog@1.1.9\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Maloric/CodeMirror-AdvancedDialog\",\"publisher\":\"Jamie Morris\",\"path\":\"/node_modules/codemirror-advanceddialog\",\"licenseFile\":\"/node_modules/codemirror-advanceddialog/LICENSE\"},\"codemirror-revisedsearch@1.0.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Maloric/CodeMirror-RevisedSearch\",\"publisher\":\"Jamie Morris\",\"path\":\"/node_modules/codemirror-revisedsearch\",\"licenseFile\":\"/node_modules/codemirror-revisedsearch/LICENSE\"},\"codemirror-spell-checker@1.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/NextStepWebs/codemirror-spell-checker\",\"publisher\":\"Wes Cossick\",\"url\":\"http://www.WesCossick.com\",\"path\":\"/node_modules/codemirror-spell-checker\",\"licenseFile\":\"/node_modules/codemirror-spell-checker/LICENSE\"},\"codemirror@5.65.13\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/codemirror/CodeMirror\",\"publisher\":\"Marijn Haverbeke\",\"email\":\"marijn@haverbeke.berlin\",\"url\":\"http://marijnhaverbeke.nl\",\"path\":\"/node_modules/codemirror\",\"licenseFile\":\"/node_modules/codemirror/LICENSE\"},\"color-convert@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Qix-/color-convert\",\"publisher\":\"Heather Arthur\",\"email\":\"fayearthur@gmail.com\",\"path\":\"/node_modules/color-convert\",\"licenseFile\":\"/node_modules/color-convert/LICENSE\"},\"color-name@1.1.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/colorjs/color-name\",\"publisher\":\"DY\",\"email\":\"dfcreative@gmail.com\",\"path\":\"/node_modules/color-name\",\"licenseFile\":\"/node_modules/color-name/LICENSE\"},\"color-string@1.9.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Qix-/color-string\",\"publisher\":\"Heather Arthur\",\"email\":\"fayearthur@gmail.com\",\"path\":\"/node_modules/color-string\",\"licenseFile\":\"/node_modules/color-string/LICENSE\"},\"color@4.2.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Qix-/color\",\"path\":\"/node_modules/color\",\"licenseFile\":\"/node_modules/color/LICENSE\"},\"colors@1.4.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Marak/colors.js\",\"publisher\":\"Marak Squires\",\"path\":\"/node_modules/colors\",\"licenseFile\":\"/node_modules/colors/LICENSE\"},\"combined-stream@1.0.8\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/felixge/node-combined-stream\",\"publisher\":\"Felix Geisendörfer\",\"email\":\"felix@debuggable.com\",\"url\":\"http://debuggable.com/\",\"path\":\"/node_modules/combined-stream\",\"licenseFile\":\"/node_modules/combined-stream/License\"},\"commander@10.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/tj/commander.js\",\"publisher\":\"TJ Holowaychuk\",\"email\":\"tj@vision-media.ca\",\"path\":\"/node_modules/editorconfig/node_modules/commander\",\"licenseFile\":\"/node_modules/editorconfig/node_modules/commander/LICENSE\"},\"commander@2.20.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/tj/commander.js\",\"publisher\":\"TJ Holowaychuk\",\"email\":\"tj@vision-media.ca\",\"path\":\"/node_modules/commander\",\"licenseFile\":\"/node_modules/commander/LICENSE\"},\"component-props@1.1.1\":{\"licenses\":\"MIT*\",\"repository\":\"https://github.com/component/props\",\"path\":\"/node_modules/component-props\",\"licenseFile\":\"/node_modules/component-props/Readme.md\"},\"component-xor@0.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/component/xor\",\"publisher\":\"Matthew Mueller\",\"path\":\"/node_modules/component-xor\",\"licenseFile\":\"/node_modules/component-xor/Readme.md\"},\"compress-commons@4.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/archiverjs/node-compress-commons\",\"publisher\":\"Chris Talkington\",\"url\":\"http://christalkington.com/\",\"path\":\"/node_modules/compress-commons\",\"licenseFile\":\"/node_modules/compress-commons/LICENSE\"},\"compressible@2.0.18\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jshttp/compressible\",\"path\":\"/node_modules/compressible\",\"licenseFile\":\"/node_modules/compressible/LICENSE\"},\"concat-map@0.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/substack/node-concat-map\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/concat-map\",\"licenseFile\":\"/node_modules/concat-map/LICENSE\"},\"concat-stream@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/maxogden/concat-stream\",\"publisher\":\"Max Ogden\",\"email\":\"max@maxogden.com\",\"path\":\"/node_modules/concat-stream\",\"licenseFile\":\"/node_modules/concat-stream/LICENSE\"},\"config-chain@1.1.13\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/dominictarr/config-chain\",\"publisher\":\"Dominic Tarr\",\"email\":\"dominic.tarr@gmail.com\",\"url\":\"http://dominictarr.com\",\"path\":\"/node_modules/config-chain\",\"licenseFile\":\"/node_modules/config-chain/LICENCE\"},\"core-util-is@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/isaacs/core-util-is\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/core-util-is\",\"licenseFile\":\"/node_modules/core-util-is/LICENSE\"},\"count-files@2.6.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/joehand/count-files\",\"publisher\":\"Joe Hand\",\"email\":\"joe@joeahand.com\",\"url\":\"http://joeahand.com/\",\"path\":\"/node_modules/count-files\",\"licenseFile\":\"/node_modules/count-files/readme.md\"},\"crc-32@1.2.2\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/SheetJS/js-crc32\",\"publisher\":\"sheetjs\",\"path\":\"/node_modules/crc-32\",\"licenseFile\":\"/node_modules/crc-32/LICENSE\"},\"crc32-stream@4.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/archiverjs/node-crc32-stream\",\"publisher\":\"Chris Talkington\",\"url\":\"http://christalkington.com/\",\"path\":\"/node_modules/crc32-stream\",\"licenseFile\":\"/node_modules/crc32-stream/LICENSE\"},\"cross-spawn@7.0.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/moxystudio/node-cross-spawn\",\"publisher\":\"André Cruz\",\"email\":\"andre@moxy.studio\",\"path\":\"/node_modules/cross-spawn\",\"licenseFile\":\"/node_modules/cross-spawn/LICENSE\"},\"currently-unhandled@0.4.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jamestalmage/currently-unhandled\",\"publisher\":\"James Talmage\",\"email\":\"james@talmage.io\",\"url\":\"github.com/jamestalmage\",\"path\":\"/node_modules/currently-unhandled\",\"licenseFile\":\"/node_modules/currently-unhandled/license\"},\"debug@4.3.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/debug-js/debug\",\"publisher\":\"Josh Junon\",\"email\":\"josh.junon@protonmail.com\",\"path\":\"/node_modules/debug\",\"licenseFile\":\"/node_modules/debug/LICENSE\"},\"decamelize@1.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/decamelize\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/decamelize\",\"licenseFile\":\"/node_modules/decamelize/license\"},\"decode-uri-component@0.2.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/SamVerschueren/decode-uri-component\",\"publisher\":\"Sam Verschueren\",\"email\":\"sam.verschueren@gmail.com\",\"url\":\"github.com/SamVerschueren\",\"path\":\"/node_modules/decode-uri-component\",\"licenseFile\":\"/node_modules/decode-uri-component/license\"},\"decompress-response@6.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/decompress-response\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/decompress-response\",\"licenseFile\":\"/node_modules/decompress-response/license\"},\"deep-extend@0.6.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/unclechu/node-deep-extend\",\"publisher\":\"Viacheslav Lotsmanov\",\"email\":\"lotsmanov89@gmail.com\",\"path\":\"/node_modules/deep-extend\",\"licenseFile\":\"/node_modules/deep-extend/LICENSE\"},\"defer-to-connect@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/szmarczak/defer-to-connect\",\"publisher\":\"Szymon Marczak\",\"path\":\"/node_modules/defer-to-connect\",\"licenseFile\":\"/node_modules/defer-to-connect/LICENSE\"},\"define-data-property@1.1.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/define-data-property\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/define-data-property\",\"licenseFile\":\"/node_modules/define-data-property/LICENSE\"},\"delay@5.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/delay\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/delay\",\"licenseFile\":\"/node_modules/delay/license\"},\"delayed-stream@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/felixge/node-delayed-stream\",\"publisher\":\"Felix Geisendörfer\",\"email\":\"felix@debuggable.com\",\"url\":\"http://debuggable.com/\",\"path\":\"/node_modules/delayed-stream\",\"licenseFile\":\"/node_modules/delayed-stream/License\"},\"detect-libc@2.0.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/lovell/detect-libc\",\"publisher\":\"Lovell Fuller\",\"email\":\"npm@lovell.info\",\"path\":\"/node_modules/detect-libc\",\"licenseFile\":\"/node_modules/detect-libc/LICENSE\"},\"diff3@0.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/axosoft/diff3\",\"publisher\":\"Tyler Wanek\",\"path\":\"/node_modules/diff3\",\"licenseFile\":\"/node_modules/diff3/README.md\"},\"dom-iterator@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/MatthewMueller/dom-iterator\",\"publisher\":\"Matthew Mueller\",\"path\":\"/node_modules/dom-iterator\",\"licenseFile\":\"/node_modules/dom-iterator/Readme.md\"},\"dom-walk@0.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Raynos/dom-walk\",\"publisher\":\"Raynos\",\"email\":\"raynos2@gmail.com\",\"path\":\"/node_modules/dom-walk\",\"licenseFile\":\"/node_modules/dom-walk/LICENCE\"},\"dompurify@3.2.6\":{\"licenses\":\"(MPL-2.0 OR Apache-2.0)\",\"repository\":\"https://github.com/cure53/DOMPurify\",\"publisher\":\"Dr.-Ing. Mario Heiderich, Cure53\",\"email\":\"mario@cure53.de\",\"url\":\"https://cure53.de/\",\"path\":\"/node_modules/dompurify\",\"licenseFile\":\"/node_modules/dompurify/LICENSE\"},\"dunder-proto@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/es-shims/dunder-proto\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/dunder-proto\",\"licenseFile\":\"/node_modules/dunder-proto/LICENSE\"},\"duplexify@4.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/duplexify\",\"publisher\":\"Mathias Buus\",\"path\":\"/node_modules/duplexify\",\"licenseFile\":\"/node_modules/duplexify/LICENSE\"},\"eastasianwidth@0.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/komagata/eastasianwidth\",\"publisher\":\"Masaki Komagata\",\"path\":\"/node_modules/eastasianwidth\",\"licenseFile\":\"/node_modules/eastasianwidth/README.md\"},\"easymde@2.18.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Ionaru/easy-markdown-editor\",\"publisher\":\"Jeroen Akkerman\",\"path\":\"/node_modules/easymde\",\"licenseFile\":\"/node_modules/easymde/LICENSE\"},\"ecdsa-sig-formatter@1.0.11\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/Brightspace/node-ecdsa-sig-formatter\",\"publisher\":\"D2L Corporation\",\"path\":\"/node_modules/ecdsa-sig-formatter\",\"licenseFile\":\"/node_modules/ecdsa-sig-formatter/LICENSE\"},\"editorconfig@1.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/editorconfig/editorconfig-core-js\",\"publisher\":\"EditorConfig Team\",\"path\":\"/node_modules/editorconfig\",\"licenseFile\":\"/node_modules/editorconfig/LICENSE\"},\"emoji-regex@8.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mathiasbynens/emoji-regex\",\"publisher\":\"Mathias Bynens\",\"url\":\"https://mathiasbynens.be/\",\"path\":\"/node_modules/emoji-regex\",\"licenseFile\":\"/node_modules/emoji-regex/LICENSE-MIT.txt\"},\"emoji-regex@9.2.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mathiasbynens/emoji-regex\",\"publisher\":\"Mathias Bynens\",\"url\":\"https://mathiasbynens.be/\",\"path\":\"/node_modules/@isaacs/cliui/node_modules/emoji-regex\",\"licenseFile\":\"/node_modules/@isaacs/cliui/node_modules/emoji-regex/LICENSE-MIT.txt\"},\"end-of-stream@1.4.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/end-of-stream\",\"publisher\":\"Mathias Buus\",\"email\":\"mathiasbuus@gmail.com\",\"path\":\"/node_modules/end-of-stream\",\"licenseFile\":\"/node_modules/end-of-stream/LICENSE\"},\"ent@2.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/substack/node-ent\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/ent\",\"licenseFile\":\"/node_modules/ent/LICENSE\"},\"err-code@2.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/IndigoUnited/js-err-code\",\"publisher\":\"IndigoUnited\",\"email\":\"hello@indigounited.com\",\"url\":\"http://indigounited.com\",\"path\":\"/node_modules/err-code\",\"licenseFile\":\"/node_modules/err-code/README.md\"},\"error-ex@1.3.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/qix-/node-error-ex\",\"path\":\"/node_modules/error-ex\",\"licenseFile\":\"/node_modules/error-ex/LICENSE\"},\"es-define-property@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/es-define-property\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/es-define-property\",\"licenseFile\":\"/node_modules/es-define-property/LICENSE\"},\"es-errors@1.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/es-errors\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/es-errors\",\"licenseFile\":\"/node_modules/es-errors/LICENSE\"},\"es-object-atoms@1.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/es-object-atoms\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/es-object-atoms\",\"licenseFile\":\"/node_modules/es-object-atoms/LICENSE\"},\"es-set-tostringtag@2.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/es-shims/es-set-tostringtag\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/es-set-tostringtag\",\"licenseFile\":\"/node_modules/es-set-tostringtag/LICENSE\"},\"escalade@3.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lukeed/escalade\",\"publisher\":\"Luke Edwards\",\"email\":\"luke.edwards05@gmail.com\",\"url\":\"https://lukeed.com\",\"path\":\"/node_modules/escalade\",\"licenseFile\":\"/node_modules/escalade/license\"},\"escape-html@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/component/escape-html\",\"path\":\"/node_modules/escape-html\",\"licenseFile\":\"/node_modules/escape-html/LICENSE\"},\"event-target-shim@5.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mysticatea/event-target-shim\",\"publisher\":\"Toru Nagashima\",\"path\":\"/node_modules/event-target-shim\",\"licenseFile\":\"/node_modules/event-target-shim/LICENSE\"},\"events@3.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Gozala/events\",\"publisher\":\"Irakli Gozalishvili\",\"email\":\"rfobic@gmail.com\",\"url\":\"http://jeditoolkit.com\",\"path\":\"/node_modules/events\",\"licenseFile\":\"/node_modules/events/LICENSE\"},\"exif-parser@0.1.12\":{\"licenses\":\"MIT*\",\"repository\":\"https://github.com/bwindels/exif-parser\",\"publisher\":\"Bruno Windels\",\"email\":\"bruno.windels@gmail.com\",\"path\":\"/node_modules/exif-parser\",\"licenseFile\":\"/node_modules/exif-parser/LICENSE.md\"},\"expand-template@2.0.3\":{\"licenses\":\"(MIT OR WTFPL)\",\"repository\":\"https://github.com/ralphtheninja/expand-template\",\"publisher\":\"LM\",\"email\":\"ralphtheninja@riseup.net\",\"path\":\"/node_modules/expand-template\",\"licenseFile\":\"/node_modules/expand-template/LICENSE\"},\"extend-shallow@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jonschlinkert/extend-shallow\",\"publisher\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\",\"path\":\"/node_modules/extend-shallow\",\"licenseFile\":\"/node_modules/extend-shallow/LICENSE\"},\"extend@3.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/justmoon/node-extend\",\"publisher\":\"Stefan Thomas\",\"email\":\"justmoon@members.fsf.org\",\"url\":\"http://www.justmoon.net\",\"path\":\"/node_modules/extend\",\"licenseFile\":\"/node_modules/extend/LICENSE\"},\"fast-content-type-parse@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/fastify/fast-content-type-parse\",\"publisher\":\"Aras Abbasi\",\"email\":\"aras.abbasi@gmail.com\",\"path\":\"/node_modules/fast-content-type-parse\",\"licenseFile\":\"/node_modules/fast-content-type-parse/LICENSE\"},\"fast-fifo@1.3.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/fast-fifo\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/fast-fifo\",\"licenseFile\":\"/node_modules/fast-fifo/LICENSE\"},\"fast-memoize@2.5.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/caiogondim/fast-memoize\",\"publisher\":\"Caio Gondim\",\"email\":\"me@caiogondim.com\",\"url\":\"http://caiogondim.com\",\"path\":\"/node_modules/fast-memoize\",\"licenseFile\":\"/node_modules/fast-memoize/LICENSE\"},\"fast-text-encoding@1.0.6\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/samthor/fast-text-encoding\",\"publisher\":\"Sam Thorogood\",\"email\":\"sam.thorogood@gmail.com\",\"path\":\"/node_modules/fast-text-encoding\",\"licenseFile\":\"/node_modules/fast-text-encoding/LICENSE\"},\"fast-xml-parser@4.4.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/NaturalIntelligence/fast-xml-parser\",\"publisher\":\"Amit Gupta\",\"url\":\"https://solothought.com\",\"path\":\"/node_modules/fast-xml-parser\",\"licenseFile\":\"/node_modules/fast-xml-parser/LICENSE\"},\"file-type@16.5.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/file-type\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/file-type\",\"licenseFile\":\"/node_modules/file-type/license\"},\"file-uri-to-path@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/TooTallNate/file-uri-to-path\",\"publisher\":\"Nathan Rajlich\",\"email\":\"nathan@tootallnate.net\",\"url\":\"http://n8.io/\",\"path\":\"/node_modules/file-uri-to-path\",\"licenseFile\":\"/node_modules/file-uri-to-path/LICENSE\"},\"filter-obj@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/filter-obj\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/filter-obj\",\"licenseFile\":\"/node_modules/filter-obj/license\"},\"find-up@1.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/find-up\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/find-up\",\"licenseFile\":\"/node_modules/find-up/license\"},\"follow-redirects@1.15.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/follow-redirects/follow-redirects\",\"publisher\":\"Ruben Verborgh\",\"email\":\"ruben@verborgh.org\",\"url\":\"https://ruben.verborgh.org/\",\"path\":\"/node_modules/image-downloader/node_modules/follow-redirects\",\"licenseFile\":\"/node_modules/image-downloader/node_modules/follow-redirects/LICENSE\"},\"follow-redirects@1.15.9\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/follow-redirects/follow-redirects\",\"publisher\":\"Ruben Verborgh\",\"email\":\"ruben@verborgh.org\",\"url\":\"https://ruben.verborgh.org/\",\"path\":\"/node_modules/centra/node_modules/follow-redirects\",\"licenseFile\":\"/node_modules/centra/node_modules/follow-redirects/LICENSE\"},\"for-each@0.3.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Raynos/for-each\",\"publisher\":\"Raynos\",\"email\":\"raynos2@gmail.com\",\"path\":\"/node_modules/for-each\",\"licenseFile\":\"/node_modules/for-each/LICENSE\"},\"foreground-child@3.3.1\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/tapjs/foreground-child\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/foreground-child\",\"licenseFile\":\"/node_modules/foreground-child/LICENSE\"},\"form-data@4.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/form-data/form-data\",\"publisher\":\"Felix Geisendörfer\",\"email\":\"felix@debuggable.com\",\"url\":\"http://debuggable.com/\",\"path\":\"/node_modules/form-data\",\"licenseFile\":\"/node_modules/form-data/License\"},\"fs-constants@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/fs-constants\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/fs-constants\",\"licenseFile\":\"/node_modules/fs-constants/LICENSE\"},\"fs-extra@11.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jprichardson/node-fs-extra\",\"publisher\":\"JP Richardson\",\"email\":\"jprichardson@gmail.com\",\"path\":\"/node_modules/fs-extra\",\"licenseFile\":\"/node_modules/fs-extra/LICENSE\"},\"fs.realpath@1.0.0\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/fs.realpath\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/fs.realpath\",\"licenseFile\":\"/node_modules/fs.realpath/LICENSE\"},\"ftp@0.3.10\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mscdex/node-ftp\",\"publisher\":\"Brian White\",\"email\":\"mscdex@mscdex.net\",\"path\":\"/node_modules/ftp\",\"licenseFile\":\"/node_modules/ftp/LICENSE\"},\"function-bind@1.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Raynos/function-bind\",\"publisher\":\"Raynos\",\"email\":\"raynos2@gmail.com\",\"path\":\"/node_modules/function-bind\",\"licenseFile\":\"/node_modules/function-bind/LICENSE\"},\"gaxios@5.1.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/gaxios\",\"publisher\":\"Google, LLC\",\"path\":\"/node_modules/gaxios\",\"licenseFile\":\"/node_modules/gaxios/LICENSE\"},\"gcp-metadata@5.3.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/gcp-metadata\",\"publisher\":\"Stephen Sawchuk\",\"path\":\"/node_modules/gcp-metadata\",\"licenseFile\":\"/node_modules/gcp-metadata/LICENSE\"},\"get-caller-file@2.0.5\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/stefanpenner/get-caller-file\",\"publisher\":\"Stefan Penner\",\"path\":\"/node_modules/get-caller-file\",\"licenseFile\":\"/node_modules/get-caller-file/LICENSE.md\"},\"get-intrinsic@1.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/get-intrinsic\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/get-intrinsic\",\"licenseFile\":\"/node_modules/get-intrinsic/LICENSE\"},\"get-proto@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/get-proto\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/get-proto\",\"licenseFile\":\"/node_modules/get-proto/LICENSE\"},\"get-stdin@4.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/get-stdin\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"http://sindresorhus.com\",\"path\":\"/node_modules/get-stdin\",\"licenseFile\":\"/node_modules/get-stdin/readme.md\"},\"get-stream@5.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/get-stream\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/get-stream\",\"licenseFile\":\"/node_modules/get-stream/license\"},\"gifwrap@0.10.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jtlapp/gifwrap\",\"publisher\":\"Joseph T. Lapp\",\"path\":\"/node_modules/gifwrap\",\"licenseFile\":\"/node_modules/gifwrap/LICENSE\"},\"github-from-package@0.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/substack/github-from-package\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/github-from-package\",\"licenseFile\":\"/node_modules/github-from-package/LICENSE\"},\"glob@10.4.5\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/node-glob\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"https://blog.izs.me/\",\"path\":\"/node_modules/js-beautify/node_modules/glob\",\"licenseFile\":\"/node_modules/js-beautify/node_modules/glob/LICENSE\"},\"glob@7.2.3\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/node-glob\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/glob\",\"licenseFile\":\"/node_modules/glob/LICENSE\"},\"global@4.4.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Raynos/global\",\"publisher\":\"Raynos\",\"email\":\"raynos2@gmail.com\",\"path\":\"/node_modules/global\",\"licenseFile\":\"/node_modules/global/LICENSE\"},\"google-auth-library@8.9.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/google-auth-library-nodejs\",\"publisher\":\"Google Inc.\",\"path\":\"/node_modules/google-auth-library\",\"licenseFile\":\"/node_modules/google-auth-library/LICENSE\"},\"google-p12-pem@4.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/googleapis/google-p12-pem\",\"publisher\":\"Ryan Seys\",\"path\":\"/node_modules/google-p12-pem\",\"licenseFile\":\"/node_modules/google-p12-pem/LICENSE\"},\"gopd@1.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/gopd\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/gopd\",\"licenseFile\":\"/node_modules/gopd/LICENSE\"},\"got@11.8.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/got\",\"path\":\"/node_modules/got\",\"licenseFile\":\"/node_modules/got/license\"},\"graceful-fs@4.2.11\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/node-graceful-fs\",\"path\":\"/node_modules/graceful-fs\",\"licenseFile\":\"/node_modules/graceful-fs/LICENSE\"},\"gtoken@6.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/google/node-gtoken\",\"publisher\":\"Google, LLC\",\"path\":\"/node_modules/gtoken\",\"licenseFile\":\"/node_modules/gtoken/LICENSE\"},\"handlebars@4.7.8\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/handlebars-lang/handlebars.js\",\"publisher\":\"Yehuda Katz\",\"path\":\"/node_modules/handlebars\",\"licenseFile\":\"/node_modules/handlebars/LICENSE\"},\"has-property-descriptors@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/has-property-descriptors\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/has-property-descriptors\",\"licenseFile\":\"/node_modules/has-property-descriptors/LICENSE\"},\"has-symbols@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/has-symbols\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"url\":\"http://ljharb.codes\",\"path\":\"/node_modules/has-symbols\",\"licenseFile\":\"/node_modules/has-symbols/LICENSE\"},\"has-tostringtag@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/has-tostringtag\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"url\":\"http://ljharb.codes\",\"path\":\"/node_modules/has-tostringtag\",\"licenseFile\":\"/node_modules/has-tostringtag/LICENSE\"},\"hasown@2.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/hasOwn\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/hasown\",\"licenseFile\":\"/node_modules/hasown/LICENSE\"},\"he@1.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mathiasbynens/he\",\"publisher\":\"Mathias Bynens\",\"url\":\"https://mathiasbynens.be/\",\"path\":\"/node_modules/he\",\"licenseFile\":\"/node_modules/he/LICENSE-MIT.txt\"},\"hosted-git-info@2.8.9\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/npm/hosted-git-info\",\"publisher\":\"Rebecca Turner\",\"email\":\"me@re-becca.org\",\"url\":\"http://re-becca.org\",\"path\":\"/node_modules/hosted-git-info\",\"licenseFile\":\"/node_modules/hosted-git-info/LICENSE\"},\"html-minifier@4.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/kangax/html-minifier\",\"publisher\":\"Juriy \\\"kangax\\\" Zaytsev\",\"path\":\"/node_modules/html-minifier\",\"licenseFile\":\"/node_modules/html-minifier/LICENSE\"},\"http-cache-semantics@4.1.1\":{\"licenses\":\"BSD-2-Clause\",\"repository\":\"https://github.com/kornelski/http-cache-semantics\",\"publisher\":\"Kornel Lesiński\",\"email\":\"kornel@geekhood.net\",\"url\":\"https://kornel.ski/\",\"path\":\"/node_modules/http-cache-semantics\",\"licenseFile\":\"/node_modules/http-cache-semantics/LICENSE\"},\"http-proxy-agent@5.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/TooTallNate/node-http-proxy-agent\",\"publisher\":\"Nathan Rajlich\",\"email\":\"nathan@tootallnate.net\",\"url\":\"http://n8.io/\",\"path\":\"/node_modules/http-proxy-agent\",\"licenseFile\":\"/node_modules/http-proxy-agent/README.md\"},\"http2-wrapper@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/szmarczak/http2-wrapper\",\"publisher\":\"Szymon Marczak\",\"path\":\"/node_modules/http2-wrapper\",\"licenseFile\":\"/node_modules/http2-wrapper/LICENSE\"},\"https-proxy-agent@5.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/TooTallNate/node-https-proxy-agent\",\"publisher\":\"Nathan Rajlich\",\"email\":\"nathan@tootallnate.net\",\"url\":\"http://n8.io/\",\"path\":\"/node_modules/https-proxy-agent\",\"licenseFile\":\"/node_modules/https-proxy-agent/README.md\"},\"ieee754@1.2.1\":{\"licenses\":\"BSD-3-Clause\",\"repository\":\"https://github.com/feross/ieee754\",\"publisher\":\"Feross Aboukhadijeh\",\"email\":\"feross@feross.org\",\"url\":\"https://feross.org\",\"path\":\"/node_modules/ieee754\",\"licenseFile\":\"/node_modules/ieee754/LICENSE\"},\"ignore@5.3.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/kaelzhang/node-ignore\",\"publisher\":\"kael\",\"path\":\"/node_modules/ignore\",\"licenseFile\":\"/node_modules/ignore/LICENSE-MIT\"},\"image-downloader@4.3.0\":{\"licenses\":\"MIT\",\"repository\":\"git+https://gitlab.com/demsking/image-downloader\",\"publisher\":\"Sébastien Demanou\",\"path\":\"/node_modules/image-downloader\",\"licenseFile\":\"/node_modules/image-downloader/LICENSE\"},\"image-q@4.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ibezkrovnyi/image-quantization\",\"path\":\"/node_modules/image-q\",\"licenseFile\":\"/node_modules/image-q/LICENSE\"},\"image-size@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/image-size/image-size\",\"publisher\":\"netroy\",\"email\":\"aditya@netroy.in\",\"url\":\"http://netroy.in/\",\"path\":\"/node_modules/image-size\",\"licenseFile\":\"/node_modules/image-size/LICENSE\"},\"indent-string@2.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/indent-string\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/indent-string\",\"licenseFile\":\"/node_modules/indent-string/license\"},\"inflight@1.0.6\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/npm/inflight\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/inflight\",\"licenseFile\":\"/node_modules/inflight/LICENSE\"},\"inherits@2.0.4\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/inherits\",\"path\":\"/node_modules/inherits\",\"licenseFile\":\"/node_modules/inherits/LICENSE\"},\"ini@1.3.8\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/ini\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/ini\",\"licenseFile\":\"/node_modules/ini/LICENSE\"},\"is-arrayish@0.2.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/qix-/node-is-arrayish\",\"publisher\":\"Qix\",\"url\":\"http://github.com/qix-\",\"path\":\"/node_modules/is-arrayish\",\"licenseFile\":\"/node_modules/is-arrayish/LICENSE\"},\"is-arrayish@0.3.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/qix-/node-is-arrayish\",\"publisher\":\"Qix\",\"url\":\"http://github.com/qix-\",\"path\":\"/node_modules/simple-swizzle/node_modules/is-arrayish\",\"licenseFile\":\"/node_modules/simple-swizzle/node_modules/is-arrayish/LICENSE\"},\"is-callable@1.2.7\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/is-callable\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"url\":\"http://ljharb.codes\",\"path\":\"/node_modules/is-callable\",\"licenseFile\":\"/node_modules/is-callable/LICENSE\"},\"is-core-module@2.13.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/is-core-module\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/is-core-module\",\"licenseFile\":\"/node_modules/is-core-module/LICENSE\"},\"is-extendable@0.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jonschlinkert/is-extendable\",\"publisher\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\",\"path\":\"/node_modules/is-extendable\",\"licenseFile\":\"/node_modules/is-extendable/LICENSE\"},\"is-finite@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/is-finite\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/is-finite\",\"licenseFile\":\"/node_modules/is-finite/license\"},\"is-fullwidth-code-point@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/is-fullwidth-code-point\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/is-fullwidth-code-point\",\"licenseFile\":\"/node_modules/is-fullwidth-code-point/license\"},\"is-fullwidth-code-point@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/is-fullwidth-code-point\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/string-width-cjs/node_modules/is-fullwidth-code-point\",\"licenseFile\":\"/node_modules/string-width-cjs/node_modules/is-fullwidth-code-point/license\"},\"is-function@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/grncdr/js-is-function\",\"publisher\":\"Stephen Sugden\",\"email\":\"me@stephensugden.com\",\"path\":\"/node_modules/is-function\",\"licenseFile\":\"/node_modules/is-function/LICENSE\"},\"is-stream@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/is-stream\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/is-stream\",\"licenseFile\":\"/node_modules/is-stream/license\"},\"is-typed-array@1.1.15\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/is-typed-array\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"url\":\"http://ljharb.codes\",\"path\":\"/node_modules/is-typed-array\",\"licenseFile\":\"/node_modules/is-typed-array/LICENSE\"},\"is-utf8@0.2.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/wayfind/is-utf8\",\"publisher\":\"wayfind\",\"path\":\"/node_modules/is-utf8\",\"licenseFile\":\"/node_modules/is-utf8/LICENSE\"},\"isarray@0.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/juliangruber/isarray\",\"publisher\":\"Julian Gruber\",\"email\":\"mail@juliangruber.com\",\"url\":\"http://juliangruber.com\",\"path\":\"/node_modules/ftp/node_modules/isarray\",\"licenseFile\":\"/node_modules/ftp/node_modules/isarray/README.md\"},\"isarray@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/juliangruber/isarray\",\"publisher\":\"Julian Gruber\",\"email\":\"mail@juliangruber.com\",\"url\":\"http://juliangruber.com\",\"path\":\"/node_modules/isarray\",\"licenseFile\":\"/node_modules/isarray/README.md\"},\"isarray@2.0.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/juliangruber/isarray\",\"publisher\":\"Julian Gruber\",\"email\":\"mail@juliangruber.com\",\"url\":\"http://juliangruber.com\",\"path\":\"/node_modules/to-buffer/node_modules/isarray\",\"licenseFile\":\"/node_modules/to-buffer/node_modules/isarray/LICENSE\"},\"isbinaryfile@5.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/gjtorikian/isBinaryFile\",\"path\":\"/node_modules/isbinaryfile\",\"licenseFile\":\"/node_modules/isbinaryfile/LICENSE.txt\"},\"isexe@2.0.0\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/isexe\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/isexe\",\"licenseFile\":\"/node_modules/isexe/LICENSE\"},\"isomorphic-fetch@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/matthew-andrews/isomorphic-fetch\",\"publisher\":\"Matt Andrews\",\"email\":\"matt@mattandre.ws\",\"path\":\"/node_modules/isomorphic-fetch\",\"licenseFile\":\"/node_modules/isomorphic-fetch/LICENSE\"},\"isomorphic-git@1.33.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/isomorphic-git/isomorphic-git\",\"publisher\":\"William Hilton\",\"email\":\"wmhilton@gmail.com\",\"path\":\"/node_modules/isomorphic-git\",\"licenseFile\":\"/node_modules/isomorphic-git/LICENSE.md\"},\"jackspeak@3.4.3\":{\"licenses\":\"BlueOak-1.0.0\",\"repository\":\"https://github.com/isaacs/jackspeak\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"path\":\"/node_modules/jackspeak\",\"licenseFile\":\"/node_modules/jackspeak/LICENSE.md\"},\"jimp@0.22.12\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jimp-dev/jimp\",\"publisher\":\"Oliver Moran\",\"email\":\"oliver.moran@gmail.com\",\"path\":\"/node_modules/jimp\",\"licenseFile\":\"/node_modules/jimp/LICENSE\"},\"jpeg-js@0.4.4\":{\"licenses\":\"BSD-3-Clause\",\"repository\":\"https://github.com/eugeneware/jpeg-js\",\"publisher\":\"Eugene Ware\",\"email\":\"eugene@noblesamurai.com\",\"path\":\"/node_modules/jpeg-js\",\"licenseFile\":\"/node_modules/jpeg-js/LICENSE\"},\"js-beautify@1.15.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/beautifier/js-beautify\",\"publisher\":\"Einar Lielmanis\",\"email\":\"einar@beautifier.io\",\"path\":\"/node_modules/js-beautify\",\"licenseFile\":\"/node_modules/js-beautify/LICENSE\"},\"js-cookie@3.0.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/js-cookie/js-cookie\",\"publisher\":\"Klaus Hartl\",\"path\":\"/node_modules/js-cookie\",\"licenseFile\":\"/node_modules/js-cookie/LICENSE\"},\"json-bigint@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sidorares/json-bigint\",\"publisher\":\"Andrey Sidorov\",\"email\":\"sidorares@yandex.ru\",\"path\":\"/node_modules/json-bigint\",\"licenseFile\":\"/node_modules/json-bigint/LICENSE\"},\"json-buffer@3.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/dominictarr/json-buffer\",\"publisher\":\"Dominic Tarr\",\"email\":\"dominic.tarr@gmail.com\",\"url\":\"http://dominictarr.com\",\"path\":\"/node_modules/json-buffer\",\"licenseFile\":\"/node_modules/json-buffer/LICENSE\"},\"jsonfile@6.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jprichardson/node-jsonfile\",\"publisher\":\"JP Richardson\",\"email\":\"jprichardson@gmail.com\",\"path\":\"/node_modules/jsonfile\",\"licenseFile\":\"/node_modules/jsonfile/LICENSE\"},\"jwa@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/brianloveswords/node-jwa\",\"publisher\":\"Brian J. Brennan\",\"email\":\"brianloveswords@gmail.com\",\"path\":\"/node_modules/jwa\",\"licenseFile\":\"/node_modules/jwa/LICENSE\"},\"jws@4.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/brianloveswords/node-jws\",\"publisher\":\"Brian J Brennan\",\"path\":\"/node_modules/jws\",\"licenseFile\":\"/node_modules/jws/LICENSE\"},\"keytar@7.9.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/atom/node-keytar\",\"path\":\"/node_modules/keytar\",\"licenseFile\":\"/node_modules/keytar/LICENSE.md\"},\"keyv@4.5.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jaredwray/keyv\",\"publisher\":\"Jared Wray\",\"email\":\"me@jaredwray.com\",\"url\":\"http://jaredwray.com\",\"path\":\"/node_modules/keyv\",\"licenseFile\":\"/node_modules/keyv/README.md\"},\"lazystream@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jpommerening/node-lazystream\",\"publisher\":\"Jonas Pommerening\",\"email\":\"jonas.pommerening@gmail.com\",\"url\":\"https://npmjs.org/~jpommerening\",\"path\":\"/node_modules/lazystream\",\"licenseFile\":\"/node_modules/lazystream/LICENSE\"},\"li@1.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jfromaniello/li\",\"publisher\":\"José F. Romaniello\",\"email\":\"jfromaniello@gmail.com\",\"url\":\"http://joseoncode.com\",\"path\":\"/node_modules/li\",\"licenseFile\":\"/node_modules/li/README.md\"},\"load-bmfont@1.4.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Jam3/load-bmfont\",\"publisher\":\"Matt DesLauriers\",\"email\":\"dave.des@gmail.com\",\"url\":\"https://github.com/mattdesl\",\"path\":\"/node_modules/load-bmfont\",\"licenseFile\":\"/node_modules/load-bmfont/LICENSE.md\"},\"load-json-file@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/load-json-file\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/load-json-file\",\"licenseFile\":\"/node_modules/load-json-file/license\"},\"lodash.defaults@4.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lodash/lodash\",\"publisher\":\"John-David Dalton\",\"email\":\"john.david.dalton@gmail.com\",\"url\":\"http://allyoucanleet.com/\",\"path\":\"/node_modules/lodash.defaults\",\"licenseFile\":\"/node_modules/lodash.defaults/LICENSE\"},\"lodash.difference@4.5.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lodash/lodash\",\"publisher\":\"John-David Dalton\",\"email\":\"john.david.dalton@gmail.com\",\"url\":\"http://allyoucanleet.com/\",\"path\":\"/node_modules/lodash.difference\",\"licenseFile\":\"/node_modules/lodash.difference/LICENSE\"},\"lodash.flatten@4.4.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lodash/lodash\",\"publisher\":\"John-David Dalton\",\"email\":\"john.david.dalton@gmail.com\",\"url\":\"http://allyoucanleet.com/\",\"path\":\"/node_modules/lodash.flatten\",\"licenseFile\":\"/node_modules/lodash.flatten/LICENSE\"},\"lodash.flattendeep@4.4.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lodash/lodash\",\"publisher\":\"John-David Dalton\",\"email\":\"john.david.dalton@gmail.com\",\"url\":\"http://allyoucanleet.com/\",\"path\":\"/node_modules/lodash.flattendeep\",\"licenseFile\":\"/node_modules/lodash.flattendeep/LICENSE\"},\"lodash.isplainobject@4.0.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lodash/lodash\",\"publisher\":\"John-David Dalton\",\"email\":\"john.david.dalton@gmail.com\",\"url\":\"http://allyoucanleet.com/\",\"path\":\"/node_modules/lodash.isplainobject\",\"licenseFile\":\"/node_modules/lodash.isplainobject/LICENSE\"},\"lodash.throttle@4.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lodash/lodash\",\"publisher\":\"John-David Dalton\",\"email\":\"john.david.dalton@gmail.com\",\"url\":\"http://allyoucanleet.com/\",\"path\":\"/node_modules/lodash.throttle\",\"licenseFile\":\"/node_modules/lodash.throttle/LICENSE\"},\"lodash.union@4.6.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lodash/lodash\",\"publisher\":\"John-David Dalton\",\"email\":\"john.david.dalton@gmail.com\",\"url\":\"http://allyoucanleet.com/\",\"path\":\"/node_modules/lodash.union\",\"licenseFile\":\"/node_modules/lodash.union/LICENSE\"},\"loud-rejection@1.6.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/loud-rejection\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/loud-rejection\",\"licenseFile\":\"/node_modules/loud-rejection/license\"},\"lower-case@1.1.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/blakeembrey/lower-case\",\"publisher\":\"Blake Embrey\",\"email\":\"hello@blakeembrey.com\",\"url\":\"http://blakeembrey.me\",\"path\":\"/node_modules/lower-case\",\"licenseFile\":\"/node_modules/lower-case/LICENSE\"},\"lowercase-keys@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/lowercase-keys\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/lowercase-keys\",\"licenseFile\":\"/node_modules/lowercase-keys/license\"},\"lru-cache@10.4.3\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/node-lru-cache\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"path\":\"/node_modules/path-scurry/node_modules/lru-cache\",\"licenseFile\":\"/node_modules/path-scurry/node_modules/lru-cache/LICENSE\"},\"lru-cache@6.0.0\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/node-lru-cache\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"path\":\"/node_modules/lru-cache\",\"licenseFile\":\"/node_modules/lru-cache/LICENSE\"},\"ls-all@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/cantidio/node-ls-all\",\"publisher\":\"Cantidio Fontes\",\"email\":\"aniquilatorbloody@gmail.com\",\"path\":\"/node_modules/ls-all\",\"licenseFile\":\"/node_modules/ls-all/LICENSE\"},\"map-obj@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/map-obj\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/map-obj\",\"licenseFile\":\"/node_modules/map-obj/license\"},\"marked@4.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/markedjs/marked\",\"publisher\":\"Christopher Jeffrey\",\"path\":\"/node_modules/easymde/node_modules/marked\",\"licenseFile\":\"/node_modules/easymde/node_modules/marked/LICENSE.md\"},\"marked@5.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/markedjs/marked\",\"publisher\":\"Christopher Jeffrey\",\"path\":\"/node_modules/marked\",\"licenseFile\":\"/node_modules/marked/LICENSE.md\"},\"material-colors@1.2.6\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/shuhei/material-colors\",\"publisher\":\"Shuhei Kagawa\",\"email\":\"shuhei.kagawa@gmail.com\",\"path\":\"/node_modules/material-colors\",\"licenseFile\":\"/node_modules/material-colors/LICENSE\"},\"math-intrinsics@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/es-shims/math-intrinsics\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/math-intrinsics\",\"licenseFile\":\"/node_modules/math-intrinsics/LICENSE\"},\"meow@3.7.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/meow\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/meow\",\"licenseFile\":\"/node_modules/meow/license\"},\"mime-db@1.52.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jshttp/mime-db\",\"path\":\"/node_modules/mime-db\",\"licenseFile\":\"/node_modules/mime-db/LICENSE\"},\"mime-types@2.1.35\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jshttp/mime-types\",\"path\":\"/node_modules/mime-types\",\"licenseFile\":\"/node_modules/mime-types/LICENSE\"},\"mime@1.6.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/broofa/node-mime\",\"publisher\":\"Robert Kieffer\",\"email\":\"robert@broofa.com\",\"url\":\"http://github.com/broofa\",\"path\":\"/node_modules/load-bmfont/node_modules/mime\",\"licenseFile\":\"/node_modules/load-bmfont/node_modules/mime/LICENSE\"},\"mime@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/broofa/mime\",\"publisher\":\"Robert Kieffer\",\"email\":\"robert@broofa.com\",\"url\":\"http://github.com/broofa\",\"path\":\"/node_modules/mime\",\"licenseFile\":\"/node_modules/mime/LICENSE\"},\"mimic-response@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/mimic-response\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/mimic-response\",\"licenseFile\":\"/node_modules/mimic-response/license\"},\"mimic-response@3.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/mimic-response\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/decompress-response/node_modules/mimic-response\",\"licenseFile\":\"/node_modules/decompress-response/node_modules/mimic-response/license\"},\"min-document@2.19.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Raynos/min-document\",\"publisher\":\"Raynos\",\"email\":\"raynos2@gmail.com\",\"path\":\"/node_modules/min-document\",\"licenseFile\":\"/node_modules/min-document/LICENCE\"},\"minimatch@3.1.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/minimatch\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me\",\"path\":\"/node_modules/minimatch\",\"licenseFile\":\"/node_modules/minimatch/LICENSE\"},\"minimatch@5.1.6\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/minimatch\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me\",\"path\":\"/node_modules/readdir-glob/node_modules/minimatch\",\"licenseFile\":\"/node_modules/readdir-glob/node_modules/minimatch/LICENSE\"},\"minimatch@9.0.1\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/minimatch\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me\",\"path\":\"/node_modules/editorconfig/node_modules/minimatch\",\"licenseFile\":\"/node_modules/editorconfig/node_modules/minimatch/LICENSE\"},\"minimatch@9.0.5\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/minimatch\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me\",\"path\":\"/node_modules/js-beautify/node_modules/minimatch\",\"licenseFile\":\"/node_modules/js-beautify/node_modules/minimatch/LICENSE\"},\"minimist@1.2.8\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/minimistjs/minimist\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/minimist\",\"licenseFile\":\"/node_modules/minimist/LICENSE\"},\"minimisted@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/kt3k/minimisted\",\"publisher\":\"Yoshiya Hinosawa\",\"email\":\"stibium121@gmail.com\",\"url\":\"https://twitter.com/kt3k\",\"path\":\"/node_modules/minimisted\",\"licenseFile\":\"/node_modules/minimisted/README.md\"},\"minipass@7.1.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/minipass\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/minipass\",\"licenseFile\":\"/node_modules/minipass/LICENSE\"},\"mkdirp-classic@0.5.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/mkdirp-classic\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/mkdirp-classic\",\"licenseFile\":\"/node_modules/mkdirp-classic/LICENSE\"},\"moment@2.29.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/moment/moment\",\"publisher\":\"Iskren Ivov Chernev\",\"email\":\"iskren.chernev@gmail.com\",\"url\":\"https://github.com/ichernev\",\"path\":\"/node_modules/moment\",\"licenseFile\":\"/node_modules/moment/LICENSE\"},\"ms@2.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/zeit/ms\",\"path\":\"/node_modules/ms\",\"licenseFile\":\"/node_modules/ms/license.md\"},\"nan@2.18.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/nan\",\"path\":\"/node_modules/nan\",\"licenseFile\":\"/node_modules/nan/LICENSE.md\"},\"nanobus@3.3.0\":{\"licenses\":\"MIT*\",\"repository\":\"https://github.com/yoshuawuyts/nanobus\",\"path\":\"/node_modules/nanobus\",\"licenseFile\":\"/node_modules/nanobus/LICENSE\"},\"nanotiming@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/yoshuawuyts/nanotiming\",\"path\":\"/node_modules/nanotiming\",\"licenseFile\":\"/node_modules/nanotiming/LICENSE\"},\"napi-build-utils@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspiredware/napi-build-utils\",\"publisher\":\"Jim Schlight\",\"path\":\"/node_modules/napi-build-utils\",\"licenseFile\":\"/node_modules/napi-build-utils/LICENSE\"},\"neat-log@1.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/joehand/neat-log\",\"publisher\":\"Joe Hand\",\"email\":\"joe@hand.email\",\"path\":\"/node_modules/neat-log\",\"licenseFile\":\"/node_modules/neat-log/LICENSE.md\"},\"neo-async@2.6.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/suguru03/neo-async\",\"path\":\"/node_modules/neo-async\",\"licenseFile\":\"/node_modules/neo-async/LICENSE\"},\"no-case@2.3.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/blakeembrey/no-case\",\"publisher\":\"Blake Embrey\",\"email\":\"hello@blakeembrey.com\",\"url\":\"http://blakeembrey.me\",\"path\":\"/node_modules/no-case\",\"licenseFile\":\"/node_modules/no-case/LICENSE\"},\"node-abi@3.75.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/electron/node-abi\",\"publisher\":\"Lukas Geiger\",\"path\":\"/node_modules/prebuild-install/node_modules/node-abi\",\"licenseFile\":\"/node_modules/prebuild-install/node_modules/node-abi/LICENSE\"},\"node-addon-api@4.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/node-addon-api\",\"path\":\"/node_modules/node-addon-api\",\"licenseFile\":\"/node_modules/node-addon-api/LICENSE.md\"},\"node-fetch@2.7.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/bitinn/node-fetch\",\"publisher\":\"David Frank\",\"path\":\"/node_modules/node-fetch\",\"licenseFile\":\"/node_modules/node-fetch/LICENSE.md\"},\"node-forge@1.3.1\":{\"licenses\":\"(BSD-3-Clause OR GPL-2.0)\",\"repository\":\"https://github.com/digitalbazaar/forge\",\"publisher\":\"Digital Bazaar, Inc.\",\"email\":\"support@digitalbazaar.com\",\"url\":\"http://digitalbazaar.com/\",\"path\":\"/node_modules/node-forge\",\"licenseFile\":\"/node_modules/node-forge/LICENSE\"},\"node-sqlite3-wasm@0.8.47\":{\"licenses\":\"MIT\",\"publisher\":\"Tobias Enderle\",\"path\":\"/node_modules/node-sqlite3-wasm\",\"licenseFile\":\"/node_modules/node-sqlite3-wasm/LICENSE\"},\"node-version-compare@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/leohihimax/node-version-compare\",\"publisher\":\"jiajun ma\",\"email\":\"leomajiajun@gmail.com\",\"path\":\"/node_modules/node-version-compare\",\"licenseFile\":\"/node_modules/node-version-compare/README.md\"},\"nopt@7.2.1\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/npm/nopt\",\"publisher\":\"GitHub Inc.\",\"path\":\"/node_modules/nopt\",\"licenseFile\":\"/node_modules/nopt/LICENSE\"},\"normalize-package-data@2.5.0\":{\"licenses\":\"BSD-2-Clause\",\"repository\":\"https://github.com/npm/normalize-package-data\",\"publisher\":\"Meryn Stol\",\"email\":\"merynstol@gmail.com\",\"path\":\"/node_modules/normalize-package-data\",\"licenseFile\":\"/node_modules/normalize-package-data/LICENSE\"},\"normalize-path@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jonschlinkert/normalize-path\",\"publisher\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\",\"path\":\"/node_modules/normalize-path\",\"licenseFile\":\"/node_modules/normalize-path/LICENSE\"},\"normalize-url@6.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/normalize-url\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/normalize-url\",\"licenseFile\":\"/node_modules/normalize-url/license\"},\"object-assign@4.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/object-assign\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/object-assign\",\"licenseFile\":\"/node_modules/object-assign/license\"},\"object-inspect@1.13.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/object-inspect\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/object-inspect\",\"licenseFile\":\"/node_modules/object-inspect/LICENSE\"},\"omggif@1.0.10\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/deanm/omggif\",\"publisher\":\"Dean McNamee\",\"email\":\"dean@gmail.com\",\"path\":\"/node_modules/omggif\",\"licenseFile\":\"/node_modules/omggif/README\"},\"once@1.4.0\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/once\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/once\",\"licenseFile\":\"/node_modules/once/LICENSE\"},\"p-cancelable@2.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/p-cancelable\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/p-cancelable\",\"licenseFile\":\"/node_modules/p-cancelable/license\"},\"p-limit@3.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/p-limit\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/p-limit\",\"licenseFile\":\"/node_modules/p-limit/license\"},\"package-json-from-dist@1.0.1\":{\"licenses\":\"BlueOak-1.0.0\",\"repository\":\"https://github.com/isaacs/package-json-from-dist\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"https://izs.me\",\"path\":\"/node_modules/package-json-from-dist\",\"licenseFile\":\"/node_modules/package-json-from-dist/LICENSE.md\"},\"pako@1.0.11\":{\"licenses\":\"(MIT AND Zlib)\",\"repository\":\"https://github.com/nodeca/pako\",\"path\":\"/node_modules/pako\",\"licenseFile\":\"/node_modules/pako/LICENSE\"},\"param-case@2.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/blakeembrey/param-case\",\"publisher\":\"Blake Embrey\",\"email\":\"hello@blakeembrey.com\",\"url\":\"http://blakeembrey.me\",\"path\":\"/node_modules/param-case\",\"licenseFile\":\"/node_modules/param-case/LICENSE\"},\"parse-bmfont-ascii@1.0.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mattdesl/parse-bmfont-ascii\",\"publisher\":\"Matt DesLauriers\",\"email\":\"dave.des@gmail.com\",\"url\":\"https://github.com/mattdesl\",\"path\":\"/node_modules/parse-bmfont-ascii\",\"licenseFile\":\"/node_modules/parse-bmfont-ascii/README.md\"},\"parse-bmfont-binary@1.0.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Jam3/parse-bmfont-binary\",\"publisher\":\"Matt DesLauriers\",\"email\":\"dave.des@gmail.com\",\"url\":\"https://github.com/mattdesl\",\"path\":\"/node_modules/parse-bmfont-binary\",\"licenseFile\":\"/node_modules/parse-bmfont-binary/LICENSE.md\"},\"parse-bmfont-xml@1.1.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mattdesl/parse-bmfont-xml\",\"publisher\":\"Matt DesLauriers\",\"email\":\"dave.des@gmail.com\",\"url\":\"https://github.com/mattdesl\",\"path\":\"/node_modules/parse-bmfont-xml\",\"licenseFile\":\"/node_modules/parse-bmfont-xml/LICENSE.md\"},\"parse-headers@2.0.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/kesla/parse-headers\",\"publisher\":\"David Björklund\",\"email\":\"david.bjorklund@gmail.com\",\"path\":\"/node_modules/parse-headers\",\"licenseFile\":\"/node_modules/parse-headers/LICENCE\"},\"parse-json@2.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/parse-json\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/parse-json\",\"licenseFile\":\"/node_modules/parse-json/license\"},\"path-browserify@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/browserify/path-browserify\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/path-browserify\",\"licenseFile\":\"/node_modules/path-browserify/LICENSE\"},\"path-exists@2.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/path-exists\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/path-exists\",\"licenseFile\":\"/node_modules/path-exists/license\"},\"path-is-absolute@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/path-is-absolute\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/path-is-absolute\",\"licenseFile\":\"/node_modules/path-is-absolute/license\"},\"path-key@3.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/path-key\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/path-key\",\"licenseFile\":\"/node_modules/path-key/license\"},\"path-parse@1.0.7\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jbgutierrez/path-parse\",\"publisher\":\"Javier Blanco\",\"email\":\"http://jbgutierrez.info\",\"path\":\"/node_modules/path-parse\",\"licenseFile\":\"/node_modules/path-parse/LICENSE\"},\"path-scurry@1.11.1\":{\"licenses\":\"BlueOak-1.0.0\",\"repository\":\"https://github.com/isaacs/path-scurry\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"https://blog.izs.me\",\"path\":\"/node_modules/path-scurry\",\"licenseFile\":\"/node_modules/path-scurry/LICENSE.md\"},\"path-type@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/path-type\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/path-type\",\"licenseFile\":\"/node_modules/path-type/license\"},\"peek-readable@4.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Borewit/peek-readable\",\"publisher\":\"Borewit\",\"url\":\"https://github.com/Borewit\",\"path\":\"/node_modules/peek-readable\",\"licenseFile\":\"/node_modules/peek-readable/LICENSE\"},\"phin@3.7.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ethanent/phin\",\"publisher\":\"Ethan Davis\",\"path\":\"/node_modules/phin\",\"licenseFile\":\"/node_modules/phin/LICENSE\"},\"pify@2.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/pify\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/load-json-file/node_modules/pify\",\"licenseFile\":\"/node_modules/load-json-file/node_modules/pify/license\"},\"pify@4.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/pify\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/pify\",\"licenseFile\":\"/node_modules/pify/license\"},\"pinkie-promise@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/floatdrop/pinkie-promise\",\"publisher\":\"Vsevolod Strukchinsky\",\"email\":\"floatdrop@gmail.com\",\"url\":\"github.com/floatdrop\",\"path\":\"/node_modules/pinkie-promise\",\"licenseFile\":\"/node_modules/pinkie-promise/license\"},\"pinkie@2.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/floatdrop/pinkie\",\"publisher\":\"Vsevolod Strukchinsky\",\"email\":\"floatdrop@gmail.com\",\"url\":\"github.com/floatdrop\",\"path\":\"/node_modules/pinkie\",\"licenseFile\":\"/node_modules/pinkie/license\"},\"pixelmatch@4.0.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/mapbox/pixelmatch\",\"publisher\":\"Vladimir Agafonkin\",\"path\":\"/node_modules/pixelmatch\",\"licenseFile\":\"/node_modules/pixelmatch/LICENSE\"},\"pngjs@3.4.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lukeapage/pngjs2\",\"path\":\"/node_modules/pixelmatch/node_modules/pngjs\",\"licenseFile\":\"/node_modules/pixelmatch/node_modules/pngjs/LICENSE\"},\"pngjs@6.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/lukeapage/pngjs\",\"path\":\"/node_modules/pngjs\",\"licenseFile\":\"/node_modules/pngjs/LICENSE\"},\"possible-typed-array-names@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/possible-typed-array-names\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/possible-typed-array-names\",\"licenseFile\":\"/node_modules/possible-typed-array-names/LICENSE\"},\"prebuild-install@7.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/prebuild/prebuild-install\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/prebuild-install\",\"licenseFile\":\"/node_modules/prebuild-install/LICENSE\"},\"prettier-bytes@1.0.4\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/Flet/prettier-bytes\",\"publisher\":\"Dan Flettre\",\"email\":\"flettre@gmail.com\",\"path\":\"/node_modules/prettier-bytes\",\"licenseFile\":\"/node_modules/prettier-bytes/LICENSE\"},\"prismjs@1.30.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/PrismJS/prism\",\"publisher\":\"Lea Verou\",\"path\":\"/node_modules/prismjs\",\"licenseFile\":\"/node_modules/prismjs/LICENSE\"},\"process-nextick-args@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/calvinmetcalf/process-nextick-args\",\"path\":\"/node_modules/process-nextick-args\",\"licenseFile\":\"/node_modules/process-nextick-args/license.md\"},\"process@0.11.10\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/shtylman/node-process\",\"publisher\":\"Roman Shtylman\",\"email\":\"shtylman@gmail.com\",\"path\":\"/node_modules/process\",\"licenseFile\":\"/node_modules/process/LICENSE\"},\"promise-retry@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/IndigoUnited/node-promise-retry\",\"publisher\":\"IndigoUnited\",\"email\":\"hello@indigounited.com\",\"url\":\"http://indigounited.com\",\"path\":\"/node_modules/promise-retry\",\"licenseFile\":\"/node_modules/promise-retry/LICENSE\"},\"proto-list@1.2.4\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/proto-list\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/proto-list\",\"licenseFile\":\"/node_modules/proto-list/LICENSE\"},\"pump@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/pump\",\"publisher\":\"Mathias Buus Madsen\",\"email\":\"mathiasbuus@gmail.com\",\"path\":\"/node_modules/pump\",\"licenseFile\":\"/node_modules/pump/LICENSE\"},\"qs@6.14.0\":{\"licenses\":\"BSD-3-Clause\",\"repository\":\"https://github.com/ljharb/qs\",\"path\":\"/node_modules/qs\",\"licenseFile\":\"/node_modules/qs/LICENSE.md\"},\"query-string@7.1.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/query-string\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/query-string\",\"licenseFile\":\"/node_modules/query-string/license\"},\"queue@6.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jessetane/queue\",\"publisher\":\"Jesse Tane\",\"email\":\"jesse.tane@gmail.com\",\"path\":\"/node_modules/queue\",\"licenseFile\":\"/node_modules/queue/LICENSE\"},\"quick-lru@5.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/quick-lru\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/quick-lru\",\"licenseFile\":\"/node_modules/quick-lru/license\"},\"rc@1.2.8\":{\"licenses\":\"(BSD-2-Clause OR MIT OR Apache-2.0)\",\"repository\":\"https://github.com/dominictarr/rc\",\"publisher\":\"Dominic Tarr\",\"email\":\"dominic.tarr@gmail.com\",\"url\":\"dominictarr.com\",\"path\":\"/node_modules/rc\",\"licenseFile\":\"/node_modules/rc/LICENSE.APACHE2\"},\"read-pkg-up@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/read-pkg-up\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/read-pkg-up\",\"licenseFile\":\"/node_modules/read-pkg-up/license\"},\"read-pkg@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/read-pkg\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/read-pkg\",\"licenseFile\":\"/node_modules/read-pkg/license\"},\"readable-stream@1.1.14\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/isaacs/readable-stream\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/ftp/node_modules/readable-stream\",\"licenseFile\":\"/node_modules/ftp/node_modules/readable-stream/LICENSE\"},\"readable-stream@2.3.8\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/readable-stream\",\"path\":\"/node_modules/lazystream/node_modules/readable-stream\",\"licenseFile\":\"/node_modules/lazystream/node_modules/readable-stream/LICENSE\"},\"readable-stream@3.6.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/readable-stream\",\"path\":\"/node_modules/readable-stream\",\"licenseFile\":\"/node_modules/readable-stream/LICENSE\"},\"readable-stream@4.7.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/readable-stream\",\"path\":\"/node_modules/readable-web-to-node-stream/node_modules/readable-stream\",\"licenseFile\":\"/node_modules/readable-web-to-node-stream/node_modules/readable-stream/LICENSE\"},\"readable-web-to-node-stream@3.0.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Borewit/readable-web-to-node-stream\",\"publisher\":\"Borewit\",\"url\":\"https://github.com/Borewit\",\"path\":\"/node_modules/readable-web-to-node-stream\",\"licenseFile\":\"/node_modules/readable-web-to-node-stream/README.md\"},\"readdir-glob@1.1.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/Yqnn/node-readdir-glob\",\"publisher\":\"Yann Armelin\",\"path\":\"/node_modules/readdir-glob\",\"licenseFile\":\"/node_modules/readdir-glob/LICENSE\"},\"redent@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/redent\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/redent\",\"licenseFile\":\"/node_modules/redent/license\"},\"regenerator-runtime@0.13.11\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/facebook/regenerator/tree/main/packages/runtime\",\"publisher\":\"Ben Newman\",\"email\":\"bn@cs.stanford.edu\",\"path\":\"/node_modules/regenerator-runtime\",\"licenseFile\":\"/node_modules/regenerator-runtime/LICENSE\"},\"relateurl@0.2.7\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/stevenvachon/relateurl\",\"publisher\":\"Steven Vachon\",\"email\":\"contact@svachon.com\",\"url\":\"http://www.svachon.com/\",\"path\":\"/node_modules/relateurl\",\"licenseFile\":\"/node_modules/relateurl/license\"},\"repeating@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/repeating\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/repeating\",\"licenseFile\":\"/node_modules/repeating/license\"},\"require-directory@2.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/troygoode/node-require-directory\",\"publisher\":\"Troy Goode\",\"email\":\"troygoode@gmail.com\",\"url\":\"http://github.com/troygoode/\",\"path\":\"/node_modules/require-directory\",\"licenseFile\":\"/node_modules/require-directory/LICENSE\"},\"resolve-alpn@1.2.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/szmarczak/resolve-alpn\",\"publisher\":\"Szymon Marczak\",\"path\":\"/node_modules/resolve-alpn\",\"licenseFile\":\"/node_modules/resolve-alpn/LICENSE\"},\"resolve@1.22.8\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/browserify/resolve\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/resolve\",\"licenseFile\":\"/node_modules/resolve/LICENSE\"},\"responselike@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/responselike\",\"publisher\":\"lukechilds\",\"path\":\"/node_modules/responselike\",\"licenseFile\":\"/node_modules/responselike/LICENSE\"},\"retry-request@5.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/stephenplusplus/retry-request\",\"publisher\":\"Stephen Sawchuk\",\"email\":\"sawchuk@gmail.com\",\"path\":\"/node_modules/retry-request\",\"licenseFile\":\"/node_modules/retry-request/license\"},\"retry@0.12.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/tim-kos/node-retry\",\"publisher\":\"Tim Koschützki\",\"email\":\"tim@debuggable.com\",\"url\":\"http://debuggable.com/\",\"path\":\"/node_modules/promise-retry/node_modules/retry\",\"licenseFile\":\"/node_modules/promise-retry/node_modules/retry/License\"},\"retry@0.13.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/tim-kos/node-retry\",\"publisher\":\"Tim Koschützki\",\"email\":\"tim@debuggable.com\",\"url\":\"http://debuggable.com/\",\"path\":\"/node_modules/retry\",\"licenseFile\":\"/node_modules/retry/License\"},\"safe-buffer@5.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/feross/safe-buffer\",\"publisher\":\"Feross Aboukhadijeh\",\"email\":\"feross@feross.org\",\"url\":\"http://feross.org\",\"path\":\"/node_modules/lazystream/node_modules/safe-buffer\",\"licenseFile\":\"/node_modules/lazystream/node_modules/safe-buffer/LICENSE\"},\"safe-buffer@5.2.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/feross/safe-buffer\",\"publisher\":\"Feross Aboukhadijeh\",\"email\":\"feross@feross.org\",\"url\":\"https://feross.org\",\"path\":\"/node_modules/safe-buffer\",\"licenseFile\":\"/node_modules/safe-buffer/LICENSE\"},\"safer-buffer@2.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ChALkeR/safer-buffer\",\"publisher\":\"Nikita Skovoroda\",\"email\":\"chalkerx@gmail.com\",\"url\":\"https://github.com/ChALkeR\",\"path\":\"/node_modules/safer-buffer\",\"licenseFile\":\"/node_modules/safer-buffer/LICENSE\"},\"sax@1.4.1\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/sax-js\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/sax\",\"licenseFile\":\"/node_modules/sax/LICENSE\"},\"semver@5.7.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/npm/node-semver\",\"publisher\":\"GitHub Inc.\",\"path\":\"/node_modules/normalize-package-data/node_modules/semver\",\"licenseFile\":\"/node_modules/normalize-package-data/node_modules/semver/LICENSE\"},\"semver@7.7.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/npm/node-semver\",\"publisher\":\"GitHub Inc.\",\"path\":\"/node_modules/prebuild-install/node_modules/semver\",\"licenseFile\":\"/node_modules/prebuild-install/node_modules/semver/LICENSE\"},\"set-function-length@1.2.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/set-function-length\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/set-function-length\",\"licenseFile\":\"/node_modules/set-function-length/LICENSE\"},\"sha.js@2.4.12\":{\"licenses\":\"(MIT AND BSD-3-Clause)\",\"repository\":\"https://github.com/crypto-browserify/sha.js\",\"publisher\":\"Dominic Tarr\",\"email\":\"dominic.tarr@gmail.com\",\"url\":\"dominictarr.com\",\"path\":\"/node_modules/sha.js\",\"licenseFile\":\"/node_modules/sha.js/LICENSE\"},\"sharp@0.34.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/lovell/sharp\",\"publisher\":\"Lovell Fuller\",\"email\":\"npm@lovell.info\",\"path\":\"/node_modules/sharp\",\"licenseFile\":\"/node_modules/sharp/LICENSE\"},\"shebang-command@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/kevva/shebang-command\",\"publisher\":\"Kevin Mårtensson\",\"email\":\"kevinmartensson@gmail.com\",\"url\":\"github.com/kevva\",\"path\":\"/node_modules/shebang-command\",\"licenseFile\":\"/node_modules/shebang-command/license\"},\"shebang-regex@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/shebang-regex\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/shebang-regex\",\"licenseFile\":\"/node_modules/shebang-regex/license\"},\"side-channel-list@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/side-channel-list\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/side-channel-list\",\"licenseFile\":\"/node_modules/side-channel-list/LICENSE\"},\"side-channel-map@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/side-channel-map\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/side-channel-map\",\"licenseFile\":\"/node_modules/side-channel-map/LICENSE\"},\"side-channel-weakmap@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/side-channel-weakmap\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/side-channel-weakmap\",\"licenseFile\":\"/node_modules/side-channel-weakmap/LICENSE\"},\"side-channel@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ljharb/side-channel\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/side-channel\",\"licenseFile\":\"/node_modules/side-channel/LICENSE\"},\"signal-exit@3.0.7\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/tapjs/signal-exit\",\"publisher\":\"Ben Coe\",\"email\":\"ben@npmjs.com\",\"path\":\"/node_modules/signal-exit\",\"licenseFile\":\"/node_modules/signal-exit/LICENSE.txt\"},\"signal-exit@4.1.0\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/tapjs/signal-exit\",\"publisher\":\"Ben Coe\",\"email\":\"ben@npmjs.com\",\"path\":\"/node_modules/foreground-child/node_modules/signal-exit\",\"licenseFile\":\"/node_modules/foreground-child/node_modules/signal-exit/LICENSE.txt\"},\"simple-concat@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/feross/simple-concat\",\"publisher\":\"Feross Aboukhadijeh\",\"email\":\"feross@feross.org\",\"url\":\"https://feross.org\",\"path\":\"/node_modules/simple-concat\",\"licenseFile\":\"/node_modules/simple-concat/LICENSE\"},\"simple-get@4.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/feross/simple-get\",\"publisher\":\"Feross Aboukhadijeh\",\"email\":\"feross@feross.org\",\"url\":\"https://feross.org\",\"path\":\"/node_modules/simple-get\",\"licenseFile\":\"/node_modules/simple-get/LICENSE\"},\"simple-swizzle@0.2.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/qix-/node-simple-swizzle\",\"publisher\":\"Qix\",\"url\":\"http://github.com/qix-\",\"path\":\"/node_modules/simple-swizzle\",\"licenseFile\":\"/node_modules/simple-swizzle/LICENSE\"},\"slug@0.9.4\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Trott/node-slug\",\"publisher\":\"dodo\",\"url\":\"https://github.com/dodo\",\"path\":\"/node_modules/slug\",\"licenseFile\":\"/node_modules/slug/LICENSE\"},\"sortablejs@1.10.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/SortableJS/Sortable\",\"path\":\"/node_modules/sortablejs\",\"licenseFile\":\"/node_modules/sortablejs/LICENSE\"},\"source-map@0.6.1\":{\"licenses\":\"BSD-3-Clause\",\"repository\":\"https://github.com/mozilla/source-map\",\"publisher\":\"Nick Fitzgerald\",\"email\":\"nfitzgerald@mozilla.com\",\"path\":\"/node_modules/source-map\",\"licenseFile\":\"/node_modules/source-map/LICENSE\"},\"spdx-correct@3.2.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/jslicense/spdx-correct.js\",\"path\":\"/node_modules/spdx-correct\",\"licenseFile\":\"/node_modules/spdx-correct/LICENSE\"},\"spdx-exceptions@2.5.0\":{\"licenses\":\"CC-BY-3.0\",\"repository\":\"https://github.com/kemitchell/spdx-exceptions.json\",\"publisher\":\"The Linux Foundation\",\"path\":\"/node_modules/spdx-exceptions\",\"licenseFile\":\"/node_modules/spdx-exceptions/README.md\"},\"spdx-expression-parse@3.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jslicense/spdx-expression-parse.js\",\"publisher\":\"Kyle E. Mitchell\",\"email\":\"kyle@kemitchell.com\",\"url\":\"https://kemitchell.com\",\"path\":\"/node_modules/spdx-expression-parse\",\"licenseFile\":\"/node_modules/spdx-expression-parse/LICENSE\"},\"spdx-license-ids@3.0.17\":{\"licenses\":\"CC0-1.0\",\"repository\":\"https://github.com/jslicense/spdx-license-ids\",\"publisher\":\"Shinnosuke Watanabe\",\"url\":\"https://github.com/shinnn\",\"path\":\"/node_modules/spdx-license-ids\",\"licenseFile\":\"/node_modules/spdx-license-ids/README.md\"},\"split-on-first@1.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/split-on-first\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/split-on-first\",\"licenseFile\":\"/node_modules/split-on-first/license\"},\"sqlstring@2.3.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mysqljs/sqlstring\",\"path\":\"/node_modules/sqlstring\",\"licenseFile\":\"/node_modules/sqlstring/LICENSE\"},\"ssh2-sftp-client@10.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/theophilusx/ssh2-sftp-client\",\"publisher\":\"Tim Cross\",\"path\":\"/node_modules/ssh2-sftp-client\",\"licenseFile\":\"/node_modules/ssh2-sftp-client/LICENSE\"},\"ssh2@1.15.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mscdex/ssh2\",\"publisher\":\"Brian White\",\"email\":\"mscdex@mscdex.net\",\"path\":\"/node_modules/ssh2\",\"licenseFile\":\"/node_modules/ssh2/LICENSE\"},\"status-logger@3.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/joehand/status-logger\",\"publisher\":\"Joe Hand\",\"email\":\"joe@joeahand.com\",\"url\":\"http://joeahand.com/\",\"path\":\"/node_modules/status-logger\",\"licenseFile\":\"/node_modules/status-logger/readme.md\"},\"stream-events@1.0.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/stephenplusplus/stream-events\",\"publisher\":\"Stephen Sawchuk\",\"path\":\"/node_modules/stream-events\",\"licenseFile\":\"/node_modules/stream-events/readme.md\"},\"stream-shift@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/stream-shift\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/stream-shift\",\"licenseFile\":\"/node_modules/stream-shift/LICENSE\"},\"streamx@2.22.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/streamx\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/streamx\",\"licenseFile\":\"/node_modules/streamx/LICENSE\"},\"strict-uri-encode@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/kevva/strict-uri-encode\",\"publisher\":\"Kevin Mårtensson\",\"email\":\"kevinmartensson@gmail.com\",\"url\":\"github.com/kevva\",\"path\":\"/node_modules/strict-uri-encode\",\"licenseFile\":\"/node_modules/strict-uri-encode/license\"},\"string-width@2.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/string-width\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/string-width\",\"licenseFile\":\"/node_modules/string-width/license\"},\"string-width@4.2.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/string-width\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/string-width-cjs\",\"licenseFile\":\"/node_modules/string-width-cjs/license\"},\"string-width@5.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/string-width\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/@isaacs/cliui/node_modules/string-width\",\"licenseFile\":\"/node_modules/@isaacs/cliui/node_modules/string-width/license\"},\"string_decoder@0.10.31\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/rvagg/string_decoder\",\"path\":\"/node_modules/ftp/node_modules/string_decoder\",\"licenseFile\":\"/node_modules/ftp/node_modules/string_decoder/LICENSE\"},\"string_decoder@1.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/string_decoder\",\"path\":\"/node_modules/lazystream/node_modules/string_decoder\",\"licenseFile\":\"/node_modules/lazystream/node_modules/string_decoder/LICENSE\"},\"string_decoder@1.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/string_decoder\",\"path\":\"/node_modules/string_decoder\",\"licenseFile\":\"/node_modules/string_decoder/LICENSE\"},\"strip-ansi@4.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/strip-ansi\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/strip-ansi\",\"licenseFile\":\"/node_modules/strip-ansi/license\"},\"strip-ansi@6.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/strip-ansi\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/string-width-cjs/node_modules/strip-ansi\",\"licenseFile\":\"/node_modules/string-width-cjs/node_modules/strip-ansi/license\"},\"strip-ansi@7.1.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/strip-ansi\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/@isaacs/cliui/node_modules/strip-ansi\",\"licenseFile\":\"/node_modules/@isaacs/cliui/node_modules/strip-ansi/license\"},\"strip-bom@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/strip-bom\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/strip-bom\",\"licenseFile\":\"/node_modules/strip-bom/license\"},\"strip-indent@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/strip-indent\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"http://sindresorhus.com\",\"path\":\"/node_modules/strip-indent\",\"licenseFile\":\"/node_modules/strip-indent/license\"},\"strip-json-comments@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/strip-json-comments\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/strip-json-comments\",\"licenseFile\":\"/node_modules/strip-json-comments/license\"},\"striptags@3.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/ericnorris/striptags\",\"publisher\":\"Eric Norris\",\"url\":\"https://github.com/ericnorris\",\"path\":\"/node_modules/striptags\",\"licenseFile\":\"/node_modules/striptags/LICENSE\"},\"strnum@1.0.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/NaturalIntelligence/strnum\",\"publisher\":\"Amit Gupta\",\"url\":\"https://amitkumargupta.work/\",\"path\":\"/node_modules/strnum\",\"licenseFile\":\"/node_modules/strnum/LICENSE\"},\"strtok3@6.3.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Borewit/strtok3\",\"publisher\":\"Borewit\",\"url\":\"https://github.com/Borewit\",\"path\":\"/node_modules/strtok3\",\"licenseFile\":\"/node_modules/strtok3/LICENSE\"},\"stubs@3.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/stephenplusplus/stubs\",\"publisher\":\"Stephen Sawchuk\",\"path\":\"/node_modules/stubs\",\"licenseFile\":\"/node_modules/stubs/readme.md\"},\"supports-preserve-symlinks-flag@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/node-supports-preserve-symlinks-flag\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/supports-preserve-symlinks-flag\",\"licenseFile\":\"/node_modules/supports-preserve-symlinks-flag/LICENSE\"},\"tar-fs@2.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/tar-fs\",\"publisher\":\"Mathias Buus\",\"path\":\"/node_modules/prebuild-install/node_modules/tar-fs\",\"licenseFile\":\"/node_modules/prebuild-install/node_modules/tar-fs/LICENSE\"},\"tar-fs@3.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/tar-fs\",\"publisher\":\"Mathias Buus\",\"path\":\"/node_modules/tar-fs\",\"licenseFile\":\"/node_modules/tar-fs/LICENSE\"},\"tar-stream@2.2.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/tar-stream\",\"publisher\":\"Mathias Buus\",\"email\":\"mathiasbuus@gmail.com\",\"path\":\"/node_modules/tar-stream\",\"licenseFile\":\"/node_modules/tar-stream/LICENSE\"},\"tar-stream@3.1.7\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/mafintosh/tar-stream\",\"publisher\":\"Mathias Buus\",\"email\":\"mathiasbuus@gmail.com\",\"path\":\"/node_modules/tar-fs/node_modules/tar-stream\",\"licenseFile\":\"/node_modules/tar-fs/node_modules/tar-stream/LICENSE\"},\"teeny-request@8.0.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/googleapis/teeny-request\",\"publisher\":\"fhinkel\",\"path\":\"/node_modules/teeny-request\",\"licenseFile\":\"/node_modules/teeny-request/LICENSE\"},\"text-decoder@1.2.3\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/holepunchto/text-decoder\",\"publisher\":\"Holepunch\",\"path\":\"/node_modules/text-decoder\",\"licenseFile\":\"/node_modules/text-decoder/LICENSE\"},\"through2@2.0.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/rvagg/through2\",\"publisher\":\"Rod Vagg\",\"email\":\"r@va.gg\",\"url\":\"https://github.com/rvagg\",\"path\":\"/node_modules/through2\",\"licenseFile\":\"/node_modules/through2/LICENSE.md\"},\"timm@1.7.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/guigrpa/timm\",\"publisher\":\"Guillermo Grau Panea\",\"path\":\"/node_modules/timm\",\"licenseFile\":\"/node_modules/timm/LICENSE\"},\"tinycolor2@1.6.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/bgrins/TinyColor\",\"publisher\":\"Brian Grinstead\",\"email\":\"briangrinstead@gmail.com\",\"url\":\"http://briangrinstead.com\",\"path\":\"/node_modules/tinycolor2\",\"licenseFile\":\"/node_modules/tinycolor2/LICENSE\"},\"to-buffer@1.2.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/browserify/to-buffer\",\"publisher\":\"Mathias Buus\",\"url\":\"@mafintosh\",\"path\":\"/node_modules/to-buffer\",\"licenseFile\":\"/node_modules/to-buffer/LICENSE\"},\"token-types@4.2.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Borewit/token-types\",\"publisher\":\"Borewit\",\"url\":\"https://github.com/Borewit\",\"path\":\"/node_modules/token-types\",\"licenseFile\":\"/node_modules/token-types/LICENSE\"},\"tr46@0.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Sebmaster/tr46.js\",\"publisher\":\"Sebastian Mayr\",\"email\":\"npm@smayr.name\",\"path\":\"/node_modules/tr46\"},\"transliteration@2.3.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/dzcpy/transliteration\",\"path\":\"/node_modules/transliteration\",\"licenseFile\":\"/node_modules/transliteration/LICENSE.txt\"},\"tree-flatten@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/cantidio/node-tree-flatten\",\"publisher\":\"Cantidio Fontes\",\"email\":\"aniquilatorbloody@gmail.com\",\"path\":\"/node_modules/tree-flatten\",\"licenseFile\":\"/node_modules/tree-flatten/LICENSE\"},\"trim-newlines@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/trim-newlines\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/trim-newlines\",\"licenseFile\":\"/node_modules/trim-newlines/license\"},\"tslib@2.6.2\":{\"licenses\":\"0BSD\",\"repository\":\"https://github.com/Microsoft/tslib\",\"publisher\":\"Microsoft Corp.\",\"path\":\"/node_modules/tslib\",\"licenseFile\":\"/node_modules/tslib/LICENSE.txt\"},\"tunnel-agent@0.6.0\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/mikeal/tunnel-agent\",\"publisher\":\"Mikeal Rogers\",\"email\":\"mikeal.rogers@gmail.com\",\"url\":\"http://www.futurealoof.com\",\"path\":\"/node_modules/tunnel-agent\",\"licenseFile\":\"/node_modules/tunnel-agent/LICENSE\"},\"tweetnacl@0.14.5\":{\"licenses\":\"Unlicense\",\"repository\":\"https://github.com/dchest/tweetnacl-js\",\"publisher\":\"TweetNaCl-js contributors\",\"path\":\"/node_modules/tweetnacl\",\"licenseFile\":\"/node_modules/tweetnacl/LICENSE\"},\"typed-array-buffer@1.0.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/typed-array-buffer\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"path\":\"/node_modules/typed-array-buffer\",\"licenseFile\":\"/node_modules/typed-array-buffer/LICENSE\"},\"typedarray@0.0.6\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/substack/typedarray\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/typedarray\",\"licenseFile\":\"/node_modules/typedarray/LICENSE\"},\"typo-js@1.2.4\":{\"licenses\":\"BSD-3-Clause\",\"repository\":\"https://github.com/cfinke/Typo.js\",\"publisher\":\"Christopher Finke\",\"email\":\"cfinke@gmail.com\",\"url\":\"http://www.chrisfinke.com/\",\"path\":\"/node_modules/typo-js\",\"licenseFile\":\"/node_modules/typo-js/README.md\"},\"uglify-js@3.17.4\":{\"licenses\":\"BSD-2-Clause\",\"repository\":\"https://github.com/mishoo/UglifyJS\",\"publisher\":\"Mihai Bazon\",\"email\":\"mihai.bazon@gmail.com\",\"url\":\"http://lisperator.net/\",\"path\":\"/node_modules/uglify-js\",\"licenseFile\":\"/node_modules/uglify-js/LICENSE\"},\"undici-types@6.21.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/nodejs/undici\",\"path\":\"/node_modules/undici-types\",\"licenseFile\":\"/node_modules/undici-types/LICENSE\"},\"unescape@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jonschlinkert/unescape\",\"publisher\":\"Jon Schlinkert\",\"url\":\"https://github.com/jonschlinkert\",\"path\":\"/node_modules/unescape\",\"licenseFile\":\"/node_modules/unescape/LICENSE\"},\"unicode@14.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/tdanecker/node-unicodetable\",\"publisher\":\"dodo\",\"url\":\"https://github.com/dodo\",\"path\":\"/node_modules/unicode\",\"licenseFile\":\"/node_modules/unicode/LICENSE\"},\"universal-user-agent@7.0.3\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/gr2m/universal-user-agent\",\"publisher\":\"Gregor Martynus\",\"url\":\"https://github.com/gr2m\",\"path\":\"/node_modules/universal-user-agent\",\"licenseFile\":\"/node_modules/universal-user-agent/LICENSE.md\"},\"universalify@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/RyanZim/universalify\",\"publisher\":\"Ryan Zimmerman\",\"email\":\"opensrc@ryanzim.com\",\"path\":\"/node_modules/universalify\",\"licenseFile\":\"/node_modules/universalify/LICENSE\"},\"upper-case@1.1.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/blakeembrey/upper-case\",\"publisher\":\"Blake Embrey\",\"email\":\"hello@blakeembrey.com\",\"url\":\"http://blakeembrey.me\",\"path\":\"/node_modules/upper-case\",\"licenseFile\":\"/node_modules/upper-case/LICENSE\"},\"utif2@4.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/photopea/UTIF.js\",\"publisher\":\"photopea\",\"url\":\"https://github.com/photopea\",\"path\":\"/node_modules/utif2\",\"licenseFile\":\"/node_modules/utif2/LICENSE\"},\"util-deprecate@1.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/TooTallNate/util-deprecate\",\"publisher\":\"Nathan Rajlich\",\"email\":\"nathan@tootallnate.net\",\"url\":\"http://n8.io/\",\"path\":\"/node_modules/util-deprecate\",\"licenseFile\":\"/node_modules/util-deprecate/LICENSE\"},\"uuid@8.3.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/uuidjs/uuid\",\"path\":\"/node_modules/uuid\",\"licenseFile\":\"/node_modules/uuid/LICENSE.md\"},\"uuid@9.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/uuidjs/uuid\",\"path\":\"/node_modules/@smithy/middleware-retry/node_modules/uuid\",\"licenseFile\":\"/node_modules/@smithy/middleware-retry/node_modules/uuid/LICENSE.md\"},\"validate-npm-package-license@3.0.4\":{\"licenses\":\"Apache-2.0\",\"repository\":\"https://github.com/kemitchell/validate-npm-package-license.js\",\"publisher\":\"Kyle E. Mitchell\",\"email\":\"kyle@kemitchell.com\",\"url\":\"https://kemitchell.com\",\"path\":\"/node_modules/validate-npm-package-license\",\"licenseFile\":\"/node_modules/validate-npm-package-license/LICENSE\"},\"vue-color@2.4.5\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/xiaokaike/vue-color\",\"publisher\":\"xiaokai\",\"email\":\"kexiaokai@gmail.com\",\"path\":\"/node_modules/vue-color\",\"licenseFile\":\"/node_modules/vue-color/LICENSE\"},\"vue-i18n@8.24.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/kazupon/vue-i18n\",\"publisher\":\"kazuya kawaguchi\",\"email\":\"kawakazu80@gmail.com\",\"path\":\"/node_modules/vue-i18n\",\"licenseFile\":\"/node_modules/vue-i18n/LICENSE\"},\"vue-multiselect@2.0.8\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/shentao/vue-multiselect\",\"publisher\":\"Damian Dulisz\",\"email\":\"damian.dulisz@gmail.com\",\"path\":\"/node_modules/vue-multiselect\",\"licenseFile\":\"/node_modules/vue-multiselect/LICENSE\"},\"vue-prism-editor@0.6.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/koca/vue-prism-editor\",\"publisher\":\"Mesut Koca\",\"email\":\"imesutkoca@gmail.com\",\"path\":\"/node_modules/vue-prism-editor\",\"licenseFile\":\"/node_modules/vue-prism-editor/LICENSE\"},\"vue-router@3.5.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/vuejs/vue-router\",\"publisher\":\"Evan You\",\"path\":\"/node_modules/vue-router\",\"licenseFile\":\"/node_modules/vue-router/LICENSE\"},\"vue@2.6.14\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/vuejs/vue\",\"publisher\":\"Evan You\",\"path\":\"/node_modules/vue\",\"licenseFile\":\"/node_modules/vue/LICENSE\"},\"vuedraggable@2.24.3\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/SortableJS/Vue.Draggable\",\"path\":\"/node_modules/vuedraggable\",\"licenseFile\":\"/node_modules/vuedraggable/LICENSE\"},\"vuex@3.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/vuejs/vuex\",\"publisher\":\"Evan You\",\"path\":\"/node_modules/vuex\",\"licenseFile\":\"/node_modules/vuex/LICENSE\"},\"webidl-conversions@3.0.1\":{\"licenses\":\"BSD-2-Clause\",\"repository\":\"https://github.com/jsdom/webidl-conversions\",\"publisher\":\"Domenic Denicola\",\"email\":\"d@domenic.me\",\"url\":\"https://domenic.me/\",\"path\":\"/node_modules/webidl-conversions\",\"licenseFile\":\"/node_modules/webidl-conversions/LICENSE.md\"},\"whatwg-fetch@3.6.20\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/github/fetch\",\"path\":\"/node_modules/whatwg-fetch\",\"licenseFile\":\"/node_modules/whatwg-fetch/LICENSE\"},\"whatwg-url@5.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/jsdom/whatwg-url\",\"publisher\":\"Sebastian Mayr\",\"email\":\"github@smayr.name\",\"path\":\"/node_modules/whatwg-url\",\"licenseFile\":\"/node_modules/whatwg-url/LICENSE.txt\"},\"which-typed-array@1.1.19\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/inspect-js/which-typed-array\",\"publisher\":\"Jordan Harband\",\"email\":\"ljharb@gmail.com\",\"url\":\"http://ljharb.codes\",\"path\":\"/node_modules/which-typed-array\",\"licenseFile\":\"/node_modules/which-typed-array/LICENSE\"},\"which@2.0.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/node-which\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me\",\"path\":\"/node_modules/which\",\"licenseFile\":\"/node_modules/which/LICENSE\"},\"wordwrap@1.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/substack/node-wordwrap\",\"publisher\":\"James Halliday\",\"email\":\"mail@substack.net\",\"url\":\"http://substack.net\",\"path\":\"/node_modules/wordwrap\",\"licenseFile\":\"/node_modules/wordwrap/LICENSE\"},\"wrap-ansi@3.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/wrap-ansi\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"sindresorhus.com\",\"path\":\"/node_modules/wrap-ansi\",\"licenseFile\":\"/node_modules/wrap-ansi/license\"},\"wrap-ansi@7.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/wrap-ansi\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/wrap-ansi-cjs\",\"licenseFile\":\"/node_modules/wrap-ansi-cjs/license\"},\"wrap-ansi@8.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/chalk/wrap-ansi\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/@isaacs/cliui/node_modules/wrap-ansi\",\"licenseFile\":\"/node_modules/@isaacs/cliui/node_modules/wrap-ansi/license\"},\"wrappy@1.0.2\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/npm/wrappy\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/wrappy\",\"licenseFile\":\"/node_modules/wrappy/LICENSE\"},\"xcase@2.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/encharm/xcase\",\"publisher\":\"Damian Kaczmarek\",\"email\":\"damian@codecharm.co.uk\",\"path\":\"/node_modules/xcase\",\"licenseFile\":\"/node_modules/xcase/LICENSE\"},\"xhr@2.6.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/naugtur/xhr\",\"publisher\":\"Raynos\",\"email\":\"raynos2@gmail.com\",\"path\":\"/node_modules/xhr\",\"licenseFile\":\"/node_modules/xhr/LICENCE\"},\"xml-parse-from-string@1.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Jam3/xml-parse-from-string\",\"publisher\":\"Matt DesLauriers\",\"email\":\"dave.des@gmail.com\",\"url\":\"https://github.com/mattdesl\",\"path\":\"/node_modules/xml-parse-from-string\",\"licenseFile\":\"/node_modules/xml-parse-from-string/LICENSE.md\"},\"xml2js@0.5.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Leonidas-from-XIV/node-xml2js\",\"publisher\":\"Marek Kubica\",\"email\":\"marek@xivilization.net\",\"url\":\"https://xivilization.net\",\"path\":\"/node_modules/xml2js\",\"licenseFile\":\"/node_modules/xml2js/LICENSE\"},\"xmlbuilder@11.0.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/oozcitak/xmlbuilder-js\",\"publisher\":\"Ozgur Ozcitak\",\"email\":\"oozcitak@gmail.com\",\"path\":\"/node_modules/xmlbuilder\",\"licenseFile\":\"/node_modules/xmlbuilder/LICENSE\"},\"xregexp@2.0.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/slevithan/XRegExp\",\"publisher\":\"Steven Levithan\",\"email\":\"steves_list@hotmail.com\",\"path\":\"/node_modules/xregexp\",\"licenseFile\":\"/node_modules/xregexp/README.md\"},\"xtend@4.0.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/Raynos/xtend\",\"publisher\":\"Raynos\",\"email\":\"raynos2@gmail.com\",\"path\":\"/node_modules/xtend\",\"licenseFile\":\"/node_modules/xtend/LICENSE\"},\"y18n@5.0.8\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/yargs/y18n\",\"publisher\":\"Ben Coe\",\"email\":\"bencoe@gmail.com\",\"path\":\"/node_modules/y18n\",\"licenseFile\":\"/node_modules/y18n/LICENSE\"},\"yallist@4.0.0\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/isaacs/yallist\",\"publisher\":\"Isaac Z. Schlueter\",\"email\":\"i@izs.me\",\"url\":\"http://blog.izs.me/\",\"path\":\"/node_modules/yallist\",\"licenseFile\":\"/node_modules/yallist/LICENSE\"},\"yargs-parser@21.1.1\":{\"licenses\":\"ISC\",\"repository\":\"https://github.com/yargs/yargs-parser\",\"publisher\":\"Ben Coe\",\"email\":\"ben@npmjs.com\",\"path\":\"/node_modules/yargs-parser\",\"licenseFile\":\"/node_modules/yargs-parser/LICENSE.txt\"},\"yargs@17.7.2\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/yargs/yargs\",\"path\":\"/node_modules/yargs\",\"licenseFile\":\"/node_modules/yargs/LICENSE\"},\"yocto-queue@0.1.0\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/sindresorhus/yocto-queue\",\"publisher\":\"Sindre Sorhus\",\"email\":\"sindresorhus@gmail.com\",\"url\":\"https://sindresorhus.com\",\"path\":\"/node_modules/yocto-queue\",\"licenseFile\":\"/node_modules/yocto-queue/license\"},\"zip-stream@4.1.1\":{\"licenses\":\"MIT\",\"repository\":\"https://github.com/archiverjs/node-zip-stream\",\"publisher\":\"Chris Talkington\",\"url\":\"http://christalkington.com/\",\"path\":\"/node_modules/zip-stream\",\"licenseFile\":\"/node_modules/zip-stream/LICENSE\"},\"electron\":{\"url\":\"https://electronjs.org/\"},\"normalize.css\":{\"url\":\"https://github.com/necolas/normalize.css\"},\"locutus\":{\"url\":\"http://locutus.io/\"},\"codemirror\":{\"url\":\"https://codemirror.net/\"},\"tinycolorpicker\":{\"url\":\"http://www.dematte.at/tinyColorPicker/\"},\"tinymce\":{\"url\":\"https://www.tiny.cloud/\"},\"libvips\":{\"url\":\"https://github.com/jcupitt/libvips\"},\"handlebars\":{\"url\":\"http://handlebarsjs.com/\"},\"jquery\":{\"url\":\"http://jquery.com/\"},\"jquery-ui\":{\"url\":\"https://jqueryui.com/\"},\"nested-sortable\":{\"url\":\"https://github.com/ilikenwf/nestedSortable\"},\"select2\":{\"url\":\"https://select2.github.io/\"},\"node-slug\":{\"url\":\"https://github.com/dodo/node-slug\"},\"feathericons\":{\"url\":\"https://feathericons.com\"},\"tabler-icons\":{\"url\":\"https://tabler-icons.io/\"},\"lucide\":{\"url\":\"https://lucide.dev/\"}}"
  },
  {
    "path": "app/licenses/assert-plus/license.txt",
    "content": "The MIT License (MIT) Copyright (c) 2012 Mark Cavage\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/base64url/license.txt",
    "content": "Copyright (c) 2013–2016 Brian J. Brennan\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/bindings/license.txt",
    "content": "Copyright (c) 2012 Nathan Rajlich &lt;nathan@tootallnate.net&gt;\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/brace-expansion/license.txt",
    "content": "(MIT)\n\nCopyright (c) 2013 Julian Gruber <julian@juliangruber.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/buffer-alloc/license.txt",
    "content": "MIT License\n\nCopyright (c) 2016, 2018 Linus Unnebäck\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "app/licenses/buffer-alloc-unsafe/license.txt",
    "content": "MIT License\n\nCopyright (c) 2016, 2018 Linus Unnebäck\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "app/licenses/buffer-equal/license.txt",
    "content": "This software is released under the MIT license:\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/buffer-fill/license.txt",
    "content": "MIT License\n\nCopyright (c) 2016, 2018 Linus Unnebäck\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "app/licenses/buffer-from/license.txt",
    "content": "MIT License\n\nCopyright (c) 2016, 2018 Linus Unnebäck\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/licenses/bufferjs/license.txt",
    "content": "Copyright (c) 2010 AJ ONeal (and Contributors)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/capture-stack-trace/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) Vsevolod Strukchinsky <floatdrop@gmail.com> (github.com/floatdrop)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/chainsaw/license.txt",
    "content": "Copyright 2010 James Halliday (mail@substack.net)\n\nThis project is free software released under the MIT license:\nhttp://www.opensource.org/licenses/mit-license.php\n"
  },
  {
    "path": "app/licenses/cli/license.txt",
    "content": "(MIT license)\n\nCopyright (c) 2010 Chris O'Hara <cohara87@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/clipboard/license.txt",
    "content": "MIT License\n\nCopyright (c) Zeno Rocha\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/licenses/codemirror/license.txt",
    "content": "MIT License\n\nCopyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/colors/license.txt",
    "content": "Original Library\n  - Copyright (c) Marak Squires\n\nAdditional Functionality\n - Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/commander/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/deep-equal/license.txt",
    "content": "\nMIT. Derived largely from node's assert module.\n"
  },
  {
    "path": "app/licenses/devtron/license.txt",
    "content": "Copyright (c) 2016 GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/diff/license.txt",
    "content": "Software License Agreement (BSD License)\n\nCopyright (c) 2009-2011, Kevin Decker kpdecker@gmail.com\n\nAll rights reserved.\n\nRedistribution and use of this software in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above\n  copyright notice, this list of conditions and the\n  following disclaimer.\n\n* Redistributions in binary form must reproduce the above\n  copyright notice, this list of conditions and the\n  following disclaimer in the documentation and/or other\n  materials provided with the distribution.\n\n* Neither the name of Kevin Decker nor the names of its\n  contributors may be used to endorse or promote products\n  derived from this software without specific prior\n  written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR\nIMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER\nIN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT\nOF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "app/licenses/electron/license.txt",
    "content": "Copyright (c) 2013-2020 GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/end-of-stream/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Mathias Buus\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/es6-promisify/license.txt",
    "content": ""
  },
  {
    "path": "app/licenses/feathericons/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013-2017 Cole Bemis\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/licenses/follow-redirects/license.txt",
    "content": "Copyright 2017 Olivier Lalonde <olalonde@gmail.com>, James Talmage <james@talmage.io>, Ruben Verborgh\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/licenses/fresh/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>\nCopyright (c) 2016-2017 Douglas Christopher Wilson <doug@somethingdoug.com>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/generate-function/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Mathias Buus\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/get-stdin/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/growl/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2009 TJ Holowaychuk <tj@vision-media.ca>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/handlebars/license.txt",
    "content": "Copyright (C) 2011-2016 by Yehuda Katz\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/has/license.txt",
    "content": "Copyright (c) 2013 Thiago de Arruda\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/he/license.txt",
    "content": "Copyright Mathias Bynens <https://mathiasbynens.be/>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/https-proxy-agent/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/humps/license.txt",
    "content": "humps is copyright © 2012+ Dom Christie and released under the MIT license."
  },
  {
    "path": "app/licenses/ignore/license.txt",
    "content": "Copyright (c) 2013 Kael Zhang <i@kael.me>, contributors\nhttp://kael.me/\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/imurmurhash/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Gary Court, Jens Taylor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/invert-kv/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/isarray/license.txt",
    "content": "(MIT)\n\nCopyright (c) 2013 Julian Gruber <julian@juliangruber.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/jquery/license.txt",
    "content": "Copyright JS Foundation and other contributors, https://js.foundation/\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/jquery-ui/license.txt",
    "content": "Copyright jQuery Foundation and other contributors, https://jquery.org/\n\nThis software consists of voluntary contributions made by many\nindividuals. For exact contribution history, see the revision history\navailable at https://github.com/jquery/jquery-ui\n\nThe following license applies to all parts of this software except as\ndocumented below:\n\n====\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n====\n\nCopyright and related rights for sample code are waived via CC0. Sample\ncode is defined as all source code contained within the demos directory.\n\nCC0: http://creativecommons.org/publicdomain/zero/1.0/\n\n====\n\nAll files located in the node_modules and external directories are\nexternally maintained libraries used by this software which have their\nown licenses; we recommend you read them, as their terms may differ from\nthe terms above.\n"
  },
  {
    "path": "app/licenses/json-schema/license.txt",
    "content": "Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com)\nLicensed under the MIT (MIT-LICENSE.txt) license.\n"
  },
  {
    "path": "app/licenses/lazystream/license.txt",
    "content": "Copyright (c) 2013 J. Pommerening, contributors.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/libvips/license.txt",
    "content": "                  GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\f\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\f\n                  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n\n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n                            NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\f\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n"
  },
  {
    "path": "app/licenses/licenses.json",
    "content": "{\n  \"aws-crypto/crc32@5.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/node_modules/@aws-crypto/crc32\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/crc32/LICENSE\"\n  },\n  \"aws-crypto/crc32c@5.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/node_modules/@aws-crypto/crc32c\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/crc32c/LICENSE\"\n  },\n  \"aws-crypto/sha1-browser@5.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/node_modules/@aws-crypto/sha1-browser\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/sha1-browser/LICENSE\"\n  },\n  \"aws-crypto/sha256-browser@5.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/node_modules/@aws-crypto/sha256-browser\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/sha256-browser/LICENSE\"\n  },\n  \"aws-crypto/sha256-js@5.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/node_modules/@aws-crypto/sha256-js\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/sha256-js/LICENSE\"\n  },\n  \"aws-crypto/supports-web-crypto@5.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/node_modules/@aws-crypto/supports-web-crypto\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/supports-web-crypto/LICENSE\"\n  },\n  \"aws-crypto/util@5.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-crypto-helpers\",\n    \"publisher\": \"AWS Crypto Tools Team\",\n    \"email\": \"aws-cryptools@amazon.com\",\n    \"url\": \"https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us\",\n    \"path\": \"/node_modules/@aws-crypto/util\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/util/LICENSE\"\n  },\n  \"aws-sdk/client-s3@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/client-s3\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/client-s3/LICENSE\"\n  },\n  \"aws-sdk/client-sso-oidc@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/client-sso-oidc\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/client-sso-oidc/LICENSE\"\n  },\n  \"aws-sdk/client-sso@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/client-sso\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/client-sso/LICENSE\"\n  },\n  \"aws-sdk/client-sts@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/client-sts\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/client-sts/LICENSE\"\n  },\n  \"aws-sdk/core@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/core\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/core/README.md\"\n  },\n  \"aws-sdk/credential-provider-env@3.620.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/credential-provider-env\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/credential-provider-env/LICENSE\"\n  },\n  \"aws-sdk/credential-provider-http@3.622.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/credential-provider-http\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/credential-provider-http/README.md\"\n  },\n  \"aws-sdk/credential-provider-ini@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/credential-provider-ini\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/credential-provider-ini/LICENSE\"\n  },\n  \"aws-sdk/credential-provider-node@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/credential-provider-node\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/credential-provider-node/LICENSE\"\n  },\n  \"aws-sdk/credential-provider-process@3.620.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/credential-provider-process\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/credential-provider-process/LICENSE\"\n  },\n  \"aws-sdk/credential-provider-sso@3.623.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/credential-provider-sso\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/credential-provider-sso/LICENSE\"\n  },\n  \"aws-sdk/credential-provider-web-identity@3.621.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/credential-provider-web-identity\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/credential-provider-web-identity/LICENSE\"\n  },\n  \"aws-sdk/middleware-bucket-endpoint@3.620.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-bucket-endpoint\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-bucket-endpoint/LICENSE\"\n  },\n  \"aws-sdk/middleware-expect-continue@3.620.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-expect-continue\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-expect-continue/LICENSE\"\n  },\n  \"aws-sdk/middleware-flexible-checksums@3.620.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-flexible-checksums\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-flexible-checksums/LICENSE\"\n  },\n  \"aws-sdk/middleware-host-header@3.620.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-host-header\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-host-header/LICENSE\"\n  },\n  \"aws-sdk/middleware-location-constraint@3.609.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-location-constraint\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-location-constraint/LICENSE\"\n  },\n  \"aws-sdk/middleware-logger@3.609.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-logger\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-logger/LICENSE\"\n  },\n  \"aws-sdk/middleware-recursion-detection@3.620.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-recursion-detection\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-recursion-detection/LICENSE\"\n  },\n  \"aws-sdk/middleware-sdk-s3@3.622.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-sdk-s3\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-sdk-s3/LICENSE\"\n  },\n  \"aws-sdk/middleware-signing@3.620.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-signing\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-signing/LICENSE\"\n  },\n  \"aws-sdk/middleware-ssec@3.609.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-ssec\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-ssec/LICENSE\"\n  },\n  \"aws-sdk/middleware-user-agent@3.620.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/middleware-user-agent\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/middleware-user-agent/LICENSE\"\n  },\n  \"aws-sdk/region-config-resolver@3.614.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/region-config-resolver\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/region-config-resolver/LICENSE\"\n  },\n  \"aws-sdk/signature-v4-multi-region@3.622.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/signature-v4-multi-region\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/signature-v4-multi-region/LICENSE\"\n  },\n  \"aws-sdk/token-providers@3.614.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/token-providers\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/token-providers/LICENSE\"\n  },\n  \"aws-sdk/types@3.609.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/types\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/types/LICENSE\"\n  },\n  \"aws-sdk/util-arn-parser@3.568.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/util-arn-parser\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/util-arn-parser/LICENSE\"\n  },\n  \"aws-sdk/util-endpoints@3.614.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/util-endpoints\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/util-endpoints/LICENSE\"\n  },\n  \"aws-sdk/util-locate-window@3.568.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/util-locate-window\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/util-locate-window/LICENSE\"\n  },\n  \"aws-sdk/util-user-agent-browser@3.609.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/util-user-agent-browser\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/util-user-agent-browser/LICENSE\"\n  },\n  \"aws-sdk/util-user-agent-node@3.614.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/util-user-agent-node\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/util-user-agent-node/LICENSE\"\n  },\n  \"aws-sdk/xml-builder@3.609.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/aws/aws-sdk-js-v3\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-sdk/xml-builder\",\n    \"licenseFile\": \"/node_modules/@aws-sdk/xml-builder/LICENSE\"\n  },\n  \"gitbeaker/core@35.8.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jdalrymple/gitbeaker\",\n    \"publisher\": \"Justin Dalrymple\",\n    \"path\": \"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/core\",\n    \"licenseFile\": \"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/core/README.md\"\n  },\n  \"gitbeaker/node@35.8.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jdalrymple/gitbeaker\",\n    \"publisher\": \"Justin Dalrymple\",\n    \"path\": \"/node_modules/@gitbeaker/node\",\n    \"licenseFile\": \"/node_modules/@gitbeaker/node/README.md\"\n  },\n  \"gitbeaker/requester-utils@35.8.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jdalrymple/gitbeaker\",\n    \"publisher\": \"Justin Dalrymple\",\n    \"path\": \"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/requester-utils\",\n    \"licenseFile\": \"/node_modules/@gitbeaker/node/node_modules/@gitbeaker/requester-utils/README.md\"\n  },\n  \"google-cloud/paginator@3.0.7\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/nodejs-paginator\",\n    \"publisher\": \"Google Inc.\",\n    \"path\": \"/node_modules/@google-cloud/paginator\",\n    \"licenseFile\": \"/node_modules/@google-cloud/paginator/LICENSE\"\n  },\n  \"google-cloud/projectify@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/nodejs-projectify\",\n    \"publisher\": \"Google Inc.\",\n    \"path\": \"/node_modules/@google-cloud/projectify\",\n    \"licenseFile\": \"/node_modules/@google-cloud/projectify/LICENSE\"\n  },\n  \"google-cloud/promisify@3.0.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/nodejs-promisify\",\n    \"publisher\": \"Google Inc.\",\n    \"path\": \"/node_modules/@google-cloud/promisify\",\n    \"licenseFile\": \"/node_modules/@google-cloud/promisify/LICENSE\"\n  },\n  \"google-cloud/storage@6.11.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/nodejs-storage\",\n    \"publisher\": \"Google Inc.\",\n    \"path\": \"/node_modules/@google-cloud/storage\",\n    \"licenseFile\": \"/node_modules/@google-cloud/storage/LICENSE\"\n  },\n  \"img/sharp-darwin-arm64@0.34.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/lovell/sharp\",\n    \"publisher\": \"Lovell Fuller\",\n    \"email\": \"npm@lovell.info\",\n    \"path\": \"/node_modules/@img/sharp-darwin-arm64\",\n    \"licenseFile\": \"/node_modules/@img/sharp-darwin-arm64/LICENSE\"\n  },\n  \"img/sharp-libvips-darwin-arm64@1.2.0\": {\n    \"licenses\": \"LGPL-3.0-or-later\",\n    \"repository\": \"https://github.com/lovell/sharp-libvips\",\n    \"publisher\": \"Lovell Fuller\",\n    \"email\": \"npm@lovell.info\",\n    \"path\": \"/node_modules/@img/sharp-libvips-darwin-arm64\",\n    \"licenseFile\": \"/node_modules/@img/sharp-libvips-darwin-arm64/README.md\"\n  },\n  \"isaacs/cliui@8.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/yargs/cliui\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"ben@npmjs.com\",\n    \"path\": \"/node_modules/@isaacs/cliui\",\n    \"licenseFile\": \"/node_modules/@isaacs/cliui/LICENSE.txt\"\n  },\n  \"jimp/bmp@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/bmp\",\n    \"licenseFile\": \"/node_modules/@jimp/bmp/LICENSE\"\n  },\n  \"jimp/core@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"publisher\": \"Oliver Moran\",\n    \"email\": \"oliver.moran@gmail.com\",\n    \"path\": \"/node_modules/@jimp/core\",\n    \"licenseFile\": \"/node_modules/@jimp/core/LICENSE\"\n  },\n  \"jimp/custom@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/custom\",\n    \"licenseFile\": \"/node_modules/@jimp/custom/LICENSE\"\n  },\n  \"jimp/gif@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/gif\",\n    \"licenseFile\": \"/node_modules/@jimp/gif/LICENSE\"\n  },\n  \"jimp/jpeg@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/jpeg\",\n    \"licenseFile\": \"/node_modules/@jimp/jpeg/LICENSE\"\n  },\n  \"jimp/plugin-blit@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-blit\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-blit/LICENSE\"\n  },\n  \"jimp/plugin-blur@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-blur\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-blur/LICENSE\"\n  },\n  \"jimp/plugin-circle@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-circle\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-circle/LICENSE\"\n  },\n  \"jimp/plugin-color@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-color\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-color/LICENSE\"\n  },\n  \"jimp/plugin-contain@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-contain\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-contain/LICENSE\"\n  },\n  \"jimp/plugin-cover@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-cover\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-cover/LICENSE\"\n  },\n  \"jimp/plugin-crop@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-crop\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-crop/LICENSE\"\n  },\n  \"jimp/plugin-displace@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-displace\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-displace/LICENSE\"\n  },\n  \"jimp/plugin-dither@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-dither\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-dither/LICENSE\"\n  },\n  \"jimp/plugin-fisheye@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-fisheye\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-fisheye/LICENSE\"\n  },\n  \"jimp/plugin-flip@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-flip\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-flip/LICENSE\"\n  },\n  \"jimp/plugin-gaussian@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-gaussian\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-gaussian/LICENSE\"\n  },\n  \"jimp/plugin-invert@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-invert\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-invert/LICENSE\"\n  },\n  \"jimp/plugin-mask@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-mask\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-mask/LICENSE\"\n  },\n  \"jimp/plugin-normalize@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-normalize\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-normalize/LICENSE\"\n  },\n  \"jimp/plugin-print@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-print\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-print/LICENSE\"\n  },\n  \"jimp/plugin-resize@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-resize\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-resize/LICENSE\"\n  },\n  \"jimp/plugin-rotate@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-rotate\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-rotate/LICENSE\"\n  },\n  \"jimp/plugin-scale@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-scale\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-scale/LICENSE\"\n  },\n  \"jimp/plugin-shadow@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-shadow\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-shadow/LICENSE\"\n  },\n  \"jimp/plugin-threshold@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugin-threshold\",\n    \"licenseFile\": \"/node_modules/@jimp/plugin-threshold/LICENSE\"\n  },\n  \"jimp/plugins@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/plugins\",\n    \"licenseFile\": \"/node_modules/@jimp/plugins/LICENSE\"\n  },\n  \"jimp/png@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/png\",\n    \"licenseFile\": \"/node_modules/@jimp/png/LICENSE\"\n  },\n  \"jimp/tiff@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/tiff\",\n    \"licenseFile\": \"/node_modules/@jimp/tiff/LICENSE\"\n  },\n  \"jimp/types@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/types\",\n    \"licenseFile\": \"/node_modules/@jimp/types/LICENSE\"\n  },\n  \"jimp/utils@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"path\": \"/node_modules/@jimp/utils\",\n    \"licenseFile\": \"/node_modules/@jimp/utils/LICENSE\"\n  },\n  \"octokit/auth-token@6.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/auth-token.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/auth-token\",\n    \"licenseFile\": \"/node_modules/@octokit/auth-token/LICENSE\"\n  },\n  \"octokit/core@7.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/core.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/core\",\n    \"licenseFile\": \"/node_modules/@octokit/core/LICENSE\"\n  },\n  \"octokit/endpoint@11.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/endpoint.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/endpoint\",\n    \"licenseFile\": \"/node_modules/@octokit/endpoint/LICENSE\"\n  },\n  \"octokit/graphql@9.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/graphql.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/graphql\",\n    \"licenseFile\": \"/node_modules/@octokit/graphql/LICENSE\"\n  },\n  \"octokit/openapi-types@25.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/openapi-types.ts\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://twitter.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/openapi-types\",\n    \"licenseFile\": \"/node_modules/@octokit/openapi-types/LICENSE\"\n  },\n  \"octokit/plugin-paginate-rest@13.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/plugin-paginate-rest.js\",\n    \"path\": \"/node_modules/@octokit/plugin-paginate-rest\",\n    \"licenseFile\": \"/node_modules/@octokit/plugin-paginate-rest/LICENSE\"\n  },\n  \"octokit/plugin-request-log@6.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/plugin-request-log.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://twitter.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/plugin-request-log\",\n    \"licenseFile\": \"/node_modules/@octokit/plugin-request-log/LICENSE\"\n  },\n  \"octokit/plugin-rest-endpoint-methods@16.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/plugin-rest-endpoint-methods.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://twitter.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/plugin-rest-endpoint-methods\",\n    \"licenseFile\": \"/node_modules/@octokit/plugin-rest-endpoint-methods/LICENSE\"\n  },\n  \"octokit/request-error@7.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/request-error.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/request-error\",\n    \"licenseFile\": \"/node_modules/@octokit/request-error/LICENSE\"\n  },\n  \"octokit/request@10.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/request.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/request\",\n    \"licenseFile\": \"/node_modules/@octokit/request/LICENSE\"\n  },\n  \"octokit/rest@22.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/rest.js\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/rest\",\n    \"licenseFile\": \"/node_modules/@octokit/rest/LICENSE\"\n  },\n  \"octokit/types@14.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/octokit/types.ts\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://twitter.com/gr2m\",\n    \"path\": \"/node_modules/@octokit/types\",\n    \"licenseFile\": \"/node_modules/@octokit/types/LICENSE\"\n  },\n  \"one-ini/wasm@0.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/one-ini/core\",\n    \"path\": \"/node_modules/@one-ini/wasm\",\n    \"licenseFile\": \"/node_modules/@one-ini/wasm/LICENSE\"\n  },\n  \"pkgjs/parseargs@0.11.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/pkgjs/parseargs\",\n    \"path\": \"/node_modules/@pkgjs/parseargs\",\n    \"licenseFile\": \"/node_modules/@pkgjs/parseargs/LICENSE\"\n  },\n  \"sindresorhus/is@4.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/@sindresorhus/is\",\n    \"licenseFile\": \"/node_modules/@sindresorhus/is/license\"\n  },\n  \"smithy/abort-controller@3.1.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/abort-controller\",\n    \"licenseFile\": \"/node_modules/@smithy/abort-controller/LICENSE\"\n  },\n  \"smithy/chunked-blob-reader-native@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/chunked-blob-reader-native\",\n    \"licenseFile\": \"/node_modules/@smithy/chunked-blob-reader-native/LICENSE\"\n  },\n  \"smithy/chunked-blob-reader@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/chunked-blob-reader\",\n    \"licenseFile\": \"/node_modules/@smithy/chunked-blob-reader/LICENSE\"\n  },\n  \"smithy/config-resolver@3.0.5\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/config-resolver\",\n    \"licenseFile\": \"/node_modules/@smithy/config-resolver/LICENSE\"\n  },\n  \"smithy/core@2.3.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS Smithy Team\",\n    \"url\": \"https://smithy.io\",\n    \"path\": \"/node_modules/@smithy/core\",\n    \"licenseFile\": \"/node_modules/@smithy/core/LICENSE\"\n  },\n  \"smithy/credential-provider-imds@3.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/credential-provider-imds\",\n    \"licenseFile\": \"/node_modules/@smithy/credential-provider-imds/LICENSE\"\n  },\n  \"smithy/eventstream-codec@3.1.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/eventstream-codec\",\n    \"licenseFile\": \"/node_modules/@smithy/eventstream-codec/LICENSE\"\n  },\n  \"smithy/eventstream-serde-browser@3.0.5\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/eventstream-serde-browser\",\n    \"licenseFile\": \"/node_modules/@smithy/eventstream-serde-browser/LICENSE\"\n  },\n  \"smithy/eventstream-serde-config-resolver@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/eventstream-serde-config-resolver\",\n    \"licenseFile\": \"/node_modules/@smithy/eventstream-serde-config-resolver/LICENSE\"\n  },\n  \"smithy/eventstream-serde-node@3.0.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/eventstream-serde-node\",\n    \"licenseFile\": \"/node_modules/@smithy/eventstream-serde-node/LICENSE\"\n  },\n  \"smithy/eventstream-serde-universal@3.0.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/eventstream-serde-universal\",\n    \"licenseFile\": \"/node_modules/@smithy/eventstream-serde-universal/LICENSE\"\n  },\n  \"smithy/fetch-http-handler@3.2.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/fetch-http-handler\",\n    \"licenseFile\": \"/node_modules/@smithy/fetch-http-handler/LICENSE\"\n  },\n  \"smithy/hash-blob-browser@3.1.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/hash-blob-browser\",\n    \"licenseFile\": \"/node_modules/@smithy/hash-blob-browser/LICENSE\"\n  },\n  \"smithy/hash-node@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/hash-node\",\n    \"licenseFile\": \"/node_modules/@smithy/hash-node/LICENSE\"\n  },\n  \"smithy/hash-stream-node@3.1.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/hash-stream-node\",\n    \"licenseFile\": \"/node_modules/@smithy/hash-stream-node/LICENSE\"\n  },\n  \"smithy/invalid-dependency@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/invalid-dependency\",\n    \"licenseFile\": \"/node_modules/@smithy/invalid-dependency/LICENSE\"\n  },\n  \"smithy/is-array-buffer@2.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer/LICENSE\"\n  },\n  \"smithy/is-array-buffer@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/is-array-buffer\",\n    \"licenseFile\": \"/node_modules/@smithy/is-array-buffer/LICENSE\"\n  },\n  \"smithy/md5-js@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/md5-js\",\n    \"licenseFile\": \"/node_modules/@smithy/md5-js/LICENSE\"\n  },\n  \"smithy/middleware-content-length@3.0.5\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/middleware-content-length\",\n    \"licenseFile\": \"/node_modules/@smithy/middleware-content-length/LICENSE\"\n  },\n  \"smithy/middleware-endpoint@3.1.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/middleware-endpoint\",\n    \"licenseFile\": \"/node_modules/@smithy/middleware-endpoint/LICENSE\"\n  },\n  \"smithy/middleware-retry@3.0.14\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/middleware-retry\",\n    \"licenseFile\": \"/node_modules/@smithy/middleware-retry/LICENSE\"\n  },\n  \"smithy/middleware-serde@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/middleware-serde\",\n    \"licenseFile\": \"/node_modules/@smithy/middleware-serde/LICENSE\"\n  },\n  \"smithy/middleware-stack@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/middleware-stack\",\n    \"licenseFile\": \"/node_modules/@smithy/middleware-stack/LICENSE\"\n  },\n  \"smithy/node-config-provider@3.1.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/node-config-provider\",\n    \"licenseFile\": \"/node_modules/@smithy/node-config-provider/LICENSE\"\n  },\n  \"smithy/node-http-handler@3.1.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/node-http-handler\",\n    \"licenseFile\": \"/node_modules/@smithy/node-http-handler/LICENSE\"\n  },\n  \"smithy/property-provider@3.1.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/property-provider\",\n    \"licenseFile\": \"/node_modules/@smithy/property-provider/LICENSE\"\n  },\n  \"smithy/protocol-http@4.1.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS Smithy Team\",\n    \"url\": \"https://smithy.io\",\n    \"path\": \"/node_modules/@smithy/protocol-http\",\n    \"licenseFile\": \"/node_modules/@smithy/protocol-http/LICENSE\"\n  },\n  \"smithy/querystring-builder@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/querystring-builder\",\n    \"licenseFile\": \"/node_modules/@smithy/querystring-builder/LICENSE\"\n  },\n  \"smithy/querystring-parser@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/querystring-parser\",\n    \"licenseFile\": \"/node_modules/@smithy/querystring-parser/LICENSE\"\n  },\n  \"smithy/service-error-classification@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/service-error-classification\",\n    \"licenseFile\": \"/node_modules/@smithy/service-error-classification/LICENSE\"\n  },\n  \"smithy/shared-ini-file-loader@3.1.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/shared-ini-file-loader\",\n    \"licenseFile\": \"/node_modules/@smithy/shared-ini-file-loader/LICENSE\"\n  },\n  \"smithy/signature-v4@4.1.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/signature-v4\",\n    \"licenseFile\": \"/node_modules/@smithy/signature-v4/LICENSE\"\n  },\n  \"smithy/smithy-client@3.1.12\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/smithy-client\",\n    \"licenseFile\": \"/node_modules/@smithy/smithy-client/LICENSE\"\n  },\n  \"smithy/types@3.3.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS Smithy Team\",\n    \"url\": \"https://smithy.io\",\n    \"path\": \"/node_modules/@smithy/types\",\n    \"licenseFile\": \"/node_modules/@smithy/types/LICENSE\"\n  },\n  \"smithy/url-parser@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/url-parser\",\n    \"licenseFile\": \"/node_modules/@smithy/url-parser/LICENSE\"\n  },\n  \"smithy/util-base64@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-base64\",\n    \"licenseFile\": \"/node_modules/@smithy/util-base64/LICENSE\"\n  },\n  \"smithy/util-body-length-browser@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-body-length-browser\",\n    \"licenseFile\": \"/node_modules/@smithy/util-body-length-browser/LICENSE\"\n  },\n  \"smithy/util-body-length-node@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-body-length-node\",\n    \"licenseFile\": \"/node_modules/@smithy/util-body-length-node/LICENSE\"\n  },\n  \"smithy/util-buffer-from@2.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from/LICENSE\"\n  },\n  \"smithy/util-buffer-from@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-buffer-from\",\n    \"licenseFile\": \"/node_modules/@smithy/util-buffer-from/LICENSE\"\n  },\n  \"smithy/util-config-provider@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-config-provider\",\n    \"licenseFile\": \"/node_modules/@smithy/util-config-provider/LICENSE\"\n  },\n  \"smithy/util-defaults-mode-browser@3.0.14\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-defaults-mode-browser\",\n    \"licenseFile\": \"/node_modules/@smithy/util-defaults-mode-browser/LICENSE\"\n  },\n  \"smithy/util-defaults-mode-node@3.0.14\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-defaults-mode-node\",\n    \"licenseFile\": \"/node_modules/@smithy/util-defaults-mode-node/LICENSE\"\n  },\n  \"smithy/util-endpoints@2.0.5\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-endpoints\",\n    \"licenseFile\": \"/node_modules/@smithy/util-endpoints/LICENSE\"\n  },\n  \"smithy/util-hex-encoding@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-hex-encoding\",\n    \"licenseFile\": \"/node_modules/@smithy/util-hex-encoding/LICENSE\"\n  },\n  \"smithy/util-middleware@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-middleware\",\n    \"licenseFile\": \"/node_modules/@smithy/util-middleware/LICENSE\"\n  },\n  \"smithy/util-retry@3.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-retry\",\n    \"licenseFile\": \"/node_modules/@smithy/util-retry/LICENSE\"\n  },\n  \"smithy/util-stream@3.1.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-stream\",\n    \"licenseFile\": \"/node_modules/@smithy/util-stream/LICENSE\"\n  },\n  \"smithy/util-uri-escape@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-uri-escape\",\n    \"licenseFile\": \"/node_modules/@smithy/util-uri-escape/LICENSE\"\n  },\n  \"smithy/util-utf8@2.3.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8\",\n    \"licenseFile\": \"/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8/LICENSE\"\n  },\n  \"smithy/util-utf8@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-utf8\",\n    \"licenseFile\": \"/node_modules/@smithy/util-utf8/LICENSE\"\n  },\n  \"smithy/util-waiter@3.1.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/awslabs/smithy-typescript\",\n    \"publisher\": \"AWS SDK for JavaScript Team\",\n    \"url\": \"https://aws.amazon.com/javascript/\",\n    \"path\": \"/node_modules/@smithy/util-waiter\",\n    \"licenseFile\": \"/node_modules/@smithy/util-waiter/LICENSE\"\n  },\n  \"szmarczak/http-timer@4.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/szmarczak/http-timer\",\n    \"publisher\": \"Szymon Marczak\",\n    \"path\": \"/node_modules/@szmarczak/http-timer\",\n    \"licenseFile\": \"/node_modules/@szmarczak/http-timer/LICENSE\"\n  },\n  \"tokenizer/token@0.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/tokenizer-token\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/node_modules/@tokenizer/token\",\n    \"licenseFile\": \"/node_modules/@tokenizer/token/README.md\"\n  },\n  \"tootallnate/once@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/once\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/node_modules/@tootallnate/once\",\n    \"licenseFile\": \"/node_modules/@tootallnate/once/LICENSE\"\n  },\n  \"types/cacheable-request@6.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/cacheable-request\",\n    \"licenseFile\": \"/node_modules/@types/cacheable-request/LICENSE\"\n  },\n  \"types/codemirror@5.60.15\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/codemirror\",\n    \"licenseFile\": \"/node_modules/@types/codemirror/LICENSE\"\n  },\n  \"types/estree@1.0.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/estree\",\n    \"licenseFile\": \"/node_modules/@types/estree/LICENSE\"\n  },\n  \"types/http-cache-semantics@4.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/http-cache-semantics\",\n    \"licenseFile\": \"/node_modules/@types/http-cache-semantics/LICENSE\"\n  },\n  \"types/keyv@3.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/keyv\",\n    \"licenseFile\": \"/node_modules/@types/keyv/LICENSE\"\n  },\n  \"types/marked@4.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/marked\",\n    \"licenseFile\": \"/node_modules/@types/marked/LICENSE\"\n  },\n  \"types/node@16.9.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/image-q/node_modules/@types/node\",\n    \"licenseFile\": \"/node_modules/image-q/node_modules/@types/node/LICENSE\"\n  },\n  \"types/node@22.17.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/node\",\n    \"licenseFile\": \"/node_modules/@types/node/LICENSE\"\n  },\n  \"types/responselike@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/responselike\",\n    \"licenseFile\": \"/node_modules/@types/responselike/LICENSE\"\n  },\n  \"types/tern@0.23.9\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/tern\",\n    \"licenseFile\": \"/node_modules/@types/tern/LICENSE\"\n  },\n  \"types/trusted-types@2.0.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/DefinitelyTyped/DefinitelyTyped\",\n    \"path\": \"/node_modules/@types/trusted-types\",\n    \"licenseFile\": \"/node_modules/@types/trusted-types/LICENSE\"\n  },\n  \"abbrev@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/abbrev-js\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/node_modules/abbrev\",\n    \"licenseFile\": \"/node_modules/abbrev/LICENSE\"\n  },\n  \"abort-controller@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mysticatea/abort-controller\",\n    \"publisher\": \"Toru Nagashima\",\n    \"url\": \"https://github.com/mysticatea\",\n    \"path\": \"/node_modules/abort-controller\",\n    \"licenseFile\": \"/node_modules/abort-controller/LICENSE\"\n  },\n  \"adm-zip@0.5.10\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/cthackers/adm-zip\",\n    \"publisher\": \"Nasca Iacob\",\n    \"email\": \"sy@another-d-mention.ro\",\n    \"url\": \"https://github.com/cthackers\",\n    \"path\": \"/node_modules/adm-zip\",\n    \"licenseFile\": \"/node_modules/adm-zip/LICENSE\"\n  },\n  \"agent-base@6.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-agent-base\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/node_modules/agent-base\",\n    \"licenseFile\": \"/node_modules/agent-base/README.md\"\n  },\n  \"ansi-diff-stream@1.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/ansi-diff-stream\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/ansi-diff-stream\",\n    \"licenseFile\": \"/node_modules/ansi-diff-stream/LICENSE\"\n  },\n  \"ansi-regex@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/ansi-regex\",\n    \"licenseFile\": \"/node_modules/ansi-regex/license\"\n  },\n  \"ansi-regex@3.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/strip-ansi/node_modules/ansi-regex\",\n    \"licenseFile\": \"/node_modules/strip-ansi/node_modules/ansi-regex/license\"\n  },\n  \"ansi-regex@5.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/string-width-cjs/node_modules/ansi-regex\",\n    \"licenseFile\": \"/node_modules/string-width-cjs/node_modules/ansi-regex/license\"\n  },\n  \"ansi-regex@6.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/@isaacs/cliui/node_modules/ansi-regex\",\n    \"licenseFile\": \"/node_modules/@isaacs/cliui/node_modules/ansi-regex/license\"\n  },\n  \"ansi-styles@4.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-styles\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/ansi-styles\",\n    \"licenseFile\": \"/node_modules/ansi-styles/license\"\n  },\n  \"ansi-styles@6.2.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/ansi-styles\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/@isaacs/cliui/node_modules/ansi-styles\",\n    \"licenseFile\": \"/node_modules/@isaacs/cliui/node_modules/ansi-styles/license\"\n  },\n  \"any-base@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/HarasimowiczKamil/any-base\",\n    \"publisher\": \"Kamil Harasimowicz\",\n    \"email\": \"mifczu@gmail.com\",\n    \"path\": \"/node_modules/any-base\",\n    \"licenseFile\": \"/node_modules/any-base/LICENSE\"\n  },\n  \"archiver-utils@2.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/archiverjs/archiver-utils\",\n    \"publisher\": \"Chris Talkington\",\n    \"url\": \"http://christalkington.com/\",\n    \"path\": \"/node_modules/archiver-utils\",\n    \"licenseFile\": \"/node_modules/archiver-utils/LICENSE\"\n  },\n  \"archiver-utils@3.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/archiverjs/archiver-utils\",\n    \"publisher\": \"Chris Talkington\",\n    \"url\": \"http://christalkington.com/\",\n    \"path\": \"/node_modules/zip-stream/node_modules/archiver-utils\",\n    \"licenseFile\": \"/node_modules/zip-stream/node_modules/archiver-utils/LICENSE\"\n  },\n  \"archiver@5.3.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/archiverjs/node-archiver\",\n    \"publisher\": \"Chris Talkington\",\n    \"url\": \"http://christalkington.com/\",\n    \"path\": \"/node_modules/archiver\",\n    \"licenseFile\": \"/node_modules/archiver/LICENSE\"\n  },\n  \"array-find-index@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/array-find-index\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/array-find-index\",\n    \"licenseFile\": \"/node_modules/array-find-index/license\"\n  },\n  \"arrify@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/arrify\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/arrify\",\n    \"licenseFile\": \"/node_modules/arrify/license\"\n  },\n  \"asn1@0.2.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/joyent/node-asn1\",\n    \"publisher\": \"Joyent\",\n    \"url\": \"joyent.com\",\n    \"path\": \"/node_modules/asn1\",\n    \"licenseFile\": \"/node_modules/asn1/LICENSE\"\n  },\n  \"async-lock@1.4.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/rogierschouten/async-lock\",\n    \"publisher\": \"Rogier Schouten\",\n    \"email\": \"github@workingcode.ninja\",\n    \"url\": \"https://github.com/rogierschouten/\",\n    \"path\": \"/node_modules/async-lock\",\n    \"licenseFile\": \"/node_modules/async-lock/LICENSE\"\n  },\n  \"async-retry@1.3.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vercel/async-retry\",\n    \"path\": \"/node_modules/async-retry\",\n    \"licenseFile\": \"/node_modules/async-retry/LICENSE.md\"\n  },\n  \"async@3.2.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/caolan/async\",\n    \"publisher\": \"Caolan McMahon\",\n    \"path\": \"/node_modules/async\",\n    \"licenseFile\": \"/node_modules/async/LICENSE\"\n  },\n  \"asynckit@0.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/alexindigo/asynckit\",\n    \"publisher\": \"Alex Indigo\",\n    \"email\": \"iam@alexindigo.com\",\n    \"path\": \"/node_modules/asynckit\",\n    \"licenseFile\": \"/node_modules/asynckit/LICENSE\"\n  },\n  \"available-typed-arrays@1.0.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/available-typed-arrays\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/available-typed-arrays\",\n    \"licenseFile\": \"/node_modules/available-typed-arrays/LICENSE\"\n  },\n  \"b4a@1.6.6\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/b4a\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/b4a\",\n    \"licenseFile\": \"/node_modules/b4a/LICENSE\"\n  },\n  \"balanced-match@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/balanced-match\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/node_modules/balanced-match\",\n    \"licenseFile\": \"/node_modules/balanced-match/LICENSE.md\"\n  },\n  \"bare-events@2.6.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/bare-events\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/bare-events\",\n    \"licenseFile\": \"/node_modules/bare-events/LICENSE\"\n  },\n  \"bare-fs@4.4.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/bare-fs\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/bare-fs\",\n    \"licenseFile\": \"/node_modules/bare-fs/LICENSE\"\n  },\n  \"bare-os@3.6.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/bare-os\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/bare-os\",\n    \"licenseFile\": \"/node_modules/bare-os/LICENSE\"\n  },\n  \"bare-path@3.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/bare-path\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/bare-path\",\n    \"licenseFile\": \"/node_modules/bare-path/LICENSE\",\n    \"noticeFile\": \"/node_modules/bare-path/NOTICE\"\n  },\n  \"bare-stream@2.7.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/bare-stream\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/bare-stream\",\n    \"licenseFile\": \"/node_modules/bare-stream/LICENSE\"\n  },\n  \"bare-url@2.2.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/bare-url\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/bare-url\",\n    \"licenseFile\": \"/node_modules/bare-url/LICENSE\"\n  },\n  \"base64-js@1.5.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/beatgammit/base64-js\",\n    \"publisher\": \"T. Jameson Little\",\n    \"email\": \"t.jameson.little@gmail.com\",\n    \"path\": \"/node_modules/base64-js\",\n    \"licenseFile\": \"/node_modules/base64-js/LICENSE\"\n  },\n  \"basic-ftp@5.0.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/patrickjuchli/basic-ftp\",\n    \"publisher\": \"Patrick Juchli\",\n    \"email\": \"patrickjuchli@gmail.com\",\n    \"path\": \"/node_modules/basic-ftp\",\n    \"licenseFile\": \"/node_modules/basic-ftp/LICENSE.txt\"\n  },\n  \"bcrypt-pbkdf@1.0.2\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/joyent/node-bcrypt-pbkdf\",\n    \"path\": \"/node_modules/bcrypt-pbkdf\",\n    \"licenseFile\": \"/node_modules/bcrypt-pbkdf/LICENSE\"\n  },\n  \"before-after-hook@4.0.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/gr2m/before-after-hook\",\n    \"publisher\": \"Gregor Martynus\",\n    \"path\": \"/node_modules/before-after-hook\",\n    \"licenseFile\": \"/node_modules/before-after-hook/LICENSE\"\n  },\n  \"better-sqlite3@12.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/WiseLibs/better-sqlite3\",\n    \"publisher\": \"Joshua Wise\",\n    \"email\": \"joshuathomaswise@gmail.com\",\n    \"path\": \"/node_modules/better-sqlite3\",\n    \"licenseFile\": \"/node_modules/better-sqlite3/LICENSE\"\n  },\n  \"bignumber.js@9.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/MikeMcl/bignumber.js\",\n    \"publisher\": \"Michael Mclaughlin\",\n    \"email\": \"M8ch88l@gmail.com\",\n    \"path\": \"/node_modules/bignumber.js\",\n    \"licenseFile\": \"/node_modules/bignumber.js/LICENCE.md\"\n  },\n  \"bindings@1.5.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-bindings\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://tootallnate.net\",\n    \"path\": \"/node_modules/bindings\",\n    \"licenseFile\": \"/node_modules/bindings/LICENSE.md\"\n  },\n  \"bl@4.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/rvagg/bl\",\n    \"path\": \"/node_modules/bl\",\n    \"licenseFile\": \"/node_modules/bl/LICENSE.md\"\n  },\n  \"bmp-js@0.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/shaozilee/bmp-js\",\n    \"publisher\": \"shaozilee\",\n    \"email\": \"shaozilee@gmail.com\",\n    \"path\": \"/node_modules/bmp-js\",\n    \"licenseFile\": \"/node_modules/bmp-js/LICENSE\"\n  },\n  \"bowser@2.11.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lancedikson/bowser\",\n    \"publisher\": \"Dustin Diaz\",\n    \"email\": \"dustin@dustindiaz.com\",\n    \"url\": \"http://dustindiaz.com\",\n    \"path\": \"/node_modules/bowser\",\n    \"licenseFile\": \"/node_modules/bowser/LICENSE\"\n  },\n  \"brace-expansion@1.1.11\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/brace-expansion\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/node_modules/brace-expansion\",\n    \"licenseFile\": \"/node_modules/brace-expansion/LICENSE\"\n  },\n  \"brace-expansion@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/brace-expansion\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/node_modules/readdir-glob/node_modules/brace-expansion\",\n    \"licenseFile\": \"/node_modules/readdir-glob/node_modules/brace-expansion/LICENSE\"\n  },\n  \"brace-expansion@2.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/brace-expansion\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/node_modules/editorconfig/node_modules/brace-expansion\",\n    \"licenseFile\": \"/node_modules/editorconfig/node_modules/brace-expansion/LICENSE\"\n  },\n  \"buffer-crc32@0.2.13\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianloveswords/buffer-crc32\",\n    \"publisher\": \"Brian J. Brennan\",\n    \"email\": \"brianloveswords@gmail.com\",\n    \"path\": \"/node_modules/buffer-crc32\",\n    \"licenseFile\": \"/node_modules/buffer-crc32/LICENSE\"\n  },\n  \"buffer-equal-constant-time@1.0.1\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/goinstant/buffer-equal-constant-time\",\n    \"publisher\": \"GoInstant Inc., a salesforce.com company\",\n    \"path\": \"/node_modules/buffer-equal-constant-time\",\n    \"licenseFile\": \"/node_modules/buffer-equal-constant-time/LICENSE.txt\"\n  },\n  \"buffer-equal@0.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/node-buffer-equal\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/buffer-equal\",\n    \"licenseFile\": \"/node_modules/buffer-equal/README.markdown\"\n  },\n  \"buffer-from@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/LinusU/buffer-from\",\n    \"path\": \"/node_modules/buffer-from\",\n    \"licenseFile\": \"/node_modules/buffer-from/LICENSE\"\n  },\n  \"buffer@5.7.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/node_modules/buffer\",\n    \"licenseFile\": \"/node_modules/buffer/LICENSE\"\n  },\n  \"buffer@6.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/node_modules/readable-web-to-node-stream/node_modules/buffer\",\n    \"licenseFile\": \"/node_modules/readable-web-to-node-stream/node_modules/buffer/LICENSE\"\n  },\n  \"cacheable-lookup@5.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/szmarczak/cacheable-lookup\",\n    \"publisher\": \"Szymon Marczak\",\n    \"path\": \"/node_modules/cacheable-lookup\",\n    \"licenseFile\": \"/node_modules/cacheable-lookup/LICENSE\"\n  },\n  \"cacheable-request@7.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lukechilds/cacheable-request\",\n    \"publisher\": \"Luke Childs\",\n    \"email\": \"lukechilds123@gmail.com\",\n    \"url\": \"http://lukechilds.co.uk\",\n    \"path\": \"/node_modules/cacheable-request\",\n    \"licenseFile\": \"/node_modules/cacheable-request/LICENSE\"\n  },\n  \"call-bind-apply-helpers@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/call-bind-apply-helpers\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/call-bind-apply-helpers\",\n    \"licenseFile\": \"/node_modules/call-bind-apply-helpers/LICENSE\"\n  },\n  \"call-bind@1.0.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/call-bind\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/call-bind\",\n    \"licenseFile\": \"/node_modules/call-bind/LICENSE\"\n  },\n  \"call-bound@1.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/call-bound\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/call-bound\",\n    \"licenseFile\": \"/node_modules/call-bound/LICENSE\"\n  },\n  \"camel-case@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/blakeembrey/camel-case\",\n    \"publisher\": \"Blake Embrey\",\n    \"email\": \"hello@blakeembrey.com\",\n    \"url\": \"http://blakeembrey.me\",\n    \"path\": \"/node_modules/camel-case\",\n    \"licenseFile\": \"/node_modules/camel-case/LICENSE\"\n  },\n  \"camelcase-keys@2.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/camelcase-keys\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"http://sindresorhus.com\",\n    \"path\": \"/node_modules/camelcase-keys\",\n    \"licenseFile\": \"/node_modules/camelcase-keys/license\"\n  },\n  \"camelcase@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/camelcase\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"http://sindresorhus.com\",\n    \"path\": \"/node_modules/camelcase\",\n    \"licenseFile\": \"/node_modules/camelcase/license\"\n  },\n  \"centra@2.7.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ethanent/centra\",\n    \"publisher\": \"Ethan Davis\",\n    \"path\": \"/node_modules/centra\",\n    \"licenseFile\": \"/node_modules/centra/LICENSE\"\n  },\n  \"chownr@1.1.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/chownr\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/chownr\",\n    \"licenseFile\": \"/node_modules/chownr/LICENSE\"\n  },\n  \"clean-css@4.2.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jakubpawlowicz/clean-css\",\n    \"publisher\": \"Jakub Pawlowicz\",\n    \"email\": \"contact@jakubpawlowicz.com\",\n    \"url\": \"http://twitter.com/jakubpawlowicz\",\n    \"path\": \"/node_modules/html-minifier/node_modules/clean-css\",\n    \"licenseFile\": \"/node_modules/html-minifier/node_modules/clean-css/LICENSE\"\n  },\n  \"clean-css@5.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/clean-css/clean-css\",\n    \"publisher\": \"Jakub Pawlowicz\",\n    \"email\": \"contact@jakubpawlowicz.com\",\n    \"path\": \"/node_modules/clean-css\",\n    \"licenseFile\": \"/node_modules/clean-css/LICENSE\"\n  },\n  \"clean-git-ref@2.0.1\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/TheSavior/clean-git-ref\",\n    \"publisher\": \"Eli White\",\n    \"email\": \"github@eli-white.com\",\n    \"path\": \"/node_modules/clean-git-ref\",\n    \"licenseFile\": \"/node_modules/clean-git-ref/README.md\"\n  },\n  \"cliui@8.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/yargs/cliui\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"ben@npmjs.com\",\n    \"path\": \"/node_modules/cliui\",\n    \"licenseFile\": \"/node_modules/cliui/LICENSE.txt\"\n  },\n  \"clone-response@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/clone-response\",\n    \"publisher\": \"Luke Childs\",\n    \"email\": \"lukechilds123@gmail.com\",\n    \"url\": \"http://lukechilds.co.uk\",\n    \"path\": \"/node_modules/clone-response\",\n    \"licenseFile\": \"/node_modules/clone-response/LICENSE\"\n  },\n  \"codemirror-advanceddialog@1.1.9\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Maloric/CodeMirror-AdvancedDialog\",\n    \"publisher\": \"Jamie Morris\",\n    \"path\": \"/node_modules/codemirror-advanceddialog\",\n    \"licenseFile\": \"/node_modules/codemirror-advanceddialog/LICENSE\"\n  },\n  \"codemirror-revisedsearch@1.0.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Maloric/CodeMirror-RevisedSearch\",\n    \"publisher\": \"Jamie Morris\",\n    \"path\": \"/node_modules/codemirror-revisedsearch\",\n    \"licenseFile\": \"/node_modules/codemirror-revisedsearch/LICENSE\"\n  },\n  \"codemirror-spell-checker@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/NextStepWebs/codemirror-spell-checker\",\n    \"publisher\": \"Wes Cossick\",\n    \"url\": \"http://www.WesCossick.com\",\n    \"path\": \"/node_modules/codemirror-spell-checker\",\n    \"licenseFile\": \"/node_modules/codemirror-spell-checker/LICENSE\"\n  },\n  \"codemirror@5.65.13\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/codemirror/CodeMirror\",\n    \"publisher\": \"Marijn Haverbeke\",\n    \"email\": \"marijn@haverbeke.berlin\",\n    \"url\": \"http://marijnhaverbeke.nl\",\n    \"path\": \"/node_modules/codemirror\",\n    \"licenseFile\": \"/node_modules/codemirror/LICENSE\"\n  },\n  \"color-convert@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Qix-/color-convert\",\n    \"publisher\": \"Heather Arthur\",\n    \"email\": \"fayearthur@gmail.com\",\n    \"path\": \"/node_modules/color-convert\",\n    \"licenseFile\": \"/node_modules/color-convert/LICENSE\"\n  },\n  \"color-name@1.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/colorjs/color-name\",\n    \"publisher\": \"DY\",\n    \"email\": \"dfcreative@gmail.com\",\n    \"path\": \"/node_modules/color-name\",\n    \"licenseFile\": \"/node_modules/color-name/LICENSE\"\n  },\n  \"color-string@1.9.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Qix-/color-string\",\n    \"publisher\": \"Heather Arthur\",\n    \"email\": \"fayearthur@gmail.com\",\n    \"path\": \"/node_modules/color-string\",\n    \"licenseFile\": \"/node_modules/color-string/LICENSE\"\n  },\n  \"color@4.2.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Qix-/color\",\n    \"path\": \"/node_modules/color\",\n    \"licenseFile\": \"/node_modules/color/LICENSE\"\n  },\n  \"colors@1.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Marak/colors.js\",\n    \"publisher\": \"Marak Squires\",\n    \"path\": \"/node_modules/colors\",\n    \"licenseFile\": \"/node_modules/colors/LICENSE\"\n  },\n  \"combined-stream@1.0.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/felixge/node-combined-stream\",\n    \"publisher\": \"Felix Geisendörfer\",\n    \"email\": \"felix@debuggable.com\",\n    \"url\": \"http://debuggable.com/\",\n    \"path\": \"/node_modules/combined-stream\",\n    \"licenseFile\": \"/node_modules/combined-stream/License\"\n  },\n  \"commander@10.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tj/commander.js\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"path\": \"/node_modules/editorconfig/node_modules/commander\",\n    \"licenseFile\": \"/node_modules/editorconfig/node_modules/commander/LICENSE\"\n  },\n  \"commander@2.20.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tj/commander.js\",\n    \"publisher\": \"TJ Holowaychuk\",\n    \"email\": \"tj@vision-media.ca\",\n    \"path\": \"/node_modules/commander\",\n    \"licenseFile\": \"/node_modules/commander/LICENSE\"\n  },\n  \"component-props@1.1.1\": {\n    \"licenses\": \"MIT*\",\n    \"repository\": \"https://github.com/component/props\",\n    \"path\": \"/node_modules/component-props\",\n    \"licenseFile\": \"/node_modules/component-props/Readme.md\"\n  },\n  \"component-xor@0.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/component/xor\",\n    \"publisher\": \"Matthew Mueller\",\n    \"path\": \"/node_modules/component-xor\",\n    \"licenseFile\": \"/node_modules/component-xor/Readme.md\"\n  },\n  \"compress-commons@4.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/archiverjs/node-compress-commons\",\n    \"publisher\": \"Chris Talkington\",\n    \"url\": \"http://christalkington.com/\",\n    \"path\": \"/node_modules/compress-commons\",\n    \"licenseFile\": \"/node_modules/compress-commons/LICENSE\"\n  },\n  \"compressible@2.0.18\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/compressible\",\n    \"path\": \"/node_modules/compressible\",\n    \"licenseFile\": \"/node_modules/compressible/LICENSE\"\n  },\n  \"concat-map@0.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/node-concat-map\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/concat-map\",\n    \"licenseFile\": \"/node_modules/concat-map/LICENSE\"\n  },\n  \"concat-stream@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/maxogden/concat-stream\",\n    \"publisher\": \"Max Ogden\",\n    \"email\": \"max@maxogden.com\",\n    \"path\": \"/node_modules/concat-stream\",\n    \"licenseFile\": \"/node_modules/concat-stream/LICENSE\"\n  },\n  \"config-chain@1.1.13\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dominictarr/config-chain\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"http://dominictarr.com\",\n    \"path\": \"/node_modules/config-chain\",\n    \"licenseFile\": \"/node_modules/config-chain/LICENCE\"\n  },\n  \"core-util-is@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/isaacs/core-util-is\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/core-util-is\",\n    \"licenseFile\": \"/node_modules/core-util-is/LICENSE\"\n  },\n  \"count-files@2.6.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/joehand/count-files\",\n    \"publisher\": \"Joe Hand\",\n    \"email\": \"joe@joeahand.com\",\n    \"url\": \"http://joeahand.com/\",\n    \"path\": \"/node_modules/count-files\",\n    \"licenseFile\": \"/node_modules/count-files/readme.md\"\n  },\n  \"crc-32@1.2.2\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/SheetJS/js-crc32\",\n    \"publisher\": \"sheetjs\",\n    \"path\": \"/node_modules/crc-32\",\n    \"licenseFile\": \"/node_modules/crc-32/LICENSE\"\n  },\n  \"crc32-stream@4.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/archiverjs/node-crc32-stream\",\n    \"publisher\": \"Chris Talkington\",\n    \"url\": \"http://christalkington.com/\",\n    \"path\": \"/node_modules/crc32-stream\",\n    \"licenseFile\": \"/node_modules/crc32-stream/LICENSE\"\n  },\n  \"cross-spawn@7.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/moxystudio/node-cross-spawn\",\n    \"publisher\": \"André Cruz\",\n    \"email\": \"andre@moxy.studio\",\n    \"path\": \"/node_modules/cross-spawn\",\n    \"licenseFile\": \"/node_modules/cross-spawn/LICENSE\"\n  },\n  \"currently-unhandled@0.4.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jamestalmage/currently-unhandled\",\n    \"publisher\": \"James Talmage\",\n    \"email\": \"james@talmage.io\",\n    \"url\": \"github.com/jamestalmage\",\n    \"path\": \"/node_modules/currently-unhandled\",\n    \"licenseFile\": \"/node_modules/currently-unhandled/license\"\n  },\n  \"debug@4.3.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/debug-js/debug\",\n    \"publisher\": \"Josh Junon\",\n    \"email\": \"josh.junon@protonmail.com\",\n    \"path\": \"/node_modules/debug\",\n    \"licenseFile\": \"/node_modules/debug/LICENSE\"\n  },\n  \"decamelize@1.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/decamelize\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/decamelize\",\n    \"licenseFile\": \"/node_modules/decamelize/license\"\n  },\n  \"decode-uri-component@0.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/SamVerschueren/decode-uri-component\",\n    \"publisher\": \"Sam Verschueren\",\n    \"email\": \"sam.verschueren@gmail.com\",\n    \"url\": \"github.com/SamVerschueren\",\n    \"path\": \"/node_modules/decode-uri-component\",\n    \"licenseFile\": \"/node_modules/decode-uri-component/license\"\n  },\n  \"decompress-response@6.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/decompress-response\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/decompress-response\",\n    \"licenseFile\": \"/node_modules/decompress-response/license\"\n  },\n  \"deep-extend@0.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/unclechu/node-deep-extend\",\n    \"publisher\": \"Viacheslav Lotsmanov\",\n    \"email\": \"lotsmanov89@gmail.com\",\n    \"path\": \"/node_modules/deep-extend\",\n    \"licenseFile\": \"/node_modules/deep-extend/LICENSE\"\n  },\n  \"defer-to-connect@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/szmarczak/defer-to-connect\",\n    \"publisher\": \"Szymon Marczak\",\n    \"path\": \"/node_modules/defer-to-connect\",\n    \"licenseFile\": \"/node_modules/defer-to-connect/LICENSE\"\n  },\n  \"define-data-property@1.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/define-data-property\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/define-data-property\",\n    \"licenseFile\": \"/node_modules/define-data-property/LICENSE\"\n  },\n  \"delay@5.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/delay\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/delay\",\n    \"licenseFile\": \"/node_modules/delay/license\"\n  },\n  \"delayed-stream@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/felixge/node-delayed-stream\",\n    \"publisher\": \"Felix Geisendörfer\",\n    \"email\": \"felix@debuggable.com\",\n    \"url\": \"http://debuggable.com/\",\n    \"path\": \"/node_modules/delayed-stream\",\n    \"licenseFile\": \"/node_modules/delayed-stream/License\"\n  },\n  \"detect-libc@2.0.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/lovell/detect-libc\",\n    \"publisher\": \"Lovell Fuller\",\n    \"email\": \"npm@lovell.info\",\n    \"path\": \"/node_modules/detect-libc\",\n    \"licenseFile\": \"/node_modules/detect-libc/LICENSE\"\n  },\n  \"diff3@0.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/axosoft/diff3\",\n    \"publisher\": \"Tyler Wanek\",\n    \"path\": \"/node_modules/diff3\",\n    \"licenseFile\": \"/node_modules/diff3/README.md\"\n  },\n  \"dom-iterator@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/MatthewMueller/dom-iterator\",\n    \"publisher\": \"Matthew Mueller\",\n    \"path\": \"/node_modules/dom-iterator\",\n    \"licenseFile\": \"/node_modules/dom-iterator/Readme.md\"\n  },\n  \"dom-walk@0.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/dom-walk\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/node_modules/dom-walk\",\n    \"licenseFile\": \"/node_modules/dom-walk/LICENCE\"\n  },\n  \"dompurify@3.2.6\": {\n    \"licenses\": \"(MPL-2.0 OR Apache-2.0)\",\n    \"repository\": \"https://github.com/cure53/DOMPurify\",\n    \"publisher\": \"Dr.-Ing. Mario Heiderich, Cure53\",\n    \"email\": \"mario@cure53.de\",\n    \"url\": \"https://cure53.de/\",\n    \"path\": \"/node_modules/dompurify\",\n    \"licenseFile\": \"/node_modules/dompurify/LICENSE\"\n  },\n  \"dunder-proto@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/es-shims/dunder-proto\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/dunder-proto\",\n    \"licenseFile\": \"/node_modules/dunder-proto/LICENSE\"\n  },\n  \"duplexify@4.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/duplexify\",\n    \"publisher\": \"Mathias Buus\",\n    \"path\": \"/node_modules/duplexify\",\n    \"licenseFile\": \"/node_modules/duplexify/LICENSE\"\n  },\n  \"eastasianwidth@0.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/komagata/eastasianwidth\",\n    \"publisher\": \"Masaki Komagata\",\n    \"path\": \"/node_modules/eastasianwidth\",\n    \"licenseFile\": \"/node_modules/eastasianwidth/README.md\"\n  },\n  \"easymde@2.18.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Ionaru/easy-markdown-editor\",\n    \"publisher\": \"Jeroen Akkerman\",\n    \"path\": \"/node_modules/easymde\",\n    \"licenseFile\": \"/node_modules/easymde/LICENSE\"\n  },\n  \"ecdsa-sig-formatter@1.0.11\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/Brightspace/node-ecdsa-sig-formatter\",\n    \"publisher\": \"D2L Corporation\",\n    \"path\": \"/node_modules/ecdsa-sig-formatter\",\n    \"licenseFile\": \"/node_modules/ecdsa-sig-formatter/LICENSE\"\n  },\n  \"editorconfig@1.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/editorconfig/editorconfig-core-js\",\n    \"publisher\": \"EditorConfig Team\",\n    \"path\": \"/node_modules/editorconfig\",\n    \"licenseFile\": \"/node_modules/editorconfig/LICENSE\"\n  },\n  \"emoji-regex@8.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mathiasbynens/emoji-regex\",\n    \"publisher\": \"Mathias Bynens\",\n    \"url\": \"https://mathiasbynens.be/\",\n    \"path\": \"/node_modules/emoji-regex\",\n    \"licenseFile\": \"/node_modules/emoji-regex/LICENSE-MIT.txt\"\n  },\n  \"emoji-regex@9.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mathiasbynens/emoji-regex\",\n    \"publisher\": \"Mathias Bynens\",\n    \"url\": \"https://mathiasbynens.be/\",\n    \"path\": \"/node_modules/@isaacs/cliui/node_modules/emoji-regex\",\n    \"licenseFile\": \"/node_modules/@isaacs/cliui/node_modules/emoji-regex/LICENSE-MIT.txt\"\n  },\n  \"end-of-stream@1.4.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/end-of-stream\",\n    \"publisher\": \"Mathias Buus\",\n    \"email\": \"mathiasbuus@gmail.com\",\n    \"path\": \"/node_modules/end-of-stream\",\n    \"licenseFile\": \"/node_modules/end-of-stream/LICENSE\"\n  },\n  \"ent@2.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/node-ent\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/ent\",\n    \"licenseFile\": \"/node_modules/ent/LICENSE\"\n  },\n  \"err-code@2.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/IndigoUnited/js-err-code\",\n    \"publisher\": \"IndigoUnited\",\n    \"email\": \"hello@indigounited.com\",\n    \"url\": \"http://indigounited.com\",\n    \"path\": \"/node_modules/err-code\",\n    \"licenseFile\": \"/node_modules/err-code/README.md\"\n  },\n  \"error-ex@1.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/qix-/node-error-ex\",\n    \"path\": \"/node_modules/error-ex\",\n    \"licenseFile\": \"/node_modules/error-ex/LICENSE\"\n  },\n  \"es-define-property@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/es-define-property\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/es-define-property\",\n    \"licenseFile\": \"/node_modules/es-define-property/LICENSE\"\n  },\n  \"es-errors@1.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/es-errors\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/es-errors\",\n    \"licenseFile\": \"/node_modules/es-errors/LICENSE\"\n  },\n  \"es-object-atoms@1.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/es-object-atoms\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/es-object-atoms\",\n    \"licenseFile\": \"/node_modules/es-object-atoms/LICENSE\"\n  },\n  \"es-set-tostringtag@2.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/es-shims/es-set-tostringtag\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/es-set-tostringtag\",\n    \"licenseFile\": \"/node_modules/es-set-tostringtag/LICENSE\"\n  },\n  \"escalade@3.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lukeed/escalade\",\n    \"publisher\": \"Luke Edwards\",\n    \"email\": \"luke.edwards05@gmail.com\",\n    \"url\": \"https://lukeed.com\",\n    \"path\": \"/node_modules/escalade\",\n    \"licenseFile\": \"/node_modules/escalade/license\"\n  },\n  \"escape-html@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/component/escape-html\",\n    \"path\": \"/node_modules/escape-html\",\n    \"licenseFile\": \"/node_modules/escape-html/LICENSE\"\n  },\n  \"event-target-shim@5.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mysticatea/event-target-shim\",\n    \"publisher\": \"Toru Nagashima\",\n    \"path\": \"/node_modules/event-target-shim\",\n    \"licenseFile\": \"/node_modules/event-target-shim/LICENSE\"\n  },\n  \"events@3.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Gozala/events\",\n    \"publisher\": \"Irakli Gozalishvili\",\n    \"email\": \"rfobic@gmail.com\",\n    \"url\": \"http://jeditoolkit.com\",\n    \"path\": \"/node_modules/events\",\n    \"licenseFile\": \"/node_modules/events/LICENSE\"\n  },\n  \"exif-parser@0.1.12\": {\n    \"licenses\": \"MIT*\",\n    \"repository\": \"https://github.com/bwindels/exif-parser\",\n    \"publisher\": \"Bruno Windels\",\n    \"email\": \"bruno.windels@gmail.com\",\n    \"path\": \"/node_modules/exif-parser\",\n    \"licenseFile\": \"/node_modules/exif-parser/LICENSE.md\"\n  },\n  \"expand-template@2.0.3\": {\n    \"licenses\": \"(MIT OR WTFPL)\",\n    \"repository\": \"https://github.com/ralphtheninja/expand-template\",\n    \"publisher\": \"LM\",\n    \"email\": \"ralphtheninja@riseup.net\",\n    \"path\": \"/node_modules/expand-template\",\n    \"licenseFile\": \"/node_modules/expand-template/LICENSE\"\n  },\n  \"extend-shallow@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jonschlinkert/extend-shallow\",\n    \"publisher\": \"Jon Schlinkert\",\n    \"url\": \"https://github.com/jonschlinkert\",\n    \"path\": \"/node_modules/extend-shallow\",\n    \"licenseFile\": \"/node_modules/extend-shallow/LICENSE\"\n  },\n  \"extend@3.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/justmoon/node-extend\",\n    \"publisher\": \"Stefan Thomas\",\n    \"email\": \"justmoon@members.fsf.org\",\n    \"url\": \"http://www.justmoon.net\",\n    \"path\": \"/node_modules/extend\",\n    \"licenseFile\": \"/node_modules/extend/LICENSE\"\n  },\n  \"fast-content-type-parse@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/fastify/fast-content-type-parse\",\n    \"publisher\": \"Aras Abbasi\",\n    \"email\": \"aras.abbasi@gmail.com\",\n    \"path\": \"/node_modules/fast-content-type-parse\",\n    \"licenseFile\": \"/node_modules/fast-content-type-parse/LICENSE\"\n  },\n  \"fast-fifo@1.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/fast-fifo\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/fast-fifo\",\n    \"licenseFile\": \"/node_modules/fast-fifo/LICENSE\"\n  },\n  \"fast-memoize@2.5.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/caiogondim/fast-memoize\",\n    \"publisher\": \"Caio Gondim\",\n    \"email\": \"me@caiogondim.com\",\n    \"url\": \"http://caiogondim.com\",\n    \"path\": \"/node_modules/fast-memoize\",\n    \"licenseFile\": \"/node_modules/fast-memoize/LICENSE\"\n  },\n  \"fast-text-encoding@1.0.6\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/samthor/fast-text-encoding\",\n    \"publisher\": \"Sam Thorogood\",\n    \"email\": \"sam.thorogood@gmail.com\",\n    \"path\": \"/node_modules/fast-text-encoding\",\n    \"licenseFile\": \"/node_modules/fast-text-encoding/LICENSE\"\n  },\n  \"fast-xml-parser@4.4.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/NaturalIntelligence/fast-xml-parser\",\n    \"publisher\": \"Amit Gupta\",\n    \"url\": \"https://solothought.com\",\n    \"path\": \"/node_modules/fast-xml-parser\",\n    \"licenseFile\": \"/node_modules/fast-xml-parser/LICENSE\"\n  },\n  \"file-type@16.5.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/file-type\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/file-type\",\n    \"licenseFile\": \"/node_modules/file-type/license\"\n  },\n  \"file-uri-to-path@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/file-uri-to-path\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/node_modules/file-uri-to-path\",\n    \"licenseFile\": \"/node_modules/file-uri-to-path/LICENSE\"\n  },\n  \"filter-obj@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/filter-obj\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/filter-obj\",\n    \"licenseFile\": \"/node_modules/filter-obj/license\"\n  },\n  \"find-up@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/find-up\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/find-up\",\n    \"licenseFile\": \"/node_modules/find-up/license\"\n  },\n  \"follow-redirects@1.15.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/follow-redirects/follow-redirects\",\n    \"publisher\": \"Ruben Verborgh\",\n    \"email\": \"ruben@verborgh.org\",\n    \"url\": \"https://ruben.verborgh.org/\",\n    \"path\": \"/node_modules/image-downloader/node_modules/follow-redirects\",\n    \"licenseFile\": \"/node_modules/image-downloader/node_modules/follow-redirects/LICENSE\"\n  },\n  \"follow-redirects@1.15.9\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/follow-redirects/follow-redirects\",\n    \"publisher\": \"Ruben Verborgh\",\n    \"email\": \"ruben@verborgh.org\",\n    \"url\": \"https://ruben.verborgh.org/\",\n    \"path\": \"/node_modules/centra/node_modules/follow-redirects\",\n    \"licenseFile\": \"/node_modules/centra/node_modules/follow-redirects/LICENSE\"\n  },\n  \"for-each@0.3.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/for-each\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/node_modules/for-each\",\n    \"licenseFile\": \"/node_modules/for-each/LICENSE\"\n  },\n  \"foreground-child@3.3.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/tapjs/foreground-child\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/foreground-child\",\n    \"licenseFile\": \"/node_modules/foreground-child/LICENSE\"\n  },\n  \"form-data@4.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/form-data/form-data\",\n    \"publisher\": \"Felix Geisendörfer\",\n    \"email\": \"felix@debuggable.com\",\n    \"url\": \"http://debuggable.com/\",\n    \"path\": \"/node_modules/form-data\",\n    \"licenseFile\": \"/node_modules/form-data/License\"\n  },\n  \"fs-constants@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/fs-constants\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/fs-constants\",\n    \"licenseFile\": \"/node_modules/fs-constants/LICENSE\"\n  },\n  \"fs-extra@11.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jprichardson/node-fs-extra\",\n    \"publisher\": \"JP Richardson\",\n    \"email\": \"jprichardson@gmail.com\",\n    \"path\": \"/node_modules/fs-extra\",\n    \"licenseFile\": \"/node_modules/fs-extra/LICENSE\"\n  },\n  \"fs.realpath@1.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/fs.realpath\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/fs.realpath\",\n    \"licenseFile\": \"/node_modules/fs.realpath/LICENSE\"\n  },\n  \"ftp@0.3.10\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mscdex/node-ftp\",\n    \"publisher\": \"Brian White\",\n    \"email\": \"mscdex@mscdex.net\",\n    \"path\": \"/node_modules/ftp\",\n    \"licenseFile\": \"/node_modules/ftp/LICENSE\"\n  },\n  \"function-bind@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/function-bind\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/node_modules/function-bind\",\n    \"licenseFile\": \"/node_modules/function-bind/LICENSE\"\n  },\n  \"gaxios@5.1.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/gaxios\",\n    \"publisher\": \"Google, LLC\",\n    \"path\": \"/node_modules/gaxios\",\n    \"licenseFile\": \"/node_modules/gaxios/LICENSE\"\n  },\n  \"gcp-metadata@5.3.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/gcp-metadata\",\n    \"publisher\": \"Stephen Sawchuk\",\n    \"path\": \"/node_modules/gcp-metadata\",\n    \"licenseFile\": \"/node_modules/gcp-metadata/LICENSE\"\n  },\n  \"get-caller-file@2.0.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/stefanpenner/get-caller-file\",\n    \"publisher\": \"Stefan Penner\",\n    \"path\": \"/node_modules/get-caller-file\",\n    \"licenseFile\": \"/node_modules/get-caller-file/LICENSE.md\"\n  },\n  \"get-intrinsic@1.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/get-intrinsic\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/get-intrinsic\",\n    \"licenseFile\": \"/node_modules/get-intrinsic/LICENSE\"\n  },\n  \"get-proto@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/get-proto\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/get-proto\",\n    \"licenseFile\": \"/node_modules/get-proto/LICENSE\"\n  },\n  \"get-stdin@4.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/get-stdin\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"http://sindresorhus.com\",\n    \"path\": \"/node_modules/get-stdin\",\n    \"licenseFile\": \"/node_modules/get-stdin/readme.md\"\n  },\n  \"get-stream@5.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/get-stream\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/get-stream\",\n    \"licenseFile\": \"/node_modules/get-stream/license\"\n  },\n  \"gifwrap@0.10.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jtlapp/gifwrap\",\n    \"publisher\": \"Joseph T. Lapp\",\n    \"path\": \"/node_modules/gifwrap\",\n    \"licenseFile\": \"/node_modules/gifwrap/LICENSE\"\n  },\n  \"github-from-package@0.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/github-from-package\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/github-from-package\",\n    \"licenseFile\": \"/node_modules/github-from-package/LICENSE\"\n  },\n  \"glob@10.4.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-glob\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://blog.izs.me/\",\n    \"path\": \"/node_modules/js-beautify/node_modules/glob\",\n    \"licenseFile\": \"/node_modules/js-beautify/node_modules/glob/LICENSE\"\n  },\n  \"glob@7.2.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-glob\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/glob\",\n    \"licenseFile\": \"/node_modules/glob/LICENSE\"\n  },\n  \"global@4.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/global\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/node_modules/global\",\n    \"licenseFile\": \"/node_modules/global/LICENSE\"\n  },\n  \"google-auth-library@8.9.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/google-auth-library-nodejs\",\n    \"publisher\": \"Google Inc.\",\n    \"path\": \"/node_modules/google-auth-library\",\n    \"licenseFile\": \"/node_modules/google-auth-library/LICENSE\"\n  },\n  \"google-p12-pem@4.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/googleapis/google-p12-pem\",\n    \"publisher\": \"Ryan Seys\",\n    \"path\": \"/node_modules/google-p12-pem\",\n    \"licenseFile\": \"/node_modules/google-p12-pem/LICENSE\"\n  },\n  \"gopd@1.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/gopd\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/gopd\",\n    \"licenseFile\": \"/node_modules/gopd/LICENSE\"\n  },\n  \"got@11.8.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/got\",\n    \"path\": \"/node_modules/got\",\n    \"licenseFile\": \"/node_modules/got/license\"\n  },\n  \"graceful-fs@4.2.11\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-graceful-fs\",\n    \"path\": \"/node_modules/graceful-fs\",\n    \"licenseFile\": \"/node_modules/graceful-fs/LICENSE\"\n  },\n  \"gtoken@6.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/google/node-gtoken\",\n    \"publisher\": \"Google, LLC\",\n    \"path\": \"/node_modules/gtoken\",\n    \"licenseFile\": \"/node_modules/gtoken/LICENSE\"\n  },\n  \"handlebars@4.7.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/handlebars-lang/handlebars.js\",\n    \"publisher\": \"Yehuda Katz\",\n    \"path\": \"/node_modules/handlebars\",\n    \"licenseFile\": \"/node_modules/handlebars/LICENSE\"\n  },\n  \"has-property-descriptors@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/has-property-descriptors\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/has-property-descriptors\",\n    \"licenseFile\": \"/node_modules/has-property-descriptors/LICENSE\"\n  },\n  \"has-symbols@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/has-symbols\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"url\": \"http://ljharb.codes\",\n    \"path\": \"/node_modules/has-symbols\",\n    \"licenseFile\": \"/node_modules/has-symbols/LICENSE\"\n  },\n  \"has-tostringtag@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/has-tostringtag\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"url\": \"http://ljharb.codes\",\n    \"path\": \"/node_modules/has-tostringtag\",\n    \"licenseFile\": \"/node_modules/has-tostringtag/LICENSE\"\n  },\n  \"hasown@2.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/hasOwn\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/hasown\",\n    \"licenseFile\": \"/node_modules/hasown/LICENSE\"\n  },\n  \"he@1.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mathiasbynens/he\",\n    \"publisher\": \"Mathias Bynens\",\n    \"url\": \"https://mathiasbynens.be/\",\n    \"path\": \"/node_modules/he\",\n    \"licenseFile\": \"/node_modules/he/LICENSE-MIT.txt\"\n  },\n  \"hosted-git-info@2.8.9\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/hosted-git-info\",\n    \"publisher\": \"Rebecca Turner\",\n    \"email\": \"me@re-becca.org\",\n    \"url\": \"http://re-becca.org\",\n    \"path\": \"/node_modules/hosted-git-info\",\n    \"licenseFile\": \"/node_modules/hosted-git-info/LICENSE\"\n  },\n  \"html-minifier@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kangax/html-minifier\",\n    \"publisher\": \"Juriy \\\"kangax\\\" Zaytsev\",\n    \"path\": \"/node_modules/html-minifier\",\n    \"licenseFile\": \"/node_modules/html-minifier/LICENSE\"\n  },\n  \"http-cache-semantics@4.1.1\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/kornelski/http-cache-semantics\",\n    \"publisher\": \"Kornel Lesiński\",\n    \"email\": \"kornel@geekhood.net\",\n    \"url\": \"https://kornel.ski/\",\n    \"path\": \"/node_modules/http-cache-semantics\",\n    \"licenseFile\": \"/node_modules/http-cache-semantics/LICENSE\"\n  },\n  \"http-proxy-agent@5.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-http-proxy-agent\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/node_modules/http-proxy-agent\",\n    \"licenseFile\": \"/node_modules/http-proxy-agent/README.md\"\n  },\n  \"http2-wrapper@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/szmarczak/http2-wrapper\",\n    \"publisher\": \"Szymon Marczak\",\n    \"path\": \"/node_modules/http2-wrapper\",\n    \"licenseFile\": \"/node_modules/http2-wrapper/LICENSE\"\n  },\n  \"https-proxy-agent@5.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/node-https-proxy-agent\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/node_modules/https-proxy-agent\",\n    \"licenseFile\": \"/node_modules/https-proxy-agent/README.md\"\n  },\n  \"ieee754@1.2.1\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/feross/ieee754\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/node_modules/ieee754\",\n    \"licenseFile\": \"/node_modules/ieee754/LICENSE\"\n  },\n  \"ignore@5.3.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kaelzhang/node-ignore\",\n    \"publisher\": \"kael\",\n    \"path\": \"/node_modules/ignore\",\n    \"licenseFile\": \"/node_modules/ignore/LICENSE-MIT\"\n  },\n  \"image-downloader@4.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"git+https://gitlab.com/demsking/image-downloader\",\n    \"publisher\": \"Sébastien Demanou\",\n    \"path\": \"/node_modules/image-downloader\",\n    \"licenseFile\": \"/node_modules/image-downloader/LICENSE\"\n  },\n  \"image-q@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ibezkrovnyi/image-quantization\",\n    \"path\": \"/node_modules/image-q\",\n    \"licenseFile\": \"/node_modules/image-q/LICENSE\"\n  },\n  \"image-size@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/image-size/image-size\",\n    \"publisher\": \"netroy\",\n    \"email\": \"aditya@netroy.in\",\n    \"url\": \"http://netroy.in/\",\n    \"path\": \"/node_modules/image-size\",\n    \"licenseFile\": \"/node_modules/image-size/LICENSE\"\n  },\n  \"indent-string@2.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/indent-string\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/indent-string\",\n    \"licenseFile\": \"/node_modules/indent-string/license\"\n  },\n  \"inflight@1.0.6\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/inflight\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/inflight\",\n    \"licenseFile\": \"/node_modules/inflight/LICENSE\"\n  },\n  \"inherits@2.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/inherits\",\n    \"path\": \"/node_modules/inherits\",\n    \"licenseFile\": \"/node_modules/inherits/LICENSE\"\n  },\n  \"ini@1.3.8\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/ini\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/ini\",\n    \"licenseFile\": \"/node_modules/ini/LICENSE\"\n  },\n  \"is-arrayish@0.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/qix-/node-is-arrayish\",\n    \"publisher\": \"Qix\",\n    \"url\": \"http://github.com/qix-\",\n    \"path\": \"/node_modules/is-arrayish\",\n    \"licenseFile\": \"/node_modules/is-arrayish/LICENSE\"\n  },\n  \"is-arrayish@0.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/qix-/node-is-arrayish\",\n    \"publisher\": \"Qix\",\n    \"url\": \"http://github.com/qix-\",\n    \"path\": \"/node_modules/simple-swizzle/node_modules/is-arrayish\",\n    \"licenseFile\": \"/node_modules/simple-swizzle/node_modules/is-arrayish/LICENSE\"\n  },\n  \"is-callable@1.2.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/is-callable\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"url\": \"http://ljharb.codes\",\n    \"path\": \"/node_modules/is-callable\",\n    \"licenseFile\": \"/node_modules/is-callable/LICENSE\"\n  },\n  \"is-core-module@2.13.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/is-core-module\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/is-core-module\",\n    \"licenseFile\": \"/node_modules/is-core-module/LICENSE\"\n  },\n  \"is-extendable@0.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jonschlinkert/is-extendable\",\n    \"publisher\": \"Jon Schlinkert\",\n    \"url\": \"https://github.com/jonschlinkert\",\n    \"path\": \"/node_modules/is-extendable\",\n    \"licenseFile\": \"/node_modules/is-extendable/LICENSE\"\n  },\n  \"is-finite@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is-finite\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/is-finite\",\n    \"licenseFile\": \"/node_modules/is-finite/license\"\n  },\n  \"is-fullwidth-code-point@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is-fullwidth-code-point\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/is-fullwidth-code-point\",\n    \"licenseFile\": \"/node_modules/is-fullwidth-code-point/license\"\n  },\n  \"is-fullwidth-code-point@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is-fullwidth-code-point\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/string-width-cjs/node_modules/is-fullwidth-code-point\",\n    \"licenseFile\": \"/node_modules/string-width-cjs/node_modules/is-fullwidth-code-point/license\"\n  },\n  \"is-function@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/grncdr/js-is-function\",\n    \"publisher\": \"Stephen Sugden\",\n    \"email\": \"me@stephensugden.com\",\n    \"path\": \"/node_modules/is-function\",\n    \"licenseFile\": \"/node_modules/is-function/LICENSE\"\n  },\n  \"is-stream@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/is-stream\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/is-stream\",\n    \"licenseFile\": \"/node_modules/is-stream/license\"\n  },\n  \"is-typed-array@1.1.15\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/is-typed-array\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"url\": \"http://ljharb.codes\",\n    \"path\": \"/node_modules/is-typed-array\",\n    \"licenseFile\": \"/node_modules/is-typed-array/LICENSE\"\n  },\n  \"is-utf8@0.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/wayfind/is-utf8\",\n    \"publisher\": \"wayfind\",\n    \"path\": \"/node_modules/is-utf8\",\n    \"licenseFile\": \"/node_modules/is-utf8/LICENSE\"\n  },\n  \"isarray@0.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/isarray\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/node_modules/ftp/node_modules/isarray\",\n    \"licenseFile\": \"/node_modules/ftp/node_modules/isarray/README.md\"\n  },\n  \"isarray@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/isarray\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/node_modules/isarray\",\n    \"licenseFile\": \"/node_modules/isarray/README.md\"\n  },\n  \"isarray@2.0.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/juliangruber/isarray\",\n    \"publisher\": \"Julian Gruber\",\n    \"email\": \"mail@juliangruber.com\",\n    \"url\": \"http://juliangruber.com\",\n    \"path\": \"/node_modules/to-buffer/node_modules/isarray\",\n    \"licenseFile\": \"/node_modules/to-buffer/node_modules/isarray/LICENSE\"\n  },\n  \"isbinaryfile@5.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/gjtorikian/isBinaryFile\",\n    \"path\": \"/node_modules/isbinaryfile\",\n    \"licenseFile\": \"/node_modules/isbinaryfile/LICENSE.txt\"\n  },\n  \"isexe@2.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/isexe\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/isexe\",\n    \"licenseFile\": \"/node_modules/isexe/LICENSE\"\n  },\n  \"isomorphic-fetch@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/matthew-andrews/isomorphic-fetch\",\n    \"publisher\": \"Matt Andrews\",\n    \"email\": \"matt@mattandre.ws\",\n    \"path\": \"/node_modules/isomorphic-fetch\",\n    \"licenseFile\": \"/node_modules/isomorphic-fetch/LICENSE\"\n  },\n  \"isomorphic-git@1.33.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/isomorphic-git/isomorphic-git\",\n    \"publisher\": \"William Hilton\",\n    \"email\": \"wmhilton@gmail.com\",\n    \"path\": \"/node_modules/isomorphic-git\",\n    \"licenseFile\": \"/node_modules/isomorphic-git/LICENSE.md\"\n  },\n  \"jackspeak@3.4.3\": {\n    \"licenses\": \"BlueOak-1.0.0\",\n    \"repository\": \"https://github.com/isaacs/jackspeak\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"path\": \"/node_modules/jackspeak\",\n    \"licenseFile\": \"/node_modules/jackspeak/LICENSE.md\"\n  },\n  \"jimp@0.22.12\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jimp-dev/jimp\",\n    \"publisher\": \"Oliver Moran\",\n    \"email\": \"oliver.moran@gmail.com\",\n    \"path\": \"/node_modules/jimp\",\n    \"licenseFile\": \"/node_modules/jimp/LICENSE\"\n  },\n  \"jpeg-js@0.4.4\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/eugeneware/jpeg-js\",\n    \"publisher\": \"Eugene Ware\",\n    \"email\": \"eugene@noblesamurai.com\",\n    \"path\": \"/node_modules/jpeg-js\",\n    \"licenseFile\": \"/node_modules/jpeg-js/LICENSE\"\n  },\n  \"js-beautify@1.15.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/beautifier/js-beautify\",\n    \"publisher\": \"Einar Lielmanis\",\n    \"email\": \"einar@beautifier.io\",\n    \"path\": \"/node_modules/js-beautify\",\n    \"licenseFile\": \"/node_modules/js-beautify/LICENSE\"\n  },\n  \"js-cookie@3.0.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/js-cookie/js-cookie\",\n    \"publisher\": \"Klaus Hartl\",\n    \"path\": \"/node_modules/js-cookie\",\n    \"licenseFile\": \"/node_modules/js-cookie/LICENSE\"\n  },\n  \"json-bigint@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sidorares/json-bigint\",\n    \"publisher\": \"Andrey Sidorov\",\n    \"email\": \"sidorares@yandex.ru\",\n    \"path\": \"/node_modules/json-bigint\",\n    \"licenseFile\": \"/node_modules/json-bigint/LICENSE\"\n  },\n  \"json-buffer@3.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dominictarr/json-buffer\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"http://dominictarr.com\",\n    \"path\": \"/node_modules/json-buffer\",\n    \"licenseFile\": \"/node_modules/json-buffer/LICENSE\"\n  },\n  \"jsonfile@6.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jprichardson/node-jsonfile\",\n    \"publisher\": \"JP Richardson\",\n    \"email\": \"jprichardson@gmail.com\",\n    \"path\": \"/node_modules/jsonfile\",\n    \"licenseFile\": \"/node_modules/jsonfile/LICENSE\"\n  },\n  \"jwa@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianloveswords/node-jwa\",\n    \"publisher\": \"Brian J. Brennan\",\n    \"email\": \"brianloveswords@gmail.com\",\n    \"path\": \"/node_modules/jwa\",\n    \"licenseFile\": \"/node_modules/jwa/LICENSE\"\n  },\n  \"jws@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/brianloveswords/node-jws\",\n    \"publisher\": \"Brian J Brennan\",\n    \"path\": \"/node_modules/jws\",\n    \"licenseFile\": \"/node_modules/jws/LICENSE\"\n  },\n  \"keytar@7.9.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/atom/node-keytar\",\n    \"path\": \"/node_modules/keytar\",\n    \"licenseFile\": \"/node_modules/keytar/LICENSE.md\"\n  },\n  \"keyv@4.5.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jaredwray/keyv\",\n    \"publisher\": \"Jared Wray\",\n    \"email\": \"me@jaredwray.com\",\n    \"url\": \"http://jaredwray.com\",\n    \"path\": \"/node_modules/keyv\",\n    \"licenseFile\": \"/node_modules/keyv/README.md\"\n  },\n  \"lazystream@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jpommerening/node-lazystream\",\n    \"publisher\": \"Jonas Pommerening\",\n    \"email\": \"jonas.pommerening@gmail.com\",\n    \"url\": \"https://npmjs.org/~jpommerening\",\n    \"path\": \"/node_modules/lazystream\",\n    \"licenseFile\": \"/node_modules/lazystream/LICENSE\"\n  },\n  \"li@1.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jfromaniello/li\",\n    \"publisher\": \"José F. Romaniello\",\n    \"email\": \"jfromaniello@gmail.com\",\n    \"url\": \"http://joseoncode.com\",\n    \"path\": \"/node_modules/li\",\n    \"licenseFile\": \"/node_modules/li/README.md\"\n  },\n  \"load-bmfont@1.4.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Jam3/load-bmfont\",\n    \"publisher\": \"Matt DesLauriers\",\n    \"email\": \"dave.des@gmail.com\",\n    \"url\": \"https://github.com/mattdesl\",\n    \"path\": \"/node_modules/load-bmfont\",\n    \"licenseFile\": \"/node_modules/load-bmfont/LICENSE.md\"\n  },\n  \"load-json-file@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/load-json-file\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/load-json-file\",\n    \"licenseFile\": \"/node_modules/load-json-file/license\"\n  },\n  \"lodash.defaults@4.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/node_modules/lodash.defaults\",\n    \"licenseFile\": \"/node_modules/lodash.defaults/LICENSE\"\n  },\n  \"lodash.difference@4.5.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/node_modules/lodash.difference\",\n    \"licenseFile\": \"/node_modules/lodash.difference/LICENSE\"\n  },\n  \"lodash.flatten@4.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/node_modules/lodash.flatten\",\n    \"licenseFile\": \"/node_modules/lodash.flatten/LICENSE\"\n  },\n  \"lodash.flattendeep@4.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/node_modules/lodash.flattendeep\",\n    \"licenseFile\": \"/node_modules/lodash.flattendeep/LICENSE\"\n  },\n  \"lodash.isplainobject@4.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/node_modules/lodash.isplainobject\",\n    \"licenseFile\": \"/node_modules/lodash.isplainobject/LICENSE\"\n  },\n  \"lodash.throttle@4.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/node_modules/lodash.throttle\",\n    \"licenseFile\": \"/node_modules/lodash.throttle/LICENSE\"\n  },\n  \"lodash.union@4.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lodash/lodash\",\n    \"publisher\": \"John-David Dalton\",\n    \"email\": \"john.david.dalton@gmail.com\",\n    \"url\": \"http://allyoucanleet.com/\",\n    \"path\": \"/node_modules/lodash.union\",\n    \"licenseFile\": \"/node_modules/lodash.union/LICENSE\"\n  },\n  \"loud-rejection@1.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/loud-rejection\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/loud-rejection\",\n    \"licenseFile\": \"/node_modules/loud-rejection/license\"\n  },\n  \"lower-case@1.1.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/blakeembrey/lower-case\",\n    \"publisher\": \"Blake Embrey\",\n    \"email\": \"hello@blakeembrey.com\",\n    \"url\": \"http://blakeembrey.me\",\n    \"path\": \"/node_modules/lower-case\",\n    \"licenseFile\": \"/node_modules/lower-case/LICENSE\"\n  },\n  \"lowercase-keys@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/lowercase-keys\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/lowercase-keys\",\n    \"licenseFile\": \"/node_modules/lowercase-keys/license\"\n  },\n  \"lru-cache@10.4.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-lru-cache\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"path\": \"/node_modules/path-scurry/node_modules/lru-cache\",\n    \"licenseFile\": \"/node_modules/path-scurry/node_modules/lru-cache/LICENSE\"\n  },\n  \"lru-cache@6.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-lru-cache\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"path\": \"/node_modules/lru-cache\",\n    \"licenseFile\": \"/node_modules/lru-cache/LICENSE\"\n  },\n  \"ls-all@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/cantidio/node-ls-all\",\n    \"publisher\": \"Cantidio Fontes\",\n    \"email\": \"aniquilatorbloody@gmail.com\",\n    \"path\": \"/node_modules/ls-all\",\n    \"licenseFile\": \"/node_modules/ls-all/LICENSE\"\n  },\n  \"map-obj@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/map-obj\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/map-obj\",\n    \"licenseFile\": \"/node_modules/map-obj/license\"\n  },\n  \"marked@4.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/markedjs/marked\",\n    \"publisher\": \"Christopher Jeffrey\",\n    \"path\": \"/node_modules/easymde/node_modules/marked\",\n    \"licenseFile\": \"/node_modules/easymde/node_modules/marked/LICENSE.md\"\n  },\n  \"marked@5.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/markedjs/marked\",\n    \"publisher\": \"Christopher Jeffrey\",\n    \"path\": \"/node_modules/marked\",\n    \"licenseFile\": \"/node_modules/marked/LICENSE.md\"\n  },\n  \"material-colors@1.2.6\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/shuhei/material-colors\",\n    \"publisher\": \"Shuhei Kagawa\",\n    \"email\": \"shuhei.kagawa@gmail.com\",\n    \"path\": \"/node_modules/material-colors\",\n    \"licenseFile\": \"/node_modules/material-colors/LICENSE\"\n  },\n  \"math-intrinsics@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/es-shims/math-intrinsics\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/math-intrinsics\",\n    \"licenseFile\": \"/node_modules/math-intrinsics/LICENSE\"\n  },\n  \"meow@3.7.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/meow\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/meow\",\n    \"licenseFile\": \"/node_modules/meow/license\"\n  },\n  \"mime-db@1.52.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/mime-db\",\n    \"path\": \"/node_modules/mime-db\",\n    \"licenseFile\": \"/node_modules/mime-db/LICENSE\"\n  },\n  \"mime-types@2.1.35\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jshttp/mime-types\",\n    \"path\": \"/node_modules/mime-types\",\n    \"licenseFile\": \"/node_modules/mime-types/LICENSE\"\n  },\n  \"mime@1.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/broofa/node-mime\",\n    \"publisher\": \"Robert Kieffer\",\n    \"email\": \"robert@broofa.com\",\n    \"url\": \"http://github.com/broofa\",\n    \"path\": \"/node_modules/load-bmfont/node_modules/mime\",\n    \"licenseFile\": \"/node_modules/load-bmfont/node_modules/mime/LICENSE\"\n  },\n  \"mime@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/broofa/mime\",\n    \"publisher\": \"Robert Kieffer\",\n    \"email\": \"robert@broofa.com\",\n    \"url\": \"http://github.com/broofa\",\n    \"path\": \"/node_modules/mime\",\n    \"licenseFile\": \"/node_modules/mime/LICENSE\"\n  },\n  \"mimic-response@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/mimic-response\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/mimic-response\",\n    \"licenseFile\": \"/node_modules/mimic-response/license\"\n  },\n  \"mimic-response@3.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/mimic-response\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/decompress-response/node_modules/mimic-response\",\n    \"licenseFile\": \"/node_modules/decompress-response/node_modules/mimic-response/license\"\n  },\n  \"min-document@2.19.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/min-document\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/node_modules/min-document\",\n    \"licenseFile\": \"/node_modules/min-document/LICENCE\"\n  },\n  \"minimatch@3.1.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minimatch\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/node_modules/minimatch\",\n    \"licenseFile\": \"/node_modules/minimatch/LICENSE\"\n  },\n  \"minimatch@5.1.6\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minimatch\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/node_modules/readdir-glob/node_modules/minimatch\",\n    \"licenseFile\": \"/node_modules/readdir-glob/node_modules/minimatch/LICENSE\"\n  },\n  \"minimatch@9.0.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minimatch\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/node_modules/editorconfig/node_modules/minimatch\",\n    \"licenseFile\": \"/node_modules/editorconfig/node_modules/minimatch/LICENSE\"\n  },\n  \"minimatch@9.0.5\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minimatch\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/node_modules/js-beautify/node_modules/minimatch\",\n    \"licenseFile\": \"/node_modules/js-beautify/node_modules/minimatch/LICENSE\"\n  },\n  \"minimist@1.2.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/minimistjs/minimist\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/minimist\",\n    \"licenseFile\": \"/node_modules/minimist/LICENSE\"\n  },\n  \"minimisted@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kt3k/minimisted\",\n    \"publisher\": \"Yoshiya Hinosawa\",\n    \"email\": \"stibium121@gmail.com\",\n    \"url\": \"https://twitter.com/kt3k\",\n    \"path\": \"/node_modules/minimisted\",\n    \"licenseFile\": \"/node_modules/minimisted/README.md\"\n  },\n  \"minipass@7.1.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/minipass\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/minipass\",\n    \"licenseFile\": \"/node_modules/minipass/LICENSE\"\n  },\n  \"mkdirp-classic@0.5.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/mkdirp-classic\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/mkdirp-classic\",\n    \"licenseFile\": \"/node_modules/mkdirp-classic/LICENSE\"\n  },\n  \"moment@2.29.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/moment/moment\",\n    \"publisher\": \"Iskren Ivov Chernev\",\n    \"email\": \"iskren.chernev@gmail.com\",\n    \"url\": \"https://github.com/ichernev\",\n    \"path\": \"/node_modules/moment\",\n    \"licenseFile\": \"/node_modules/moment/LICENSE\"\n  },\n  \"ms@2.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/zeit/ms\",\n    \"path\": \"/node_modules/ms\",\n    \"licenseFile\": \"/node_modules/ms/license.md\"\n  },\n  \"nan@2.18.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/nan\",\n    \"path\": \"/node_modules/nan\",\n    \"licenseFile\": \"/node_modules/nan/LICENSE.md\"\n  },\n  \"nanobus@3.3.0\": {\n    \"licenses\": \"MIT*\",\n    \"repository\": \"https://github.com/yoshuawuyts/nanobus\",\n    \"path\": \"/node_modules/nanobus\",\n    \"licenseFile\": \"/node_modules/nanobus/LICENSE\"\n  },\n  \"nanotiming@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/yoshuawuyts/nanotiming\",\n    \"path\": \"/node_modules/nanotiming\",\n    \"licenseFile\": \"/node_modules/nanotiming/LICENSE\"\n  },\n  \"napi-build-utils@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspiredware/napi-build-utils\",\n    \"publisher\": \"Jim Schlight\",\n    \"path\": \"/node_modules/napi-build-utils\",\n    \"licenseFile\": \"/node_modules/napi-build-utils/LICENSE\"\n  },\n  \"neat-log@1.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/joehand/neat-log\",\n    \"publisher\": \"Joe Hand\",\n    \"email\": \"joe@hand.email\",\n    \"path\": \"/node_modules/neat-log\",\n    \"licenseFile\": \"/node_modules/neat-log/LICENSE.md\"\n  },\n  \"neo-async@2.6.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/suguru03/neo-async\",\n    \"path\": \"/node_modules/neo-async\",\n    \"licenseFile\": \"/node_modules/neo-async/LICENSE\"\n  },\n  \"no-case@2.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/blakeembrey/no-case\",\n    \"publisher\": \"Blake Embrey\",\n    \"email\": \"hello@blakeembrey.com\",\n    \"url\": \"http://blakeembrey.me\",\n    \"path\": \"/node_modules/no-case\",\n    \"licenseFile\": \"/node_modules/no-case/LICENSE\"\n  },\n  \"node-abi@3.75.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/electron/node-abi\",\n    \"publisher\": \"Lukas Geiger\",\n    \"path\": \"/node_modules/prebuild-install/node_modules/node-abi\",\n    \"licenseFile\": \"/node_modules/prebuild-install/node_modules/node-abi/LICENSE\"\n  },\n  \"node-addon-api@4.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/node-addon-api\",\n    \"path\": \"/node_modules/node-addon-api\",\n    \"licenseFile\": \"/node_modules/node-addon-api/LICENSE.md\"\n  },\n  \"node-fetch@2.7.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/bitinn/node-fetch\",\n    \"publisher\": \"David Frank\",\n    \"path\": \"/node_modules/node-fetch\",\n    \"licenseFile\": \"/node_modules/node-fetch/LICENSE.md\"\n  },\n  \"node-forge@1.3.1\": {\n    \"licenses\": \"(BSD-3-Clause OR GPL-2.0)\",\n    \"repository\": \"https://github.com/digitalbazaar/forge\",\n    \"publisher\": \"Digital Bazaar, Inc.\",\n    \"email\": \"support@digitalbazaar.com\",\n    \"url\": \"http://digitalbazaar.com/\",\n    \"path\": \"/node_modules/node-forge\",\n    \"licenseFile\": \"/node_modules/node-forge/LICENSE\"\n  },\n  \"node-sqlite3-wasm@0.8.47\": {\n    \"licenses\": \"MIT\",\n    \"publisher\": \"Tobias Enderle\",\n    \"path\": \"/node_modules/node-sqlite3-wasm\",\n    \"licenseFile\": \"/node_modules/node-sqlite3-wasm/LICENSE\"\n  },\n  \"node-version-compare@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/leohihimax/node-version-compare\",\n    \"publisher\": \"jiajun ma\",\n    \"email\": \"leomajiajun@gmail.com\",\n    \"path\": \"/node_modules/node-version-compare\",\n    \"licenseFile\": \"/node_modules/node-version-compare/README.md\"\n  },\n  \"nopt@7.2.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/nopt\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/node_modules/nopt\",\n    \"licenseFile\": \"/node_modules/nopt/LICENSE\"\n  },\n  \"normalize-package-data@2.5.0\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/npm/normalize-package-data\",\n    \"publisher\": \"Meryn Stol\",\n    \"email\": \"merynstol@gmail.com\",\n    \"path\": \"/node_modules/normalize-package-data\",\n    \"licenseFile\": \"/node_modules/normalize-package-data/LICENSE\"\n  },\n  \"normalize-path@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jonschlinkert/normalize-path\",\n    \"publisher\": \"Jon Schlinkert\",\n    \"url\": \"https://github.com/jonschlinkert\",\n    \"path\": \"/node_modules/normalize-path\",\n    \"licenseFile\": \"/node_modules/normalize-path/LICENSE\"\n  },\n  \"normalize-url@6.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/normalize-url\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/normalize-url\",\n    \"licenseFile\": \"/node_modules/normalize-url/license\"\n  },\n  \"object-assign@4.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/object-assign\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/object-assign\",\n    \"licenseFile\": \"/node_modules/object-assign/license\"\n  },\n  \"object-inspect@1.13.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/object-inspect\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/object-inspect\",\n    \"licenseFile\": \"/node_modules/object-inspect/LICENSE\"\n  },\n  \"omggif@1.0.10\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/deanm/omggif\",\n    \"publisher\": \"Dean McNamee\",\n    \"email\": \"dean@gmail.com\",\n    \"path\": \"/node_modules/omggif\",\n    \"licenseFile\": \"/node_modules/omggif/README\"\n  },\n  \"once@1.4.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/once\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/once\",\n    \"licenseFile\": \"/node_modules/once/LICENSE\"\n  },\n  \"p-cancelable@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/p-cancelable\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/p-cancelable\",\n    \"licenseFile\": \"/node_modules/p-cancelable/license\"\n  },\n  \"p-limit@3.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/p-limit\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/p-limit\",\n    \"licenseFile\": \"/node_modules/p-limit/license\"\n  },\n  \"package-json-from-dist@1.0.1\": {\n    \"licenses\": \"BlueOak-1.0.0\",\n    \"repository\": \"https://github.com/isaacs/package-json-from-dist\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://izs.me\",\n    \"path\": \"/node_modules/package-json-from-dist\",\n    \"licenseFile\": \"/node_modules/package-json-from-dist/LICENSE.md\"\n  },\n  \"pako@1.0.11\": {\n    \"licenses\": \"(MIT AND Zlib)\",\n    \"repository\": \"https://github.com/nodeca/pako\",\n    \"path\": \"/node_modules/pako\",\n    \"licenseFile\": \"/node_modules/pako/LICENSE\"\n  },\n  \"param-case@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/blakeembrey/param-case\",\n    \"publisher\": \"Blake Embrey\",\n    \"email\": \"hello@blakeembrey.com\",\n    \"url\": \"http://blakeembrey.me\",\n    \"path\": \"/node_modules/param-case\",\n    \"licenseFile\": \"/node_modules/param-case/LICENSE\"\n  },\n  \"parse-bmfont-ascii@1.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mattdesl/parse-bmfont-ascii\",\n    \"publisher\": \"Matt DesLauriers\",\n    \"email\": \"dave.des@gmail.com\",\n    \"url\": \"https://github.com/mattdesl\",\n    \"path\": \"/node_modules/parse-bmfont-ascii\",\n    \"licenseFile\": \"/node_modules/parse-bmfont-ascii/README.md\"\n  },\n  \"parse-bmfont-binary@1.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Jam3/parse-bmfont-binary\",\n    \"publisher\": \"Matt DesLauriers\",\n    \"email\": \"dave.des@gmail.com\",\n    \"url\": \"https://github.com/mattdesl\",\n    \"path\": \"/node_modules/parse-bmfont-binary\",\n    \"licenseFile\": \"/node_modules/parse-bmfont-binary/LICENSE.md\"\n  },\n  \"parse-bmfont-xml@1.1.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mattdesl/parse-bmfont-xml\",\n    \"publisher\": \"Matt DesLauriers\",\n    \"email\": \"dave.des@gmail.com\",\n    \"url\": \"https://github.com/mattdesl\",\n    \"path\": \"/node_modules/parse-bmfont-xml\",\n    \"licenseFile\": \"/node_modules/parse-bmfont-xml/LICENSE.md\"\n  },\n  \"parse-headers@2.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kesla/parse-headers\",\n    \"publisher\": \"David Björklund\",\n    \"email\": \"david.bjorklund@gmail.com\",\n    \"path\": \"/node_modules/parse-headers\",\n    \"licenseFile\": \"/node_modules/parse-headers/LICENCE\"\n  },\n  \"parse-json@2.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/parse-json\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/parse-json\",\n    \"licenseFile\": \"/node_modules/parse-json/license\"\n  },\n  \"path-browserify@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/browserify/path-browserify\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/path-browserify\",\n    \"licenseFile\": \"/node_modules/path-browserify/LICENSE\"\n  },\n  \"path-exists@2.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/path-exists\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/path-exists\",\n    \"licenseFile\": \"/node_modules/path-exists/license\"\n  },\n  \"path-is-absolute@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/path-is-absolute\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/path-is-absolute\",\n    \"licenseFile\": \"/node_modules/path-is-absolute/license\"\n  },\n  \"path-key@3.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/path-key\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/path-key\",\n    \"licenseFile\": \"/node_modules/path-key/license\"\n  },\n  \"path-parse@1.0.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jbgutierrez/path-parse\",\n    \"publisher\": \"Javier Blanco\",\n    \"email\": \"http://jbgutierrez.info\",\n    \"path\": \"/node_modules/path-parse\",\n    \"licenseFile\": \"/node_modules/path-parse/LICENSE\"\n  },\n  \"path-scurry@1.11.1\": {\n    \"licenses\": \"BlueOak-1.0.0\",\n    \"repository\": \"https://github.com/isaacs/path-scurry\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"https://blog.izs.me\",\n    \"path\": \"/node_modules/path-scurry\",\n    \"licenseFile\": \"/node_modules/path-scurry/LICENSE.md\"\n  },\n  \"path-type@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/path-type\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/path-type\",\n    \"licenseFile\": \"/node_modules/path-type/license\"\n  },\n  \"peek-readable@4.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/peek-readable\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/node_modules/peek-readable\",\n    \"licenseFile\": \"/node_modules/peek-readable/LICENSE\"\n  },\n  \"phin@3.7.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ethanent/phin\",\n    \"publisher\": \"Ethan Davis\",\n    \"path\": \"/node_modules/phin\",\n    \"licenseFile\": \"/node_modules/phin/LICENSE\"\n  },\n  \"pify@2.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/pify\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/load-json-file/node_modules/pify\",\n    \"licenseFile\": \"/node_modules/load-json-file/node_modules/pify/license\"\n  },\n  \"pify@4.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/pify\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/pify\",\n    \"licenseFile\": \"/node_modules/pify/license\"\n  },\n  \"pinkie-promise@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/floatdrop/pinkie-promise\",\n    \"publisher\": \"Vsevolod Strukchinsky\",\n    \"email\": \"floatdrop@gmail.com\",\n    \"url\": \"github.com/floatdrop\",\n    \"path\": \"/node_modules/pinkie-promise\",\n    \"licenseFile\": \"/node_modules/pinkie-promise/license\"\n  },\n  \"pinkie@2.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/floatdrop/pinkie\",\n    \"publisher\": \"Vsevolod Strukchinsky\",\n    \"email\": \"floatdrop@gmail.com\",\n    \"url\": \"github.com/floatdrop\",\n    \"path\": \"/node_modules/pinkie\",\n    \"licenseFile\": \"/node_modules/pinkie/license\"\n  },\n  \"pixelmatch@4.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/mapbox/pixelmatch\",\n    \"publisher\": \"Vladimir Agafonkin\",\n    \"path\": \"/node_modules/pixelmatch\",\n    \"licenseFile\": \"/node_modules/pixelmatch/LICENSE\"\n  },\n  \"pngjs@3.4.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lukeapage/pngjs2\",\n    \"path\": \"/node_modules/pixelmatch/node_modules/pngjs\",\n    \"licenseFile\": \"/node_modules/pixelmatch/node_modules/pngjs/LICENSE\"\n  },\n  \"pngjs@6.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/lukeapage/pngjs\",\n    \"path\": \"/node_modules/pngjs\",\n    \"licenseFile\": \"/node_modules/pngjs/LICENSE\"\n  },\n  \"possible-typed-array-names@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/possible-typed-array-names\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/possible-typed-array-names\",\n    \"licenseFile\": \"/node_modules/possible-typed-array-names/LICENSE\"\n  },\n  \"prebuild-install@7.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/prebuild/prebuild-install\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/prebuild-install\",\n    \"licenseFile\": \"/node_modules/prebuild-install/LICENSE\"\n  },\n  \"prettier-bytes@1.0.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/Flet/prettier-bytes\",\n    \"publisher\": \"Dan Flettre\",\n    \"email\": \"flettre@gmail.com\",\n    \"path\": \"/node_modules/prettier-bytes\",\n    \"licenseFile\": \"/node_modules/prettier-bytes/LICENSE\"\n  },\n  \"prismjs@1.30.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/PrismJS/prism\",\n    \"publisher\": \"Lea Verou\",\n    \"path\": \"/node_modules/prismjs\",\n    \"licenseFile\": \"/node_modules/prismjs/LICENSE\"\n  },\n  \"process-nextick-args@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/calvinmetcalf/process-nextick-args\",\n    \"path\": \"/node_modules/process-nextick-args\",\n    \"licenseFile\": \"/node_modules/process-nextick-args/license.md\"\n  },\n  \"process@0.11.10\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/shtylman/node-process\",\n    \"publisher\": \"Roman Shtylman\",\n    \"email\": \"shtylman@gmail.com\",\n    \"path\": \"/node_modules/process\",\n    \"licenseFile\": \"/node_modules/process/LICENSE\"\n  },\n  \"promise-retry@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/IndigoUnited/node-promise-retry\",\n    \"publisher\": \"IndigoUnited\",\n    \"email\": \"hello@indigounited.com\",\n    \"url\": \"http://indigounited.com\",\n    \"path\": \"/node_modules/promise-retry\",\n    \"licenseFile\": \"/node_modules/promise-retry/LICENSE\"\n  },\n  \"proto-list@1.2.4\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/proto-list\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/proto-list\",\n    \"licenseFile\": \"/node_modules/proto-list/LICENSE\"\n  },\n  \"pump@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/pump\",\n    \"publisher\": \"Mathias Buus Madsen\",\n    \"email\": \"mathiasbuus@gmail.com\",\n    \"path\": \"/node_modules/pump\",\n    \"licenseFile\": \"/node_modules/pump/LICENSE\"\n  },\n  \"qs@6.14.0\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/ljharb/qs\",\n    \"path\": \"/node_modules/qs\",\n    \"licenseFile\": \"/node_modules/qs/LICENSE.md\"\n  },\n  \"query-string@7.1.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/query-string\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/query-string\",\n    \"licenseFile\": \"/node_modules/query-string/license\"\n  },\n  \"queue@6.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jessetane/queue\",\n    \"publisher\": \"Jesse Tane\",\n    \"email\": \"jesse.tane@gmail.com\",\n    \"path\": \"/node_modules/queue\",\n    \"licenseFile\": \"/node_modules/queue/LICENSE\"\n  },\n  \"quick-lru@5.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/quick-lru\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/quick-lru\",\n    \"licenseFile\": \"/node_modules/quick-lru/license\"\n  },\n  \"rc@1.2.8\": {\n    \"licenses\": \"(BSD-2-Clause OR MIT OR Apache-2.0)\",\n    \"repository\": \"https://github.com/dominictarr/rc\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"dominictarr.com\",\n    \"path\": \"/node_modules/rc\",\n    \"licenseFile\": \"/node_modules/rc/LICENSE.APACHE2\"\n  },\n  \"read-pkg-up@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/read-pkg-up\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/read-pkg-up\",\n    \"licenseFile\": \"/node_modules/read-pkg-up/license\"\n  },\n  \"read-pkg@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/read-pkg\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/read-pkg\",\n    \"licenseFile\": \"/node_modules/read-pkg/license\"\n  },\n  \"readable-stream@1.1.14\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/isaacs/readable-stream\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/ftp/node_modules/readable-stream\",\n    \"licenseFile\": \"/node_modules/ftp/node_modules/readable-stream/LICENSE\"\n  },\n  \"readable-stream@2.3.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/readable-stream\",\n    \"path\": \"/node_modules/lazystream/node_modules/readable-stream\",\n    \"licenseFile\": \"/node_modules/lazystream/node_modules/readable-stream/LICENSE\"\n  },\n  \"readable-stream@3.6.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/readable-stream\",\n    \"path\": \"/node_modules/readable-stream\",\n    \"licenseFile\": \"/node_modules/readable-stream/LICENSE\"\n  },\n  \"readable-stream@4.7.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/readable-stream\",\n    \"path\": \"/node_modules/readable-web-to-node-stream/node_modules/readable-stream\",\n    \"licenseFile\": \"/node_modules/readable-web-to-node-stream/node_modules/readable-stream/LICENSE\"\n  },\n  \"readable-web-to-node-stream@3.0.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/readable-web-to-node-stream\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/node_modules/readable-web-to-node-stream\",\n    \"licenseFile\": \"/node_modules/readable-web-to-node-stream/README.md\"\n  },\n  \"readdir-glob@1.1.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/Yqnn/node-readdir-glob\",\n    \"publisher\": \"Yann Armelin\",\n    \"path\": \"/node_modules/readdir-glob\",\n    \"licenseFile\": \"/node_modules/readdir-glob/LICENSE\"\n  },\n  \"redent@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/redent\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/redent\",\n    \"licenseFile\": \"/node_modules/redent/license\"\n  },\n  \"regenerator-runtime@0.13.11\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/facebook/regenerator/tree/main/packages/runtime\",\n    \"publisher\": \"Ben Newman\",\n    \"email\": \"bn@cs.stanford.edu\",\n    \"path\": \"/node_modules/regenerator-runtime\",\n    \"licenseFile\": \"/node_modules/regenerator-runtime/LICENSE\"\n  },\n  \"relateurl@0.2.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/stevenvachon/relateurl\",\n    \"publisher\": \"Steven Vachon\",\n    \"email\": \"contact@svachon.com\",\n    \"url\": \"http://www.svachon.com/\",\n    \"path\": \"/node_modules/relateurl\",\n    \"licenseFile\": \"/node_modules/relateurl/license\"\n  },\n  \"repeating@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/repeating\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/repeating\",\n    \"licenseFile\": \"/node_modules/repeating/license\"\n  },\n  \"require-directory@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/troygoode/node-require-directory\",\n    \"publisher\": \"Troy Goode\",\n    \"email\": \"troygoode@gmail.com\",\n    \"url\": \"http://github.com/troygoode/\",\n    \"path\": \"/node_modules/require-directory\",\n    \"licenseFile\": \"/node_modules/require-directory/LICENSE\"\n  },\n  \"resolve-alpn@1.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/szmarczak/resolve-alpn\",\n    \"publisher\": \"Szymon Marczak\",\n    \"path\": \"/node_modules/resolve-alpn\",\n    \"licenseFile\": \"/node_modules/resolve-alpn/LICENSE\"\n  },\n  \"resolve@1.22.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/browserify/resolve\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/resolve\",\n    \"licenseFile\": \"/node_modules/resolve/LICENSE\"\n  },\n  \"responselike@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/responselike\",\n    \"publisher\": \"lukechilds\",\n    \"path\": \"/node_modules/responselike\",\n    \"licenseFile\": \"/node_modules/responselike/LICENSE\"\n  },\n  \"retry-request@5.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/stephenplusplus/retry-request\",\n    \"publisher\": \"Stephen Sawchuk\",\n    \"email\": \"sawchuk@gmail.com\",\n    \"path\": \"/node_modules/retry-request\",\n    \"licenseFile\": \"/node_modules/retry-request/license\"\n  },\n  \"retry@0.12.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tim-kos/node-retry\",\n    \"publisher\": \"Tim Koschützki\",\n    \"email\": \"tim@debuggable.com\",\n    \"url\": \"http://debuggable.com/\",\n    \"path\": \"/node_modules/promise-retry/node_modules/retry\",\n    \"licenseFile\": \"/node_modules/promise-retry/node_modules/retry/License\"\n  },\n  \"retry@0.13.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tim-kos/node-retry\",\n    \"publisher\": \"Tim Koschützki\",\n    \"email\": \"tim@debuggable.com\",\n    \"url\": \"http://debuggable.com/\",\n    \"path\": \"/node_modules/retry\",\n    \"licenseFile\": \"/node_modules/retry/License\"\n  },\n  \"safe-buffer@5.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/safe-buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"http://feross.org\",\n    \"path\": \"/node_modules/lazystream/node_modules/safe-buffer\",\n    \"licenseFile\": \"/node_modules/lazystream/node_modules/safe-buffer/LICENSE\"\n  },\n  \"safe-buffer@5.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/safe-buffer\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/node_modules/safe-buffer\",\n    \"licenseFile\": \"/node_modules/safe-buffer/LICENSE\"\n  },\n  \"safer-buffer@2.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ChALkeR/safer-buffer\",\n    \"publisher\": \"Nikita Skovoroda\",\n    \"email\": \"chalkerx@gmail.com\",\n    \"url\": \"https://github.com/ChALkeR\",\n    \"path\": \"/node_modules/safer-buffer\",\n    \"licenseFile\": \"/node_modules/safer-buffer/LICENSE\"\n  },\n  \"sax@1.4.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/sax-js\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/sax\",\n    \"licenseFile\": \"/node_modules/sax/LICENSE\"\n  },\n  \"semver@5.7.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/node-semver\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/node_modules/normalize-package-data/node_modules/semver\",\n    \"licenseFile\": \"/node_modules/normalize-package-data/node_modules/semver/LICENSE\"\n  },\n  \"semver@7.7.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/node-semver\",\n    \"publisher\": \"GitHub Inc.\",\n    \"path\": \"/node_modules/prebuild-install/node_modules/semver\",\n    \"licenseFile\": \"/node_modules/prebuild-install/node_modules/semver/LICENSE\"\n  },\n  \"set-function-length@1.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/set-function-length\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/set-function-length\",\n    \"licenseFile\": \"/node_modules/set-function-length/LICENSE\"\n  },\n  \"sha.js@2.4.12\": {\n    \"licenses\": \"(MIT AND BSD-3-Clause)\",\n    \"repository\": \"https://github.com/crypto-browserify/sha.js\",\n    \"publisher\": \"Dominic Tarr\",\n    \"email\": \"dominic.tarr@gmail.com\",\n    \"url\": \"dominictarr.com\",\n    \"path\": \"/node_modules/sha.js\",\n    \"licenseFile\": \"/node_modules/sha.js/LICENSE\"\n  },\n  \"sharp@0.34.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/lovell/sharp\",\n    \"publisher\": \"Lovell Fuller\",\n    \"email\": \"npm@lovell.info\",\n    \"path\": \"/node_modules/sharp\",\n    \"licenseFile\": \"/node_modules/sharp/LICENSE\"\n  },\n  \"shebang-command@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kevva/shebang-command\",\n    \"publisher\": \"Kevin Mårtensson\",\n    \"email\": \"kevinmartensson@gmail.com\",\n    \"url\": \"github.com/kevva\",\n    \"path\": \"/node_modules/shebang-command\",\n    \"licenseFile\": \"/node_modules/shebang-command/license\"\n  },\n  \"shebang-regex@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/shebang-regex\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/shebang-regex\",\n    \"licenseFile\": \"/node_modules/shebang-regex/license\"\n  },\n  \"side-channel-list@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/side-channel-list\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/side-channel-list\",\n    \"licenseFile\": \"/node_modules/side-channel-list/LICENSE\"\n  },\n  \"side-channel-map@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/side-channel-map\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/side-channel-map\",\n    \"licenseFile\": \"/node_modules/side-channel-map/LICENSE\"\n  },\n  \"side-channel-weakmap@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/side-channel-weakmap\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/side-channel-weakmap\",\n    \"licenseFile\": \"/node_modules/side-channel-weakmap/LICENSE\"\n  },\n  \"side-channel@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ljharb/side-channel\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/side-channel\",\n    \"licenseFile\": \"/node_modules/side-channel/LICENSE\"\n  },\n  \"signal-exit@3.0.7\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/tapjs/signal-exit\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"ben@npmjs.com\",\n    \"path\": \"/node_modules/signal-exit\",\n    \"licenseFile\": \"/node_modules/signal-exit/LICENSE.txt\"\n  },\n  \"signal-exit@4.1.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/tapjs/signal-exit\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"ben@npmjs.com\",\n    \"path\": \"/node_modules/foreground-child/node_modules/signal-exit\",\n    \"licenseFile\": \"/node_modules/foreground-child/node_modules/signal-exit/LICENSE.txt\"\n  },\n  \"simple-concat@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/simple-concat\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/node_modules/simple-concat\",\n    \"licenseFile\": \"/node_modules/simple-concat/LICENSE\"\n  },\n  \"simple-get@4.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/feross/simple-get\",\n    \"publisher\": \"Feross Aboukhadijeh\",\n    \"email\": \"feross@feross.org\",\n    \"url\": \"https://feross.org\",\n    \"path\": \"/node_modules/simple-get\",\n    \"licenseFile\": \"/node_modules/simple-get/LICENSE\"\n  },\n  \"simple-swizzle@0.2.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/qix-/node-simple-swizzle\",\n    \"publisher\": \"Qix\",\n    \"url\": \"http://github.com/qix-\",\n    \"path\": \"/node_modules/simple-swizzle\",\n    \"licenseFile\": \"/node_modules/simple-swizzle/LICENSE\"\n  },\n  \"slug@0.9.4\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Trott/node-slug\",\n    \"publisher\": \"dodo\",\n    \"url\": \"https://github.com/dodo\",\n    \"path\": \"/node_modules/slug\",\n    \"licenseFile\": \"/node_modules/slug/LICENSE\"\n  },\n  \"sortablejs@1.10.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/SortableJS/Sortable\",\n    \"path\": \"/node_modules/sortablejs\",\n    \"licenseFile\": \"/node_modules/sortablejs/LICENSE\"\n  },\n  \"source-map@0.6.1\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/mozilla/source-map\",\n    \"publisher\": \"Nick Fitzgerald\",\n    \"email\": \"nfitzgerald@mozilla.com\",\n    \"path\": \"/node_modules/source-map\",\n    \"licenseFile\": \"/node_modules/source-map/LICENSE\"\n  },\n  \"spdx-correct@3.2.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/jslicense/spdx-correct.js\",\n    \"path\": \"/node_modules/spdx-correct\",\n    \"licenseFile\": \"/node_modules/spdx-correct/LICENSE\"\n  },\n  \"spdx-exceptions@2.5.0\": {\n    \"licenses\": \"CC-BY-3.0\",\n    \"repository\": \"https://github.com/kemitchell/spdx-exceptions.json\",\n    \"publisher\": \"The Linux Foundation\",\n    \"path\": \"/node_modules/spdx-exceptions\",\n    \"licenseFile\": \"/node_modules/spdx-exceptions/README.md\"\n  },\n  \"spdx-expression-parse@3.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jslicense/spdx-expression-parse.js\",\n    \"publisher\": \"Kyle E. Mitchell\",\n    \"email\": \"kyle@kemitchell.com\",\n    \"url\": \"https://kemitchell.com\",\n    \"path\": \"/node_modules/spdx-expression-parse\",\n    \"licenseFile\": \"/node_modules/spdx-expression-parse/LICENSE\"\n  },\n  \"spdx-license-ids@3.0.17\": {\n    \"licenses\": \"CC0-1.0\",\n    \"repository\": \"https://github.com/jslicense/spdx-license-ids\",\n    \"publisher\": \"Shinnosuke Watanabe\",\n    \"url\": \"https://github.com/shinnn\",\n    \"path\": \"/node_modules/spdx-license-ids\",\n    \"licenseFile\": \"/node_modules/spdx-license-ids/README.md\"\n  },\n  \"split-on-first@1.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/split-on-first\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/split-on-first\",\n    \"licenseFile\": \"/node_modules/split-on-first/license\"\n  },\n  \"sqlstring@2.3.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mysqljs/sqlstring\",\n    \"path\": \"/node_modules/sqlstring\",\n    \"licenseFile\": \"/node_modules/sqlstring/LICENSE\"\n  },\n  \"ssh2-sftp-client@10.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/theophilusx/ssh2-sftp-client\",\n    \"publisher\": \"Tim Cross\",\n    \"path\": \"/node_modules/ssh2-sftp-client\",\n    \"licenseFile\": \"/node_modules/ssh2-sftp-client/LICENSE\"\n  },\n  \"ssh2@1.15.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mscdex/ssh2\",\n    \"publisher\": \"Brian White\",\n    \"email\": \"mscdex@mscdex.net\",\n    \"path\": \"/node_modules/ssh2\",\n    \"licenseFile\": \"/node_modules/ssh2/LICENSE\"\n  },\n  \"status-logger@3.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/joehand/status-logger\",\n    \"publisher\": \"Joe Hand\",\n    \"email\": \"joe@joeahand.com\",\n    \"url\": \"http://joeahand.com/\",\n    \"path\": \"/node_modules/status-logger\",\n    \"licenseFile\": \"/node_modules/status-logger/readme.md\"\n  },\n  \"stream-events@1.0.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/stephenplusplus/stream-events\",\n    \"publisher\": \"Stephen Sawchuk\",\n    \"path\": \"/node_modules/stream-events\",\n    \"licenseFile\": \"/node_modules/stream-events/readme.md\"\n  },\n  \"stream-shift@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/stream-shift\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/stream-shift\",\n    \"licenseFile\": \"/node_modules/stream-shift/LICENSE\"\n  },\n  \"streamx@2.22.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/streamx\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/streamx\",\n    \"licenseFile\": \"/node_modules/streamx/LICENSE\"\n  },\n  \"strict-uri-encode@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kevva/strict-uri-encode\",\n    \"publisher\": \"Kevin Mårtensson\",\n    \"email\": \"kevinmartensson@gmail.com\",\n    \"url\": \"github.com/kevva\",\n    \"path\": \"/node_modules/strict-uri-encode\",\n    \"licenseFile\": \"/node_modules/strict-uri-encode/license\"\n  },\n  \"string-width@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/string-width\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/string-width\",\n    \"licenseFile\": \"/node_modules/string-width/license\"\n  },\n  \"string-width@4.2.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/string-width\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/string-width-cjs\",\n    \"licenseFile\": \"/node_modules/string-width-cjs/license\"\n  },\n  \"string-width@5.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/string-width\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/@isaacs/cliui/node_modules/string-width\",\n    \"licenseFile\": \"/node_modules/@isaacs/cliui/node_modules/string-width/license\"\n  },\n  \"string_decoder@0.10.31\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/rvagg/string_decoder\",\n    \"path\": \"/node_modules/ftp/node_modules/string_decoder\",\n    \"licenseFile\": \"/node_modules/ftp/node_modules/string_decoder/LICENSE\"\n  },\n  \"string_decoder@1.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/string_decoder\",\n    \"path\": \"/node_modules/lazystream/node_modules/string_decoder\",\n    \"licenseFile\": \"/node_modules/lazystream/node_modules/string_decoder/LICENSE\"\n  },\n  \"string_decoder@1.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/string_decoder\",\n    \"path\": \"/node_modules/string_decoder\",\n    \"licenseFile\": \"/node_modules/string_decoder/LICENSE\"\n  },\n  \"strip-ansi@4.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/strip-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/strip-ansi\",\n    \"licenseFile\": \"/node_modules/strip-ansi/license\"\n  },\n  \"strip-ansi@6.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/strip-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/string-width-cjs/node_modules/strip-ansi\",\n    \"licenseFile\": \"/node_modules/string-width-cjs/node_modules/strip-ansi/license\"\n  },\n  \"strip-ansi@7.1.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/strip-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/@isaacs/cliui/node_modules/strip-ansi\",\n    \"licenseFile\": \"/node_modules/@isaacs/cliui/node_modules/strip-ansi/license\"\n  },\n  \"strip-bom@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/strip-bom\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/strip-bom\",\n    \"licenseFile\": \"/node_modules/strip-bom/license\"\n  },\n  \"strip-indent@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/strip-indent\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"http://sindresorhus.com\",\n    \"path\": \"/node_modules/strip-indent\",\n    \"licenseFile\": \"/node_modules/strip-indent/license\"\n  },\n  \"strip-json-comments@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/strip-json-comments\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/strip-json-comments\",\n    \"licenseFile\": \"/node_modules/strip-json-comments/license\"\n  },\n  \"striptags@3.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/ericnorris/striptags\",\n    \"publisher\": \"Eric Norris\",\n    \"url\": \"https://github.com/ericnorris\",\n    \"path\": \"/node_modules/striptags\",\n    \"licenseFile\": \"/node_modules/striptags/LICENSE\"\n  },\n  \"strnum@1.0.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/NaturalIntelligence/strnum\",\n    \"publisher\": \"Amit Gupta\",\n    \"url\": \"https://amitkumargupta.work/\",\n    \"path\": \"/node_modules/strnum\",\n    \"licenseFile\": \"/node_modules/strnum/LICENSE\"\n  },\n  \"strtok3@6.3.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/strtok3\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/node_modules/strtok3\",\n    \"licenseFile\": \"/node_modules/strtok3/LICENSE\"\n  },\n  \"stubs@3.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/stephenplusplus/stubs\",\n    \"publisher\": \"Stephen Sawchuk\",\n    \"path\": \"/node_modules/stubs\",\n    \"licenseFile\": \"/node_modules/stubs/readme.md\"\n  },\n  \"supports-preserve-symlinks-flag@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/node-supports-preserve-symlinks-flag\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/supports-preserve-symlinks-flag\",\n    \"licenseFile\": \"/node_modules/supports-preserve-symlinks-flag/LICENSE\"\n  },\n  \"tar-fs@2.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/tar-fs\",\n    \"publisher\": \"Mathias Buus\",\n    \"path\": \"/node_modules/prebuild-install/node_modules/tar-fs\",\n    \"licenseFile\": \"/node_modules/prebuild-install/node_modules/tar-fs/LICENSE\"\n  },\n  \"tar-fs@3.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/tar-fs\",\n    \"publisher\": \"Mathias Buus\",\n    \"path\": \"/node_modules/tar-fs\",\n    \"licenseFile\": \"/node_modules/tar-fs/LICENSE\"\n  },\n  \"tar-stream@2.2.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/tar-stream\",\n    \"publisher\": \"Mathias Buus\",\n    \"email\": \"mathiasbuus@gmail.com\",\n    \"path\": \"/node_modules/tar-stream\",\n    \"licenseFile\": \"/node_modules/tar-stream/LICENSE\"\n  },\n  \"tar-stream@3.1.7\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/mafintosh/tar-stream\",\n    \"publisher\": \"Mathias Buus\",\n    \"email\": \"mathiasbuus@gmail.com\",\n    \"path\": \"/node_modules/tar-fs/node_modules/tar-stream\",\n    \"licenseFile\": \"/node_modules/tar-fs/node_modules/tar-stream/LICENSE\"\n  },\n  \"teeny-request@8.0.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/googleapis/teeny-request\",\n    \"publisher\": \"fhinkel\",\n    \"path\": \"/node_modules/teeny-request\",\n    \"licenseFile\": \"/node_modules/teeny-request/LICENSE\"\n  },\n  \"text-decoder@1.2.3\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/holepunchto/text-decoder\",\n    \"publisher\": \"Holepunch\",\n    \"path\": \"/node_modules/text-decoder\",\n    \"licenseFile\": \"/node_modules/text-decoder/LICENSE\"\n  },\n  \"through2@2.0.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/rvagg/through2\",\n    \"publisher\": \"Rod Vagg\",\n    \"email\": \"r@va.gg\",\n    \"url\": \"https://github.com/rvagg\",\n    \"path\": \"/node_modules/through2\",\n    \"licenseFile\": \"/node_modules/through2/LICENSE.md\"\n  },\n  \"timm@1.7.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/guigrpa/timm\",\n    \"publisher\": \"Guillermo Grau Panea\",\n    \"path\": \"/node_modules/timm\",\n    \"licenseFile\": \"/node_modules/timm/LICENSE\"\n  },\n  \"tinycolor2@1.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/bgrins/TinyColor\",\n    \"publisher\": \"Brian Grinstead\",\n    \"email\": \"briangrinstead@gmail.com\",\n    \"url\": \"http://briangrinstead.com\",\n    \"path\": \"/node_modules/tinycolor2\",\n    \"licenseFile\": \"/node_modules/tinycolor2/LICENSE\"\n  },\n  \"to-buffer@1.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/browserify/to-buffer\",\n    \"publisher\": \"Mathias Buus\",\n    \"url\": \"@mafintosh\",\n    \"path\": \"/node_modules/to-buffer\",\n    \"licenseFile\": \"/node_modules/to-buffer/LICENSE\"\n  },\n  \"token-types@4.2.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Borewit/token-types\",\n    \"publisher\": \"Borewit\",\n    \"url\": \"https://github.com/Borewit\",\n    \"path\": \"/node_modules/token-types\",\n    \"licenseFile\": \"/node_modules/token-types/LICENSE\"\n  },\n  \"tr46@0.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Sebmaster/tr46.js\",\n    \"publisher\": \"Sebastian Mayr\",\n    \"email\": \"npm@smayr.name\",\n    \"path\": \"/node_modules/tr46\"\n  },\n  \"transliteration@2.3.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/dzcpy/transliteration\",\n    \"path\": \"/node_modules/transliteration\",\n    \"licenseFile\": \"/node_modules/transliteration/LICENSE.txt\"\n  },\n  \"tree-flatten@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/cantidio/node-tree-flatten\",\n    \"publisher\": \"Cantidio Fontes\",\n    \"email\": \"aniquilatorbloody@gmail.com\",\n    \"path\": \"/node_modules/tree-flatten\",\n    \"licenseFile\": \"/node_modules/tree-flatten/LICENSE\"\n  },\n  \"trim-newlines@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/trim-newlines\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/trim-newlines\",\n    \"licenseFile\": \"/node_modules/trim-newlines/license\"\n  },\n  \"tslib@2.6.2\": {\n    \"licenses\": \"0BSD\",\n    \"repository\": \"https://github.com/Microsoft/tslib\",\n    \"publisher\": \"Microsoft Corp.\",\n    \"path\": \"/node_modules/tslib\",\n    \"licenseFile\": \"/node_modules/tslib/LICENSE.txt\"\n  },\n  \"tunnel-agent@0.6.0\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/mikeal/tunnel-agent\",\n    \"publisher\": \"Mikeal Rogers\",\n    \"email\": \"mikeal.rogers@gmail.com\",\n    \"url\": \"http://www.futurealoof.com\",\n    \"path\": \"/node_modules/tunnel-agent\",\n    \"licenseFile\": \"/node_modules/tunnel-agent/LICENSE\"\n  },\n  \"tweetnacl@0.14.5\": {\n    \"licenses\": \"Unlicense\",\n    \"repository\": \"https://github.com/dchest/tweetnacl-js\",\n    \"publisher\": \"TweetNaCl-js contributors\",\n    \"path\": \"/node_modules/tweetnacl\",\n    \"licenseFile\": \"/node_modules/tweetnacl/LICENSE\"\n  },\n  \"typed-array-buffer@1.0.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/typed-array-buffer\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"path\": \"/node_modules/typed-array-buffer\",\n    \"licenseFile\": \"/node_modules/typed-array-buffer/LICENSE\"\n  },\n  \"typedarray@0.0.6\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/typedarray\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/typedarray\",\n    \"licenseFile\": \"/node_modules/typedarray/LICENSE\"\n  },\n  \"typo-js@1.2.4\": {\n    \"licenses\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/cfinke/Typo.js\",\n    \"publisher\": \"Christopher Finke\",\n    \"email\": \"cfinke@gmail.com\",\n    \"url\": \"http://www.chrisfinke.com/\",\n    \"path\": \"/node_modules/typo-js\",\n    \"licenseFile\": \"/node_modules/typo-js/README.md\"\n  },\n  \"uglify-js@3.17.4\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/mishoo/UglifyJS\",\n    \"publisher\": \"Mihai Bazon\",\n    \"email\": \"mihai.bazon@gmail.com\",\n    \"url\": \"http://lisperator.net/\",\n    \"path\": \"/node_modules/uglify-js\",\n    \"licenseFile\": \"/node_modules/uglify-js/LICENSE\"\n  },\n  \"undici-types@6.21.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/nodejs/undici\",\n    \"path\": \"/node_modules/undici-types\",\n    \"licenseFile\": \"/node_modules/undici-types/LICENSE\"\n  },\n  \"unescape@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jonschlinkert/unescape\",\n    \"publisher\": \"Jon Schlinkert\",\n    \"url\": \"https://github.com/jonschlinkert\",\n    \"path\": \"/node_modules/unescape\",\n    \"licenseFile\": \"/node_modules/unescape/LICENSE\"\n  },\n  \"unicode@14.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/tdanecker/node-unicodetable\",\n    \"publisher\": \"dodo\",\n    \"url\": \"https://github.com/dodo\",\n    \"path\": \"/node_modules/unicode\",\n    \"licenseFile\": \"/node_modules/unicode/LICENSE\"\n  },\n  \"universal-user-agent@7.0.3\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/gr2m/universal-user-agent\",\n    \"publisher\": \"Gregor Martynus\",\n    \"url\": \"https://github.com/gr2m\",\n    \"path\": \"/node_modules/universal-user-agent\",\n    \"licenseFile\": \"/node_modules/universal-user-agent/LICENSE.md\"\n  },\n  \"universalify@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/RyanZim/universalify\",\n    \"publisher\": \"Ryan Zimmerman\",\n    \"email\": \"opensrc@ryanzim.com\",\n    \"path\": \"/node_modules/universalify\",\n    \"licenseFile\": \"/node_modules/universalify/LICENSE\"\n  },\n  \"upper-case@1.1.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/blakeembrey/upper-case\",\n    \"publisher\": \"Blake Embrey\",\n    \"email\": \"hello@blakeembrey.com\",\n    \"url\": \"http://blakeembrey.me\",\n    \"path\": \"/node_modules/upper-case\",\n    \"licenseFile\": \"/node_modules/upper-case/LICENSE\"\n  },\n  \"utif2@4.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/photopea/UTIF.js\",\n    \"publisher\": \"photopea\",\n    \"url\": \"https://github.com/photopea\",\n    \"path\": \"/node_modules/utif2\",\n    \"licenseFile\": \"/node_modules/utif2/LICENSE\"\n  },\n  \"util-deprecate@1.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/TooTallNate/util-deprecate\",\n    \"publisher\": \"Nathan Rajlich\",\n    \"email\": \"nathan@tootallnate.net\",\n    \"url\": \"http://n8.io/\",\n    \"path\": \"/node_modules/util-deprecate\",\n    \"licenseFile\": \"/node_modules/util-deprecate/LICENSE\"\n  },\n  \"uuid@8.3.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/uuidjs/uuid\",\n    \"path\": \"/node_modules/uuid\",\n    \"licenseFile\": \"/node_modules/uuid/LICENSE.md\"\n  },\n  \"uuid@9.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/uuidjs/uuid\",\n    \"path\": \"/node_modules/@smithy/middleware-retry/node_modules/uuid\",\n    \"licenseFile\": \"/node_modules/@smithy/middleware-retry/node_modules/uuid/LICENSE.md\"\n  },\n  \"validate-npm-package-license@3.0.4\": {\n    \"licenses\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/kemitchell/validate-npm-package-license.js\",\n    \"publisher\": \"Kyle E. Mitchell\",\n    \"email\": \"kyle@kemitchell.com\",\n    \"url\": \"https://kemitchell.com\",\n    \"path\": \"/node_modules/validate-npm-package-license\",\n    \"licenseFile\": \"/node_modules/validate-npm-package-license/LICENSE\"\n  },\n  \"vue-color@2.4.5\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/xiaokaike/vue-color\",\n    \"publisher\": \"xiaokai\",\n    \"email\": \"kexiaokai@gmail.com\",\n    \"path\": \"/node_modules/vue-color\",\n    \"licenseFile\": \"/node_modules/vue-color/LICENSE\"\n  },\n  \"vue-i18n@8.24.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/kazupon/vue-i18n\",\n    \"publisher\": \"kazuya kawaguchi\",\n    \"email\": \"kawakazu80@gmail.com\",\n    \"path\": \"/node_modules/vue-i18n\",\n    \"licenseFile\": \"/node_modules/vue-i18n/LICENSE\"\n  },\n  \"vue-multiselect@2.0.8\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/shentao/vue-multiselect\",\n    \"publisher\": \"Damian Dulisz\",\n    \"email\": \"damian.dulisz@gmail.com\",\n    \"path\": \"/node_modules/vue-multiselect\",\n    \"licenseFile\": \"/node_modules/vue-multiselect/LICENSE\"\n  },\n  \"vue-prism-editor@0.6.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/koca/vue-prism-editor\",\n    \"publisher\": \"Mesut Koca\",\n    \"email\": \"imesutkoca@gmail.com\",\n    \"path\": \"/node_modules/vue-prism-editor\",\n    \"licenseFile\": \"/node_modules/vue-prism-editor/LICENSE\"\n  },\n  \"vue-router@3.5.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vuejs/vue-router\",\n    \"publisher\": \"Evan You\",\n    \"path\": \"/node_modules/vue-router\",\n    \"licenseFile\": \"/node_modules/vue-router/LICENSE\"\n  },\n  \"vue@2.6.14\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vuejs/vue\",\n    \"publisher\": \"Evan You\",\n    \"path\": \"/node_modules/vue\",\n    \"licenseFile\": \"/node_modules/vue/LICENSE\"\n  },\n  \"vuedraggable@2.24.3\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/SortableJS/Vue.Draggable\",\n    \"path\": \"/node_modules/vuedraggable\",\n    \"licenseFile\": \"/node_modules/vuedraggable/LICENSE\"\n  },\n  \"vuex@3.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/vuejs/vuex\",\n    \"publisher\": \"Evan You\",\n    \"path\": \"/node_modules/vuex\",\n    \"licenseFile\": \"/node_modules/vuex/LICENSE\"\n  },\n  \"webidl-conversions@3.0.1\": {\n    \"licenses\": \"BSD-2-Clause\",\n    \"repository\": \"https://github.com/jsdom/webidl-conversions\",\n    \"publisher\": \"Domenic Denicola\",\n    \"email\": \"d@domenic.me\",\n    \"url\": \"https://domenic.me/\",\n    \"path\": \"/node_modules/webidl-conversions\",\n    \"licenseFile\": \"/node_modules/webidl-conversions/LICENSE.md\"\n  },\n  \"whatwg-fetch@3.6.20\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/github/fetch\",\n    \"path\": \"/node_modules/whatwg-fetch\",\n    \"licenseFile\": \"/node_modules/whatwg-fetch/LICENSE\"\n  },\n  \"whatwg-url@5.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/jsdom/whatwg-url\",\n    \"publisher\": \"Sebastian Mayr\",\n    \"email\": \"github@smayr.name\",\n    \"path\": \"/node_modules/whatwg-url\",\n    \"licenseFile\": \"/node_modules/whatwg-url/LICENSE.txt\"\n  },\n  \"which-typed-array@1.1.19\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/inspect-js/which-typed-array\",\n    \"publisher\": \"Jordan Harband\",\n    \"email\": \"ljharb@gmail.com\",\n    \"url\": \"http://ljharb.codes\",\n    \"path\": \"/node_modules/which-typed-array\",\n    \"licenseFile\": \"/node_modules/which-typed-array/LICENSE\"\n  },\n  \"which@2.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/node-which\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me\",\n    \"path\": \"/node_modules/which\",\n    \"licenseFile\": \"/node_modules/which/LICENSE\"\n  },\n  \"wordwrap@1.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/substack/node-wordwrap\",\n    \"publisher\": \"James Halliday\",\n    \"email\": \"mail@substack.net\",\n    \"url\": \"http://substack.net\",\n    \"path\": \"/node_modules/wordwrap\",\n    \"licenseFile\": \"/node_modules/wordwrap/LICENSE\"\n  },\n  \"wrap-ansi@3.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/wrap-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"sindresorhus.com\",\n    \"path\": \"/node_modules/wrap-ansi\",\n    \"licenseFile\": \"/node_modules/wrap-ansi/license\"\n  },\n  \"wrap-ansi@7.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/wrap-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/wrap-ansi-cjs\",\n    \"licenseFile\": \"/node_modules/wrap-ansi-cjs/license\"\n  },\n  \"wrap-ansi@8.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/chalk/wrap-ansi\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/@isaacs/cliui/node_modules/wrap-ansi\",\n    \"licenseFile\": \"/node_modules/@isaacs/cliui/node_modules/wrap-ansi/license\"\n  },\n  \"wrappy@1.0.2\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/npm/wrappy\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/wrappy\",\n    \"licenseFile\": \"/node_modules/wrappy/LICENSE\"\n  },\n  \"xcase@2.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/encharm/xcase\",\n    \"publisher\": \"Damian Kaczmarek\",\n    \"email\": \"damian@codecharm.co.uk\",\n    \"path\": \"/node_modules/xcase\",\n    \"licenseFile\": \"/node_modules/xcase/LICENSE\"\n  },\n  \"xhr@2.6.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/naugtur/xhr\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/node_modules/xhr\",\n    \"licenseFile\": \"/node_modules/xhr/LICENCE\"\n  },\n  \"xml-parse-from-string@1.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Jam3/xml-parse-from-string\",\n    \"publisher\": \"Matt DesLauriers\",\n    \"email\": \"dave.des@gmail.com\",\n    \"url\": \"https://github.com/mattdesl\",\n    \"path\": \"/node_modules/xml-parse-from-string\",\n    \"licenseFile\": \"/node_modules/xml-parse-from-string/LICENSE.md\"\n  },\n  \"xml2js@0.5.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Leonidas-from-XIV/node-xml2js\",\n    \"publisher\": \"Marek Kubica\",\n    \"email\": \"marek@xivilization.net\",\n    \"url\": \"https://xivilization.net\",\n    \"path\": \"/node_modules/xml2js\",\n    \"licenseFile\": \"/node_modules/xml2js/LICENSE\"\n  },\n  \"xmlbuilder@11.0.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/oozcitak/xmlbuilder-js\",\n    \"publisher\": \"Ozgur Ozcitak\",\n    \"email\": \"oozcitak@gmail.com\",\n    \"path\": \"/node_modules/xmlbuilder\",\n    \"licenseFile\": \"/node_modules/xmlbuilder/LICENSE\"\n  },\n  \"xregexp@2.0.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/slevithan/XRegExp\",\n    \"publisher\": \"Steven Levithan\",\n    \"email\": \"steves_list@hotmail.com\",\n    \"path\": \"/node_modules/xregexp\",\n    \"licenseFile\": \"/node_modules/xregexp/README.md\"\n  },\n  \"xtend@4.0.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/Raynos/xtend\",\n    \"publisher\": \"Raynos\",\n    \"email\": \"raynos2@gmail.com\",\n    \"path\": \"/node_modules/xtend\",\n    \"licenseFile\": \"/node_modules/xtend/LICENSE\"\n  },\n  \"y18n@5.0.8\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/yargs/y18n\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"bencoe@gmail.com\",\n    \"path\": \"/node_modules/y18n\",\n    \"licenseFile\": \"/node_modules/y18n/LICENSE\"\n  },\n  \"yallist@4.0.0\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/isaacs/yallist\",\n    \"publisher\": \"Isaac Z. Schlueter\",\n    \"email\": \"i@izs.me\",\n    \"url\": \"http://blog.izs.me/\",\n    \"path\": \"/node_modules/yallist\",\n    \"licenseFile\": \"/node_modules/yallist/LICENSE\"\n  },\n  \"yargs-parser@21.1.1\": {\n    \"licenses\": \"ISC\",\n    \"repository\": \"https://github.com/yargs/yargs-parser\",\n    \"publisher\": \"Ben Coe\",\n    \"email\": \"ben@npmjs.com\",\n    \"path\": \"/node_modules/yargs-parser\",\n    \"licenseFile\": \"/node_modules/yargs-parser/LICENSE.txt\"\n  },\n  \"yargs@17.7.2\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/yargs/yargs\",\n    \"path\": \"/node_modules/yargs\",\n    \"licenseFile\": \"/node_modules/yargs/LICENSE\"\n  },\n  \"yocto-queue@0.1.0\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/sindresorhus/yocto-queue\",\n    \"publisher\": \"Sindre Sorhus\",\n    \"email\": \"sindresorhus@gmail.com\",\n    \"url\": \"https://sindresorhus.com\",\n    \"path\": \"/node_modules/yocto-queue\",\n    \"licenseFile\": \"/node_modules/yocto-queue/license\"\n  },\n  \"zip-stream@4.1.1\": {\n    \"licenses\": \"MIT\",\n    \"repository\": \"https://github.com/archiverjs/node-zip-stream\",\n    \"publisher\": \"Chris Talkington\",\n    \"url\": \"http://christalkington.com/\",\n    \"path\": \"/node_modules/zip-stream\",\n    \"licenseFile\": \"/node_modules/zip-stream/LICENSE\"\n  }\n}\n\n"
  },
  {
    "path": "app/licenses/locutus/license.txt",
    "content": "Copyright (c) 2007-2016 Kevin van Zonneveld (http://kvz.io)\nand Contributors (http://locutus.io/authors)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/licenses/log-driver/license.txt",
    "content": "Copyright (c) 2014, Gregg Caines, gregg@caines.ca\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "app/licenses/lucide/license.txt",
    "content": "ISC License\n\nCopyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
  },
  {
    "path": "app/licenses/mocha/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2011-2017 JS Foundation and contributors, https://js.foundation\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/ncname/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/nested-sortable/license.txt",
    "content": "Copyright (c) 2010-2016 Manuele J Sarfatti and contributors\nLicensed under the MIT License\nhttp://www.opensource.org/licenses/mit-license.php\n"
  },
  {
    "path": "app/licenses/node-slug/license.txt",
    "content": "Copyright (c) 2014 ▟ ▖▟ ▖(dodo)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/normalize.css/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright © Nicolas Gallagher and Jonathan Neal\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/parse-bmfont-ascii/license.txt",
    "content": "The MIT License (MIT) Copyright (c) 2015 Matt DesLauriers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/parse-bmfont-xml/license.txt",
    "content": "The MIT License (MIT) Copyright (c) 2015 Matt DesLauriers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/punycode/license.txt",
    "content": "Copyright Mathias Bynens <https://mathiasbynens.be/>\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/range-parser/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>\nCopyright (c) 2015-2016 Douglas Christopher Wilson <doug@somethingdoug.com\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/read-chunk/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/select2/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2012-2015 Kevin Brown, Igor Vaynberg, and Select2 contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/send/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2012 TJ Holowaychuk &lt;tj@vision-media.ca&gt;\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/slash/license.txt",
    "content": "MIT License\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/sortablejs/license.txt",
    "content": "MIT LICENSE\nCopyright 2013-2017 Lebedev Konstantin ibnRubaXa@gmail.com http://rubaxa.github.io/Sortable/\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/source-map/license.txt",
    "content": "Copyright (c) 2009-2011, Mozilla Foundation and contributors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the names of the Mozilla Foundation nor the names of project\n  contributors may be used to endorse or promote products derived from this\n  software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "app/licenses/stream-consume/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Aron Nopanen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/stream-events/license.txt",
    "content": "The MIT Licence (Expat).\n\nCopyright (c) 2018 Stephen Sawchuk\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/stream-to/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Jonathan Ong me@jongleberry.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/stream-to-buffer/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Jonathan Ong me@jongleberry.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/stubs/license.txt",
    "content": "The MIT Licence (Expat).\n\nCopyright (c) 2017 Stephen Sawchuk\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/tabler-icons/license.txt",
    "content": "MIT License\n\nCopyright (c) 2020-2023 Paweł Kuna\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/licenses/tinycolorpicker/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Peter Dematté\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/tinymce/license.txt",
    "content": "      GNU LESSER GENERAL PUBLIC LICENSE\n           Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n          Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\n      GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n          NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n         END OF TERMS AND CONDITIONS\n\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n\n\n"
  },
  {
    "path": "app/licenses/tootallnate/once/license.txt",
    "content": "MIT License\n\nCopyright (c) 2020 Nathan Rajlich\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "app/licenses/topo/license.txt",
    "content": "Copyright (c) 2012-2016, Project contributors\nCopyright (c) 2012-2014, Walmart\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * The names of any contributors may not be used to endorse or promote\n      products derived from this software without specific prior written\n      permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n                                  *   *   *\n\nThe complete list of contributors can be found at: https://github.com/hapijs/topo/graphs/contributors\n"
  },
  {
    "path": "app/licenses/tr46/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) Sebastian Mayr\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "app/licenses/trim/license.txt",
    "content": "(The MIT License)\n\nCopyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/typo-js/license.txt",
    "content": "Copyright (c) 2011, Christopher Finke\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * The name of the author may not be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR FINKE BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "app/licenses/uri-js/license.txt",
    "content": "Copyright 2011 Gary Court. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY GARY COURT \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nThe views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Gary Court.\n"
  },
  {
    "path": "app/licenses/vendor-licenses.json",
    "content": "{\n    \"electron\": {\n        \"url\": \"https://electronjs.org/\"\n    },\n    \"normalize.css\": {\n        \"url\": \"https://github.com/necolas/normalize.css\"\n    },\n    \"locutus\": {\n        \"url\": \"http://locutus.io/\"\n    },\n    \"codemirror\": {\n        \"url\": \"https://codemirror.net/\"\n    },\n    \"tinycolorpicker\": {\n        \"url\": \"http://www.dematte.at/tinyColorPicker/\"\n    },\n    \"tinymce\": {\n        \"url\": \"https://www.tiny.cloud/\"\n    },\n    \"libvips\": {\n        \"url\": \"https://github.com/jcupitt/libvips\"\n    },\n    \"handlebars\": {\n        \"url\": \"http://handlebarsjs.com/\"\n    },\n    \"jquery\": {\n        \"url\": \"http://jquery.com/\"\n    },\n    \"jquery-ui\": {\n        \"url\": \"https://jqueryui.com/\"\n    },\n    \"nested-sortable\": {\n        \"url\": \"https://github.com/ilikenwf/nestedSortable\"\n    },\n    \"select2\": {\n        \"url\": \"https://select2.github.io/\"\n    },\n    \"node-slug\": {\n        \"url\": \"https://github.com/dodo/node-slug\"\n    },\n    \"feathericons\": {\n        \"url\": \"https://feathericons.com\"\n    },\n    \"tabler-icons\": {\n        \"url\": \"https://tabler-icons.io/\"\n    },\n    \"lucide\": {\n        \"url\": \"https://lucide.dev/\"\n    }\n}\n"
  },
  {
    "path": "app/licenses/window-size/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-2017, Jon Schlinkert\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/wordwrap/license.txt",
    "content": "This software is released under the MIT license:\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/xml-char-classes/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/xml-parse-from-string/license.txt",
    "content": "The MIT License (MIT) Copyright (c) 2015 Jam3\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/xml2json/license.txt",
    "content": "Copyright (c) 2016 xml2json AUTHORS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to\ndeal in the Software without restriction, including without limitation the\nrights to use, copy, modify, merge, publish, distribute, sublicense, and/or\nsell copies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\nIN THE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/xmlbuilder/license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Ozgur Ozcitak\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/licenses/xregexp/license.txt",
    "content": "The MIT License\n\nCopyright (c) 2007-2017 Steven Levithan <http://xregexp.com/>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "app/main.js",
    "content": "'use strict';\n\nconst electron = require('electron');\nconst webContents = electron.webContents;\nconst Menu = electron.Menu;\nconst electronApp = electron.app;\nconst dialog = electron.dialog;\nconst ipcMain = electron.ipcMain;\nconst nativeTheme = electron.nativeTheme;\nconst os = require('os');\nconst App = require('./back-end/app.js');\nconst createSlug = require('./back-end/helpers/slug.js');\nconst passwordSafeStorage = require('keytar');\nconst ContextMenuBuilder = require('./back-end/helpers/context-menu-builder.js');\nconst fs = require('fs');\nconst crypto = require('crypto');\nconst normalizePath = require('normalize-path');\n\nif (typeof process.env.NODE_ENV === 'undefined') {\n    process.env.NODE_ENV = 'production';\n}\n\n// Keep a global reference of the window object for avoiding Garbage Collector\nlet mainWindow;\nlet appInstance;\n\nelectronApp.on('window-all-closed', function () {\n    electronApp.quit();\n});\n\nelectronApp.on('ready', function () {\n    // Start the app\n    let startupSettings = {\n        'mainWindow': mainWindow,\n        'app': electronApp,\n        'basedir': __dirname\n    };\n\n    appInstance = new App(startupSettings);\n    \n    ipcMain.on('publii-set-spellchecker-language', (event, language) => {\n        global.spellCheckerLanguage = new String(language).replace(/[^a-z\\-_]/gmi, '');\n    });\n\n    ipcMain.handle('publii-shell-show-item-in-folder', (event, url) => electron.shell.showItemInFolder(url));\n    ipcMain.handle('publii-shell-open-path', (event, filePath) => electron.shell.openPath(filePath));\n    ipcMain.handle('publii-shell-open-external', (event, url) => electron.shell.openExternal(url));\n    ipcMain.handle('publii-native-exists-sync', (event, pathToCheck) => fs.existsSync(pathToCheck));\n    ipcMain.handle('publii-native-md5', (event, value) => crypto.createHash('md5').update(value).digest('hex'));\n    ipcMain.handle('publii-native-normalize-path', (event, pathToNormalize) => normalizePath(pathToNormalize));\n    ipcMain.handle('publii-get-spellchecker-language', (event) => global.spellCheckerLanguage);\n    ipcMain.handle('app-main-set-spellchecker-language-for-webview', (event, webContentsID, languages) => webContents.fromId(webContentsID).session.setSpellCheckerLanguages(languages));\n    \n    ipcMain.handle('app-main-webview-search-find-in-page', (event, searchPhrase, searchConfig = null) => {\n        if (searchConfig) {\n            appInstance.getMainWindow().webContents.findInPage(searchPhrase, searchConfig);\n        } else {\n            appInstance.getMainWindow().webContents.findInPage(searchPhrase)\n        }\n    });\n\n    ipcMain.handle('app-main-webview-search-stop-find-in-page', (event) => {\n        appInstance.getMainWindow().webContents.stopFindInPage('clearSelection');\n    });\n\n    // App theme mode\n    ipcMain.handle('app-theme-mode:set-light', () => {\n        nativeTheme.themeSource = 'light';\n    });\n\n    ipcMain.handle('app-theme-mode:set-dark', () => {\n        nativeTheme.themeSource = 'dark';\n    });\n\n    ipcMain.handle('app-theme-mode:get-theme', () => {\n        return nativeTheme.shouldUseDarkColors ? 'dark' : 'default';\n    });\n\n    ipcMain.handle('app-theme-mode:set-system', () => {\n        nativeTheme.themeSource = 'system';\n    });\n\n    nativeTheme.on('updated', () => {\n        appInstance.getMainWindow().webContents.send('app-theme-mode:changed');\n    });\n\n    // App window\n    ipcMain.handle('app-window:minimize', () => {\n        appInstance.getMainWindow().minimize();\n    });\n\n    ipcMain.handle('app-window:maximize', () => {\n        appInstance.getMainWindow().maximize();\n    });\n\n    ipcMain.handle('app-window:unmaximize', () => {\n        appInstance.getMainWindow().unmaximize();\n    });\n\n    ipcMain.handle('app-window:close', () => {\n        appInstance.getMainWindow().close();\n    });\n\n    // App credits list\n    ipcMain.handle('app-credits-list:get-app-path', () => {\n        return electronApp.getAppPath();\n    });\n\n    // Use Electron API to create slugs\n    ipcMain.handle('app-main-process-create-slug', (event, input) => {\n        return createSlug(input);\n    });\n\n    // Load password from Keytar\n    ipcMain.handle('app-main-process-load-password', async (event, type, passwordKey) => {\n        if (passwordKey && passwordKey.indexOf(type) === 0) {\n            let passwordData = passwordKey.split(' ');\n            let service = passwordData[0];\n            let account = passwordData[1];\n            let retrievedPassword = '';\n\n            if (passwordSafeStorage) {\n                try {\n                    retrievedPassword = await passwordSafeStorage.getPassword(service, account);\n                } catch (e) {\n                    console.log('(!) Cannot retrieve password via keytar');\n                }\n            }\n\n            if (retrievedPassword === null || retrievedPassword === true || retrievedPassword === false) {\n                retrievedPassword = '';\n            }\n\n            return retrievedPassword;\n        }\n\n        return '';\n    });\n\n    // Export OS version\n    ipcMain.handle('app-main-process-is-osx11-or-higher', () => {\n        let version = parseInt(os.release().split('.')[0], 10);\n        \n        if (process.platform === 'darwin' && version >= 20) {\n            return true;\n        }\n\n        return false;\n    });\n\n    // Use Electron API to display directory selection dialog\n    ipcMain.handle('app-main-process-select-directory', (event, fieldName = false) => {\n        let mainWindowHandler = appInstance.getMainWindow();\n\n        dialog.showOpenDialog(mainWindowHandler, {\n            properties: ['openDirectory']\n        }).then(selectedPath => {\n            mainWindowHandler.webContents.send('app-directory-selected', {\n                path: selectedPath,\n                fieldName: fieldName\n            });\n        });\n    });\n\n    // Use Electron API to display file selection dialog\n    ipcMain.handle('app-main-process-select-file', (event, fieldName = false) => {\n        let mainWindowHandler = appInstance.getMainWindow();\n\n        dialog.showOpenDialog(mainWindowHandler, {\n            properties: ['openFile', 'showHiddenFiles']\n        }).then(selectedPath => {\n            mainWindowHandler.webContents.send('app-file-selected', {\n                path: selectedPath,\n                fieldName: fieldName\n            });\n        });\n    });\n\n    // Use Electron API to display files selection dialog\n    ipcMain.handle('app-main-process-select-files', (event, fieldName = false, filters = []) => {\n        let mainWindowHandler = appInstance.getMainWindow();\n\n        dialog.showOpenDialog(mainWindowHandler, {\n            properties: ['openFile', 'multiSelections'],\n            filters: filters\n        }).then(selectedPaths => {\n            mainWindowHandler.webContents.send('app-files-selected', {\n                paths: selectedPaths,\n                fieldName: fieldName\n            });\n        });\n    });\n\n    // Get available spellchecker languages\n    ipcMain.handle('app-main-get-spellchecker-languages', (event) => appInstance.getMainWindow().webContents.session.availableSpellCheckerLanguages);\n\n    if (process.env.NODE_ENV !== 'development') {\n        const template = [{\n            label: \"Publii\",\n            submenu: [{\n                label: \"About Application\",\n                selector: \"orderFrontStandardAboutPanel:\"\n            }, {\n                type: \"separator\"\n            }, {\n                label: \"Quit\",\n                accelerator: \"CmdOrCtrl+Q\",\n                click: () => { \n                    electronApp.quit();\n                }\n            }]\n        }, {\n            label: \"Edit\",\n            submenu: [\n                {\n                    label: \"Undo\",\n                    accelerator: \"CmdOrCtrl+Z\",\n                    selector: \"undo:\"\n                },\n                {\n                    label: \"Redo\",\n                    accelerator: \"Shift+CmdOrCtrl+Z\",\n                    selector: \"redo:\"\n                },\n                {\n                    type: \"separator\"\n                },\n                {\n                    label: \"Cut\",\n                    accelerator: \"CmdOrCtrl+X\",\n                    selector: \"cut:\"\n                },\n                {\n                    label: \"Copy\",\n                    accelerator: \"CmdOrCtrl+C\",\n                    selector: \"copy:\"\n                },\n                {\n                    label: \"Paste\",\n                    accelerator: \"CmdOrCtrl+V\",\n                    selector: \"paste:\"\n                },\n                {\n                    label: \"Select All\",\n                    accelerator: \"CmdOrCtrl+A\",\n                    selector: \"selectAll:\"\n                }\n            ]\n        }];\n\n        const menu = Menu.buildFromTemplate(template);\n        Menu.setApplicationMenu(menu);\n    }\n\n    // Remove application menu on Linux\n    if (process.platform === 'linux') {\n        Menu.setApplicationMenu(null);\n    }\n\n    // Load language translations and set language as used in the app\n    ipcMain.handle('app-main-load-language', (event, lang, type) => {\n        try {\n            appInstance.loadLanguage(lang, type);\n            let languageChanged = false;\n            \n            if (!appInstance.languageLoadingError) {\n                languageChanged = appInstance.setLanguage(lang, type);\n            }\n\n            return {\n                languageChanged: languageChanged,\n                lang: appInstance.currentLanguageName,\n                type: appInstance.currentLanguageType,\n                translations: appInstance.currentLanguageTranslations,\n                momentLocale: appInstance.currentLanguageMomentLocale,\n                wysiwygTranslation: appInstance.currentWysiwygTranslation,\n                languageLoadingError: appInstance.languageLoadingError\n            };\n        } catch (error) {\n            return false;\n        }\n    });\n});\n"
  },
  {
    "path": "app/package.json",
    "content": "{\n  \"productName\": \"Publii\",\n  \"name\": \"Publii\",\n  \"version\": \"0.47.5\",\n  \"description\": \"Static Site CMS\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"start\": \"electron main.js\"\n  },\n  \"keywords\": [],\n  \"author\": {\n    \"name\": \"TidyCustoms\",\n    \"email\": \"bob@tidycustoms.net\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"repository\": {\n    \"url\": \"https://github.com/GetPublii/Publii/\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/GetPublii/Publii/issues\"\n  },\n  \"files\": [\n    \"**/*\",\n    \"dist/**/*\"\n  ],\n  \"homepage\": \"https://getpublii.com\",\n  \"dependencies\": {\n    \"@aws-sdk/client-s3\": \"3.623.0\",\n    \"@gitbeaker/node\": \"35.8.1\",\n    \"@google-cloud/storage\": \"6.11.0\",\n    \"@octokit/rest\": \"22.0.0\",\n    \"adm-zip\": \"0.5.10\",\n    \"archiver\": \"5.3.1\",\n    \"basic-ftp\": \"5.0.5\",\n    \"better-sqlite3\": \"12.2.0\",\n    \"clean-css\": \"5.3.2\",\n    \"codemirror\": \"5.65.13\",\n    \"codemirror-revisedsearch\": \"1.0.12\",\n    \"count-files\": \"2.6.2\",\n    \"dompurify\": \"3.2.6\",\n    \"easymde\": \"2.18.0\",\n    \"fast-memoize\": \"2.5.2\",\n    \"fast-xml-parser\": \"4.4.1\",\n    \"fs-extra\": \"11.1.1\",\n    \"ftp\": \"0.3.10\",\n    \"handlebars\": \"4.7.8\",\n    \"html-minifier\": \"4.0.0\",\n    \"image-downloader\": \"4.3.0\",\n    \"image-size\": \"1.0.2\",\n    \"isbinaryfile\": \"5.0.0\",\n    \"isomorphic-git\": \"1.33.1\",\n    \"jimp\": \"1.6.0\",\n    \"js-beautify\": \"1.15.4\",\n    \"keytar\": \"7.9.0\",\n    \"ls-all\": \"1.1.0\",\n    \"marked\": \"5.1.1\",\n    \"mime\": \"3.0.0\",\n    \"moment\": \"2.29.4\",\n    \"node-sqlite3-wasm\": \"0.8.47\",\n    \"node-version-compare\": \"1.0.3\",\n    \"normalize-path\": \"3.0.0\",\n    \"prismjs\": \"1.30.0\",\n    \"sharp\": \"0.34.3\",\n    \"slug\": \"0.9.4\",\n    \"sqlstring\": \"2.3.3\",\n    \"ssh2-sftp-client\": \"10.0.3\",\n    \"striptags\": \"3.2.0\",\n    \"tar-fs\": \"3.1.0\",\n    \"transliteration\": \"2.3.5\",\n    \"vue\": \"2.6.14\",\n    \"vue-color\": \"2.4.5\",\n    \"vue-i18n\": \"8.24.2\",\n    \"vue-multiselect\": \"2.0.8\",\n    \"vue-prism-editor\": \"0.6.1\",\n    \"vue-router\": \"3.5.3\",\n    \"vuedraggable\": \"2.24.3\",\n    \"vuex\": \"3.1.1\"\n  },\n  \"devDependencies\": {\n    \"electron\": \"37.10.3\",\n    \"node-abi\": \"4.12.0\"\n  }\n}\n"
  },
  {
    "path": "app/src/assets/vendor/css/codemirror.css",
    "content": "/* BASICS */\n\n.CodeMirror {\n  /* Set height, width, borders, and global font properties here */\n  font-family: Lucida Console, Monaco, Monospace;\n  height: 300px;\n  color: var(--text-primary-color);\n}\n\n/* PADDING */\n\n.CodeMirror-lines {\n  padding: 4px 0; /* Vertical padding around content */\n}\n.CodeMirror pre {\n  padding: 0 4px; /* Horizontal padding of content */\n}\n\n.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {\n  background-color: var(--bg-primary); /* The little square between H and V scrollbars */\n}\n\n/* GUTTER */\n\n.CodeMirror-gutters {\n  border-right: 1px solid var(--input-border-color);\n  background-color: var(--input-bg-light);\n  white-space: nowrap;\n}\n.CodeMirror-linenumbers {}\n.CodeMirror-linenumber {\n  font-size: 1.4rem;\n  padding: 0 3px 0 5px;\n  min-width: 20px;\n  text-align: right;\n  color: var(--text-light-color);\n  white-space: nowrap;\n}\n\n.CodeMirror-guttermarker { color: var(--code-21); }\n.CodeMirror-guttermarker-subtle { color: #999; }\n\n/* CURSOR */\n\n.CodeMirror-cursor {\n  border-left: 1px solid var(--text-primary-color);\n  border-right: none;\n  width: 0;\n}\n/* Shown when moving in bi-directional text */\n.CodeMirror div.CodeMirror-secondarycursor {\n  border-left: 1px solid silver;\n}\n.cm-fat-cursor .CodeMirror-cursor {\n  width: auto;\n  border: 0 !important;\n  background: var(--success);\n}\n.cm-fat-cursor div.CodeMirror-cursors {\n  z-index: 1;\n}\n\n.cm-animate-fat-cursor {\n  width: auto;\n  border: 0;\n  -webkit-animation: blink 1.06s steps(1) infinite;\n  animation: blink 1.06s steps(1) infinite;\n  background-color: var(--success);\n}\n@-webkit-keyframes blink {\n  0% {}\n  50% { background-color: transparent; }\n  100% {}\n}\n@keyframes blink {\n  0% {}\n  50% { background-color: transparent; }\n  100% {}\n}\n\n/* Can style cursor different in overwrite (non-insert) mode */\n.CodeMirror-overwrite .CodeMirror-cursor {}\n\n.cm-tab { display: inline-block; text-decoration: inherit; }\n\n.CodeMirror-rulers {\n  position: absolute;\n  left: 0; right: 0; top: -50px; bottom: -20px;\n  overflow: hidden;\n}\n.CodeMirror-ruler {\n  border-left: 1px solid var(--input-border-color);\n  top: 0; bottom: 0;\n  position: absolute;\n}\n\n/* DEFAULT THEME */\n\n.cm-s-default { font-weight: 400; }\n.cm-s-default .cm-header {color: var(--code-4);}\n.cm-s-default .cm-quote {color: var(--code-18);}\n.cm-negative {color: var(--code-8);}\n.cm-positive {color: var(--code-17);}\n.cm-header, .cm-strong {font-weight: bold;}\n.cm-em {font-style: italic;}\n.cm-link {text-decoration: underline;}\n.cm-strikethrough {text-decoration: line-through;}\n\n.cm-s-default .cm-keyword {color: var(--code-1);}\n.cm-s-default .cm-atom {color: var(--code-2);}\n.cm-s-default .cm-number {color: var(--code-3);}\n.cm-s-default .cm-def {color: var(--code-4);}\n.cm-s-default .cm-variable,\n.cm-s-default .cm-punctuation,\n.cm-s-default .cm-property,\n.cm-s-default .cm-operator {}\n.cm-s-default .cm-variable-2 {color: var(--code-5);}\n.cm-s-default .cm-variable-3 {color: var(--code-6);}\n.cm-s-default .cm-comment {color: var(--code-7);}\n.cm-s-default .cm-string {color: var(--code-8);}\n.cm-s-default .cm-string-2 {color: var(--code-9);}\n.cm-s-default .cm-meta {color: var(--code-10);}\n.cm-s-default .cm-qualifier {color: var(--code-10);}\n.cm-s-default .cm-builtin {color: var(--code-11);}\n.cm-s-default .cm-bracket {color: var(--code-19);}\n.cm-s-default .cm-tag {color: var(--code-12);}\n.cm-s-default .cm-attribute {color: var(--code-14);}\n.cm-s-default .cm-hr {color: var(--code-15);}\n.cm-s-default .cm-link {color: var(--code-14);}\n\n.cm-s-default .cm-error {color: var(--code-16);}\n.cm-invalidchar {color: var(--code-16);}\n\n.CodeMirror-composing { border-bottom: 2px solid; }\n\n/* Default styles for common addons */\n\ndiv.CodeMirror span.CodeMirror-matchingbracket {color: var(--code-18);}\ndiv.CodeMirror span.CodeMirror-nonmatchingbracket {color: var(--code-16);}\n.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }\n.CodeMirror-activeline-background {background: var(--code-20);}\n\n/* STOP */\n\n/* The rest of this file contains styles related to the mechanics of\n   the editor. You probably shouldn't touch them. */\n\n.CodeMirror {\n  position: relative;\n  overflow: hidden;\n  background: var(--bg-primary);\n}\n\n.CodeMirror-scroll {\n  overflow: scroll !important; /* Things will break if this is overridden */\n  /* 30px is the magic margin used to hide the element's real scrollbars */\n  /* See overflow: hidden in .CodeMirror */\n  margin-bottom: -30px;\n  padding-bottom: 30px;\n  height: 100%;\n  outline: none; /* Prevent dragging from highlighting the element */\n  position: relative;\n}\n.CodeMirror-sizer {\n  position: relative;\n  border-right: 30px solid transparent;\n}\n\n/* The fake, visible scrollbars. Used to force redraw during scrolling\n   before actual scrolling happens, thus preventing shaking and\n   flickering artifacts. */\n.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {\n  position: absolute;\n  z-index: 6;\n  display: none;\n}\n.CodeMirror-vscrollbar {\n  right: 0; top: 0;\n  overflow-x: hidden;\n  overflow-y: inherit;\n}\n.CodeMirror-hscrollbar {\n  bottom: 0; left: 0;\n  overflow-y: hidden;\n  overflow-x: scroll;\n}\n.CodeMirror-scrollbar-filler {\n  right: 0; bottom: 0;\n}\n.CodeMirror-gutter-filler {\n  left: 0; bottom: 0;\n}\n\n.CodeMirror-gutters {\n  position: absolute; left: 0; top: 0;\n  min-height: 100%;\n  z-index: 3;\n}\n.CodeMirror-gutter {\n  font-size: 1.4rem;\n  white-space: normal;\n  height: 100%;\n  display: inline-block;\n  vertical-align: top;\n  margin-bottom: -30px;\n}\n.CodeMirror-gutter-wrapper {\n  position: absolute;\n  z-index: 4;\n  background: none !important;\n  border: none !important;\n}\n.CodeMirror-gutter-background {\n  position: absolute;\n  top: 0; bottom: 0;\n  z-index: 4;\n}\n.CodeMirror-gutter-elt {\n  position: absolute;\n  cursor: default;\n  z-index: 4;\n}\n.CodeMirror-gutter-wrapper {\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n      user-select: none;\n}\n\n.CodeMirror-lines {\n  cursor: text;\n  min-height: 1px; /* prevents collapsing before first draw */\n}\n.CodeMirror pre {\n  /* Reset some styles that the rest of the page might have set */ border-radius: 0;\n  border-width: 0;\n  background: transparent;\n  font-family: inherit;\n  font-size: 1.4rem;\n  margin: 0;\n  white-space: pre;\n  word-wrap: normal;\n  line-height: inherit;\n  color: inherit;\n  z-index: 2;\n  position: relative;\n  overflow: visible;\n  -webkit-tap-highlight-color: transparent;\n  -webkit-font-variant-ligatures: none;\n  font-variant-ligatures: none;\n}\n.CodeMirror-wrap pre {\n  word-wrap: break-word;\n  white-space: pre-wrap;\n  word-break: normal;\n}\n\n.CodeMirror-linebackground {\n  position: absolute;\n  left: 0; right: 0; top: 0; bottom: 0;\n  z-index: 0;\n}\n\n.CodeMirror-linewidget {\n  position: relative;\n  z-index: 2;\n  overflow: auto;\n}\n\n.CodeMirror-widget {}\n\n.CodeMirror-code {\n  outline: none;\n}\n\n/* Force content-box sizing for the elements where we expect it */\n.CodeMirror-scroll,\n.CodeMirror-sizer,\n.CodeMirror-gutter,\n.CodeMirror-gutters,\n.CodeMirror-linenumber {\n  -webkit-box-sizing: content-box;\n          box-sizing: content-box;\n}\n\n.CodeMirror-measure {\n  position: absolute;\n  width: 100%;\n  height: 0;\n  overflow: hidden;\n  visibility: hidden;\n}\n\n.CodeMirror-cursor {\n  position: absolute;\n  pointer-events: none;\n}\n.CodeMirror-measure pre { position: static; }\n\ndiv.CodeMirror-cursors {\n  visibility: hidden;\n  position: relative;\n  z-index: 3;\n}\ndiv.CodeMirror-dragcursors {\n  visibility: visible;\n}\n\n.CodeMirror-focused div.CodeMirror-cursors {\n  visibility: visible;\n}\n\n.CodeMirror-selected { background: var(--code-20); }\n.CodeMirror-focused .CodeMirror-selected { background: var(--code-20); }\n.CodeMirror-crosshair { cursor: crosshair; }\n.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: var(--code-20); }\n.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: var(--code-20); }\n.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: var(--code-20); }\n\n.cm-searching {\n  background: #ffa;\n  background: rgba(255, 255, 0, .4);\n}\n\n/* Used to force a border model for a node */\n.cm-force-border { padding-right: .1px; }\n\n@media print {\n  /* Hide the cursor when printing */\n  .CodeMirror div.CodeMirror-cursors {\n    visibility: hidden;\n  }\n}\n\n/* See issue #2901 */\n.cm-tab-wrap-hack:after { content: ''; }\n\n/* Help users use markselection to safely style text background */\nspan.CodeMirror-selectedtext { background: none; }\n"
  },
  {
    "path": "app/src/assets/vendor/css/normalize.css",
    "content": "/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */\n\n/**\n * 1. Change the default font family in all browsers (opinionated).\n * 2. Prevent adjustments of font size after orientation changes in IE and iOS.\n */\n\nhtml {\n    font-family: sans-serif; /* 1 */\n    -ms-text-size-adjust: 100%; /* 2 */\n    -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/**\n * Remove the margin in all browsers (opinionated).\n */\n\nbody {\n    margin: 0;\n}\n\n/* HTML5 display definitions\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 9-.\n * 1. Add the correct display in Edge, IE, and Firefox.\n * 2. Add the correct display in IE.\n */\n\narticle,\naside,\ndetails, /* 1 */\nfigcaption,\nfigure,\nfooter,\nheader,\nmain, /* 2 */\nmenu,\nnav,\nsection,\nsummary { /* 1 */\n    display: block;\n}\n\n/**\n * Add the correct display in IE 9-.\n */\n\naudio,\ncanvas,\nprogress,\nvideo {\n    display: inline-block;\n}\n\n/**\n * Add the correct display in iOS 4-7.\n */\n\naudio:not([controls]) {\n    display: none;\n    height: 0;\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n    vertical-align: baseline;\n}\n\n/**\n * Add the correct display in IE 10-.\n * 1. Add the correct display in IE.\n */\n\ntemplate, /* 1 */\n[hidden] {\n    display: none;\n}\n\n/* Links\n   ========================================================================== */\n\n/**\n * 1. Remove the gray background on active links in IE 10.\n * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.\n */\n\na {\n    background-color: transparent; /* 1 */\n    -webkit-text-decoration-skip: objects; /* 2 */\n}\n\n/**\n * Remove the outline on focused links when they are also active or hovered\n * in all browsers (opinionated).\n */\n\na:active,\na:hover {\n    outline-width: 0;\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * 1. Remove the bottom border in Firefox 39-.\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n    border-bottom: none; /* 1 */\n    text-decoration: underline; /* 2 */\n    text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Prevent the duplicate application of `bolder` by the next rule in Safari 6.\n */\n\nb,\nstrong {\n    font-weight: inherit;\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n    font-weight: 600;\n}\n\n/**\n * Add the correct font style in Android 4.3-.\n */\n\ndfn {\n    font-style: italic;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n    font-size: 2em;\n    margin: 0.67em 0;\n}\n\n/**\n * Add the correct background and color in IE 9-.\n */\n\nmark {\n    background-color: #ff0;\n    color: #000;\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n    font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n}\n\nsub {\n    bottom: -0.25em;\n}\n\nsup {\n    top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10-.\n */\n\nimg {\n    border-style: none;\n}\n\n/**\n * Hide the overflow in IE.\n */\n\nsvg:not(:root) {\n    overflow: hidden;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\npre,\nsamp {\n    font-family: monospace, monospace; /* 1 */\n    font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct margin in IE 8.\n */\n\nfigure {\n    margin: 1em 40px;\n}\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n    box-sizing: content-box; /* 1 */\n    height: 0; /* 1 */\n    overflow: visible; /* 2 */\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change font properties to `inherit` in all browsers (opinionated).\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\nselect,\ntextarea {\n    font: inherit; /* 1 */\n    margin: 0; /* 2 */\n}\n\n/**\n * Restore the font weight unset by the previous rule.\n */\n\noptgroup {\n    font-weight: bold;\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n    overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n    text-transform: none;\n}\n\n/**\n * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n *    controls in Android 4.\n * 2. Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\nhtml [type=\"button\"], /* 1 */\n[type=\"reset\"],\n[type=\"submit\"] {\n    -webkit-appearance: button; /* 2 */\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n    border-style: none;\n    padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n    outline: 1px dotted ButtonText;\n}\n\n/**\n * Change the border, margin, and padding in all browsers (opinionated).\n */\n\nfieldset {\n    border: 1px solid #c0c0c0;\n    margin: 0 2px;\n    padding: 0.35em 0.625em 0.75em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n    box-sizing: border-box; /* 1 */\n    color: inherit; /* 2 */\n    display: table; /* 1 */\n    max-width: 100%; /* 1 */\n    padding: 0; /* 3 */\n    white-space: normal; /* 1 */\n}\n\n/**\n * Remove the default vertical scrollbar in IE.\n */\n\ntextarea {\n    overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10-.\n * 2. Remove the padding in IE 10-.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n    box-sizing: border-box; /* 1 */\n    padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n    height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n    -webkit-appearance: textfield; /* 1 */\n    outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding and cancel buttons in Chrome and Safari on OS X.\n */\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n    -webkit-appearance: none;\n}\n\n/**\n * Correct the text style of placeholders in Chrome, Edge, and Safari.\n */\n\n::-webkit-input-placeholder {\n    color: inherit;\n    opacity: 0.54;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n    -webkit-appearance: button; /* 1 */\n    font: inherit; /* 2 */\n}\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/display/autorefresh.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"))\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod)\n  else // Plain browser env\n    mod(CodeMirror)\n})(function(CodeMirror) {\n  \"use strict\"\n\n  CodeMirror.defineOption(\"autoRefresh\", false, function(cm, val) {\n    if (cm.state.autoRefresh) {\n      stopListening(cm, cm.state.autoRefresh)\n      cm.state.autoRefresh = null\n    }\n    if (val && cm.display.wrapper.offsetHeight == 0)\n      startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})\n  })\n\n  function startListening(cm, state) {\n    function check() {\n      if (cm.display.wrapper.offsetHeight) {\n        stopListening(cm, state)\n        if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)\n          cm.refresh()\n      } else {\n        state.timeout = setTimeout(check, state.delay)\n      }\n    }\n    state.timeout = setTimeout(check, state.delay)\n    state.hurry = function() {\n      clearTimeout(state.timeout)\n      state.timeout = setTimeout(check, 50)\n    }\n    CodeMirror.on(window, \"mouseup\", state.hurry)\n    CodeMirror.on(window, \"keyup\", state.hurry)\n  }\n\n  function stopListening(_cm, state) {\n    clearTimeout(state.timeout)\n    CodeMirror.off(window, \"mouseup\", state.hurry)\n    CodeMirror.off(window, \"keyup\", state.hurry)\n  }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/display/fullscreen.css",
    "content": ".CodeMirror-fullscreen {\n  position: fixed;\n  top: 0; left: 0; right: 0; bottom: 0;\n  height: auto;\n  z-index: 9;\n}\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/display/fullscreen.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  CodeMirror.defineOption(\"fullScreen\", false, function(cm, val, old) {\n    if (old == CodeMirror.Init) old = false;\n    if (!old == !val) return;\n    if (val) setFullscreen(cm);\n    else setNormal(cm);\n  });\n\n  function setFullscreen(cm) {\n    var wrap = cm.getWrapperElement();\n    cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset,\n                                  width: wrap.style.width, height: wrap.style.height};\n    wrap.style.width = \"\";\n    wrap.style.height = \"auto\";\n    wrap.className += \" CodeMirror-fullscreen\";\n    document.documentElement.style.overflow = \"hidden\";\n    cm.refresh();\n  }\n\n  function setNormal(cm) {\n    var wrap = cm.getWrapperElement();\n    wrap.className = wrap.className.replace(/\\s*CodeMirror-fullscreen\\b/, \"\");\n    document.documentElement.style.overflow = \"\";\n    var info = cm.state.fullScreenRestore;\n    wrap.style.width = info.width; wrap.style.height = info.height;\n    window.scrollTo(info.scrollLeft, info.scrollTop);\n    cm.refresh();\n  }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/display/panel.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  CodeMirror.defineExtension(\"addPanel\", function(node, options) {\n    options = options || {};\n\n    if (!this.state.panels) initPanels(this);\n\n    var info = this.state.panels;\n    var wrapper = info.wrapper;\n    var cmWrapper = this.getWrapperElement();\n\n    if (options.after instanceof Panel && !options.after.cleared) {\n      wrapper.insertBefore(node, options.before.node.nextSibling);\n    } else if (options.before instanceof Panel && !options.before.cleared) {\n      wrapper.insertBefore(node, options.before.node);\n    } else if (options.replace instanceof Panel && !options.replace.cleared) {\n      wrapper.insertBefore(node, options.replace.node);\n      options.replace.clear();\n    } else if (options.position == \"bottom\") {\n      wrapper.appendChild(node);\n    } else if (options.position == \"before-bottom\") {\n      wrapper.insertBefore(node, cmWrapper.nextSibling);\n    } else if (options.position == \"after-top\") {\n      wrapper.insertBefore(node, cmWrapper);\n    } else {\n      wrapper.insertBefore(node, wrapper.firstChild);\n    }\n\n    var height = (options && options.height) || node.offsetHeight;\n    this._setSize(null, info.heightLeft -= height);\n    info.panels++;\n    if (options.stable && isAtTop(this, node))\n      this.scrollTo(null, this.getScrollInfo().top + height)\n\n    return new Panel(this, node, options, height);\n  });\n\n  function Panel(cm, node, options, height) {\n    this.cm = cm;\n    this.node = node;\n    this.options = options;\n    this.height = height;\n    this.cleared = false;\n  }\n\n  Panel.prototype.clear = function() {\n    if (this.cleared) return;\n    this.cleared = true;\n    var info = this.cm.state.panels;\n    this.cm._setSize(null, info.heightLeft += this.height);\n    if (this.options.stable && isAtTop(this.cm, this.node))\n      this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)\n    info.wrapper.removeChild(this.node);\n    if (--info.panels == 0) removePanels(this.cm);\n  };\n\n  Panel.prototype.changed = function(height) {\n    var newHeight = height == null ? this.node.offsetHeight : height;\n    var info = this.cm.state.panels;\n    this.cm._setSize(null, info.heightLeft -= (newHeight - this.height));\n    this.height = newHeight;\n  };\n\n  function initPanels(cm) {\n    var wrap = cm.getWrapperElement();\n    var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;\n    var height = parseInt(style.height);\n    var info = cm.state.panels = {\n      setHeight: wrap.style.height,\n      heightLeft: height,\n      panels: 0,\n      wrapper: document.createElement(\"div\")\n    };\n    wrap.parentNode.insertBefore(info.wrapper, wrap);\n    var hasFocus = cm.hasFocus();\n    info.wrapper.appendChild(wrap);\n    if (hasFocus) cm.focus();\n\n    cm._setSize = cm.setSize;\n    if (height != null) cm.setSize = function(width, newHeight) {\n      if (newHeight == null) return this._setSize(width, newHeight);\n      info.setHeight = newHeight;\n      if (typeof newHeight != \"number\") {\n        var px = /^(\\d+\\.?\\d*)px$/.exec(newHeight);\n        if (px) {\n          newHeight = Number(px[1]);\n        } else {\n          info.wrapper.style.height = newHeight;\n          newHeight = info.wrapper.offsetHeight;\n          info.wrapper.style.height = \"\";\n        }\n      }\n      cm._setSize(width, info.heightLeft += (newHeight - height));\n      height = newHeight;\n    };\n  }\n\n  function removePanels(cm) {\n    var info = cm.state.panels;\n    cm.state.panels = null;\n\n    var wrap = cm.getWrapperElement();\n    info.wrapper.parentNode.replaceChild(wrap, info.wrapper);\n    wrap.style.height = info.setHeight;\n    cm.setSize = cm._setSize;\n    cm.setSize();\n  }\n\n  function isAtTop(cm, dom) {\n    for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)\n      if (sibling == cm.getWrapperElement()) return true\n    return false\n  }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/display/placeholder.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  CodeMirror.defineOption(\"placeholder\", \"\", function(cm, val, old) {\n    var prev = old && old != CodeMirror.Init;\n    if (val && !prev) {\n      cm.on(\"blur\", onBlur);\n      cm.on(\"change\", onChange);\n      cm.on(\"swapDoc\", onChange);\n      onChange(cm);\n    } else if (!val && prev) {\n      cm.off(\"blur\", onBlur);\n      cm.off(\"change\", onChange);\n      cm.off(\"swapDoc\", onChange);\n      clearPlaceholder(cm);\n      var wrapper = cm.getWrapperElement();\n      wrapper.className = wrapper.className.replace(\" CodeMirror-empty\", \"\");\n    }\n\n    if (val && !cm.hasFocus()) onBlur(cm);\n  });\n\n  function clearPlaceholder(cm) {\n    if (cm.state.placeholder) {\n      cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);\n      cm.state.placeholder = null;\n    }\n  }\n  function setPlaceholder(cm) {\n    clearPlaceholder(cm);\n    var elt = cm.state.placeholder = document.createElement(\"pre\");\n    elt.style.cssText = \"height: 0; overflow: visible\";\n    elt.className = \"CodeMirror-placeholder\";\n    var placeHolder = cm.getOption(\"placeholder\")\n    if (typeof placeHolder == \"string\") placeHolder = document.createTextNode(placeHolder)\n    elt.appendChild(placeHolder)\n    cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);\n  }\n\n  function onBlur(cm) {\n    if (isEmpty(cm)) setPlaceholder(cm);\n  }\n  function onChange(cm) {\n    var wrapper = cm.getWrapperElement(), empty = isEmpty(cm);\n    wrapper.className = wrapper.className.replace(\" CodeMirror-empty\", \"\") + (empty ? \" CodeMirror-empty\" : \"\");\n\n    if (empty) setPlaceholder(cm);\n    else clearPlaceholder(cm);\n  }\n\n  function isEmpty(cm) {\n    return (cm.lineCount() === 1) && (cm.getLine(0) === \"\");\n  }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/display/rulers.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  CodeMirror.defineOption(\"rulers\", false, function(cm, val) {\n    if (cm.state.rulerDiv) {\n      cm.state.rulerDiv.parentElement.removeChild(cm.state.rulerDiv)\n      cm.state.rulerDiv = null\n      cm.off(\"refresh\", drawRulers)\n    }\n    if (val && val.length) {\n      cm.state.rulerDiv = cm.display.lineSpace.parentElement.insertBefore(document.createElement(\"div\"), cm.display.lineSpace)\n      cm.state.rulerDiv.className = \"CodeMirror-rulers\"\n      drawRulers(cm)\n      cm.on(\"refresh\", drawRulers)\n    }\n  });\n\n  function drawRulers(cm) {\n    cm.state.rulerDiv.textContent = \"\"\n    var val = cm.getOption(\"rulers\");\n    var cw = cm.defaultCharWidth();\n    var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), \"div\").left;\n    cm.state.rulerDiv.style.minHeight = (cm.display.scroller.offsetHeight + 30) + \"px\";\n    for (var i = 0; i < val.length; i++) {\n      var elt = document.createElement(\"div\");\n      elt.className = \"CodeMirror-ruler\";\n      var col, conf = val[i];\n      if (typeof conf == \"number\") {\n        col = conf;\n      } else {\n        col = conf.column;\n        if (conf.className) elt.className += \" \" + conf.className;\n        if (conf.color) elt.style.borderColor = conf.color;\n        if (conf.lineStyle) elt.style.borderLeftStyle = conf.lineStyle;\n        if (conf.width) elt.style.borderLeftWidth = conf.width;\n      }\n      elt.style.left = (left + col * cw) + \"px\";\n      cm.state.rulerDiv.appendChild(elt)\n    }\n  }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/scroll/annotatescrollbar.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  CodeMirror.defineExtension(\"annotateScrollbar\", function(options) {\n    if (typeof options == \"string\") options = {className: options};\n    return new Annotation(this, options);\n  });\n\n  CodeMirror.defineOption(\"scrollButtonHeight\", 0);\n\n  function Annotation(cm, options) {\n    this.cm = cm;\n    this.options = options;\n    this.buttonHeight = options.scrollButtonHeight || cm.getOption(\"scrollButtonHeight\");\n    this.annotations = [];\n    this.doRedraw = this.doUpdate = null;\n    this.div = cm.getWrapperElement().appendChild(document.createElement(\"div\"));\n    this.div.style.cssText = \"position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none\";\n    this.computeScale();\n\n    function scheduleRedraw(delay) {\n      clearTimeout(self.doRedraw);\n      self.doRedraw = setTimeout(function() { self.redraw(); }, delay);\n    }\n\n    var self = this;\n    cm.on(\"refresh\", this.resizeHandler = function() {\n      clearTimeout(self.doUpdate);\n      self.doUpdate = setTimeout(function() {\n        if (self.computeScale()) scheduleRedraw(20);\n      }, 100);\n    });\n    cm.on(\"markerAdded\", this.resizeHandler);\n    cm.on(\"markerCleared\", this.resizeHandler);\n    if (options.listenForChanges !== false)\n      cm.on(\"change\", this.changeHandler = function() {\n        scheduleRedraw(250);\n      });\n  }\n\n  Annotation.prototype.computeScale = function() {\n    var cm = this.cm;\n    var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) /\n      cm.getScrollerElement().scrollHeight\n    if (hScale != this.hScale) {\n      this.hScale = hScale;\n      return true;\n    }\n  };\n\n  Annotation.prototype.update = function(annotations) {\n    this.annotations = annotations;\n    this.redraw();\n  };\n\n  Annotation.prototype.redraw = function(compute) {\n    if (compute !== false) this.computeScale();\n    var cm = this.cm, hScale = this.hScale;\n\n    var frag = document.createDocumentFragment(), anns = this.annotations;\n\n    var wrapping = cm.getOption(\"lineWrapping\");\n    var singleLineH = wrapping && cm.defaultTextHeight() * 1.5;\n    var curLine = null, curLineObj = null;\n    function getY(pos, top) {\n      if (curLine != pos.line) {\n        curLine = pos.line;\n        curLineObj = cm.getLineHandle(curLine);\n      }\n      if ((curLineObj.widgets && curLineObj.widgets.length) ||\n          (wrapping && curLineObj.height > singleLineH))\n        return cm.charCoords(pos, \"local\")[top ? \"top\" : \"bottom\"];\n      var topY = cm.heightAtLine(curLineObj, \"local\");\n      return topY + (top ? 0 : curLineObj.height);\n    }\n\n    var lastLine = cm.lastLine()\n    if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {\n      var ann = anns[i];\n      if (ann.to.line > lastLine) continue;\n      var top = nextTop || getY(ann.from, true) * hScale;\n      var bottom = getY(ann.to, false) * hScale;\n      while (i < anns.length - 1) {\n        if (anns[i + 1].to.line > lastLine) break;\n        nextTop = getY(anns[i + 1].from, true) * hScale;\n        if (nextTop > bottom + .9) break;\n        ann = anns[++i];\n        bottom = getY(ann.to, false) * hScale;\n      }\n      if (bottom == top) continue;\n      var height = Math.max(bottom - top, 3);\n\n      var elt = frag.appendChild(document.createElement(\"div\"));\n      elt.style.cssText = \"position: absolute; right: 0px; width: \" + Math.max(cm.display.barWidth - 1, 2) + \"px; top: \"\n        + (top + this.buttonHeight) + \"px; height: \" + height + \"px\";\n      elt.className = this.options.className;\n      if (ann.id) {\n        elt.setAttribute(\"annotation-id\", ann.id);\n      }\n    }\n    this.div.textContent = \"\";\n    this.div.appendChild(frag);\n  };\n\n  Annotation.prototype.clear = function() {\n    this.cm.off(\"refresh\", this.resizeHandler);\n    this.cm.off(\"markerAdded\", this.resizeHandler);\n    this.cm.off(\"markerCleared\", this.resizeHandler);\n    if (this.changeHandler) this.cm.off(\"change\", this.changeHandler);\n    this.div.parentNode.removeChild(this.div);\n  };\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/scroll/scrollpastend.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  CodeMirror.defineOption(\"scrollPastEnd\", false, function(cm, val, old) {\n    if (old && old != CodeMirror.Init) {\n      cm.off(\"change\", onChange);\n      cm.off(\"refresh\", updateBottomMargin);\n      cm.display.lineSpace.parentNode.style.paddingBottom = \"\";\n      cm.state.scrollPastEndPadding = null;\n    }\n    if (val) {\n      cm.on(\"change\", onChange);\n      cm.on(\"refresh\", updateBottomMargin);\n      updateBottomMargin(cm);\n    }\n  });\n\n  function onChange(cm, change) {\n    if (CodeMirror.changeEnd(change).line == cm.lastLine())\n      updateBottomMargin(cm);\n  }\n\n  function updateBottomMargin(cm) {\n    var padding = \"\";\n    if (cm.lineCount() > 1) {\n      var totalH = cm.display.scroller.clientHeight - 30,\n          lastLineH = cm.getLineHandle(cm.lastLine()).height;\n      padding = (totalH - lastLineH) + \"px\";\n    }\n    if (cm.state.scrollPastEndPadding != padding) {\n      cm.state.scrollPastEndPadding = padding;\n      cm.display.lineSpace.parentNode.style.paddingBottom = padding;\n      cm.off(\"refresh\", updateBottomMargin);\n      cm.setSize();\n      cm.on(\"refresh\", updateBottomMargin);\n    }\n  }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/scroll/simplescrollbars.css",
    "content": ".CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div {\n  position: absolute;\n  background: #ccc;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n  border: 1px solid #bbb;\n  border-radius: 2px;\n}\n\n.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical {\n  position: absolute;\n  z-index: 6;\n  background: #eee;\n}\n\n.CodeMirror-simplescroll-horizontal {\n  bottom: 0; left: 0;\n  height: 8px;\n}\n.CodeMirror-simplescroll-horizontal div {\n  bottom: 0;\n  height: 100%;\n}\n\n.CodeMirror-simplescroll-vertical {\n  right: 0; top: 0;\n  width: 8px;\n}\n.CodeMirror-simplescroll-vertical div {\n  right: 0;\n  width: 100%;\n}\n\n\n.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler {\n  display: none;\n}\n\n.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {\n  position: absolute;\n  background: #bcd;\n  border-radius: 3px;\n}\n\n.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical {\n  position: absolute;\n  z-index: 6;\n}\n\n.CodeMirror-overlayscroll-horizontal {\n  bottom: 0; left: 0;\n  height: 6px;\n}\n.CodeMirror-overlayscroll-horizontal div {\n  bottom: 0;\n  height: 100%;\n}\n\n.CodeMirror-overlayscroll-vertical {\n  right: 0; top: 0;\n  width: 6px;\n}\n.CodeMirror-overlayscroll-vertical div {\n  right: 0;\n  width: 100%;\n}\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/scroll/simplescrollbars.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  function Bar(cls, orientation, scroll) {\n    this.orientation = orientation;\n    this.scroll = scroll;\n    this.screen = this.total = this.size = 1;\n    this.pos = 0;\n\n    this.node = document.createElement(\"div\");\n    this.node.className = cls + \"-\" + orientation;\n    this.inner = this.node.appendChild(document.createElement(\"div\"));\n\n    var self = this;\n    CodeMirror.on(this.inner, \"mousedown\", function(e) {\n      if (e.which != 1) return;\n      CodeMirror.e_preventDefault(e);\n      var axis = self.orientation == \"horizontal\" ? \"pageX\" : \"pageY\";\n      var start = e[axis], startpos = self.pos;\n      function done() {\n        CodeMirror.off(document, \"mousemove\", move);\n        CodeMirror.off(document, \"mouseup\", done);\n      }\n      function move(e) {\n        if (e.which != 1) return done();\n        self.moveTo(startpos + (e[axis] - start) * (self.total / self.size));\n      }\n      CodeMirror.on(document, \"mousemove\", move);\n      CodeMirror.on(document, \"mouseup\", done);\n    });\n\n    CodeMirror.on(this.node, \"click\", function(e) {\n      CodeMirror.e_preventDefault(e);\n      var innerBox = self.inner.getBoundingClientRect(), where;\n      if (self.orientation == \"horizontal\")\n        where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0;\n      else\n        where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0;\n      self.moveTo(self.pos + where * self.screen);\n    });\n\n    function onWheel(e) {\n      var moved = CodeMirror.wheelEventPixels(e)[self.orientation == \"horizontal\" ? \"x\" : \"y\"];\n      var oldPos = self.pos;\n      self.moveTo(self.pos + moved);\n      if (self.pos != oldPos) CodeMirror.e_preventDefault(e);\n    }\n    CodeMirror.on(this.node, \"mousewheel\", onWheel);\n    CodeMirror.on(this.node, \"DOMMouseScroll\", onWheel);\n  }\n\n  Bar.prototype.setPos = function(pos, force) {\n    if (pos < 0) pos = 0;\n    if (pos > this.total - this.screen) pos = this.total - this.screen;\n    if (!force && pos == this.pos) return false;\n    this.pos = pos;\n    this.inner.style[this.orientation == \"horizontal\" ? \"left\" : \"top\"] =\n      (pos * (this.size / this.total)) + \"px\";\n    return true\n  };\n\n  Bar.prototype.moveTo = function(pos) {\n    if (this.setPos(pos)) this.scroll(pos, this.orientation);\n  }\n\n  var minButtonSize = 10;\n\n  Bar.prototype.update = function(scrollSize, clientSize, barSize) {\n    var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize\n    if (sizeChanged) {\n      this.screen = clientSize;\n      this.total = scrollSize;\n      this.size = barSize;\n    }\n\n    var buttonSize = this.screen * (this.size / this.total);\n    if (buttonSize < minButtonSize) {\n      this.size -= minButtonSize - buttonSize;\n      buttonSize = minButtonSize;\n    }\n    this.inner.style[this.orientation == \"horizontal\" ? \"width\" : \"height\"] =\n      buttonSize + \"px\";\n    this.setPos(this.pos, sizeChanged);\n  };\n\n  function SimpleScrollbars(cls, place, scroll) {\n    this.addClass = cls;\n    this.horiz = new Bar(cls, \"horizontal\", scroll);\n    place(this.horiz.node);\n    this.vert = new Bar(cls, \"vertical\", scroll);\n    place(this.vert.node);\n    this.width = null;\n  }\n\n  SimpleScrollbars.prototype.update = function(measure) {\n    if (this.width == null) {\n      var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle;\n      if (style) this.width = parseInt(style.height);\n    }\n    var width = this.width || 0;\n\n    var needsH = measure.scrollWidth > measure.clientWidth + 1;\n    var needsV = measure.scrollHeight > measure.clientHeight + 1;\n    this.vert.node.style.display = needsV ? \"block\" : \"none\";\n    this.horiz.node.style.display = needsH ? \"block\" : \"none\";\n\n    if (needsV) {\n      this.vert.update(measure.scrollHeight, measure.clientHeight,\n                       measure.viewHeight - (needsH ? width : 0));\n      this.vert.node.style.bottom = needsH ? width + \"px\" : \"0\";\n    }\n    if (needsH) {\n      this.horiz.update(measure.scrollWidth, measure.clientWidth,\n                        measure.viewWidth - (needsV ? width : 0) - measure.barLeft);\n      this.horiz.node.style.right = needsV ? width + \"px\" : \"0\";\n      this.horiz.node.style.left = measure.barLeft + \"px\";\n    }\n\n    return {right: needsV ? width : 0, bottom: needsH ? width : 0};\n  };\n\n  SimpleScrollbars.prototype.setScrollTop = function(pos) {\n    this.vert.setPos(pos);\n  };\n\n  SimpleScrollbars.prototype.setScrollLeft = function(pos) {\n    this.horiz.setPos(pos);\n  };\n\n  SimpleScrollbars.prototype.clear = function() {\n    var parent = this.horiz.node.parentNode;\n    parent.removeChild(this.horiz.node);\n    parent.removeChild(this.vert.node);\n  };\n\n  CodeMirror.scrollbarModel.simple = function(place, scroll) {\n    return new SimpleScrollbars(\"CodeMirror-simplescroll\", place, scroll);\n  };\n  CodeMirror.scrollbarModel.overlay = function(place, scroll) {\n    return new SimpleScrollbars(\"CodeMirror-overlayscroll\", place, scroll);\n  };\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/search/jump-to-line.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n// Defines jumpToLine command. Uses dialog.js if present.\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"), require(\"../dialog/dialog\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\", \"../dialog/dialog\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  function dialog(cm, text, shortText, deflt, f) {\n    if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});\n    else f(prompt(shortText, deflt));\n  }\n\n  var jumpDialog =\n      'Jump to line: <input type=\"text\" style=\"width: 10em\" class=\"CodeMirror-search-field\"/> <span style=\"color: #888\" class=\"CodeMirror-search-hint\">(Use line:column or scroll% syntax)</span>';\n\n  function interpretLine(cm, string) {\n    var num = Number(string)\n    if (/^[-+]/.test(string)) return cm.getCursor().line + num\n    else return num - 1\n  }\n\n  CodeMirror.commands.jumpToLine = function(cm) {\n    var cur = cm.getCursor();\n    dialog(cm, jumpDialog, \"Jump to line:\", (cur.line + 1) + \":\" + cur.ch, function(posStr) {\n      if (!posStr) return;\n\n      var match;\n      if (match = /^\\s*([\\+\\-]?\\d+)\\s*\\:\\s*(\\d+)\\s*$/.exec(posStr)) {\n        cm.setCursor(interpretLine(cm, match[1]), Number(match[2]))\n      } else if (match = /^\\s*([\\+\\-]?\\d+(\\.\\d+)?)\\%\\s*/.exec(posStr)) {\n        var line = Math.round(cm.lineCount() * Number(match[1]) / 100);\n        if (/^[-+]/.test(match[1])) line = cur.line + line + 1;\n        cm.setCursor(line - 1, cur.ch);\n      } else if (match = /^\\s*\\:?\\s*([\\+\\-]?\\d+)\\s*/.exec(posStr)) {\n        cm.setCursor(interpretLine(cm, match[1]), cur.ch);\n      }\n    });\n  };\n\n  CodeMirror.keyMap[\"default\"][\"Alt-G\"] = \"jumpToLine\";\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/search/match-highlighter.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n// Highlighting text that matches the selection\n//\n// Defines an option highlightSelectionMatches, which, when enabled,\n// will style strings that match the selection throughout the\n// document.\n//\n// The option can be set to true to simply enable it, or to a\n// {minChars, style, wordsOnly, showToken, delay} object to explicitly\n// configure it. minChars is the minimum amount of characters that should be\n// selected for the behavior to occur, and style is the token style to\n// apply to the matches. This will be prefixed by \"cm-\" to create an\n// actual CSS class name. If wordsOnly is enabled, the matches will be\n// highlighted only if the selected text is a word. showToken, when enabled,\n// will cause the current token to be highlighted when nothing is selected.\n// delay is used to specify how much time to wait, in milliseconds, before\n// highlighting the matches. If annotateScrollbar is enabled, the occurences\n// will be highlighted on the scrollbar via the matchesonscrollbar addon.\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"), require(\"./matchesonscrollbar\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\", \"./matchesonscrollbar\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  var defaults = {\n    style: \"matchhighlight\",\n    minChars: 2,\n    delay: 100,\n    wordsOnly: false,\n    annotateScrollbar: false,\n    showToken: false,\n    trim: true\n  }\n\n  function State(options) {\n    this.options = {}\n    for (var name in defaults)\n      this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]\n    this.overlay = this.timeout = null;\n    this.matchesonscroll = null;\n    this.active = false;\n  }\n\n  CodeMirror.defineOption(\"highlightSelectionMatches\", false, function(cm, val, old) {\n    if (old && old != CodeMirror.Init) {\n      removeOverlay(cm);\n      clearTimeout(cm.state.matchHighlighter.timeout);\n      cm.state.matchHighlighter = null;\n      cm.off(\"cursorActivity\", cursorActivity);\n      cm.off(\"focus\", onFocus)\n    }\n    if (val) {\n      var state = cm.state.matchHighlighter = new State(val);\n      if (cm.hasFocus()) {\n        state.active = true\n        highlightMatches(cm)\n      } else {\n        cm.on(\"focus\", onFocus)\n      }\n      cm.on(\"cursorActivity\", cursorActivity);\n    }\n  });\n\n  function cursorActivity(cm) {\n    var state = cm.state.matchHighlighter;\n    if (state.active || cm.hasFocus()) scheduleHighlight(cm, state)\n  }\n\n  function onFocus(cm) {\n    var state = cm.state.matchHighlighter\n    if (!state.active) {\n      state.active = true\n      scheduleHighlight(cm, state)\n    }\n  }\n\n  function scheduleHighlight(cm, state) {\n    clearTimeout(state.timeout);\n    state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);\n  }\n\n  function addOverlay(cm, query, hasBoundary, style) {\n    var state = cm.state.matchHighlighter;\n    cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));\n    if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {\n      var searchFor = hasBoundary ? new RegExp(\"\\\\b\" + query + \"\\\\b\") : query;\n      state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,\n        {className: \"CodeMirror-selection-highlight-scrollbar\"});\n    }\n  }\n\n  function removeOverlay(cm) {\n    var state = cm.state.matchHighlighter;\n    if (state.overlay) {\n      cm.removeOverlay(state.overlay);\n      state.overlay = null;\n      if (state.matchesonscroll) {\n        state.matchesonscroll.clear();\n        state.matchesonscroll = null;\n      }\n    }\n  }\n\n  function highlightMatches(cm) {\n    cm.operation(function() {\n      var state = cm.state.matchHighlighter;\n      removeOverlay(cm);\n      if (!cm.somethingSelected() && state.options.showToken) {\n        var re = state.options.showToken === true ? /[\\w$]/ : state.options.showToken;\n        var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;\n        while (start && re.test(line.charAt(start - 1))) --start;\n        while (end < line.length && re.test(line.charAt(end))) ++end;\n        if (start < end)\n          addOverlay(cm, line.slice(start, end), re, state.options.style);\n        return;\n      }\n      var from = cm.getCursor(\"from\"), to = cm.getCursor(\"to\");\n      if (from.line != to.line) return;\n      if (state.options.wordsOnly && !isWord(cm, from, to)) return;\n      var selection = cm.getRange(from, to)\n      if (state.options.trim) selection = selection.replace(/^\\s+|\\s+$/g, \"\")\n      if (selection.length >= state.options.minChars)\n        addOverlay(cm, selection, false, state.options.style);\n    });\n  }\n\n  function isWord(cm, from, to) {\n    var str = cm.getRange(from, to);\n    if (str.match(/^\\w+$/) !== null) {\n        if (from.ch > 0) {\n            var pos = {line: from.line, ch: from.ch - 1};\n            var chr = cm.getRange(pos, from);\n            if (chr.match(/\\W/) === null) return false;\n        }\n        if (to.ch < cm.getLine(from.line).length) {\n            var pos = {line: to.line, ch: to.ch + 1};\n            var chr = cm.getRange(to, pos);\n            if (chr.match(/\\W/) === null) return false;\n        }\n        return true;\n    } else return false;\n  }\n\n  function boundariesAround(stream, re) {\n    return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&\n      (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));\n  }\n\n  function makeOverlay(query, hasBoundary, style) {\n    return {token: function(stream) {\n      if (stream.match(query) &&\n          (!hasBoundary || boundariesAround(stream, hasBoundary)))\n        return style;\n      stream.next();\n      stream.skipTo(query.charAt(0)) || stream.skipToEnd();\n    }};\n  }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/search/matchesonscrollbar.css",
    "content": ".CodeMirror-search-match {\n  background: gold;\n  border-top: 1px solid orange;\n  border-bottom: 1px solid orange;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n  opacity: .5;\n}\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/search/matchesonscrollbar.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"), require(\"./searchcursor\"), require(\"../scroll/annotatescrollbar\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\", \"./searchcursor\", \"../scroll/annotatescrollbar\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  CodeMirror.defineExtension(\"showMatchesOnScrollbar\", function(query, caseFold, options) {\n    if (typeof options == \"string\") options = {className: options};\n    if (!options) options = {};\n    return new SearchAnnotation(this, query, caseFold, options);\n  });\n\n  function SearchAnnotation(cm, query, caseFold, options) {\n    this.cm = cm;\n    this.options = options;\n    var annotateOptions = {listenForChanges: false};\n    for (var prop in options) annotateOptions[prop] = options[prop];\n    if (!annotateOptions.className) annotateOptions.className = \"CodeMirror-search-match\";\n    this.annotation = cm.annotateScrollbar(annotateOptions);\n    this.query = query;\n    this.caseFold = caseFold;\n    this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1};\n    this.matches = [];\n    this.update = null;\n\n    this.findMatches();\n    this.annotation.update(this.matches);\n\n    var self = this;\n    cm.on(\"change\", this.changeHandler = function(_cm, change) { self.onChange(change); });\n  }\n\n  var MAX_MATCHES = 1000;\n\n  SearchAnnotation.prototype.findMatches = function() {\n    if (!this.gap) return;\n    for (var i = 0; i < this.matches.length; i++) {\n      var match = this.matches[i];\n      if (match.from.line >= this.gap.to) break;\n      if (match.to.line >= this.gap.from) this.matches.splice(i--, 1);\n    }\n    var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold);\n    var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES;\n    while (cursor.findNext()) {\n      var match = {from: cursor.from(), to: cursor.to()};\n      if (match.from.line >= this.gap.to) break;\n      this.matches.splice(i++, 0, match);\n      if (this.matches.length > maxMatches) break;\n    }\n    this.gap = null;\n  };\n\n  function offsetLine(line, changeStart, sizeChange) {\n    if (line <= changeStart) return line;\n    return Math.max(changeStart, line + sizeChange);\n  }\n\n  SearchAnnotation.prototype.onChange = function(change) {\n    var startLine = change.from.line;\n    var endLine = CodeMirror.changeEnd(change).line;\n    var sizeChange = endLine - change.to.line;\n    if (this.gap) {\n      this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line);\n      this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line);\n    } else {\n      this.gap = {from: change.from.line, to: endLine + 1};\n    }\n\n    if (sizeChange) for (var i = 0; i < this.matches.length; i++) {\n      var match = this.matches[i];\n      var newFrom = offsetLine(match.from.line, startLine, sizeChange);\n      if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch);\n      var newTo = offsetLine(match.to.line, startLine, sizeChange);\n      if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch);\n    }\n    clearTimeout(this.update);\n    var self = this;\n    this.update = setTimeout(function() { self.updateAfterChange(); }, 250);\n  };\n\n  SearchAnnotation.prototype.updateAfterChange = function() {\n    this.findMatches();\n    this.annotation.update(this.matches);\n  };\n\n  SearchAnnotation.prototype.clear = function() {\n    this.cm.off(\"change\", this.changeHandler);\n    this.annotation.clear();\n  };\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/search/search.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n// Define search commands. Depends on dialog.js or another\n// implementation of the openDialog method.\n\n// Replace works a little oddly -- it will do the replace on the next\n// Ctrl-G (or whatever is bound to findNext) press. You prevent a\n// replace by making sure the match is no longer selected when hitting\n// Ctrl-G.\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"), require(\"./searchcursor\"), require(\"../dialog/dialog\"));\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\", \"./searchcursor\", \"../dialog/dialog\"], mod);\n  else // Plain browser env\n    mod(CodeMirror);\n})(function(CodeMirror) {\n  \"use strict\";\n\n  function searchOverlay(query, caseInsensitive) {\n    if (typeof query == \"string\")\n      query = new RegExp(query.replace(/[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\\^\\$\\|]/g, \"\\\\$&\"), caseInsensitive ? \"gi\" : \"g\");\n    else if (!query.global)\n      query = new RegExp(query.source, query.ignoreCase ? \"gi\" : \"g\");\n\n    return {token: function(stream) {\n      query.lastIndex = stream.pos;\n      var match = query.exec(stream.string);\n      if (match && match.index == stream.pos) {\n        stream.pos += match[0].length || 1;\n        return \"searching\";\n      } else if (match) {\n        stream.pos = match.index;\n      } else {\n        stream.skipToEnd();\n      }\n    }};\n  }\n\n  function SearchState() {\n    this.posFrom = this.posTo = this.lastQuery = this.query = null;\n    this.overlay = null;\n  }\n\n  function getSearchState(cm) {\n    return cm.state.search || (cm.state.search = new SearchState());\n  }\n\n  function queryCaseInsensitive(query) {\n    return typeof query == \"string\" && query == query.toLowerCase();\n  }\n\n  function getSearchCursor(cm, query, pos) {\n    // Heuristic: if the query string is all lowercase, do a case insensitive search.\n    return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true});\n  }\n\n  function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {\n    cm.openDialog(text, onEnter, {\n      value: deflt,\n      selectValueOnOpen: true,\n      closeOnEnter: false,\n      onClose: function() { clearSearch(cm); },\n      onKeyDown: onKeyDown\n    });\n  }\n\n  function dialog(cm, text, shortText, deflt, f) {\n    if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});\n    else f(prompt(shortText, deflt));\n  }\n\n  function confirmDialog(cm, text, shortText, fs) {\n    if (cm.openConfirm) cm.openConfirm(text, fs);\n    else if (confirm(shortText)) fs[0]();\n  }\n\n  function parseString(string) {\n    return string.replace(/\\\\(.)/g, function(_, ch) {\n      if (ch == \"n\") return \"\\n\"\n      if (ch == \"r\") return \"\\r\"\n      return ch\n    })\n  }\n\n  function parseQuery(query) {\n    var isRE = query.match(/^\\/(.*)\\/([a-z]*)$/);\n    if (isRE) {\n      try { query = new RegExp(isRE[1], isRE[2].indexOf(\"i\") == -1 ? \"\" : \"i\"); }\n      catch(e) {} // Not a regular expression after all, do a string search\n    } else {\n      query = parseString(query)\n    }\n    if (typeof query == \"string\" ? query == \"\" : query.test(\"\"))\n      query = /x^/;\n    return query;\n  }\n\n  var queryDialog =\n    '<span class=\"CodeMirror-search-label\">Search:</span> <input type=\"text\" style=\"width: 10em\" class=\"CodeMirror-search-field\"/> <span style=\"color: #888\" class=\"CodeMirror-search-hint\">(Use /re/ syntax for regexp search)</span>';\n\n  function startSearch(cm, state, query) {\n    state.queryText = query;\n    state.query = parseQuery(query);\n    cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));\n    state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));\n    cm.addOverlay(state.overlay);\n    if (cm.showMatchesOnScrollbar) {\n      if (state.annotate) { state.annotate.clear(); state.annotate = null; }\n      state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));\n    }\n  }\n\n  function doSearch(cm, rev, persistent, immediate) {\n    var state = getSearchState(cm);\n    if (state.query) return findNext(cm, rev);\n    var q = cm.getSelection() || state.lastQuery;\n    if (q instanceof RegExp && q.source == \"x^\") q = null\n    if (persistent && cm.openDialog) {\n      var hiding = null\n      var searchNext = function(query, event) {\n        CodeMirror.e_stop(event);\n        if (!query) return;\n        if (query != state.queryText) {\n          startSearch(cm, state, query);\n          state.posFrom = state.posTo = cm.getCursor();\n        }\n        if (hiding) hiding.style.opacity = 1\n        findNext(cm, event.shiftKey, function(_, to) {\n          var dialog\n          if (to.line < 3 && document.querySelector &&\n              (dialog = cm.display.wrapper.querySelector(\".CodeMirror-dialog\")) &&\n              dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, \"window\").top)\n            (hiding = dialog).style.opacity = .4\n        })\n      };\n      persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {\n        var keyName = CodeMirror.keyName(event)\n        var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption(\"keyMap\")][keyName]\n        if (cmd == \"findNext\" || cmd == \"findPrev\" ||\n          cmd == \"findPersistentNext\" || cmd == \"findPersistentPrev\") {\n          CodeMirror.e_stop(event);\n          startSearch(cm, getSearchState(cm), query);\n          cm.execCommand(cmd);\n        } else if (cmd == \"find\" || cmd == \"findPersistent\") {\n          CodeMirror.e_stop(event);\n          searchNext(query, event);\n        }\n      });\n      if (immediate && q) {\n        startSearch(cm, state, q);\n        findNext(cm, rev);\n      }\n    } else {\n      dialog(cm, queryDialog, \"Search for:\", q, function(query) {\n        if (query && !state.query) cm.operation(function() {\n          startSearch(cm, state, query);\n          state.posFrom = state.posTo = cm.getCursor();\n          findNext(cm, rev);\n        });\n      });\n    }\n  }\n\n  function findNext(cm, rev, callback) {cm.operation(function() {\n    var state = getSearchState(cm);\n    var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);\n    if (!cursor.find(rev)) {\n      cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));\n      if (!cursor.find(rev)) return;\n    }\n    cm.setSelection(cursor.from(), cursor.to());\n    cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);\n    state.posFrom = cursor.from(); state.posTo = cursor.to();\n    if (callback) callback(cursor.from(), cursor.to())\n  });}\n\n  function clearSearch(cm) {cm.operation(function() {\n    var state = getSearchState(cm);\n    state.lastQuery = state.query;\n    if (!state.query) return;\n    state.query = state.queryText = null;\n    cm.removeOverlay(state.overlay);\n    if (state.annotate) { state.annotate.clear(); state.annotate = null; }\n  });}\n\n  var replaceQueryDialog =\n    ' <input type=\"text\" style=\"width: 10em\" class=\"CodeMirror-search-field\"/> <span style=\"color: #888\" class=\"CodeMirror-search-hint\">(Use /re/ syntax for regexp search)</span>';\n  var replacementQueryDialog = '<span class=\"CodeMirror-search-label\">With:</span> <input type=\"text\" style=\"width: 10em\" class=\"CodeMirror-search-field\"/>';\n  var doReplaceConfirm = '<span class=\"CodeMirror-search-label\">Replace?</span> <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>';\n\n  function replaceAll(cm, query, text) {\n    cm.operation(function() {\n      for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {\n        if (typeof query != \"string\") {\n          var match = cm.getRange(cursor.from(), cursor.to()).match(query);\n          cursor.replace(text.replace(/\\$(\\d)/g, function(_, i) {return match[i];}));\n        } else cursor.replace(text);\n      }\n    });\n  }\n\n  function replace(cm, all) {\n    if (cm.getOption(\"readOnly\")) return;\n    var query = cm.getSelection() || getSearchState(cm).lastQuery;\n    var dialogText = '<span class=\"CodeMirror-search-label\">' + (all ? 'Replace all:' : 'Replace:') + '</span>';\n    dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {\n      if (!query) return;\n      query = parseQuery(query);\n      dialog(cm, replacementQueryDialog, \"Replace with:\", \"\", function(text) {\n        text = parseString(text)\n        if (all) {\n          replaceAll(cm, query, text)\n        } else {\n          clearSearch(cm);\n          var cursor = getSearchCursor(cm, query, cm.getCursor(\"from\"));\n          var advance = function() {\n            var start = cursor.from(), match;\n            if (!(match = cursor.findNext())) {\n              cursor = getSearchCursor(cm, query);\n              if (!(match = cursor.findNext()) ||\n                  (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;\n            }\n            cm.setSelection(cursor.from(), cursor.to());\n            cm.scrollIntoView({from: cursor.from(), to: cursor.to()});\n            confirmDialog(cm, doReplaceConfirm, \"Replace?\",\n                          [function() {doReplace(match);}, advance,\n                           function() {replaceAll(cm, query, text)}]);\n          };\n          var doReplace = function(match) {\n            cursor.replace(typeof query == \"string\" ? text :\n                           text.replace(/\\$(\\d)/g, function(_, i) {return match[i];}));\n            advance();\n          };\n          advance();\n        }\n      });\n    });\n  }\n\n  CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};\n  CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};\n  CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};\n  CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};\n  CodeMirror.commands.findNext = doSearch;\n  CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};\n  CodeMirror.commands.clearSearch = clearSearch;\n  CodeMirror.commands.replace = replace;\n  CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/addon/search/searchcursor.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n  if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n    mod(require(\"../../lib/codemirror\"))\n  else if (typeof define == \"function\" && define.amd) // AMD\n    define([\"../../lib/codemirror\"], mod)\n  else // Plain browser env\n    mod(CodeMirror)\n})(function(CodeMirror) {\n  \"use strict\"\n  var Pos = CodeMirror.Pos\n\n  function regexpFlags(regexp) {\n    var flags = regexp.flags\n    return flags != null ? flags : (regexp.ignoreCase ? \"i\" : \"\")\n      + (regexp.global ? \"g\" : \"\")\n      + (regexp.multiline ? \"m\" : \"\")\n  }\n\n  function ensureGlobal(regexp) {\n    return regexp.global ? regexp : new RegExp(regexp.source, regexpFlags(regexp) + \"g\")\n  }\n\n  function maybeMultiline(regexp) {\n    return /\\\\s|\\\\n|\\n|\\\\W|\\\\D|\\[\\^/.test(regexp.source)\n  }\n\n  function searchRegexpForward(doc, regexp, start) {\n    regexp = ensureGlobal(regexp)\n    for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) {\n      regexp.lastIndex = ch\n      var string = doc.getLine(line), match = regexp.exec(string)\n      if (match)\n        return {from: Pos(line, match.index),\n                to: Pos(line, match.index + match[0].length),\n                match: match}\n    }\n  }\n\n  function searchRegexpForwardMultiline(doc, regexp, start) {\n    if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start)\n\n    regexp = ensureGlobal(regexp)\n    var string, chunk = 1\n    for (var line = start.line, last = doc.lastLine(); line <= last;) {\n      // This grows the search buffer in exponentially-sized chunks\n      // between matches, so that nearby matches are fast and don't\n      // require concatenating the whole document (in case we're\n      // searching for something that has tons of matches), but at the\n      // same time, the amount of retries is limited.\n      for (var i = 0; i < chunk; i++) {\n        var curLine = doc.getLine(line++)\n        string = string == null ? curLine : string + \"\\n\" + curLine\n      }\n      chunk = chunk * 2\n      regexp.lastIndex = start.ch\n      var match = regexp.exec(string)\n      if (match) {\n        var before = string.slice(0, match.index).split(\"\\n\"), inside = match[0].split(\"\\n\")\n        var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length\n        return {from: Pos(startLine, startCh),\n                to: Pos(startLine + inside.length - 1,\n                        inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),\n                match: match}\n      }\n    }\n  }\n\n  function lastMatchIn(string, regexp) {\n    var cutOff = 0, match\n    for (;;) {\n      regexp.lastIndex = cutOff\n      var newMatch = regexp.exec(string)\n      if (!newMatch) return match\n      match = newMatch\n      cutOff = match.index + (match[0].length || 1)\n      if (cutOff == string.length) return match\n    }\n  }\n\n  function searchRegexpBackward(doc, regexp, start) {\n    regexp = ensureGlobal(regexp)\n    for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) {\n      var string = doc.getLine(line)\n      if (ch > -1) string = string.slice(0, ch)\n      var match = lastMatchIn(string, regexp)\n      if (match)\n        return {from: Pos(line, match.index),\n                to: Pos(line, match.index + match[0].length),\n                match: match}\n    }\n  }\n\n  function searchRegexpBackwardMultiline(doc, regexp, start) {\n    regexp = ensureGlobal(regexp)\n    var string, chunk = 1\n    for (var line = start.line, first = doc.firstLine(); line >= first;) {\n      for (var i = 0; i < chunk; i++) {\n        var curLine = doc.getLine(line--)\n        string = string == null ? curLine.slice(0, start.ch) : curLine + \"\\n\" + string\n      }\n      chunk *= 2\n\n      var match = lastMatchIn(string, regexp)\n      if (match) {\n        var before = string.slice(0, match.index).split(\"\\n\"), inside = match[0].split(\"\\n\")\n        var startLine = line + before.length, startCh = before[before.length - 1].length\n        return {from: Pos(startLine, startCh),\n                to: Pos(startLine + inside.length - 1,\n                        inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),\n                match: match}\n      }\n    }\n  }\n\n  var doFold, noFold\n  if (String.prototype.normalize) {\n    doFold = function(str) { return str.normalize(\"NFD\").toLowerCase() }\n    noFold = function(str) { return str.normalize(\"NFD\") }\n  } else {\n    doFold = function(str) { return str.toLowerCase() }\n    noFold = function(str) { return str }\n  }\n\n  // Maps a position in a case-folded line back to a position in the original line\n  // (compensating for codepoints increasing in number during folding)\n  function adjustPos(orig, folded, pos, foldFunc) {\n    if (orig.length == folded.length) return pos\n    for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) {\n      if (min == max) return min\n      var mid = (min + max) >> 1\n      var len = foldFunc(orig.slice(0, mid)).length\n      if (len == pos) return mid\n      else if (len > pos) max = mid\n      else min = mid + 1\n    }\n  }\n\n  function searchStringForward(doc, query, start, caseFold) {\n    // Empty string would match anything and never progress, so we\n    // define it to match nothing instead.\n    if (!query.length) return null\n    var fold = caseFold ? doFold : noFold\n    var lines = fold(query).split(/\\r|\\n\\r?/)\n\n    search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) {\n      var orig = doc.getLine(line).slice(ch), string = fold(orig)\n      if (lines.length == 1) {\n        var found = string.indexOf(lines[0])\n        if (found == -1) continue search\n        var start = adjustPos(orig, string, found, fold) + ch\n        return {from: Pos(line, adjustPos(orig, string, found, fold) + ch),\n                to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)}\n      } else {\n        var cutFrom = string.length - lines[0].length\n        if (string.slice(cutFrom) != lines[0]) continue search\n        for (var i = 1; i < lines.length - 1; i++)\n          if (fold(doc.getLine(line + i)) != lines[i]) continue search\n        var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1]\n        if (end.slice(0, lastLine.length) != lastLine) continue search\n        return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch),\n                to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))}\n      }\n    }\n  }\n\n  function searchStringBackward(doc, query, start, caseFold) {\n    if (!query.length) return null\n    var fold = caseFold ? doFold : noFold\n    var lines = fold(query).split(/\\r|\\n\\r?/)\n\n    search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) {\n      var orig = doc.getLine(line)\n      if (ch > -1) orig = orig.slice(0, ch)\n      var string = fold(orig)\n      if (lines.length == 1) {\n        var found = string.lastIndexOf(lines[0])\n        if (found == -1) continue search\n        return {from: Pos(line, adjustPos(orig, string, found, fold)),\n                to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))}\n      } else {\n        var lastLine = lines[lines.length - 1]\n        if (string.slice(0, lastLine.length) != lastLine) continue search\n        for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++)\n          if (fold(doc.getLine(start + i)) != lines[i]) continue search\n        var top = doc.getLine(line + 1 - lines.length), topString = fold(top)\n        if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search\n        return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)),\n                to: Pos(line, adjustPos(orig, string, lastLine.length, fold))}\n      }\n    }\n  }\n\n  function SearchCursor(doc, query, pos, options) {\n    this.atOccurrence = false\n    this.doc = doc\n    pos = pos ? doc.clipPos(pos) : Pos(0, 0)\n    this.pos = {from: pos, to: pos}\n\n    var caseFold\n    if (typeof options == \"object\") {\n      caseFold = options.caseFold\n    } else { // Backwards compat for when caseFold was the 4th argument\n      caseFold = options\n      options = null\n    }\n\n    if (typeof query == \"string\") {\n      if (caseFold == null) caseFold = false\n      this.matches = function(reverse, pos) {\n        return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold)\n      }\n    } else {\n      query = ensureGlobal(query)\n      if (!options || options.multiline !== false)\n        this.matches = function(reverse, pos) {\n          return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos)\n        }\n      else\n        this.matches = function(reverse, pos) {\n          return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos)\n        }\n    }\n  }\n\n  SearchCursor.prototype = {\n    findNext: function() {return this.find(false)},\n    findPrevious: function() {return this.find(true)},\n\n    find: function(reverse) {\n      var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))\n\n      // Implements weird auto-growing behavior on null-matches for\n      // backwards-compatiblity with the vim code (unfortunately)\n      while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {\n        if (reverse) {\n          if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)\n          else if (result.from.line == this.doc.firstLine()) result = null\n          else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))\n        } else {\n          if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)\n          else if (result.to.line == this.doc.lastLine()) result = null\n          else result = this.matches(reverse, Pos(result.to.line + 1, 0))\n        }\n      }\n\n      if (result) {\n        this.pos = result\n        this.atOccurrence = true\n        return this.pos.match || true\n      } else {\n        var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0)\n        this.pos = {from: end, to: end}\n        return this.atOccurrence = false\n      }\n    },\n\n    from: function() {if (this.atOccurrence) return this.pos.from},\n    to: function() {if (this.atOccurrence) return this.pos.to},\n\n    replace: function(newText, origin) {\n      if (!this.atOccurrence) return\n      var lines = CodeMirror.splitLines(newText)\n      this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin)\n      this.pos.to = Pos(this.pos.from.line + lines.length - 1,\n                        lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))\n    }\n  }\n\n  CodeMirror.defineExtension(\"getSearchCursor\", function(query, pos, caseFold) {\n    return new SearchCursor(this.doc, query, pos, caseFold)\n  })\n  CodeMirror.defineDocExtension(\"getSearchCursor\", function(query, pos, caseFold) {\n    return new SearchCursor(this, query, pos, caseFold)\n  })\n\n  CodeMirror.defineExtension(\"selectMatches\", function(query, caseFold) {\n    var ranges = []\n    var cur = this.getSearchCursor(query, this.getCursor(\"from\"), caseFold)\n    while (cur.findNext()) {\n      if (CodeMirror.cmpPos(cur.to(), this.getCursor(\"to\")) > 0) break\n      ranges.push({anchor: cur.from(), head: cur.to()})\n    }\n    if (ranges.length)\n      this.setSelections(ranges, 0)\n  })\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/autorefresh.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n(function(mod) {\n    if (typeof exports == \"object\" && typeof module == \"object\") // CommonJS\n        mod(require(\"../../lib/codemirror\"))\n    else if (typeof define == \"function\" && define.amd) // AMD\n        define([\"../../lib/codemirror\"], mod)\n    else // Plain browser env\n        mod(CodeMirror)\n})(function(CodeMirror) {\n    \"use strict\"\n\n    CodeMirror.defineOption(\"autoRefresh\", false, function(cm, val) {\n        if (cm.state.autoRefresh) {\n            stopListening(cm, cm.state.autoRefresh)\n            cm.state.autoRefresh = null\n        }\n        if (val && cm.display.wrapper.offsetHeight == 0)\n            startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})\n    })\n\n    function startListening(cm, state) {\n        function check() {\n            if (cm.display.wrapper.offsetHeight) {\n                stopListening(cm, state)\n                if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)\n                    cm.refresh()\n            } else {\n                state.timeout = setTimeout(check, state.delay)\n            }\n        }\n        state.timeout = setTimeout(check, state.delay)\n        state.hurry = function() {\n            clearTimeout(state.timeout)\n            state.timeout = setTimeout(check, 50)\n        }\n        CodeMirror.on(window, \"mouseup\", state.hurry)\n        CodeMirror.on(window, \"keyup\", state.hurry)\n    }\n\n    function stopListening(_cm, state) {\n        clearTimeout(state.timeout)\n        CodeMirror.off(window, \"mouseup\", state.hurry)\n        CodeMirror.off(window, \"keyup\", state.hurry)\n    }\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/codemirror.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\n\n// This is CodeMirror (http://codemirror.net), a code editor\n// implemented in JavaScript on top of the browser's DOM.\n//\n// You can find some technical background for some of the code below\n// at http://marijnhaverbeke.nl/blog/#cm-internals .\n\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global.CodeMirror = factory());\n}(this, (function () { 'use strict';\n\n// Kludges for bugs and behavior differences that can't be feature\n// detected are enabled based on userAgent etc sniffing.\nvar userAgent = navigator.userAgent\nvar platform = navigator.platform\n\nvar gecko = /gecko\\/\\d/i.test(userAgent)\nvar ie_upto10 = /MSIE \\d/.test(userAgent)\nvar ie_11up = /Trident\\/(?:[7-9]|\\d{2,})\\..*rv:(\\d+)/.exec(userAgent)\nvar ie = ie_upto10 || ie_11up\nvar ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1])\nvar webkit = /WebKit\\//.test(userAgent)\nvar qtwebkit = webkit && /Qt\\/\\d+\\.\\d+/.test(userAgent)\nvar chrome = /Chrome\\//.test(userAgent)\nvar presto = /Opera\\//.test(userAgent)\nvar safari = /Apple Computer/.test(navigator.vendor)\nvar mac_geMountainLion = /Mac OS X 1\\d\\D([8-9]|\\d\\d)\\D/.test(userAgent)\nvar phantom = /PhantomJS/.test(userAgent)\n\nvar ios = /AppleWebKit/.test(userAgent) && /Mobile\\/\\w+/.test(userAgent)\n// This is woefully incomplete. Suggestions for alternative methods welcome.\nvar mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent)\nvar mac = ios || /Mac/.test(platform)\nvar chromeOS = /\\bCrOS\\b/.test(userAgent)\nvar windows = /win/i.test(platform)\n\nvar presto_version = presto && userAgent.match(/Version\\/(\\d*\\.\\d*)/)\nif (presto_version) { presto_version = Number(presto_version[1]) }\nif (presto_version && presto_version >= 15) { presto = false; webkit = true }\n// Some browsers use the wrong event properties to signal cmd/ctrl on OS X\nvar flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11))\nvar captureRightClick = gecko || (ie && ie_version >= 9)\n\nfunction classTest(cls) { return new RegExp(\"(^|\\\\s)\" + cls + \"(?:$|\\\\s)\\\\s*\") }\n\nvar rmClass = function(node, cls) {\n  var current = node.className\n  var match = classTest(cls).exec(current)\n  if (match) {\n    var after = current.slice(match.index + match[0].length)\n    node.className = current.slice(0, match.index) + (after ? match[1] + after : \"\")\n  }\n}\n\nfunction removeChildren(e) {\n  for (var count = e.childNodes.length; count > 0; --count)\n    { e.removeChild(e.firstChild) }\n  return e\n}\n\nfunction removeChildrenAndAdd(parent, e) {\n  return removeChildren(parent).appendChild(e)\n}\n\nfunction elt(tag, content, className, style) {\n  var e = document.createElement(tag)\n  if (className) { e.className = className }\n  if (style) { e.style.cssText = style }\n  if (typeof content == \"string\") { e.appendChild(document.createTextNode(content)) }\n  else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]) } }\n  return e\n}\n\nvar range\nif (document.createRange) { range = function(node, start, end, endNode) {\n  var r = document.createRange()\n  r.setEnd(endNode || node, end)\n  r.setStart(node, start)\n  return r\n} }\nelse { range = function(node, start, end) {\n  var r = document.body.createTextRange()\n  try { r.moveToElementText(node.parentNode) }\n  catch(e) { return r }\n  r.collapse(true)\n  r.moveEnd(\"character\", end)\n  r.moveStart(\"character\", start)\n  return r\n} }\n\nfunction contains(parent, child) {\n  if (child.nodeType == 3) // Android browser always returns false when child is a textnode\n    { child = child.parentNode }\n  if (parent.contains)\n    { return parent.contains(child) }\n  do {\n    if (child.nodeType == 11) { child = child.host }\n    if (child == parent) { return true }\n  } while (child = child.parentNode)\n}\n\nvar activeElt = function() {\n  var activeElement = document.activeElement\n  while (activeElement && activeElement.root && activeElement.root.activeElement)\n    { activeElement = activeElement.root.activeElement }\n  return activeElement\n}\n// Older versions of IE throws unspecified error when touching\n// document.activeElement in some cases (during loading, in iframe)\nif (ie && ie_version < 11) { activeElt = function() {\n  try { return document.activeElement }\n  catch(e) { return document.body }\n} }\n\nfunction addClass(node, cls) {\n  var current = node.className\n  if (!classTest(cls).test(current)) { node.className += (current ? \" \" : \"\") + cls }\n}\nfunction joinClasses(a, b) {\n  var as = a.split(\" \")\n  for (var i = 0; i < as.length; i++)\n    { if (as[i] && !classTest(as[i]).test(b)) { b += \" \" + as[i] } }\n  return b\n}\n\nvar selectInput = function(node) { node.select() }\nif (ios) // Mobile Safari apparently has a bug where select() is broken.\n  { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length } }\nelse if (ie) // Suppress mysterious IE10 errors\n  { selectInput = function(node) { try { node.select() } catch(_e) {} } }\n\nfunction bind(f) {\n  var args = Array.prototype.slice.call(arguments, 1)\n  return function(){return f.apply(null, args)}\n}\n\nfunction copyObj(obj, target, overwrite) {\n  if (!target) { target = {} }\n  for (var prop in obj)\n    { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))\n      { target[prop] = obj[prop] } }\n  return target\n}\n\n// Counts the column offset in a string, taking tabs into account.\n// Used mostly to find indentation.\nfunction countColumn(string, end, tabSize, startIndex, startValue) {\n  if (end == null) {\n    end = string.search(/[^\\s\\u00a0]/)\n    if (end == -1) { end = string.length }\n  }\n  for (var i = startIndex || 0, n = startValue || 0;;) {\n    var nextTab = string.indexOf(\"\\t\", i)\n    if (nextTab < 0 || nextTab >= end)\n      { return n + (end - i) }\n    n += nextTab - i\n    n += tabSize - (n % tabSize)\n    i = nextTab + 1\n  }\n}\n\nfunction Delayed() {this.id = null}\nDelayed.prototype.set = function(ms, f) {\n  clearTimeout(this.id)\n  this.id = setTimeout(f, ms)\n}\n\nfunction indexOf(array, elt) {\n  for (var i = 0; i < array.length; ++i)\n    { if (array[i] == elt) { return i } }\n  return -1\n}\n\n// Number of pixels added to scroller and sizer to hide scrollbar\nvar scrollerGap = 30\n\n// Returned or thrown by various protocols to signal 'I'm not\n// handling this'.\nvar Pass = {toString: function(){return \"CodeMirror.Pass\"}}\n\n// Reused option objects for setSelection & friends\nvar sel_dontScroll = {scroll: false};\nvar sel_mouse = {origin: \"*mouse\"};\nvar sel_move = {origin: \"+move\"}\n\n// The inverse of countColumn -- find the offset that corresponds to\n// a particular column.\nfunction findColumn(string, goal, tabSize) {\n  for (var pos = 0, col = 0;;) {\n    var nextTab = string.indexOf(\"\\t\", pos)\n    if (nextTab == -1) { nextTab = string.length }\n    var skipped = nextTab - pos\n    if (nextTab == string.length || col + skipped >= goal)\n      { return pos + Math.min(skipped, goal - col) }\n    col += nextTab - pos\n    col += tabSize - (col % tabSize)\n    pos = nextTab + 1\n    if (col >= goal) { return pos }\n  }\n}\n\nvar spaceStrs = [\"\"]\nfunction spaceStr(n) {\n  while (spaceStrs.length <= n)\n    { spaceStrs.push(lst(spaceStrs) + \" \") }\n  return spaceStrs[n]\n}\n\nfunction lst(arr) { return arr[arr.length-1] }\n\nfunction map(array, f) {\n  var out = []\n  for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i) }\n  return out\n}\n\nfunction insertSorted(array, value, score) {\n  var pos = 0, priority = score(value)\n  while (pos < array.length && score(array[pos]) <= priority) { pos++ }\n  array.splice(pos, 0, value)\n}\n\nfunction nothing() {}\n\nfunction createObj(base, props) {\n  var inst\n  if (Object.create) {\n    inst = Object.create(base)\n  } else {\n    nothing.prototype = base\n    inst = new nothing()\n  }\n  if (props) { copyObj(props, inst) }\n  return inst\n}\n\nvar nonASCIISingleCaseWordChar = /[\\u00df\\u0587\\u0590-\\u05f4\\u0600-\\u06ff\\u3040-\\u309f\\u30a0-\\u30ff\\u3400-\\u4db5\\u4e00-\\u9fcc\\uac00-\\ud7af]/\nfunction isWordCharBasic(ch) {\n  return /\\w/.test(ch) || ch > \"\\x80\" &&\n    (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch))\n}\nfunction isWordChar(ch, helper) {\n  if (!helper) { return isWordCharBasic(ch) }\n  if (helper.source.indexOf(\"\\\\w\") > -1 && isWordCharBasic(ch)) { return true }\n  return helper.test(ch)\n}\n\nfunction isEmpty(obj) {\n  for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } }\n  return true\n}\n\n// Extending unicode characters. A series of a non-extending char +\n// any number of extending chars is treated as a single unit as far\n// as editing and measuring is concerned. This is not fully correct,\n// since some scripts/fonts/browsers also treat other configurations\n// of code points as a group.\nvar extendingChars = /[\\u0300-\\u036f\\u0483-\\u0489\\u0591-\\u05bd\\u05bf\\u05c1\\u05c2\\u05c4\\u05c5\\u05c7\\u0610-\\u061a\\u064b-\\u065e\\u0670\\u06d6-\\u06dc\\u06de-\\u06e4\\u06e7\\u06e8\\u06ea-\\u06ed\\u0711\\u0730-\\u074a\\u07a6-\\u07b0\\u07eb-\\u07f3\\u0816-\\u0819\\u081b-\\u0823\\u0825-\\u0827\\u0829-\\u082d\\u0900-\\u0902\\u093c\\u0941-\\u0948\\u094d\\u0951-\\u0955\\u0962\\u0963\\u0981\\u09bc\\u09be\\u09c1-\\u09c4\\u09cd\\u09d7\\u09e2\\u09e3\\u0a01\\u0a02\\u0a3c\\u0a41\\u0a42\\u0a47\\u0a48\\u0a4b-\\u0a4d\\u0a51\\u0a70\\u0a71\\u0a75\\u0a81\\u0a82\\u0abc\\u0ac1-\\u0ac5\\u0ac7\\u0ac8\\u0acd\\u0ae2\\u0ae3\\u0b01\\u0b3c\\u0b3e\\u0b3f\\u0b41-\\u0b44\\u0b4d\\u0b56\\u0b57\\u0b62\\u0b63\\u0b82\\u0bbe\\u0bc0\\u0bcd\\u0bd7\\u0c3e-\\u0c40\\u0c46-\\u0c48\\u0c4a-\\u0c4d\\u0c55\\u0c56\\u0c62\\u0c63\\u0cbc\\u0cbf\\u0cc2\\u0cc6\\u0ccc\\u0ccd\\u0cd5\\u0cd6\\u0ce2\\u0ce3\\u0d3e\\u0d41-\\u0d44\\u0d4d\\u0d57\\u0d62\\u0d63\\u0dca\\u0dcf\\u0dd2-\\u0dd4\\u0dd6\\u0ddf\\u0e31\\u0e34-\\u0e3a\\u0e47-\\u0e4e\\u0eb1\\u0eb4-\\u0eb9\\u0ebb\\u0ebc\\u0ec8-\\u0ecd\\u0f18\\u0f19\\u0f35\\u0f37\\u0f39\\u0f71-\\u0f7e\\u0f80-\\u0f84\\u0f86\\u0f87\\u0f90-\\u0f97\\u0f99-\\u0fbc\\u0fc6\\u102d-\\u1030\\u1032-\\u1037\\u1039\\u103a\\u103d\\u103e\\u1058\\u1059\\u105e-\\u1060\\u1071-\\u1074\\u1082\\u1085\\u1086\\u108d\\u109d\\u135f\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17b7-\\u17bd\\u17c6\\u17c9-\\u17d3\\u17dd\\u180b-\\u180d\\u18a9\\u1920-\\u1922\\u1927\\u1928\\u1932\\u1939-\\u193b\\u1a17\\u1a18\\u1a56\\u1a58-\\u1a5e\\u1a60\\u1a62\\u1a65-\\u1a6c\\u1a73-\\u1a7c\\u1a7f\\u1b00-\\u1b03\\u1b34\\u1b36-\\u1b3a\\u1b3c\\u1b42\\u1b6b-\\u1b73\\u1b80\\u1b81\\u1ba2-\\u1ba5\\u1ba8\\u1ba9\\u1c2c-\\u1c33\\u1c36\\u1c37\\u1cd0-\\u1cd2\\u1cd4-\\u1ce0\\u1ce2-\\u1ce8\\u1ced\\u1dc0-\\u1de6\\u1dfd-\\u1dff\\u200c\\u200d\\u20d0-\\u20f0\\u2cef-\\u2cf1\\u2de0-\\u2dff\\u302a-\\u302f\\u3099\\u309a\\ua66f-\\ua672\\ua67c\\ua67d\\ua6f0\\ua6f1\\ua802\\ua806\\ua80b\\ua825\\ua826\\ua8c4\\ua8e0-\\ua8f1\\ua926-\\ua92d\\ua947-\\ua951\\ua980-\\ua982\\ua9b3\\ua9b6-\\ua9b9\\ua9bc\\uaa29-\\uaa2e\\uaa31\\uaa32\\uaa35\\uaa36\\uaa43\\uaa4c\\uaab0\\uaab2-\\uaab4\\uaab7\\uaab8\\uaabe\\uaabf\\uaac1\\uabe5\\uabe8\\uabed\\udc00-\\udfff\\ufb1e\\ufe00-\\ufe0f\\ufe20-\\ufe26\\uff9e\\uff9f]/\nfunction isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) }\n\n// The display handles the DOM integration, both for input reading\n// and content drawing. It holds references to DOM nodes and\n// display-related state.\n\nfunction Display(place, doc, input) {\n  var d = this\n  this.input = input\n\n  // Covers bottom-right square when both scrollbars are present.\n  d.scrollbarFiller = elt(\"div\", null, \"CodeMirror-scrollbar-filler\")\n  d.scrollbarFiller.setAttribute(\"cm-not-content\", \"true\")\n  // Covers bottom of gutter when coverGutterNextToScrollbar is on\n  // and h scrollbar is present.\n  d.gutterFiller = elt(\"div\", null, \"CodeMirror-gutter-filler\")\n  d.gutterFiller.setAttribute(\"cm-not-content\", \"true\")\n  // Will contain the actual code, positioned to cover the viewport.\n  d.lineDiv = elt(\"div\", null, \"CodeMirror-code\")\n  // Elements are added to these to represent selection and cursors.\n  d.selectionDiv = elt(\"div\", null, null, \"position: relative; z-index: 1\")\n  d.cursorDiv = elt(\"div\", null, \"CodeMirror-cursors\")\n  // A visibility: hidden element used to find the size of things.\n  d.measure = elt(\"div\", null, \"CodeMirror-measure\")\n  // When lines outside of the viewport are measured, they are drawn in this.\n  d.lineMeasure = elt(\"div\", null, \"CodeMirror-measure\")\n  // Wraps everything that needs to exist inside the vertically-padded coordinate system\n  d.lineSpace = elt(\"div\", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv],\n                    null, \"position: relative; outline: none\")\n  // Moved around its parent to cover visible view.\n  d.mover = elt(\"div\", [elt(\"div\", [d.lineSpace], \"CodeMirror-lines\")], null, \"position: relative\")\n  // Set to the height of the document, allowing scrolling.\n  d.sizer = elt(\"div\", [d.mover], \"CodeMirror-sizer\")\n  d.sizerWidth = null\n  // Behavior of elts with overflow: auto and padding is\n  // inconsistent across browsers. This is used to ensure the\n  // scrollable area is big enough.\n  d.heightForcer = elt(\"div\", null, null, \"position: absolute; height: \" + scrollerGap + \"px; width: 1px;\")\n  // Will contain the gutters, if any.\n  d.gutters = elt(\"div\", null, \"CodeMirror-gutters\")\n  d.lineGutter = null\n  // Actual scrollable element.\n  d.scroller = elt(\"div\", [d.sizer, d.heightForcer, d.gutters], \"CodeMirror-scroll\")\n  d.scroller.setAttribute(\"tabIndex\", \"-1\")\n  // The element in which the editor lives.\n  d.wrapper = elt(\"div\", [d.scrollbarFiller, d.gutterFiller, d.scroller], \"CodeMirror\")\n\n  // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)\n  if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0 }\n  if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true }\n\n  if (place) {\n    if (place.appendChild) { place.appendChild(d.wrapper) }\n    else { place(d.wrapper) }\n  }\n\n  // Current rendered range (may be bigger than the view window).\n  d.viewFrom = d.viewTo = doc.first\n  d.reportedViewFrom = d.reportedViewTo = doc.first\n  // Information about the rendered lines.\n  d.view = []\n  d.renderedView = null\n  // Holds info about a single rendered line when it was rendered\n  // for measurement, while not in view.\n  d.externalMeasured = null\n  // Empty space (in pixels) above the view\n  d.viewOffset = 0\n  d.lastWrapHeight = d.lastWrapWidth = 0\n  d.updateLineNumbers = null\n\n  d.nativeBarWidth = d.barHeight = d.barWidth = 0\n  d.scrollbarsClipped = false\n\n  // Used to only resize the line number gutter when necessary (when\n  // the amount of lines crosses a boundary that makes its width change)\n  d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null\n  // Set to true when a non-horizontal-scrolling line widget is\n  // added. As an optimization, line widget aligning is skipped when\n  // this is false.\n  d.alignWidgets = false\n\n  d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null\n\n  // Tracks the maximum line length so that the horizontal scrollbar\n  // can be kept static when scrolling.\n  d.maxLine = null\n  d.maxLineLength = 0\n  d.maxLineChanged = false\n\n  // Used for measuring wheel scrolling granularity\n  d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null\n\n  // True when shift is held down.\n  d.shift = false\n\n  // Used to track whether anything happened since the context menu\n  // was opened.\n  d.selForContextMenu = null\n\n  d.activeTouch = null\n\n  input.init(d)\n}\n\n// Find the line object corresponding to the given line number.\nfunction getLine(doc, n) {\n  n -= doc.first\n  if (n < 0 || n >= doc.size) { throw new Error(\"There is no line \" + (n + doc.first) + \" in the document.\") }\n  var chunk = doc\n  while (!chunk.lines) {\n    for (var i = 0;; ++i) {\n      var child = chunk.children[i], sz = child.chunkSize()\n      if (n < sz) { chunk = child; break }\n      n -= sz\n    }\n  }\n  return chunk.lines[n]\n}\n\n// Get the part of a document between two positions, as an array of\n// strings.\nfunction getBetween(doc, start, end) {\n  var out = [], n = start.line\n  doc.iter(start.line, end.line + 1, function (line) {\n    var text = line.text\n    if (n == end.line) { text = text.slice(0, end.ch) }\n    if (n == start.line) { text = text.slice(start.ch) }\n    out.push(text)\n    ++n\n  })\n  return out\n}\n// Get the lines between from and to, as array of strings.\nfunction getLines(doc, from, to) {\n  var out = []\n  doc.iter(from, to, function (line) { out.push(line.text) }) // iter aborts when callback returns truthy value\n  return out\n}\n\n// Update the height of a line, propagating the height change\n// upwards to parent nodes.\nfunction updateLineHeight(line, height) {\n  var diff = height - line.height\n  if (diff) { for (var n = line; n; n = n.parent) { n.height += diff } }\n}\n\n// Given a line object, find its line number by walking up through\n// its parent links.\nfunction lineNo(line) {\n  if (line.parent == null) { return null }\n  var cur = line.parent, no = indexOf(cur.lines, line)\n  for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {\n    for (var i = 0;; ++i) {\n      if (chunk.children[i] == cur) { break }\n      no += chunk.children[i].chunkSize()\n    }\n  }\n  return no + cur.first\n}\n\n// Find the line at the given vertical position, using the height\n// information in the document tree.\nfunction lineAtHeight(chunk, h) {\n  var n = chunk.first\n  outer: do {\n    for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) {\n      var child = chunk.children[i$1], ch = child.height\n      if (h < ch) { chunk = child; continue outer }\n      h -= ch\n      n += child.chunkSize()\n    }\n    return n\n  } while (!chunk.lines)\n  var i = 0\n  for (; i < chunk.lines.length; ++i) {\n    var line = chunk.lines[i], lh = line.height\n    if (h < lh) { break }\n    h -= lh\n  }\n  return n + i\n}\n\nfunction isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size}\n\nfunction lineNumberFor(options, i) {\n  return String(options.lineNumberFormatter(i + options.firstLineNumber))\n}\n\n// A Pos instance represents a position within the text.\nfunction Pos (line, ch) {\n  if (!(this instanceof Pos)) { return new Pos(line, ch) }\n  this.line = line; this.ch = ch\n}\n\n// Compare two positions, return 0 if they are the same, a negative\n// number when a is less, and a positive number otherwise.\nfunction cmp(a, b) { return a.line - b.line || a.ch - b.ch }\n\nfunction copyPos(x) {return Pos(x.line, x.ch)}\nfunction maxPos(a, b) { return cmp(a, b) < 0 ? b : a }\nfunction minPos(a, b) { return cmp(a, b) < 0 ? a : b }\n\n// Most of the external API clips given positions to make sure they\n// actually exist within the document.\nfunction clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))}\nfunction clipPos(doc, pos) {\n  if (pos.line < doc.first) { return Pos(doc.first, 0) }\n  var last = doc.first + doc.size - 1\n  if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) }\n  return clipToLen(pos, getLine(doc, pos.line).text.length)\n}\nfunction clipToLen(pos, linelen) {\n  var ch = pos.ch\n  if (ch == null || ch > linelen) { return Pos(pos.line, linelen) }\n  else if (ch < 0) { return Pos(pos.line, 0) }\n  else { return pos }\n}\nfunction clipPosArray(doc, array) {\n  var out = []\n  for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]) }\n  return out\n}\n\n// Optimize some code when these features are not used.\nvar sawReadOnlySpans = false;\nvar sawCollapsedSpans = false\n\nfunction seeReadOnlySpans() {\n  sawReadOnlySpans = true\n}\n\nfunction seeCollapsedSpans() {\n  sawCollapsedSpans = true\n}\n\n// TEXTMARKER SPANS\n\nfunction MarkedSpan(marker, from, to) {\n  this.marker = marker\n  this.from = from; this.to = to\n}\n\n// Search an array of spans for a span matching the given marker.\nfunction getMarkedSpanFor(spans, marker) {\n  if (spans) { for (var i = 0; i < spans.length; ++i) {\n    var span = spans[i]\n    if (span.marker == marker) { return span }\n  } }\n}\n// Remove a span from an array, returning undefined if no spans are\n// left (we don't store arrays for lines without spans).\nfunction removeMarkedSpan(spans, span) {\n  var r\n  for (var i = 0; i < spans.length; ++i)\n    { if (spans[i] != span) { (r || (r = [])).push(spans[i]) } }\n  return r\n}\n// Add a span to a line.\nfunction addMarkedSpan(line, span) {\n  line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]\n  span.marker.attachLine(line)\n}\n\n// Used for the algorithm that adjusts markers for a change in the\n// document. These functions cut an array of spans at a given\n// character position, returning an array of remaining chunks (or\n// undefined if nothing remains).\nfunction markedSpansBefore(old, startCh, isInsert) {\n  var nw\n  if (old) { for (var i = 0; i < old.length; ++i) {\n    var span = old[i], marker = span.marker\n    var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh)\n    if (startsBefore || span.from == startCh && marker.type == \"bookmark\" && (!isInsert || !span.marker.insertLeft)) {\n      var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to))\n    }\n  } }\n  return nw\n}\nfunction markedSpansAfter(old, endCh, isInsert) {\n  var nw\n  if (old) { for (var i = 0; i < old.length; ++i) {\n    var span = old[i], marker = span.marker\n    var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh)\n    if (endsAfter || span.from == endCh && marker.type == \"bookmark\" && (!isInsert || span.marker.insertLeft)) {\n      var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh,\n                                            span.to == null ? null : span.to - endCh))\n    }\n  } }\n  return nw\n}\n\n// Given a change object, compute the new set of marker spans that\n// cover the line in which the change took place. Removes spans\n// entirely within the change, reconnects spans belonging to the\n// same marker that appear on both sides of the change, and cuts off\n// spans partially within the change. Returns an array of span\n// arrays with one element for each line in (after) the change.\nfunction stretchSpansOverChange(doc, change) {\n  if (change.full) { return null }\n  var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans\n  var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans\n  if (!oldFirst && !oldLast) { return null }\n\n  var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0\n  // Get the spans that 'stick out' on both sides\n  var first = markedSpansBefore(oldFirst, startCh, isInsert)\n  var last = markedSpansAfter(oldLast, endCh, isInsert)\n\n  // Next, merge those two ends\n  var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0)\n  if (first) {\n    // Fix up .to properties of first\n    for (var i = 0; i < first.length; ++i) {\n      var span = first[i]\n      if (span.to == null) {\n        var found = getMarkedSpanFor(last, span.marker)\n        if (!found) { span.to = startCh }\n        else if (sameLine) { span.to = found.to == null ? null : found.to + offset }\n      }\n    }\n  }\n  if (last) {\n    // Fix up .from in last (or move them into first in case of sameLine)\n    for (var i$1 = 0; i$1 < last.length; ++i$1) {\n      var span$1 = last[i$1]\n      if (span$1.to != null) { span$1.to += offset }\n      if (span$1.from == null) {\n        var found$1 = getMarkedSpanFor(first, span$1.marker)\n        if (!found$1) {\n          span$1.from = offset\n          if (sameLine) { (first || (first = [])).push(span$1) }\n        }\n      } else {\n        span$1.from += offset\n        if (sameLine) { (first || (first = [])).push(span$1) }\n      }\n    }\n  }\n  // Make sure we didn't create any zero-length spans\n  if (first) { first = clearEmptySpans(first) }\n  if (last && last != first) { last = clearEmptySpans(last) }\n\n  var newMarkers = [first]\n  if (!sameLine) {\n    // Fill gap with whole-line-spans\n    var gap = change.text.length - 2, gapMarkers\n    if (gap > 0 && first)\n      { for (var i$2 = 0; i$2 < first.length; ++i$2)\n        { if (first[i$2].to == null)\n          { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)) } } }\n    for (var i$3 = 0; i$3 < gap; ++i$3)\n      { newMarkers.push(gapMarkers) }\n    newMarkers.push(last)\n  }\n  return newMarkers\n}\n\n// Remove spans that are empty and don't have a clearWhenEmpty\n// option of false.\nfunction clearEmptySpans(spans) {\n  for (var i = 0; i < spans.length; ++i) {\n    var span = spans[i]\n    if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)\n      { spans.splice(i--, 1) }\n  }\n  if (!spans.length) { return null }\n  return spans\n}\n\n// Used to 'clip' out readOnly ranges when making a change.\nfunction removeReadOnlyRanges(doc, from, to) {\n  var markers = null\n  doc.iter(from.line, to.line + 1, function (line) {\n    if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) {\n      var mark = line.markedSpans[i].marker\n      if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))\n        { (markers || (markers = [])).push(mark) }\n    } }\n  })\n  if (!markers) { return null }\n  var parts = [{from: from, to: to}]\n  for (var i = 0; i < markers.length; ++i) {\n    var mk = markers[i], m = mk.find(0)\n    for (var j = 0; j < parts.length; ++j) {\n      var p = parts[j]\n      if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue }\n      var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to)\n      if (dfrom < 0 || !mk.inclusiveLeft && !dfrom)\n        { newParts.push({from: p.from, to: m.from}) }\n      if (dto > 0 || !mk.inclusiveRight && !dto)\n        { newParts.push({from: m.to, to: p.to}) }\n      parts.splice.apply(parts, newParts)\n      j += newParts.length - 1\n    }\n  }\n  return parts\n}\n\n// Connect or disconnect spans from a line.\nfunction detachMarkedSpans(line) {\n  var spans = line.markedSpans\n  if (!spans) { return }\n  for (var i = 0; i < spans.length; ++i)\n    { spans[i].marker.detachLine(line) }\n  line.markedSpans = null\n}\nfunction attachMarkedSpans(line, spans) {\n  if (!spans) { return }\n  for (var i = 0; i < spans.length; ++i)\n    { spans[i].marker.attachLine(line) }\n  line.markedSpans = spans\n}\n\n// Helpers used when computing which overlapping collapsed span\n// counts as the larger one.\nfunction extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 }\nfunction extraRight(marker) { return marker.inclusiveRight ? 1 : 0 }\n\n// Returns a number indicating which of two overlapping collapsed\n// spans is larger (and thus includes the other). Falls back to\n// comparing ids when the spans cover exactly the same range.\nfunction compareCollapsedMarkers(a, b) {\n  var lenDiff = a.lines.length - b.lines.length\n  if (lenDiff != 0) { return lenDiff }\n  var aPos = a.find(), bPos = b.find()\n  var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b)\n  if (fromCmp) { return -fromCmp }\n  var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b)\n  if (toCmp) { return toCmp }\n  return b.id - a.id\n}\n\n// Find out whether a line ends or starts in a collapsed span. If\n// so, return the marker for that span.\nfunction collapsedSpanAtSide(line, start) {\n  var sps = sawCollapsedSpans && line.markedSpans, found\n  if (sps) { for (var sp = void 0, i = 0; i < sps.length; ++i) {\n    sp = sps[i]\n    if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&\n        (!found || compareCollapsedMarkers(found, sp.marker) < 0))\n      { found = sp.marker }\n  } }\n  return found\n}\nfunction collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) }\nfunction collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) }\n\n// Test whether there exists a collapsed span that partially\n// overlaps (covers the start or end, but not both) of a new span.\n// Such overlap is not allowed.\nfunction conflictingCollapsedRange(doc, lineNo$$1, from, to, marker) {\n  var line = getLine(doc, lineNo$$1)\n  var sps = sawCollapsedSpans && line.markedSpans\n  if (sps) { for (var i = 0; i < sps.length; ++i) {\n    var sp = sps[i]\n    if (!sp.marker.collapsed) { continue }\n    var found = sp.marker.find(0)\n    var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker)\n    var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker)\n    if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue }\n    if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||\n        fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))\n      { return true }\n  } }\n}\n\n// A visual line is a line as drawn on the screen. Folding, for\n// example, can cause multiple logical lines to appear on the same\n// visual line. This finds the start of the visual line that the\n// given line is part of (usually that is the line itself).\nfunction visualLine(line) {\n  var merged\n  while (merged = collapsedSpanAtStart(line))\n    { line = merged.find(-1, true).line }\n  return line\n}\n\n// Returns an array of logical lines that continue the visual line\n// started by the argument, or undefined if there are no such lines.\nfunction visualLineContinued(line) {\n  var merged, lines\n  while (merged = collapsedSpanAtEnd(line)) {\n    line = merged.find(1, true).line\n    ;(lines || (lines = [])).push(line)\n  }\n  return lines\n}\n\n// Get the line number of the start of the visual line that the\n// given line number is part of.\nfunction visualLineNo(doc, lineN) {\n  var line = getLine(doc, lineN), vis = visualLine(line)\n  if (line == vis) { return lineN }\n  return lineNo(vis)\n}\n\n// Get the line number of the start of the next visual line after\n// the given line.\nfunction visualLineEndNo(doc, lineN) {\n  if (lineN > doc.lastLine()) { return lineN }\n  var line = getLine(doc, lineN), merged\n  if (!lineIsHidden(doc, line)) { return lineN }\n  while (merged = collapsedSpanAtEnd(line))\n    { line = merged.find(1, true).line }\n  return lineNo(line) + 1\n}\n\n// Compute whether a line is hidden. Lines count as hidden when they\n// are part of a visual line that starts with another line, or when\n// they are entirely covered by collapsed, non-widget span.\nfunction lineIsHidden(doc, line) {\n  var sps = sawCollapsedSpans && line.markedSpans\n  if (sps) { for (var sp = void 0, i = 0; i < sps.length; ++i) {\n    sp = sps[i]\n    if (!sp.marker.collapsed) { continue }\n    if (sp.from == null) { return true }\n    if (sp.marker.widgetNode) { continue }\n    if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))\n      { return true }\n  } }\n}\nfunction lineIsHiddenInner(doc, line, span) {\n  if (span.to == null) {\n    var end = span.marker.find(1, true)\n    return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker))\n  }\n  if (span.marker.inclusiveRight && span.to == line.text.length)\n    { return true }\n  for (var sp = void 0, i = 0; i < line.markedSpans.length; ++i) {\n    sp = line.markedSpans[i]\n    if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to &&\n        (sp.to == null || sp.to != span.from) &&\n        (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&\n        lineIsHiddenInner(doc, line, sp)) { return true }\n  }\n}\n\n// Find the height above the given line.\nfunction heightAtLine(lineObj) {\n  lineObj = visualLine(lineObj)\n\n  var h = 0, chunk = lineObj.parent\n  for (var i = 0; i < chunk.lines.length; ++i) {\n    var line = chunk.lines[i]\n    if (line == lineObj) { break }\n    else { h += line.height }\n  }\n  for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {\n    for (var i$1 = 0; i$1 < p.children.length; ++i$1) {\n      var cur = p.children[i$1]\n      if (cur == chunk) { break }\n      else { h += cur.height }\n    }\n  }\n  return h\n}\n\n// Compute the character length of a line, taking into account\n// collapsed ranges (see markText) that might hide parts, and join\n// other lines onto it.\nfunction lineLength(line) {\n  if (line.height == 0) { return 0 }\n  var len = line.text.length, merged, cur = line\n  while (merged = collapsedSpanAtStart(cur)) {\n    var found = merged.find(0, true)\n    cur = found.from.line\n    len += found.from.ch - found.to.ch\n  }\n  cur = line\n  while (merged = collapsedSpanAtEnd(cur)) {\n    var found$1 = merged.find(0, true)\n    len -= cur.text.length - found$1.from.ch\n    cur = found$1.to.line\n    len += cur.text.length - found$1.to.ch\n  }\n  return len\n}\n\n// Find the longest line in the document.\nfunction findMaxLine(cm) {\n  var d = cm.display, doc = cm.doc\n  d.maxLine = getLine(doc, doc.first)\n  d.maxLineLength = lineLength(d.maxLine)\n  d.maxLineChanged = true\n  doc.iter(function (line) {\n    var len = lineLength(line)\n    if (len > d.maxLineLength) {\n      d.maxLineLength = len\n      d.maxLine = line\n    }\n  })\n}\n\n// BIDI HELPERS\n\nfunction iterateBidiSections(order, from, to, f) {\n  if (!order) { return f(from, to, \"ltr\") }\n  var found = false\n  for (var i = 0; i < order.length; ++i) {\n    var part = order[i]\n    if (part.from < to && part.to > from || from == to && part.to == from) {\n      f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? \"rtl\" : \"ltr\")\n      found = true\n    }\n  }\n  if (!found) { f(from, to, \"ltr\") }\n}\n\nfunction bidiLeft(part) { return part.level % 2 ? part.to : part.from }\nfunction bidiRight(part) { return part.level % 2 ? part.from : part.to }\n\nfunction lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0 }\nfunction lineRight(line) {\n  var order = getOrder(line)\n  if (!order) { return line.text.length }\n  return bidiRight(lst(order))\n}\n\nfunction compareBidiLevel(order, a, b) {\n  var linedir = order[0].level\n  if (a == linedir) { return true }\n  if (b == linedir) { return false }\n  return a < b\n}\n\nvar bidiOther = null\nfunction getBidiPartAt(order, pos) {\n  var found\n  bidiOther = null\n  for (var i = 0; i < order.length; ++i) {\n    var cur = order[i]\n    if (cur.from < pos && cur.to > pos) { return i }\n    if ((cur.from == pos || cur.to == pos)) {\n      if (found == null) {\n        found = i\n      } else if (compareBidiLevel(order, cur.level, order[found].level)) {\n        if (cur.from != cur.to) { bidiOther = found }\n        return i\n      } else {\n        if (cur.from != cur.to) { bidiOther = i }\n        return found\n      }\n    }\n  }\n  return found\n}\n\nfunction moveInLine(line, pos, dir, byUnit) {\n  if (!byUnit) { return pos + dir }\n  do { pos += dir }\n  while (pos > 0 && isExtendingChar(line.text.charAt(pos)))\n  return pos\n}\n\n// This is needed in order to move 'visually' through bi-directional\n// text -- i.e., pressing left should make the cursor go left, even\n// when in RTL text. The tricky part is the 'jumps', where RTL and\n// LTR text touch each other. This often requires the cursor offset\n// to move more than one unit, in order to visually move one unit.\nfunction moveVisually(line, start, dir, byUnit) {\n  var bidi = getOrder(line)\n  if (!bidi) { return moveLogically(line, start, dir, byUnit) }\n  var pos = getBidiPartAt(bidi, start), part = bidi[pos]\n  var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit)\n\n  for (;;) {\n    if (target > part.from && target < part.to) { return target }\n    if (target == part.from || target == part.to) {\n      if (getBidiPartAt(bidi, target) == pos) { return target }\n      part = bidi[pos += dir]\n      return (dir > 0) == part.level % 2 ? part.to : part.from\n    } else {\n      part = bidi[pos += dir]\n      if (!part) { return null }\n      if ((dir > 0) == part.level % 2)\n        { target = moveInLine(line, part.to, -1, byUnit) }\n      else\n        { target = moveInLine(line, part.from, 1, byUnit) }\n    }\n  }\n}\n\nfunction moveLogically(line, start, dir, byUnit) {\n  var target = start + dir\n  if (byUnit) { while (target > 0 && isExtendingChar(line.text.charAt(target))) { target += dir } }\n  return target < 0 || target > line.text.length ? null : target\n}\n\n// Bidirectional ordering algorithm\n// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm\n// that this (partially) implements.\n\n// One-char codes used for character types:\n// L (L):   Left-to-Right\n// R (R):   Right-to-Left\n// r (AL):  Right-to-Left Arabic\n// 1 (EN):  European Number\n// + (ES):  European Number Separator\n// % (ET):  European Number Terminator\n// n (AN):  Arabic Number\n// , (CS):  Common Number Separator\n// m (NSM): Non-Spacing Mark\n// b (BN):  Boundary Neutral\n// s (B):   Paragraph Separator\n// t (S):   Segment Separator\n// w (WS):  Whitespace\n// N (ON):  Other Neutrals\n\n// Returns null if characters are ordered as they appear\n// (left-to-right), or an array of sections ({from, to, level}\n// objects) in the order in which they occur visually.\nvar bidiOrdering = (function() {\n  // Character types for codepoints 0 to 0xff\n  var lowTypes = \"bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN\"\n  // Character types for codepoints 0x600 to 0x6ff\n  var arabicTypes = \"rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm\"\n  function charType(code) {\n    if (code <= 0xf7) { return lowTypes.charAt(code) }\n    else if (0x590 <= code && code <= 0x5f4) { return \"R\" }\n    else if (0x600 <= code && code <= 0x6ed) { return arabicTypes.charAt(code - 0x600) }\n    else if (0x6ee <= code && code <= 0x8ac) { return \"r\" }\n    else if (0x2000 <= code && code <= 0x200b) { return \"w\" }\n    else if (code == 0x200c) { return \"b\" }\n    else { return \"L\" }\n  }\n\n  var bidiRE = /[\\u0590-\\u05f4\\u0600-\\u06ff\\u0700-\\u08ac]/\n  var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/\n  // Browsers seem to always treat the boundaries of block elements as being L.\n  var outerType = \"L\"\n\n  function BidiSpan(level, from, to) {\n    this.level = level\n    this.from = from; this.to = to\n  }\n\n  return function(str) {\n    if (!bidiRE.test(str)) { return false }\n    var len = str.length, types = []\n    for (var i = 0; i < len; ++i)\n      { types.push(charType(str.charCodeAt(i))) }\n\n    // W1. Examine each non-spacing mark (NSM) in the level run, and\n    // change the type of the NSM to the type of the previous\n    // character. If the NSM is at the start of the level run, it will\n    // get the type of sor.\n    for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) {\n      var type = types[i$1]\n      if (type == \"m\") { types[i$1] = prev }\n      else { prev = type }\n    }\n\n    // W2. Search backwards from each instance of a European number\n    // until the first strong type (R, L, AL, or sor) is found. If an\n    // AL is found, change the type of the European number to Arabic\n    // number.\n    // W3. Change all ALs to R.\n    for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) {\n      var type$1 = types[i$2]\n      if (type$1 == \"1\" && cur == \"r\") { types[i$2] = \"n\" }\n      else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == \"r\") { types[i$2] = \"R\" } }\n    }\n\n    // W4. A single European separator between two European numbers\n    // changes to a European number. A single common separator between\n    // two numbers of the same type changes to that type.\n    for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) {\n      var type$2 = types[i$3]\n      if (type$2 == \"+\" && prev$1 == \"1\" && types[i$3+1] == \"1\") { types[i$3] = \"1\" }\n      else if (type$2 == \",\" && prev$1 == types[i$3+1] &&\n               (prev$1 == \"1\" || prev$1 == \"n\")) { types[i$3] = prev$1 }\n      prev$1 = type$2\n    }\n\n    // W5. A sequence of European terminators adjacent to European\n    // numbers changes to all European numbers.\n    // W6. Otherwise, separators and terminators change to Other\n    // Neutral.\n    for (var i$4 = 0; i$4 < len; ++i$4) {\n      var type$3 = types[i$4]\n      if (type$3 == \",\") { types[i$4] = \"N\" }\n      else if (type$3 == \"%\") {\n        var end = void 0\n        for (end = i$4 + 1; end < len && types[end] == \"%\"; ++end) {}\n        var replace = (i$4 && types[i$4-1] == \"!\") || (end < len && types[end] == \"1\") ? \"1\" : \"N\"\n        for (var j = i$4; j < end; ++j) { types[j] = replace }\n        i$4 = end - 1\n      }\n    }\n\n    // W7. Search backwards from each instance of a European number\n    // until the first strong type (R, L, or sor) is found. If an L is\n    // found, then change the type of the European number to L.\n    for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) {\n      var type$4 = types[i$5]\n      if (cur$1 == \"L\" && type$4 == \"1\") { types[i$5] = \"L\" }\n      else if (isStrong.test(type$4)) { cur$1 = type$4 }\n    }\n\n    // N1. A sequence of neutrals takes the direction of the\n    // surrounding strong text if the text on both sides has the same\n    // direction. European and Arabic numbers act as if they were R in\n    // terms of their influence on neutrals. Start-of-level-run (sor)\n    // and end-of-level-run (eor) are used at level run boundaries.\n    // N2. Any remaining neutrals take the embedding direction.\n    for (var i$6 = 0; i$6 < len; ++i$6) {\n      if (isNeutral.test(types[i$6])) {\n        var end$1 = void 0\n        for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {}\n        var before = (i$6 ? types[i$6-1] : outerType) == \"L\"\n        var after = (end$1 < len ? types[end$1] : outerType) == \"L\"\n        var replace$1 = before || after ? \"L\" : \"R\"\n        for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1 }\n        i$6 = end$1 - 1\n      }\n    }\n\n    // Here we depart from the documented algorithm, in order to avoid\n    // building up an actual levels array. Since there are only three\n    // levels (0, 1, 2) in an implementation that doesn't take\n    // explicit embedding into account, we can build up the order on\n    // the fly, without following the level-based algorithm.\n    var order = [], m\n    for (var i$7 = 0; i$7 < len;) {\n      if (countsAsLeft.test(types[i$7])) {\n        var start = i$7\n        for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {}\n        order.push(new BidiSpan(0, start, i$7))\n      } else {\n        var pos = i$7, at = order.length\n        for (++i$7; i$7 < len && types[i$7] != \"L\"; ++i$7) {}\n        for (var j$2 = pos; j$2 < i$7;) {\n          if (countsAsNum.test(types[j$2])) {\n            if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)) }\n            var nstart = j$2\n            for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {}\n            order.splice(at, 0, new BidiSpan(2, nstart, j$2))\n            pos = j$2\n          } else { ++j$2 }\n        }\n        if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)) }\n      }\n    }\n    if (order[0].level == 1 && (m = str.match(/^\\s+/))) {\n      order[0].from = m[0].length\n      order.unshift(new BidiSpan(0, 0, m[0].length))\n    }\n    if (lst(order).level == 1 && (m = str.match(/\\s+$/))) {\n      lst(order).to -= m[0].length\n      order.push(new BidiSpan(0, len - m[0].length, len))\n    }\n    if (order[0].level == 2)\n      { order.unshift(new BidiSpan(1, order[0].to, order[0].to)) }\n    if (order[0].level != lst(order).level)\n      { order.push(new BidiSpan(order[0].level, len, len)) }\n\n    return order\n  }\n})()\n\n// Get the bidi ordering for the given line (and cache it). Returns\n// false for lines that are fully left-to-right, and an array of\n// BidiSpan objects otherwise.\nfunction getOrder(line) {\n  var order = line.order\n  if (order == null) { order = line.order = bidiOrdering(line.text) }\n  return order\n}\n\n// EVENT HANDLING\n\n// Lightweight event framework. on/off also work on DOM nodes,\n// registering native DOM handlers.\n\nvar on = function(emitter, type, f) {\n  if (emitter.addEventListener)\n    { emitter.addEventListener(type, f, false) }\n  else if (emitter.attachEvent)\n    { emitter.attachEvent(\"on\" + type, f) }\n  else {\n    var map$$1 = emitter._handlers || (emitter._handlers = {})\n    var arr = map$$1[type] || (map$$1[type] = [])\n    arr.push(f)\n  }\n}\n\nvar noHandlers = []\nfunction getHandlers(emitter, type, copy) {\n  var arr = emitter._handlers && emitter._handlers[type]\n  if (copy) { return arr && arr.length > 0 ? arr.slice() : noHandlers }\n  else { return arr || noHandlers }\n}\n\nfunction off(emitter, type, f) {\n  if (emitter.removeEventListener)\n    { emitter.removeEventListener(type, f, false) }\n  else if (emitter.detachEvent)\n    { emitter.detachEvent(\"on\" + type, f) }\n  else {\n    var handlers = getHandlers(emitter, type, false)\n    for (var i = 0; i < handlers.length; ++i)\n      { if (handlers[i] == f) { handlers.splice(i, 1); break } }\n  }\n}\n\nfunction signal(emitter, type /*, values...*/) {\n  var handlers = getHandlers(emitter, type, true)\n  if (!handlers.length) { return }\n  var args = Array.prototype.slice.call(arguments, 2)\n  for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args) }\n}\n\n// The DOM events that CodeMirror handles can be overridden by\n// registering a (non-DOM) handler on the editor for the event name,\n// and preventDefault-ing the event in that handler.\nfunction signalDOMEvent(cm, e, override) {\n  if (typeof e == \"string\")\n    { e = {type: e, preventDefault: function() { this.defaultPrevented = true }} }\n  signal(cm, override || e.type, cm, e)\n  return e_defaultPrevented(e) || e.codemirrorIgnore\n}\n\nfunction signalCursorActivity(cm) {\n  var arr = cm._handlers && cm._handlers.cursorActivity\n  if (!arr) { return }\n  var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = [])\n  for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1)\n    { set.push(arr[i]) } }\n}\n\nfunction hasHandler(emitter, type) {\n  return getHandlers(emitter, type).length > 0\n}\n\n// Add on and off methods to a constructor's prototype, to make\n// registering events on such objects more convenient.\nfunction eventMixin(ctor) {\n  ctor.prototype.on = function(type, f) {on(this, type, f)}\n  ctor.prototype.off = function(type, f) {off(this, type, f)}\n}\n\n// Due to the fact that we still support jurassic IE versions, some\n// compatibility wrappers are needed.\n\nfunction e_preventDefault(e) {\n  if (e.preventDefault) { e.preventDefault() }\n  else { e.returnValue = false }\n}\nfunction e_stopPropagation(e) {\n  if (e.stopPropagation) { e.stopPropagation() }\n  else { e.cancelBubble = true }\n}\nfunction e_defaultPrevented(e) {\n  return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false\n}\nfunction e_stop(e) {e_preventDefault(e); e_stopPropagation(e)}\n\nfunction e_target(e) {return e.target || e.srcElement}\nfunction e_button(e) {\n  var b = e.which\n  if (b == null) {\n    if (e.button & 1) { b = 1 }\n    else if (e.button & 2) { b = 3 }\n    else if (e.button & 4) { b = 2 }\n  }\n  if (mac && e.ctrlKey && b == 1) { b = 3 }\n  return b\n}\n\n// Detect drag-and-drop\nvar dragAndDrop = function() {\n  // There is *some* kind of drag-and-drop support in IE6-8, but I\n  // couldn't get it to work yet.\n  if (ie && ie_version < 9) { return false }\n  var div = elt('div')\n  return \"draggable\" in div || \"dragDrop\" in div\n}()\n\nvar zwspSupported\nfunction zeroWidthElement(measure) {\n  if (zwspSupported == null) {\n    var test = elt(\"span\", \"\\u200b\")\n    removeChildrenAndAdd(measure, elt(\"span\", [test, document.createTextNode(\"x\")]))\n    if (measure.firstChild.offsetHeight != 0)\n      { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8) }\n  }\n  var node = zwspSupported ? elt(\"span\", \"\\u200b\") :\n    elt(\"span\", \"\\u00a0\", null, \"display: inline-block; width: 1px; margin-right: -1px\")\n  node.setAttribute(\"cm-text\", \"\")\n  return node\n}\n\n// Feature-detect IE's crummy client rect reporting for bidi text\nvar badBidiRects\nfunction hasBadBidiRects(measure) {\n  if (badBidiRects != null) { return badBidiRects }\n  var txt = removeChildrenAndAdd(measure, document.createTextNode(\"A\\u062eA\"))\n  var r0 = range(txt, 0, 1).getBoundingClientRect()\n  var r1 = range(txt, 1, 2).getBoundingClientRect()\n  removeChildren(measure)\n  if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780)\n  return badBidiRects = (r1.right - r0.right < 3)\n}\n\n// See if \"\".split is the broken IE version, if so, provide an\n// alternative way to split lines.\nvar splitLinesAuto = \"\\n\\nb\".split(/\\n/).length != 3 ? function (string) {\n  var pos = 0, result = [], l = string.length\n  while (pos <= l) {\n    var nl = string.indexOf(\"\\n\", pos)\n    if (nl == -1) { nl = string.length }\n    var line = string.slice(pos, string.charAt(nl - 1) == \"\\r\" ? nl - 1 : nl)\n    var rt = line.indexOf(\"\\r\")\n    if (rt != -1) {\n      result.push(line.slice(0, rt))\n      pos += rt + 1\n    } else {\n      result.push(line)\n      pos = nl + 1\n    }\n  }\n  return result\n} : function (string) { return string.split(/\\r\\n?|\\n/); }\n\nvar hasSelection = window.getSelection ? function (te) {\n  try { return te.selectionStart != te.selectionEnd }\n  catch(e) { return false }\n} : function (te) {\n  var range$$1\n  try {range$$1 = te.ownerDocument.selection.createRange()}\n  catch(e) {}\n  if (!range$$1 || range$$1.parentElement() != te) { return false }\n  return range$$1.compareEndPoints(\"StartToEnd\", range$$1) != 0\n}\n\nvar hasCopyEvent = (function () {\n  var e = elt(\"div\")\n  if (\"oncopy\" in e) { return true }\n  e.setAttribute(\"oncopy\", \"return;\")\n  return typeof e.oncopy == \"function\"\n})()\n\nvar badZoomedRects = null\nfunction hasBadZoomedRects(measure) {\n  if (badZoomedRects != null) { return badZoomedRects }\n  var node = removeChildrenAndAdd(measure, elt(\"span\", \"x\"))\n  var normal = node.getBoundingClientRect()\n  var fromRange = range(node, 0, 1).getBoundingClientRect()\n  return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1\n}\n\n// Known modes, by name and by MIME\nvar modes = {};\nvar mimeModes = {}\n\n// Extra arguments are stored as the mode's dependencies, which is\n// used by (legacy) mechanisms like loadmode.js to automatically\n// load a mode. (Preferred mechanism is the require/define calls.)\nfunction defineMode(name, mode) {\n  if (arguments.length > 2)\n    { mode.dependencies = Array.prototype.slice.call(arguments, 2) }\n  modes[name] = mode\n}\n\nfunction defineMIME(mime, spec) {\n  mimeModes[mime] = spec\n}\n\n// Given a MIME type, a {name, ...options} config object, or a name\n// string, return a mode config object.\nfunction resolveMode(spec) {\n  if (typeof spec == \"string\" && mimeModes.hasOwnProperty(spec)) {\n    spec = mimeModes[spec]\n  } else if (spec && typeof spec.name == \"string\" && mimeModes.hasOwnProperty(spec.name)) {\n    var found = mimeModes[spec.name]\n    if (typeof found == \"string\") { found = {name: found} }\n    spec = createObj(found, spec)\n    spec.name = found.name\n  } else if (typeof spec == \"string\" && /^[\\w\\-]+\\/[\\w\\-]+\\+xml$/.test(spec)) {\n    return resolveMode(\"application/xml\")\n  } else if (typeof spec == \"string\" && /^[\\w\\-]+\\/[\\w\\-]+\\+json$/.test(spec)) {\n    return resolveMode(\"application/json\")\n  }\n  if (typeof spec == \"string\") { return {name: spec} }\n  else { return spec || {name: \"null\"} }\n}\n\n// Given a mode spec (anything that resolveMode accepts), find and\n// initialize an actual mode object.\nfunction getMode(options, spec) {\n  spec = resolveMode(spec)\n  var mfactory = modes[spec.name]\n  if (!mfactory) { return getMode(options, \"text/plain\") }\n  var modeObj = mfactory(options, spec)\n  if (modeExtensions.hasOwnProperty(spec.name)) {\n    var exts = modeExtensions[spec.name]\n    for (var prop in exts) {\n      if (!exts.hasOwnProperty(prop)) { continue }\n      if (modeObj.hasOwnProperty(prop)) { modeObj[\"_\" + prop] = modeObj[prop] }\n      modeObj[prop] = exts[prop]\n    }\n  }\n  modeObj.name = spec.name\n  if (spec.helperType) { modeObj.helperType = spec.helperType }\n  if (spec.modeProps) { for (var prop$1 in spec.modeProps)\n    { modeObj[prop$1] = spec.modeProps[prop$1] } }\n\n  return modeObj\n}\n\n// This can be used to attach properties to mode objects from\n// outside the actual mode definition.\nvar modeExtensions = {}\nfunction extendMode(mode, properties) {\n  var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {})\n  copyObj(properties, exts)\n}\n\nfunction copyState(mode, state) {\n  if (state === true) { return state }\n  if (mode.copyState) { return mode.copyState(state) }\n  var nstate = {}\n  for (var n in state) {\n    var val = state[n]\n    if (val instanceof Array) { val = val.concat([]) }\n    nstate[n] = val\n  }\n  return nstate\n}\n\n// Given a mode and a state (for that mode), find the inner mode and\n// state at the position that the state refers to.\nfunction innerMode(mode, state) {\n  var info\n  while (mode.innerMode) {\n    info = mode.innerMode(state)\n    if (!info || info.mode == mode) { break }\n    state = info.state\n    mode = info.mode\n  }\n  return info || {mode: mode, state: state}\n}\n\nfunction startState(mode, a1, a2) {\n  return mode.startState ? mode.startState(a1, a2) : true\n}\n\n// STRING STREAM\n\n// Fed to the mode parsers, provides helper functions to make\n// parsers more succinct.\n\nvar StringStream = function(string, tabSize) {\n  this.pos = this.start = 0\n  this.string = string\n  this.tabSize = tabSize || 8\n  this.lastColumnPos = this.lastColumnValue = 0\n  this.lineStart = 0\n}\n\nStringStream.prototype = {\n  eol: function() {return this.pos >= this.string.length},\n  sol: function() {return this.pos == this.lineStart},\n  peek: function() {return this.string.charAt(this.pos) || undefined},\n  next: function() {\n    if (this.pos < this.string.length)\n      { return this.string.charAt(this.pos++) }\n  },\n  eat: function(match) {\n    var ch = this.string.charAt(this.pos)\n    var ok\n    if (typeof match == \"string\") { ok = ch == match }\n    else { ok = ch && (match.test ? match.test(ch) : match(ch)) }\n    if (ok) {++this.pos; return ch}\n  },\n  eatWhile: function(match) {\n    var start = this.pos\n    while (this.eat(match)){}\n    return this.pos > start\n  },\n  eatSpace: function() {\n    var this$1 = this;\n\n    var start = this.pos\n    while (/[\\s\\u00a0]/.test(this.string.charAt(this.pos))) { ++this$1.pos }\n    return this.pos > start\n  },\n  skipToEnd: function() {this.pos = this.string.length},\n  skipTo: function(ch) {\n    var found = this.string.indexOf(ch, this.pos)\n    if (found > -1) {this.pos = found; return true}\n  },\n  backUp: function(n) {this.pos -= n},\n  column: function() {\n    if (this.lastColumnPos < this.start) {\n      this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue)\n      this.lastColumnPos = this.start\n    }\n    return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0)\n  },\n  indentation: function() {\n    return countColumn(this.string, null, this.tabSize) -\n      (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0)\n  },\n  match: function(pattern, consume, caseInsensitive) {\n    if (typeof pattern == \"string\") {\n      var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }\n      var substr = this.string.substr(this.pos, pattern.length)\n      if (cased(substr) == cased(pattern)) {\n        if (consume !== false) { this.pos += pattern.length }\n        return true\n      }\n    } else {\n      var match = this.string.slice(this.pos).match(pattern)\n      if (match && match.index > 0) { return null }\n      if (match && consume !== false) { this.pos += match[0].length }\n      return match\n    }\n  },\n  current: function(){return this.string.slice(this.start, this.pos)},\n  hideFirstChars: function(n, inner) {\n    this.lineStart += n\n    try { return inner() }\n    finally { this.lineStart -= n }\n  }\n}\n\n// Compute a style array (an array starting with a mode generation\n// -- for invalidation -- followed by pairs of end positions and\n// style strings), which is used to highlight the tokens on the\n// line.\nfunction highlightLine(cm, line, state, forceToEnd) {\n  // A styles array always starts with a number identifying the\n  // mode/overlays that it is based on (for easy invalidation).\n  var st = [cm.state.modeGen], lineClasses = {}\n  // Compute the base array of styles\n  runMode(cm, line.text, cm.doc.mode, state, function (end, style) { return st.push(end, style); },\n    lineClasses, forceToEnd)\n\n  // Run overlays, adjust style array.\n  var loop = function ( o ) {\n    var overlay = cm.state.overlays[o], i = 1, at = 0\n    runMode(cm, line.text, overlay.mode, true, function (end, style) {\n      var start = i\n      // Ensure there's a token end at the current position, and that i points at it\n      while (at < end) {\n        var i_end = st[i]\n        if (i_end > end)\n          { st.splice(i, 1, end, st[i+1], i_end) }\n        i += 2\n        at = Math.min(end, i_end)\n      }\n      if (!style) { return }\n      if (overlay.opaque) {\n        st.splice(start, i - start, end, \"overlay \" + style)\n        i = start + 2\n      } else {\n        for (; start < i; start += 2) {\n          var cur = st[start+1]\n          st[start+1] = (cur ? cur + \" \" : \"\") + \"overlay \" + style\n        }\n      }\n    }, lineClasses)\n  };\n\n  for (var o = 0; o < cm.state.overlays.length; ++o) loop( o );\n\n  return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}\n}\n\nfunction getLineStyles(cm, line, updateFrontier) {\n  if (!line.styles || line.styles[0] != cm.state.modeGen) {\n    var state = getStateBefore(cm, lineNo(line))\n    var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state)\n    line.stateAfter = state\n    line.styles = result.styles\n    if (result.classes) { line.styleClasses = result.classes }\n    else if (line.styleClasses) { line.styleClasses = null }\n    if (updateFrontier === cm.doc.frontier) { cm.doc.frontier++ }\n  }\n  return line.styles\n}\n\nfunction getStateBefore(cm, n, precise) {\n  var doc = cm.doc, display = cm.display\n  if (!doc.mode.startState) { return true }\n  var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter\n  if (!state) { state = startState(doc.mode) }\n  else { state = copyState(doc.mode, state) }\n  doc.iter(pos, n, function (line) {\n    processLine(cm, line.text, state)\n    var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo\n    line.stateAfter = save ? copyState(doc.mode, state) : null\n    ++pos\n  })\n  if (precise) { doc.frontier = pos }\n  return state\n}\n\n// Lightweight form of highlight -- proceed over this line and\n// update state, but don't save a style array. Used for lines that\n// aren't currently visible.\nfunction processLine(cm, text, state, startAt) {\n  var mode = cm.doc.mode\n  var stream = new StringStream(text, cm.options.tabSize)\n  stream.start = stream.pos = startAt || 0\n  if (text == \"\") { callBlankLine(mode, state) }\n  while (!stream.eol()) {\n    readToken(mode, stream, state)\n    stream.start = stream.pos\n  }\n}\n\nfunction callBlankLine(mode, state) {\n  if (mode.blankLine) { return mode.blankLine(state) }\n  if (!mode.innerMode) { return }\n  var inner = innerMode(mode, state)\n  if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) }\n}\n\nfunction readToken(mode, stream, state, inner) {\n  for (var i = 0; i < 10; i++) {\n    if (inner) { inner[0] = innerMode(mode, state).mode }\n    var style = mode.token(stream, state)\n    if (stream.pos > stream.start) { return style }\n  }\n  throw new Error(\"Mode \" + mode.name + \" failed to advance stream.\")\n}\n\n// Utility for getTokenAt and getLineTokens\nfunction takeToken(cm, pos, precise, asArray) {\n  var getObj = function (copy) { return ({\n    start: stream.start, end: stream.pos,\n    string: stream.current(),\n    type: style || null,\n    state: copy ? copyState(doc.mode, state) : state\n  }); }\n\n  var doc = cm.doc, mode = doc.mode, style\n  pos = clipPos(doc, pos)\n  var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise)\n  var stream = new StringStream(line.text, cm.options.tabSize), tokens\n  if (asArray) { tokens = [] }\n  while ((asArray || stream.pos < pos.ch) && !stream.eol()) {\n    stream.start = stream.pos\n    style = readToken(mode, stream, state)\n    if (asArray) { tokens.push(getObj(true)) }\n  }\n  return asArray ? tokens : getObj()\n}\n\nfunction extractLineClasses(type, output) {\n  if (type) { for (;;) {\n    var lineClass = type.match(/(?:^|\\s+)line-(background-)?(\\S+)/)\n    if (!lineClass) { break }\n    type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length)\n    var prop = lineClass[1] ? \"bgClass\" : \"textClass\"\n    if (output[prop] == null)\n      { output[prop] = lineClass[2] }\n    else if (!(new RegExp(\"(?:^|\\s)\" + lineClass[2] + \"(?:$|\\s)\")).test(output[prop]))\n      { output[prop] += \" \" + lineClass[2] }\n  } }\n  return type\n}\n\n// Run the given mode's parser over a line, calling f for each token.\nfunction runMode(cm, text, mode, state, f, lineClasses, forceToEnd) {\n  var flattenSpans = mode.flattenSpans\n  if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans }\n  var curStart = 0, curStyle = null\n  var stream = new StringStream(text, cm.options.tabSize), style\n  var inner = cm.options.addModeClass && [null]\n  if (text == \"\") { extractLineClasses(callBlankLine(mode, state), lineClasses) }\n  while (!stream.eol()) {\n    if (stream.pos > cm.options.maxHighlightLength) {\n      flattenSpans = false\n      if (forceToEnd) { processLine(cm, text, state, stream.pos) }\n      stream.pos = text.length\n      style = null\n    } else {\n      style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses)\n    }\n    if (inner) {\n      var mName = inner[0].name\n      if (mName) { style = \"m-\" + (style ? mName + \" \" + style : mName) }\n    }\n    if (!flattenSpans || curStyle != style) {\n      while (curStart < stream.start) {\n        curStart = Math.min(stream.start, curStart + 5000)\n        f(curStart, curStyle)\n      }\n      curStyle = style\n    }\n    stream.start = stream.pos\n  }\n  while (curStart < stream.pos) {\n    // Webkit seems to refuse to render text nodes longer than 57444\n    // characters, and returns inaccurate measurements in nodes\n    // starting around 5000 chars.\n    var pos = Math.min(stream.pos, curStart + 5000)\n    f(pos, curStyle)\n    curStart = pos\n  }\n}\n\n// Finds the line to start with when starting a parse. Tries to\n// find a line with a stateAfter, so that it can start with a\n// valid state. If that fails, it returns the line with the\n// smallest indentation, which tends to need the least context to\n// parse correctly.\nfunction findStartLine(cm, n, precise) {\n  var minindent, minline, doc = cm.doc\n  var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100)\n  for (var search = n; search > lim; --search) {\n    if (search <= doc.first) { return doc.first }\n    var line = getLine(doc, search - 1)\n    if (line.stateAfter && (!precise || search <= doc.frontier)) { return search }\n    var indented = countColumn(line.text, null, cm.options.tabSize)\n    if (minline == null || minindent > indented) {\n      minline = search - 1\n      minindent = indented\n    }\n  }\n  return minline\n}\n\n// LINE DATA STRUCTURE\n\n// Line objects. These hold state related to a line, including\n// highlighting info (the styles array).\nfunction Line(text, markedSpans, estimateHeight) {\n  this.text = text\n  attachMarkedSpans(this, markedSpans)\n  this.height = estimateHeight ? estimateHeight(this) : 1\n}\neventMixin(Line)\nLine.prototype.lineNo = function() { return lineNo(this) }\n\n// Change the content (text, markers) of a line. Automatically\n// invalidates cached information and tries to re-estimate the\n// line's height.\nfunction updateLine(line, text, markedSpans, estimateHeight) {\n  line.text = text\n  if (line.stateAfter) { line.stateAfter = null }\n  if (line.styles) { line.styles = null }\n  if (line.order != null) { line.order = null }\n  detachMarkedSpans(line)\n  attachMarkedSpans(line, markedSpans)\n  var estHeight = estimateHeight ? estimateHeight(line) : 1\n  if (estHeight != line.height) { updateLineHeight(line, estHeight) }\n}\n\n// Detach a line from the document tree and its markers.\nfunction cleanUpLine(line) {\n  line.parent = null\n  detachMarkedSpans(line)\n}\n\n// Convert a style as returned by a mode (either null, or a string\n// containing one or more styles) to a CSS style. This is cached,\n// and also looks for line-wide styles.\nvar styleToClassCache = {};\nvar styleToClassCacheWithMode = {}\nfunction interpretTokenStyle(style, options) {\n  if (!style || /^\\s*$/.test(style)) { return null }\n  var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache\n  return cache[style] ||\n    (cache[style] = style.replace(/\\S+/g, \"cm-$&\"))\n}\n\n// Render the DOM representation of the text of a line. Also builds\n// up a 'line map', which points at the DOM nodes that represent\n// specific stretches of text, and is used by the measuring code.\n// The returned object contains the DOM node, this map, and\n// information about line-wide styles that were set by the mode.\nfunction buildLineContent(cm, lineView) {\n  // The padding-right forces the element to have a 'border', which\n  // is needed on Webkit to be able to get line-level bounding\n  // rectangles for it (in measureChar).\n  var content = elt(\"span\", null, null, webkit ? \"padding-right: .1px\" : null)\n  var builder = {pre: elt(\"pre\", [content], \"CodeMirror-line\"), content: content,\n                 col: 0, pos: 0, cm: cm,\n                 trailingSpace: false,\n                 splitSpaces: (ie || webkit) && cm.getOption(\"lineWrapping\")}\n  lineView.measure = {}\n\n  // Iterate over the logical lines that make up this visual line.\n  for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) {\n    var line = i ? lineView.rest[i - 1] : lineView.line, order = void 0\n    builder.pos = 0\n    builder.addToken = buildToken\n    // Optionally wire in some hacks into the token-rendering\n    // algorithm, to deal with browser quirks.\n    if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line)))\n      { builder.addToken = buildTokenBadBidi(builder.addToken, order) }\n    builder.map = []\n    var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line)\n    insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate))\n    if (line.styleClasses) {\n      if (line.styleClasses.bgClass)\n        { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || \"\") }\n      if (line.styleClasses.textClass)\n        { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || \"\") }\n    }\n\n    // Ensure at least a single node is present, for measuring.\n    if (builder.map.length == 0)\n      { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))) }\n\n    // Store the map and a cache object for the current logical line\n    if (i == 0) {\n      lineView.measure.map = builder.map\n      lineView.measure.cache = {}\n    } else {\n      (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map)\n      ;(lineView.measure.caches || (lineView.measure.caches = [])).push({})\n    }\n  }\n\n  // See issue #2901\n  if (webkit) {\n    var last = builder.content.lastChild\n    if (/\\bcm-tab\\b/.test(last.className) || (last.querySelector && last.querySelector(\".cm-tab\")))\n      { builder.content.className = \"cm-tab-wrap-hack\" }\n  }\n\n  signal(cm, \"renderLine\", cm, lineView.line, builder.pre)\n  if (builder.pre.className)\n    { builder.textClass = joinClasses(builder.pre.className, builder.textClass || \"\") }\n\n  return builder\n}\n\nfunction defaultSpecialCharPlaceholder(ch) {\n  var token = elt(\"span\", \"\\u2022\", \"cm-invalidchar\")\n  token.title = \"\\\\u\" + ch.charCodeAt(0).toString(16)\n  token.setAttribute(\"aria-label\", token.title)\n  return token\n}\n\n// Build up the DOM representation for a single token, and add it to\n// the line map. Takes care to render special characters separately.\nfunction buildToken(builder, text, style, startStyle, endStyle, title, css) {\n  if (!text) { return }\n  var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text\n  var special = builder.cm.state.specialChars, mustWrap = false\n  var content\n  if (!special.test(text)) {\n    builder.col += text.length\n    content = document.createTextNode(displayText)\n    builder.map.push(builder.pos, builder.pos + text.length, content)\n    if (ie && ie_version < 9) { mustWrap = true }\n    builder.pos += text.length\n  } else {\n    content = document.createDocumentFragment()\n    var pos = 0\n    while (true) {\n      special.lastIndex = pos\n      var m = special.exec(text)\n      var skipped = m ? m.index - pos : text.length - pos\n      if (skipped) {\n        var txt = document.createTextNode(displayText.slice(pos, pos + skipped))\n        if (ie && ie_version < 9) { content.appendChild(elt(\"span\", [txt])) }\n        else { content.appendChild(txt) }\n        builder.map.push(builder.pos, builder.pos + skipped, txt)\n        builder.col += skipped\n        builder.pos += skipped\n      }\n      if (!m) { break }\n      pos += skipped + 1\n      var txt$1 = void 0\n      if (m[0] == \"\\t\") {\n        var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize\n        txt$1 = content.appendChild(elt(\"span\", spaceStr(tabWidth), \"cm-tab\"))\n        txt$1.setAttribute(\"role\", \"presentation\")\n        txt$1.setAttribute(\"cm-text\", \"\\t\")\n        builder.col += tabWidth\n      } else if (m[0] == \"\\r\" || m[0] == \"\\n\") {\n        txt$1 = content.appendChild(elt(\"span\", m[0] == \"\\r\" ? \"\\u240d\" : \"\\u2424\", \"cm-invalidchar\"))\n        txt$1.setAttribute(\"cm-text\", m[0])\n        builder.col += 1\n      } else {\n        txt$1 = builder.cm.options.specialCharPlaceholder(m[0])\n        txt$1.setAttribute(\"cm-text\", m[0])\n        if (ie && ie_version < 9) { content.appendChild(elt(\"span\", [txt$1])) }\n        else { content.appendChild(txt$1) }\n        builder.col += 1\n      }\n      builder.map.push(builder.pos, builder.pos + 1, txt$1)\n      builder.pos++\n    }\n  }\n  builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32\n  if (style || startStyle || endStyle || mustWrap || css) {\n    var fullStyle = style || \"\"\n    if (startStyle) { fullStyle += startStyle }\n    if (endStyle) { fullStyle += endStyle }\n    var token = elt(\"span\", [content], fullStyle, css)\n    if (title) { token.title = title }\n    return builder.content.appendChild(token)\n  }\n  builder.content.appendChild(content)\n}\n\nfunction splitSpaces(text, trailingBefore) {\n  if (text.length > 1 && !/  /.test(text)) { return text }\n  var spaceBefore = trailingBefore, result = \"\"\n  for (var i = 0; i < text.length; i++) {\n    var ch = text.charAt(i)\n    if (ch == \" \" && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32))\n      { ch = \"\\u00a0\" }\n    result += ch\n    spaceBefore = ch == \" \"\n  }\n  return result\n}\n\n// Work around nonsense dimensions being reported for stretches of\n// right-to-left text.\nfunction buildTokenBadBidi(inner, order) {\n  return function (builder, text, style, startStyle, endStyle, title, css) {\n    style = style ? style + \" cm-force-border\" : \"cm-force-border\"\n    var start = builder.pos, end = start + text.length\n    for (;;) {\n      // Find the part that overlaps with the start of this text\n      var part = void 0\n      for (var i = 0; i < order.length; i++) {\n        part = order[i]\n        if (part.to > start && part.from <= start) { break }\n      }\n      if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, title, css) }\n      inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css)\n      startStyle = null\n      text = text.slice(part.to - start)\n      start = part.to\n    }\n  }\n}\n\nfunction buildCollapsedSpan(builder, size, marker, ignoreWidget) {\n  var widget = !ignoreWidget && marker.widgetNode\n  if (widget) { builder.map.push(builder.pos, builder.pos + size, widget) }\n  if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {\n    if (!widget)\n      { widget = builder.content.appendChild(document.createElement(\"span\")) }\n    widget.setAttribute(\"cm-marker\", marker.id)\n  }\n  if (widget) {\n    builder.cm.display.input.setUneditable(widget)\n    builder.content.appendChild(widget)\n  }\n  builder.pos += size\n  builder.trailingSpace = false\n}\n\n// Outputs a number of spans to make up a line, taking highlighting\n// and marked text into account.\nfunction insertLineContent(line, builder, styles) {\n  var spans = line.markedSpans, allText = line.text, at = 0\n  if (!spans) {\n    for (var i$1 = 1; i$1 < styles.length; i$1+=2)\n      { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)) }\n    return\n  }\n\n  var len = allText.length, pos = 0, i = 1, text = \"\", style, css\n  var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed\n  for (;;) {\n    if (nextChange == pos) { // Update current marker set\n      spanStyle = spanEndStyle = spanStartStyle = title = css = \"\"\n      collapsed = null; nextChange = Infinity\n      var foundBookmarks = [], endStyles = void 0\n      for (var j = 0; j < spans.length; ++j) {\n        var sp = spans[j], m = sp.marker\n        if (m.type == \"bookmark\" && sp.from == pos && m.widgetNode) {\n          foundBookmarks.push(m)\n        } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {\n          if (sp.to != null && sp.to != pos && nextChange > sp.to) {\n            nextChange = sp.to\n            spanEndStyle = \"\"\n          }\n          if (m.className) { spanStyle += \" \" + m.className }\n          if (m.css) { css = (css ? css + \";\" : \"\") + m.css }\n          if (m.startStyle && sp.from == pos) { spanStartStyle += \" \" + m.startStyle }\n          if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to) }\n          if (m.title && !title) { title = m.title }\n          if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))\n            { collapsed = sp }\n        } else if (sp.from > pos && nextChange > sp.from) {\n          nextChange = sp.from\n        }\n      }\n      if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2)\n        { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += \" \" + endStyles[j$1] } } }\n\n      if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2)\n        { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]) } }\n      if (collapsed && (collapsed.from || 0) == pos) {\n        buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,\n                           collapsed.marker, collapsed.from == null)\n        if (collapsed.to == null) { return }\n        if (collapsed.to == pos) { collapsed = false }\n      }\n    }\n    if (pos >= len) { break }\n\n    var upto = Math.min(len, nextChange)\n    while (true) {\n      if (text) {\n        var end = pos + text.length\n        if (!collapsed) {\n          var tokenText = end > upto ? text.slice(0, upto - pos) : text\n          builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,\n                           spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : \"\", title, css)\n        }\n        if (end >= upto) {text = text.slice(upto - pos); pos = upto; break}\n        pos = end\n        spanStartStyle = \"\"\n      }\n      text = allText.slice(at, at = styles[i++])\n      style = interpretTokenStyle(styles[i++], builder.cm.options)\n    }\n  }\n}\n\n\n// These objects are used to represent the visible (currently drawn)\n// part of the document. A LineView may correspond to multiple\n// logical lines, if those are connected by collapsed ranges.\nfunction LineView(doc, line, lineN) {\n  // The starting line\n  this.line = line\n  // Continuing lines, if any\n  this.rest = visualLineContinued(line)\n  // Number of logical lines in this visual line\n  this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1\n  this.node = this.text = null\n  this.hidden = lineIsHidden(doc, line)\n}\n\n// Create a range of LineView objects for the given lines.\nfunction buildViewArray(cm, from, to) {\n  var array = [], nextPos\n  for (var pos = from; pos < to; pos = nextPos) {\n    var view = new LineView(cm.doc, getLine(cm.doc, pos), pos)\n    nextPos = pos + view.size\n    array.push(view)\n  }\n  return array\n}\n\nvar operationGroup = null\n\nfunction pushOperation(op) {\n  if (operationGroup) {\n    operationGroup.ops.push(op)\n  } else {\n    op.ownsGroup = operationGroup = {\n      ops: [op],\n      delayedCallbacks: []\n    }\n  }\n}\n\nfunction fireCallbacksForOps(group) {\n  // Calls delayed callbacks and cursorActivity handlers until no\n  // new ones appear\n  var callbacks = group.delayedCallbacks, i = 0\n  do {\n    for (; i < callbacks.length; i++)\n      { callbacks[i].call(null) }\n    for (var j = 0; j < group.ops.length; j++) {\n      var op = group.ops[j]\n      if (op.cursorActivityHandlers)\n        { while (op.cursorActivityCalled < op.cursorActivityHandlers.length)\n          { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm) } }\n    }\n  } while (i < callbacks.length)\n}\n\nfunction finishOperation(op, endCb) {\n  var group = op.ownsGroup\n  if (!group) { return }\n\n  try { fireCallbacksForOps(group) }\n  finally {\n    operationGroup = null\n    endCb(group)\n  }\n}\n\nvar orphanDelayedCallbacks = null\n\n// Often, we want to signal events at a point where we are in the\n// middle of some work, but don't want the handler to start calling\n// other methods on the editor, which might be in an inconsistent\n// state or simply not expect any other events to happen.\n// signalLater looks whether there are any handlers, and schedules\n// them to be executed when the last operation ends, or, if no\n// operation is active, when a timeout fires.\nfunction signalLater(emitter, type /*, values...*/) {\n  var arr = getHandlers(emitter, type, false)\n  if (!arr.length) { return }\n  var args = Array.prototype.slice.call(arguments, 2), list\n  if (operationGroup) {\n    list = operationGroup.delayedCallbacks\n  } else if (orphanDelayedCallbacks) {\n    list = orphanDelayedCallbacks\n  } else {\n    list = orphanDelayedCallbacks = []\n    setTimeout(fireOrphanDelayed, 0)\n  }\n  var loop = function ( i ) {\n    list.push(function () { return arr[i].apply(null, args); })\n  };\n\n  for (var i = 0; i < arr.length; ++i)\n    loop( i );\n}\n\nfunction fireOrphanDelayed() {\n  var delayed = orphanDelayedCallbacks\n  orphanDelayedCallbacks = null\n  for (var i = 0; i < delayed.length; ++i) { delayed[i]() }\n}\n\n// When an aspect of a line changes, a string is added to\n// lineView.changes. This updates the relevant part of the line's\n// DOM structure.\nfunction updateLineForChanges(cm, lineView, lineN, dims) {\n  for (var j = 0; j < lineView.changes.length; j++) {\n    var type = lineView.changes[j]\n    if (type == \"text\") { updateLineText(cm, lineView) }\n    else if (type == \"gutter\") { updateLineGutter(cm, lineView, lineN, dims) }\n    else if (type == \"class\") { updateLineClasses(lineView) }\n    else if (type == \"widget\") { updateLineWidgets(cm, lineView, dims) }\n  }\n  lineView.changes = null\n}\n\n// Lines with gutter elements, widgets or a background class need to\n// be wrapped, and have the extra elements added to the wrapper div\nfunction ensureLineWrapped(lineView) {\n  if (lineView.node == lineView.text) {\n    lineView.node = elt(\"div\", null, null, \"position: relative\")\n    if (lineView.text.parentNode)\n      { lineView.text.parentNode.replaceChild(lineView.node, lineView.text) }\n    lineView.node.appendChild(lineView.text)\n    if (ie && ie_version < 8) { lineView.node.style.zIndex = 2 }\n  }\n  return lineView.node\n}\n\nfunction updateLineBackground(lineView) {\n  var cls = lineView.bgClass ? lineView.bgClass + \" \" + (lineView.line.bgClass || \"\") : lineView.line.bgClass\n  if (cls) { cls += \" CodeMirror-linebackground\" }\n  if (lineView.background) {\n    if (cls) { lineView.background.className = cls }\n    else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null }\n  } else if (cls) {\n    var wrap = ensureLineWrapped(lineView)\n    lineView.background = wrap.insertBefore(elt(\"div\", null, cls), wrap.firstChild)\n  }\n}\n\n// Wrapper around buildLineContent which will reuse the structure\n// in display.externalMeasured when possible.\nfunction getLineContent(cm, lineView) {\n  var ext = cm.display.externalMeasured\n  if (ext && ext.line == lineView.line) {\n    cm.display.externalMeasured = null\n    lineView.measure = ext.measure\n    return ext.built\n  }\n  return buildLineContent(cm, lineView)\n}\n\n// Redraw the line's text. Interacts with the background and text\n// classes because the mode may output tokens that influence these\n// classes.\nfunction updateLineText(cm, lineView) {\n  var cls = lineView.text.className\n  var built = getLineContent(cm, lineView)\n  if (lineView.text == lineView.node) { lineView.node = built.pre }\n  lineView.text.parentNode.replaceChild(built.pre, lineView.text)\n  lineView.text = built.pre\n  if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) {\n    lineView.bgClass = built.bgClass\n    lineView.textClass = built.textClass\n    updateLineClasses(lineView)\n  } else if (cls) {\n    lineView.text.className = cls\n  }\n}\n\nfunction updateLineClasses(lineView) {\n  updateLineBackground(lineView)\n  if (lineView.line.wrapClass)\n    { ensureLineWrapped(lineView).className = lineView.line.wrapClass }\n  else if (lineView.node != lineView.text)\n    { lineView.node.className = \"\" }\n  var textClass = lineView.textClass ? lineView.textClass + \" \" + (lineView.line.textClass || \"\") : lineView.line.textClass\n  lineView.text.className = textClass || \"\"\n}\n\nfunction updateLineGutter(cm, lineView, lineN, dims) {\n  if (lineView.gutter) {\n    lineView.node.removeChild(lineView.gutter)\n    lineView.gutter = null\n  }\n  if (lineView.gutterBackground) {\n    lineView.node.removeChild(lineView.gutterBackground)\n    lineView.gutterBackground = null\n  }\n  if (lineView.line.gutterClass) {\n    var wrap = ensureLineWrapped(lineView)\n    lineView.gutterBackground = elt(\"div\", null, \"CodeMirror-gutter-background \" + lineView.line.gutterClass,\n                                    (\"left: \" + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + \"px; width: \" + (dims.gutterTotalWidth) + \"px\"))\n    wrap.insertBefore(lineView.gutterBackground, lineView.text)\n  }\n  var markers = lineView.line.gutterMarkers\n  if (cm.options.lineNumbers || markers) {\n    var wrap$1 = ensureLineWrapped(lineView)\n    var gutterWrap = lineView.gutter = elt(\"div\", null, \"CodeMirror-gutter-wrapper\", (\"left: \" + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + \"px\"))\n    cm.display.input.setUneditable(gutterWrap)\n    wrap$1.insertBefore(gutterWrap, lineView.text)\n    if (lineView.line.gutterClass)\n      { gutterWrap.className += \" \" + lineView.line.gutterClass }\n    if (cm.options.lineNumbers && (!markers || !markers[\"CodeMirror-linenumbers\"]))\n      { lineView.lineNumber = gutterWrap.appendChild(\n        elt(\"div\", lineNumberFor(cm.options, lineN),\n            \"CodeMirror-linenumber CodeMirror-gutter-elt\",\n            (\"left: \" + (dims.gutterLeft[\"CodeMirror-linenumbers\"]) + \"px; width: \" + (cm.display.lineNumInnerWidth) + \"px\"))) }\n    if (markers) { for (var k = 0; k < cm.options.gutters.length; ++k) {\n      var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]\n      if (found)\n        { gutterWrap.appendChild(elt(\"div\", [found], \"CodeMirror-gutter-elt\",\n                                   (\"left: \" + (dims.gutterLeft[id]) + \"px; width: \" + (dims.gutterWidth[id]) + \"px\"))) }\n    } }\n  }\n}\n\nfunction updateLineWidgets(cm, lineView, dims) {\n  if (lineView.alignable) { lineView.alignable = null }\n  for (var node = lineView.node.firstChild, next = void 0; node; node = next) {\n    next = node.nextSibling\n    if (node.className == \"CodeMirror-linewidget\")\n      { lineView.node.removeChild(node) }\n  }\n  insertLineWidgets(cm, lineView, dims)\n}\n\n// Build a line's DOM representation from scratch\nfunction buildLineElement(cm, lineView, lineN, dims) {\n  var built = getLineContent(cm, lineView)\n  lineView.text = lineView.node = built.pre\n  if (built.bgClass) { lineView.bgClass = built.bgClass }\n  if (built.textClass) { lineView.textClass = built.textClass }\n\n  updateLineClasses(lineView)\n  updateLineGutter(cm, lineView, lineN, dims)\n  insertLineWidgets(cm, lineView, dims)\n  return lineView.node\n}\n\n// A lineView may contain multiple logical lines (when merged by\n// collapsed spans). The widgets for all of them need to be drawn.\nfunction insertLineWidgets(cm, lineView, dims) {\n  insertLineWidgetsFor(cm, lineView.line, lineView, dims, true)\n  if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++)\n    { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false) } }\n}\n\nfunction insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) {\n  if (!line.widgets) { return }\n  var wrap = ensureLineWrapped(lineView)\n  for (var i = 0, ws = line.widgets; i < ws.length; ++i) {\n    var widget = ws[i], node = elt(\"div\", [widget.node], \"CodeMirror-linewidget\")\n    if (!widget.handleMouseEvents) { node.setAttribute(\"cm-ignore-events\", \"true\") }\n    positionLineWidget(widget, node, lineView, dims)\n    cm.display.input.setUneditable(node)\n    if (allowAbove && widget.above)\n      { wrap.insertBefore(node, lineView.gutter || lineView.text) }\n    else\n      { wrap.appendChild(node) }\n    signalLater(widget, \"redraw\")\n  }\n}\n\nfunction positionLineWidget(widget, node, lineView, dims) {\n  if (widget.noHScroll) {\n    (lineView.alignable || (lineView.alignable = [])).push(node)\n    var width = dims.wrapperWidth\n    node.style.left = dims.fixedPos + \"px\"\n    if (!widget.coverGutter) {\n      width -= dims.gutterTotalWidth\n      node.style.paddingLeft = dims.gutterTotalWidth + \"px\"\n    }\n    node.style.width = width + \"px\"\n  }\n  if (widget.coverGutter) {\n    node.style.zIndex = 5\n    node.style.position = \"relative\"\n    if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + \"px\" }\n  }\n}\n\nfunction widgetHeight(widget) {\n  if (widget.height != null) { return widget.height }\n  var cm = widget.doc.cm\n  if (!cm) { return 0 }\n  if (!contains(document.body, widget.node)) {\n    var parentStyle = \"position: relative;\"\n    if (widget.coverGutter)\n      { parentStyle += \"margin-left: -\" + cm.display.gutters.offsetWidth + \"px;\" }\n    if (widget.noHScroll)\n      { parentStyle += \"width: \" + cm.display.wrapper.clientWidth + \"px;\" }\n    removeChildrenAndAdd(cm.display.measure, elt(\"div\", [widget.node], null, parentStyle))\n  }\n  return widget.height = widget.node.parentNode.offsetHeight\n}\n\n// Return true when the given mouse event happened in a widget\nfunction eventInWidget(display, e) {\n  for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {\n    if (!n || (n.nodeType == 1 && n.getAttribute(\"cm-ignore-events\") == \"true\") ||\n        (n.parentNode == display.sizer && n != display.mover))\n      { return true }\n  }\n}\n\n// POSITION MEASUREMENT\n\nfunction paddingTop(display) {return display.lineSpace.offsetTop}\nfunction paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight}\nfunction paddingH(display) {\n  if (display.cachedPaddingH) { return display.cachedPaddingH }\n  var e = removeChildrenAndAdd(display.measure, elt(\"pre\", \"x\"))\n  var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle\n  var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}\n  if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data }\n  return data\n}\n\nfunction scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth }\nfunction displayWidth(cm) {\n  return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth\n}\nfunction displayHeight(cm) {\n  return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight\n}\n\n// Ensure the lineView.wrapping.heights array is populated. This is\n// an array of bottom offsets for the lines that make up a drawn\n// line. When lineWrapping is on, there might be more than one\n// height.\nfunction ensureLineHeights(cm, lineView, rect) {\n  var wrapping = cm.options.lineWrapping\n  var curWidth = wrapping && displayWidth(cm)\n  if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) {\n    var heights = lineView.measure.heights = []\n    if (wrapping) {\n      lineView.measure.width = curWidth\n      var rects = lineView.text.firstChild.getClientRects()\n      for (var i = 0; i < rects.length - 1; i++) {\n        var cur = rects[i], next = rects[i + 1]\n        if (Math.abs(cur.bottom - next.bottom) > 2)\n          { heights.push((cur.bottom + next.top) / 2 - rect.top) }\n      }\n    }\n    heights.push(rect.bottom - rect.top)\n  }\n}\n\n// Find a line map (mapping character offsets to text nodes) and a\n// measurement cache for the given line number. (A line view might\n// contain multiple lines when collapsed ranges are present.)\nfunction mapFromLineView(lineView, line, lineN) {\n  if (lineView.line == line)\n    { return {map: lineView.measure.map, cache: lineView.measure.cache} }\n  for (var i = 0; i < lineView.rest.length; i++)\n    { if (lineView.rest[i] == line)\n      { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } }\n  for (var i$1 = 0; i$1 < lineView.rest.length; i$1++)\n    { if (lineNo(lineView.rest[i$1]) > lineN)\n      { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } }\n}\n\n// Render a line into the hidden node display.externalMeasured. Used\n// when measurement is needed for a line that's not in the viewport.\nfunction updateExternalMeasurement(cm, line) {\n  line = visualLine(line)\n  var lineN = lineNo(line)\n  var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN)\n  view.lineN = lineN\n  var built = view.built = buildLineContent(cm, view)\n  view.text = built.pre\n  removeChildrenAndAdd(cm.display.lineMeasure, built.pre)\n  return view\n}\n\n// Get a {top, bottom, left, right} box (in line-local coordinates)\n// for a given character.\nfunction measureChar(cm, line, ch, bias) {\n  return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias)\n}\n\n// Find a line view that corresponds to the given line number.\nfunction findViewForLine(cm, lineN) {\n  if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo)\n    { return cm.display.view[findViewIndex(cm, lineN)] }\n  var ext = cm.display.externalMeasured\n  if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size)\n    { return ext }\n}\n\n// Measurement can be split in two steps, the set-up work that\n// applies to the whole line, and the measurement of the actual\n// character. Functions like coordsChar, that need to do a lot of\n// measurements in a row, can thus ensure that the set-up work is\n// only done once.\nfunction prepareMeasureForLine(cm, line) {\n  var lineN = lineNo(line)\n  var view = findViewForLine(cm, lineN)\n  if (view && !view.text) {\n    view = null\n  } else if (view && view.changes) {\n    updateLineForChanges(cm, view, lineN, getDimensions(cm))\n    cm.curOp.forceUpdate = true\n  }\n  if (!view)\n    { view = updateExternalMeasurement(cm, line) }\n\n  var info = mapFromLineView(view, line, lineN)\n  return {\n    line: line, view: view, rect: null,\n    map: info.map, cache: info.cache, before: info.before,\n    hasHeights: false\n  }\n}\n\n// Given a prepared measurement object, measures the position of an\n// actual character (or fetches it from the cache).\nfunction measureCharPrepared(cm, prepared, ch, bias, varHeight) {\n  if (prepared.before) { ch = -1 }\n  var key = ch + (bias || \"\"), found\n  if (prepared.cache.hasOwnProperty(key)) {\n    found = prepared.cache[key]\n  } else {\n    if (!prepared.rect)\n      { prepared.rect = prepared.view.text.getBoundingClientRect() }\n    if (!prepared.hasHeights) {\n      ensureLineHeights(cm, prepared.view, prepared.rect)\n      prepared.hasHeights = true\n    }\n    found = measureCharInner(cm, prepared, ch, bias)\n    if (!found.bogus) { prepared.cache[key] = found }\n  }\n  return {left: found.left, right: found.right,\n          top: varHeight ? found.rtop : found.top,\n          bottom: varHeight ? found.rbottom : found.bottom}\n}\n\nvar nullRect = {left: 0, right: 0, top: 0, bottom: 0}\n\nfunction nodeAndOffsetInLineMap(map$$1, ch, bias) {\n  var node, start, end, collapse, mStart, mEnd\n  // First, search the line map for the text node corresponding to,\n  // or closest to, the target character.\n  for (var i = 0; i < map$$1.length; i += 3) {\n    mStart = map$$1[i]\n    mEnd = map$$1[i + 1]\n    if (ch < mStart) {\n      start = 0; end = 1\n      collapse = \"left\"\n    } else if (ch < mEnd) {\n      start = ch - mStart\n      end = start + 1\n    } else if (i == map$$1.length - 3 || ch == mEnd && map$$1[i + 3] > ch) {\n      end = mEnd - mStart\n      start = end - 1\n      if (ch >= mEnd) { collapse = \"right\" }\n    }\n    if (start != null) {\n      node = map$$1[i + 2]\n      if (mStart == mEnd && bias == (node.insertLeft ? \"left\" : \"right\"))\n        { collapse = bias }\n      if (bias == \"left\" && start == 0)\n        { while (i && map$$1[i - 2] == map$$1[i - 3] && map$$1[i - 1].insertLeft) {\n          node = map$$1[(i -= 3) + 2]\n          collapse = \"left\"\n        } }\n      if (bias == \"right\" && start == mEnd - mStart)\n        { while (i < map$$1.length - 3 && map$$1[i + 3] == map$$1[i + 4] && !map$$1[i + 5].insertLeft) {\n          node = map$$1[(i += 3) + 2]\n          collapse = \"right\"\n        } }\n      break\n    }\n  }\n  return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}\n}\n\nfunction getUsefulRect(rects, bias) {\n  var rect = nullRect\n  if (bias == \"left\") { for (var i = 0; i < rects.length; i++) {\n    if ((rect = rects[i]).left != rect.right) { break }\n  } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) {\n    if ((rect = rects[i$1]).left != rect.right) { break }\n  } }\n  return rect\n}\n\nfunction measureCharInner(cm, prepared, ch, bias) {\n  var place = nodeAndOffsetInLineMap(prepared.map, ch, bias)\n  var node = place.node, start = place.start, end = place.end, collapse = place.collapse\n\n  var rect\n  if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.\n    for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned\n      while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start }\n      while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end }\n      if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart)\n        { rect = node.parentNode.getBoundingClientRect() }\n      else\n        { rect = getUsefulRect(range(node, start, end).getClientRects(), bias) }\n      if (rect.left || rect.right || start == 0) { break }\n      end = start\n      start = start - 1\n      collapse = \"right\"\n    }\n    if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect) }\n  } else { // If it is a widget, simply get the box for the whole widget.\n    if (start > 0) { collapse = bias = \"right\" }\n    var rects\n    if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1)\n      { rect = rects[bias == \"right\" ? rects.length - 1 : 0] }\n    else\n      { rect = node.getBoundingClientRect() }\n  }\n  if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) {\n    var rSpan = node.parentNode.getClientRects()[0]\n    if (rSpan)\n      { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom} }\n    else\n      { rect = nullRect }\n  }\n\n  var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top\n  var mid = (rtop + rbot) / 2\n  var heights = prepared.view.measure.heights\n  var i = 0\n  for (; i < heights.length - 1; i++)\n    { if (mid < heights[i]) { break } }\n  var top = i ? heights[i - 1] : 0, bot = heights[i]\n  var result = {left: (collapse == \"right\" ? rect.right : rect.left) - prepared.rect.left,\n                right: (collapse == \"left\" ? rect.left : rect.right) - prepared.rect.left,\n                top: top, bottom: bot}\n  if (!rect.left && !rect.right) { result.bogus = true }\n  if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot }\n\n  return result\n}\n\n// Work around problem with bounding client rects on ranges being\n// returned incorrectly when zoomed on IE10 and below.\nfunction maybeUpdateRectForZooming(measure, rect) {\n  if (!window.screen || screen.logicalXDPI == null ||\n      screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))\n    { return rect }\n  var scaleX = screen.logicalXDPI / screen.deviceXDPI\n  var scaleY = screen.logicalYDPI / screen.deviceYDPI\n  return {left: rect.left * scaleX, right: rect.right * scaleX,\n          top: rect.top * scaleY, bottom: rect.bottom * scaleY}\n}\n\nfunction clearLineMeasurementCacheFor(lineView) {\n  if (lineView.measure) {\n    lineView.measure.cache = {}\n    lineView.measure.heights = null\n    if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++)\n      { lineView.measure.caches[i] = {} } }\n  }\n}\n\nfunction clearLineMeasurementCache(cm) {\n  cm.display.externalMeasure = null\n  removeChildren(cm.display.lineMeasure)\n  for (var i = 0; i < cm.display.view.length; i++)\n    { clearLineMeasurementCacheFor(cm.display.view[i]) }\n}\n\nfunction clearCaches(cm) {\n  clearLineMeasurementCache(cm)\n  cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null\n  if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true }\n  cm.display.lineNumChars = null\n}\n\nfunction pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft }\nfunction pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop }\n\n// Converts a {top, bottom, left, right} box from line-local\n// coordinates into another coordinate system. Context may be one of\n// \"line\", \"div\" (display.lineDiv), \"local\"./null (editor), \"window\",\n// or \"page\".\nfunction intoCoordSystem(cm, lineObj, rect, context) {\n  if (lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) {\n    var size = widgetHeight(lineObj.widgets[i])\n    rect.top += size; rect.bottom += size\n  } } }\n  if (context == \"line\") { return rect }\n  if (!context) { context = \"local\" }\n  var yOff = heightAtLine(lineObj)\n  if (context == \"local\") { yOff += paddingTop(cm.display) }\n  else { yOff -= cm.display.viewOffset }\n  if (context == \"page\" || context == \"window\") {\n    var lOff = cm.display.lineSpace.getBoundingClientRect()\n    yOff += lOff.top + (context == \"window\" ? 0 : pageScrollY())\n    var xOff = lOff.left + (context == \"window\" ? 0 : pageScrollX())\n    rect.left += xOff; rect.right += xOff\n  }\n  rect.top += yOff; rect.bottom += yOff\n  return rect\n}\n\n// Coverts a box from \"div\" coords to another coordinate system.\n// Context may be \"window\", \"page\", \"div\", or \"local\"./null.\nfunction fromCoordSystem(cm, coords, context) {\n  if (context == \"div\") { return coords }\n  var left = coords.left, top = coords.top\n  // First move into \"page\" coordinate system\n  if (context == \"page\") {\n    left -= pageScrollX()\n    top -= pageScrollY()\n  } else if (context == \"local\" || !context) {\n    var localBox = cm.display.sizer.getBoundingClientRect()\n    left += localBox.left\n    top += localBox.top\n  }\n\n  var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect()\n  return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}\n}\n\nfunction charCoords(cm, pos, context, lineObj, bias) {\n  if (!lineObj) { lineObj = getLine(cm.doc, pos.line) }\n  return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context)\n}\n\n// Returns a box for a given cursor position, which may have an\n// 'other' property containing the position of the secondary cursor\n// on a bidi boundary.\nfunction cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) {\n  lineObj = lineObj || getLine(cm.doc, pos.line)\n  if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj) }\n  function get(ch, right) {\n    var m = measureCharPrepared(cm, preparedMeasure, ch, right ? \"right\" : \"left\", varHeight)\n    if (right) { m.left = m.right; } else { m.right = m.left }\n    return intoCoordSystem(cm, lineObj, m, context)\n  }\n  function getBidi(ch, partPos) {\n    var part = order[partPos], right = part.level % 2\n    if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {\n      part = order[--partPos]\n      ch = bidiRight(part) - (part.level % 2 ? 0 : 1)\n      right = true\n    } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {\n      part = order[++partPos]\n      ch = bidiLeft(part) - part.level % 2\n      right = false\n    }\n    if (right && ch == part.to && ch > part.from) { return get(ch - 1) }\n    return get(ch, right)\n  }\n  var order = getOrder(lineObj), ch = pos.ch\n  if (!order) { return get(ch) }\n  var partPos = getBidiPartAt(order, ch)\n  var val = getBidi(ch, partPos)\n  if (bidiOther != null) { val.other = getBidi(ch, bidiOther) }\n  return val\n}\n\n// Used to cheaply estimate the coordinates for a position. Used for\n// intermediate scroll updates.\nfunction estimateCoords(cm, pos) {\n  var left = 0\n  pos = clipPos(cm.doc, pos)\n  if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch }\n  var lineObj = getLine(cm.doc, pos.line)\n  var top = heightAtLine(lineObj) + paddingTop(cm.display)\n  return {left: left, right: left, top: top, bottom: top + lineObj.height}\n}\n\n// Positions returned by coordsChar contain some extra information.\n// xRel is the relative x position of the input coordinates compared\n// to the found position (so xRel > 0 means the coordinates are to\n// the right of the character position, for example). When outside\n// is true, that means the coordinates lie outside the line's\n// vertical range.\nfunction PosWithInfo(line, ch, outside, xRel) {\n  var pos = Pos(line, ch)\n  pos.xRel = xRel\n  if (outside) { pos.outside = true }\n  return pos\n}\n\n// Compute the character position closest to the given coordinates.\n// Input must be lineSpace-local (\"div\" coordinate system).\nfunction coordsChar(cm, x, y) {\n  var doc = cm.doc\n  y += cm.display.viewOffset\n  if (y < 0) { return PosWithInfo(doc.first, 0, true, -1) }\n  var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1\n  if (lineN > last)\n    { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1) }\n  if (x < 0) { x = 0 }\n\n  var lineObj = getLine(doc, lineN)\n  for (;;) {\n    var found = coordsCharInner(cm, lineObj, lineN, x, y)\n    var merged = collapsedSpanAtEnd(lineObj)\n    var mergedPos = merged && merged.find(0, true)\n    if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))\n      { lineN = lineNo(lineObj = mergedPos.to.line) }\n    else\n      { return found }\n  }\n}\n\nfunction coordsCharInner(cm, lineObj, lineNo$$1, x, y) {\n  var innerOff = y - heightAtLine(lineObj)\n  var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth\n  var preparedMeasure = prepareMeasureForLine(cm, lineObj)\n\n  function getX(ch) {\n    var sp = cursorCoords(cm, Pos(lineNo$$1, ch), \"line\", lineObj, preparedMeasure)\n    wrongLine = true\n    if (innerOff > sp.bottom) { return sp.left - adjust }\n    else if (innerOff < sp.top) { return sp.left + adjust }\n    else { wrongLine = false }\n    return sp.left\n  }\n\n  var bidi = getOrder(lineObj), dist = lineObj.text.length\n  var from = lineLeft(lineObj), to = lineRight(lineObj)\n  var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine\n\n  if (x > toX) { return PosWithInfo(lineNo$$1, to, toOutside, 1) }\n  // Do a binary search between these bounds.\n  for (;;) {\n    if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {\n      var ch = x < fromX || x - fromX <= toX - x ? from : to\n      var outside = ch == from ? fromOutside : toOutside\n      var xDiff = x - (ch == from ? fromX : toX)\n      // This is a kludge to handle the case where the coordinates\n      // are after a line-wrapped line. We should replace it with a\n      // more general handling of cursor positions around line\n      // breaks. (Issue #4078)\n      if (toOutside && !bidi && !/\\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 &&\n          ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) {\n        var charSize = measureCharPrepared(cm, preparedMeasure, ch, \"right\")\n        if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) {\n          outside = false\n          ch++\n          xDiff = x - charSize.right\n        }\n      }\n      while (isExtendingChar(lineObj.text.charAt(ch))) { ++ch }\n      var pos = PosWithInfo(lineNo$$1, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0)\n      return pos\n    }\n    var step = Math.ceil(dist / 2), middle = from + step\n    if (bidi) {\n      middle = from\n      for (var i = 0; i < step; ++i) { middle = moveVisually(lineObj, middle, 1) }\n    }\n    var middleX = getX(middle)\n    if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) { toX += 1000; } dist = step}\n    else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step}\n  }\n}\n\nvar measureText\n// Compute the default text height.\nfunction textHeight(display) {\n  if (display.cachedTextHeight != null) { return display.cachedTextHeight }\n  if (measureText == null) {\n    measureText = elt(\"pre\")\n    // Measure a bunch of lines, for browsers that compute\n    // fractional heights.\n    for (var i = 0; i < 49; ++i) {\n      measureText.appendChild(document.createTextNode(\"x\"))\n      measureText.appendChild(elt(\"br\"))\n    }\n    measureText.appendChild(document.createTextNode(\"x\"))\n  }\n  removeChildrenAndAdd(display.measure, measureText)\n  var height = measureText.offsetHeight / 50\n  if (height > 3) { display.cachedTextHeight = height }\n  removeChildren(display.measure)\n  return height || 1\n}\n\n// Compute the default character width.\nfunction charWidth(display) {\n  if (display.cachedCharWidth != null) { return display.cachedCharWidth }\n  var anchor = elt(\"span\", \"xxxxxxxxxx\")\n  var pre = elt(\"pre\", [anchor])\n  removeChildrenAndAdd(display.measure, pre)\n  var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10\n  if (width > 2) { display.cachedCharWidth = width }\n  return width || 10\n}\n\n// Do a bulk-read of the DOM positions and sizes needed to draw the\n// view, so that we don't interleave reading and writing to the DOM.\nfunction getDimensions(cm) {\n  var d = cm.display, left = {}, width = {}\n  var gutterLeft = d.gutters.clientLeft\n  for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {\n    left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft\n    width[cm.options.gutters[i]] = n.clientWidth\n  }\n  return {fixedPos: compensateForHScroll(d),\n          gutterTotalWidth: d.gutters.offsetWidth,\n          gutterLeft: left,\n          gutterWidth: width,\n          wrapperWidth: d.wrapper.clientWidth}\n}\n\n// Computes display.scroller.scrollLeft + display.gutters.offsetWidth,\n// but using getBoundingClientRect to get a sub-pixel-accurate\n// result.\nfunction compensateForHScroll(display) {\n  return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left\n}\n\n// Returns a function that estimates the height of a line, to use as\n// first approximation until the line becomes visible (and is thus\n// properly measurable).\nfunction estimateHeight(cm) {\n  var th = textHeight(cm.display), wrapping = cm.options.lineWrapping\n  var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3)\n  return function (line) {\n    if (lineIsHidden(cm.doc, line)) { return 0 }\n\n    var widgetsHeight = 0\n    if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) {\n      if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height }\n    } }\n\n    if (wrapping)\n      { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th }\n    else\n      { return widgetsHeight + th }\n  }\n}\n\nfunction estimateLineHeights(cm) {\n  var doc = cm.doc, est = estimateHeight(cm)\n  doc.iter(function (line) {\n    var estHeight = est(line)\n    if (estHeight != line.height) { updateLineHeight(line, estHeight) }\n  })\n}\n\n// Given a mouse event, find the corresponding position. If liberal\n// is false, it checks whether a gutter or scrollbar was clicked,\n// and returns null if it was. forRect is used by rectangular\n// selections, and tries to estimate a character position even for\n// coordinates beyond the right of the text.\nfunction posFromMouse(cm, e, liberal, forRect) {\n  var display = cm.display\n  if (!liberal && e_target(e).getAttribute(\"cm-not-content\") == \"true\") { return null }\n\n  var x, y, space = display.lineSpace.getBoundingClientRect()\n  // Fails unpredictably on IE[67] when mouse is dragged around quickly.\n  try { x = e.clientX - space.left; y = e.clientY - space.top }\n  catch (e) { return null }\n  var coords = coordsChar(cm, x, y), line\n  if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) {\n    var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length\n    coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff))\n  }\n  return coords\n}\n\n// Find the view element corresponding to a given line. Return null\n// when the line isn't visible.\nfunction findViewIndex(cm, n) {\n  if (n >= cm.display.viewTo) { return null }\n  n -= cm.display.viewFrom\n  if (n < 0) { return null }\n  var view = cm.display.view\n  for (var i = 0; i < view.length; i++) {\n    n -= view[i].size\n    if (n < 0) { return i }\n  }\n}\n\nfunction updateSelection(cm) {\n  cm.display.input.showSelection(cm.display.input.prepareSelection())\n}\n\nfunction prepareSelection(cm, primary) {\n  var doc = cm.doc, result = {}\n  var curFragment = result.cursors = document.createDocumentFragment()\n  var selFragment = result.selection = document.createDocumentFragment()\n\n  for (var i = 0; i < doc.sel.ranges.length; i++) {\n    if (primary === false && i == doc.sel.primIndex) { continue }\n    var range$$1 = doc.sel.ranges[i]\n    if (range$$1.from().line >= cm.display.viewTo || range$$1.to().line < cm.display.viewFrom) { continue }\n    var collapsed = range$$1.empty()\n    if (collapsed || cm.options.showCursorWhenSelecting)\n      { drawSelectionCursor(cm, range$$1.head, curFragment) }\n    if (!collapsed)\n      { drawSelectionRange(cm, range$$1, selFragment) }\n  }\n  return result\n}\n\n// Draws a cursor for the given range\nfunction drawSelectionCursor(cm, head, output) {\n  var pos = cursorCoords(cm, head, \"div\", null, null, !cm.options.singleCursorHeightPerLine)\n\n  var cursor = output.appendChild(elt(\"div\", \"\\u00a0\", \"CodeMirror-cursor\"))\n  cursor.style.left = pos.left + \"px\"\n  cursor.style.top = pos.top + \"px\"\n  cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + \"px\"\n\n  if (pos.other) {\n    // Secondary cursor, shown when on a 'jump' in bi-directional text\n    var otherCursor = output.appendChild(elt(\"div\", \"\\u00a0\", \"CodeMirror-cursor CodeMirror-secondarycursor\"))\n    otherCursor.style.display = \"\"\n    otherCursor.style.left = pos.other.left + \"px\"\n    otherCursor.style.top = pos.other.top + \"px\"\n    otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + \"px\"\n  }\n}\n\n// Draws the given range as a highlighted selection\nfunction drawSelectionRange(cm, range$$1, output) {\n  var display = cm.display, doc = cm.doc\n  var fragment = document.createDocumentFragment()\n  var padding = paddingH(cm.display), leftSide = padding.left\n  var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right\n\n  function add(left, top, width, bottom) {\n    if (top < 0) { top = 0 }\n    top = Math.round(top)\n    bottom = Math.round(bottom)\n    fragment.appendChild(elt(\"div\", null, \"CodeMirror-selected\", (\"position: absolute; left: \" + left + \"px;\\n                             top: \" + top + \"px; width: \" + (width == null ? rightSide - left : width) + \"px;\\n                             height: \" + (bottom - top) + \"px\")))\n  }\n\n  function drawForLine(line, fromArg, toArg) {\n    var lineObj = getLine(doc, line)\n    var lineLen = lineObj.text.length\n    var start, end\n    function coords(ch, bias) {\n      return charCoords(cm, Pos(line, ch), \"div\", lineObj, bias)\n    }\n\n    iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir) {\n      var leftPos = coords(from, \"left\"), rightPos, left, right\n      if (from == to) {\n        rightPos = leftPos\n        left = right = leftPos.left\n      } else {\n        rightPos = coords(to - 1, \"right\")\n        if (dir == \"rtl\") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp }\n        left = leftPos.left\n        right = rightPos.right\n      }\n      if (fromArg == null && from == 0) { left = leftSide }\n      if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part\n        add(left, leftPos.top, null, leftPos.bottom)\n        left = leftSide\n        if (leftPos.bottom < rightPos.top) { add(left, leftPos.bottom, null, rightPos.top) }\n      }\n      if (toArg == null && to == lineLen) { right = rightSide }\n      if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left)\n        { start = leftPos }\n      if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right)\n        { end = rightPos }\n      if (left < leftSide + 1) { left = leftSide }\n      add(left, rightPos.top, right - left, rightPos.bottom)\n    })\n    return {start: start, end: end}\n  }\n\n  var sFrom = range$$1.from(), sTo = range$$1.to()\n  if (sFrom.line == sTo.line) {\n    drawForLine(sFrom.line, sFrom.ch, sTo.ch)\n  } else {\n    var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line)\n    var singleVLine = visualLine(fromLine) == visualLine(toLine)\n    var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end\n    var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start\n    if (singleVLine) {\n      if (leftEnd.top < rightStart.top - 2) {\n        add(leftEnd.right, leftEnd.top, null, leftEnd.bottom)\n        add(leftSide, rightStart.top, rightStart.left, rightStart.bottom)\n      } else {\n        add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom)\n      }\n    }\n    if (leftEnd.bottom < rightStart.top)\n      { add(leftSide, leftEnd.bottom, null, rightStart.top) }\n  }\n\n  output.appendChild(fragment)\n}\n\n// Cursor-blinking\nfunction restartBlink(cm) {\n  if (!cm.state.focused) { return }\n  var display = cm.display\n  clearInterval(display.blinker)\n  var on = true\n  display.cursorDiv.style.visibility = \"\"\n  if (cm.options.cursorBlinkRate > 0)\n    { display.blinker = setInterval(function () { return display.cursorDiv.style.visibility = (on = !on) ? \"\" : \"hidden\"; },\n      cm.options.cursorBlinkRate) }\n  else if (cm.options.cursorBlinkRate < 0)\n    { display.cursorDiv.style.visibility = \"hidden\" }\n}\n\nfunction ensureFocus(cm) {\n  if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm) }\n}\n\nfunction delayBlurEvent(cm) {\n  cm.state.delayingBlurEvent = true\n  setTimeout(function () { if (cm.state.delayingBlurEvent) {\n    cm.state.delayingBlurEvent = false\n    onBlur(cm)\n  } }, 100)\n}\n\nfunction onFocus(cm, e) {\n  if (cm.state.delayingBlurEvent) { cm.state.delayingBlurEvent = false }\n\n  if (cm.options.readOnly == \"nocursor\") { return }\n  if (!cm.state.focused) {\n    signal(cm, \"focus\", cm, e)\n    cm.state.focused = true\n    addClass(cm.display.wrapper, \"CodeMirror-focused\")\n    // This test prevents this from firing when a context\n    // menu is closed (since the input reset would kill the\n    // select-all detection hack)\n    if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) {\n      cm.display.input.reset()\n      if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20) } // Issue #1730\n    }\n    cm.display.input.receivedFocus()\n  }\n  restartBlink(cm)\n}\nfunction onBlur(cm, e) {\n  if (cm.state.delayingBlurEvent) { return }\n\n  if (cm.state.focused) {\n    signal(cm, \"blur\", cm, e)\n    cm.state.focused = false\n    rmClass(cm.display.wrapper, \"CodeMirror-focused\")\n  }\n  clearInterval(cm.display.blinker)\n  setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false } }, 150)\n}\n\n// Re-align line numbers and gutter marks to compensate for\n// horizontal scrolling.\nfunction alignHorizontally(cm) {\n  var display = cm.display, view = display.view\n  if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return }\n  var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft\n  var gutterW = display.gutters.offsetWidth, left = comp + \"px\"\n  for (var i = 0; i < view.length; i++) { if (!view[i].hidden) {\n    if (cm.options.fixedGutter) {\n      if (view[i].gutter)\n        { view[i].gutter.style.left = left }\n      if (view[i].gutterBackground)\n        { view[i].gutterBackground.style.left = left }\n    }\n    var align = view[i].alignable\n    if (align) { for (var j = 0; j < align.length; j++)\n      { align[j].style.left = left } }\n  } }\n  if (cm.options.fixedGutter)\n    { display.gutters.style.left = (comp + gutterW) + \"px\" }\n}\n\n// Used to ensure that the line number gutter is still the right\n// size for the current document size. Returns true when an update\n// is needed.\nfunction maybeUpdateLineNumberWidth(cm) {\n  if (!cm.options.lineNumbers) { return false }\n  var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display\n  if (last.length != display.lineNumChars) {\n    var test = display.measure.appendChild(elt(\"div\", [elt(\"div\", last)],\n                                               \"CodeMirror-linenumber CodeMirror-gutter-elt\"))\n    var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW\n    display.lineGutter.style.width = \"\"\n    display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1\n    display.lineNumWidth = display.lineNumInnerWidth + padding\n    display.lineNumChars = display.lineNumInnerWidth ? last.length : -1\n    display.lineGutter.style.width = display.lineNumWidth + \"px\"\n    updateGutterSpace(cm)\n    return true\n  }\n  return false\n}\n\n// Read the actual heights of the rendered lines, and update their\n// stored heights to match.\nfunction updateHeightsInViewport(cm) {\n  var display = cm.display\n  var prevBottom = display.lineDiv.offsetTop\n  for (var i = 0; i < display.view.length; i++) {\n    var cur = display.view[i], height = void 0\n    if (cur.hidden) { continue }\n    if (ie && ie_version < 8) {\n      var bot = cur.node.offsetTop + cur.node.offsetHeight\n      height = bot - prevBottom\n      prevBottom = bot\n    } else {\n      var box = cur.node.getBoundingClientRect()\n      height = box.bottom - box.top\n    }\n    var diff = cur.line.height - height\n    if (height < 2) { height = textHeight(display) }\n    if (diff > .001 || diff < -.001) {\n      updateLineHeight(cur.line, height)\n      updateWidgetHeight(cur.line)\n      if (cur.rest) { for (var j = 0; j < cur.rest.length; j++)\n        { updateWidgetHeight(cur.rest[j]) } }\n    }\n  }\n}\n\n// Read and store the height of line widgets associated with the\n// given line.\nfunction updateWidgetHeight(line) {\n  if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i)\n    { line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight } }\n}\n\n// Compute the lines that are visible in a given viewport (defaults\n// the the current scroll position). viewport may contain top,\n// height, and ensure (see op.scrollToPos) properties.\nfunction visibleLines(display, doc, viewport) {\n  var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop\n  top = Math.floor(top - paddingTop(display))\n  var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight\n\n  var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom)\n  // Ensure is a {from: {line, ch}, to: {line, ch}} object, and\n  // forces those lines into the viewport (if possible).\n  if (viewport && viewport.ensure) {\n    var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line\n    if (ensureFrom < from) {\n      from = ensureFrom\n      to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight)\n    } else if (Math.min(ensureTo, doc.lastLine()) >= to) {\n      from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight)\n      to = ensureTo\n    }\n  }\n  return {from: from, to: Math.max(to, from + 1)}\n}\n\n// Sync the scrollable area and scrollbars, ensure the viewport\n// covers the visible area.\nfunction setScrollTop(cm, val) {\n  if (Math.abs(cm.doc.scrollTop - val) < 2) { return }\n  cm.doc.scrollTop = val\n  if (!gecko) { updateDisplaySimple(cm, {top: val}) }\n  if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val }\n  cm.display.scrollbars.setScrollTop(val)\n  if (gecko) { updateDisplaySimple(cm) }\n  startWorker(cm, 100)\n}\n// Sync scroller and scrollbar, ensure the gutter elements are\n// aligned.\nfunction setScrollLeft(cm, val, isScroller) {\n  if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) { return }\n  val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth)\n  cm.doc.scrollLeft = val\n  alignHorizontally(cm)\n  if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val }\n  cm.display.scrollbars.setScrollLeft(val)\n}\n\n// Since the delta values reported on mouse wheel events are\n// unstandardized between browsers and even browser versions, and\n// generally horribly unpredictable, this code starts by measuring\n// the scroll effect that the first few mouse wheel events have,\n// and, from that, detects the way it can convert deltas to pixel\n// offsets afterwards.\n//\n// The reason we want to know the amount a wheel event will scroll\n// is that it gives us a chance to update the display before the\n// actual scrolling happens, reducing flickering.\n\nvar wheelSamples = 0;\nvar wheelPixelsPerUnit = null\n// Fill in a browser-detected starting value on browsers where we\n// know one. These don't have to be accurate -- the result of them\n// being wrong would just be a slight flicker on the first wheel\n// scroll (if it is large enough).\nif (ie) { wheelPixelsPerUnit = -.53 }\nelse if (gecko) { wheelPixelsPerUnit = 15 }\nelse if (chrome) { wheelPixelsPerUnit = -.7 }\nelse if (safari) { wheelPixelsPerUnit = -1/3 }\n\nfunction wheelEventDelta(e) {\n  var dx = e.wheelDeltaX, dy = e.wheelDeltaY\n  if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail }\n  if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail }\n  else if (dy == null) { dy = e.wheelDelta }\n  return {x: dx, y: dy}\n}\nfunction wheelEventPixels(e) {\n  var delta = wheelEventDelta(e)\n  delta.x *= wheelPixelsPerUnit\n  delta.y *= wheelPixelsPerUnit\n  return delta\n}\n\nfunction onScrollWheel(cm, e) {\n  var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y\n\n  var display = cm.display, scroll = display.scroller\n  // Quit if there's nothing to scroll here\n  var canScrollX = scroll.scrollWidth > scroll.clientWidth\n  var canScrollY = scroll.scrollHeight > scroll.clientHeight\n  if (!(dx && canScrollX || dy && canScrollY)) { return }\n\n  // Webkit browsers on OS X abort momentum scrolls when the target\n  // of the scroll event is removed from the scrollable element.\n  // This hack (see related code in patchDisplay) makes sure the\n  // element is kept around.\n  if (dy && mac && webkit) {\n    outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) {\n      for (var i = 0; i < view.length; i++) {\n        if (view[i].node == cur) {\n          cm.display.currentWheelTarget = cur\n          break outer\n        }\n      }\n    }\n  }\n\n  // On some browsers, horizontal scrolling will cause redraws to\n  // happen before the gutter has been realigned, causing it to\n  // wriggle around in a most unseemly way. When we have an\n  // estimated pixels/delta value, we just handle horizontal\n  // scrolling entirely here. It'll be slightly off from native, but\n  // better than glitching out.\n  if (dx && !gecko && !presto && wheelPixelsPerUnit != null) {\n    if (dy && canScrollY)\n      { setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))) }\n    setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)))\n    // Only prevent default scrolling if vertical scrolling is\n    // actually possible. Otherwise, it causes vertical scroll\n    // jitter on OSX trackpads when deltaX is small and deltaY\n    // is large (issue #3579)\n    if (!dy || (dy && canScrollY))\n      { e_preventDefault(e) }\n    display.wheelStartX = null // Abort measurement, if in progress\n    return\n  }\n\n  // 'Project' the visible viewport to cover the area that is being\n  // scrolled into view (if we know enough to estimate it).\n  if (dy && wheelPixelsPerUnit != null) {\n    var pixels = dy * wheelPixelsPerUnit\n    var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight\n    if (pixels < 0) { top = Math.max(0, top + pixels - 50) }\n    else { bot = Math.min(cm.doc.height, bot + pixels + 50) }\n    updateDisplaySimple(cm, {top: top, bottom: bot})\n  }\n\n  if (wheelSamples < 20) {\n    if (display.wheelStartX == null) {\n      display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop\n      display.wheelDX = dx; display.wheelDY = dy\n      setTimeout(function () {\n        if (display.wheelStartX == null) { return }\n        var movedX = scroll.scrollLeft - display.wheelStartX\n        var movedY = scroll.scrollTop - display.wheelStartY\n        var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||\n          (movedX && display.wheelDX && movedX / display.wheelDX)\n        display.wheelStartX = display.wheelStartY = null\n        if (!sample) { return }\n        wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1)\n        ++wheelSamples\n      }, 200)\n    } else {\n      display.wheelDX += dx; display.wheelDY += dy\n    }\n  }\n}\n\n// SCROLLBARS\n\n// Prepare DOM reads needed to update the scrollbars. Done in one\n// shot to minimize update/measure roundtrips.\nfunction measureForScrollbars(cm) {\n  var d = cm.display, gutterW = d.gutters.offsetWidth\n  var docH = Math.round(cm.doc.height + paddingVert(cm.display))\n  return {\n    clientHeight: d.scroller.clientHeight,\n    viewHeight: d.wrapper.clientHeight,\n    scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth,\n    viewWidth: d.wrapper.clientWidth,\n    barLeft: cm.options.fixedGutter ? gutterW : 0,\n    docHeight: docH,\n    scrollHeight: docH + scrollGap(cm) + d.barHeight,\n    nativeBarWidth: d.nativeBarWidth,\n    gutterWidth: gutterW\n  }\n}\n\nfunction NativeScrollbars(place, scroll, cm) {\n  this.cm = cm\n  var vert = this.vert = elt(\"div\", [elt(\"div\", null, null, \"min-width: 1px\")], \"CodeMirror-vscrollbar\")\n  var horiz = this.horiz = elt(\"div\", [elt(\"div\", null, null, \"height: 100%; min-height: 1px\")], \"CodeMirror-hscrollbar\")\n  place(vert); place(horiz)\n\n  on(vert, \"scroll\", function () {\n    if (vert.clientHeight) { scroll(vert.scrollTop, \"vertical\") }\n  })\n  on(horiz, \"scroll\", function () {\n    if (horiz.clientWidth) { scroll(horiz.scrollLeft, \"horizontal\") }\n  })\n\n  this.checkedZeroWidth = false\n  // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).\n  if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = \"18px\" }\n}\n\nNativeScrollbars.prototype = copyObj({\n  update: function(measure) {\n    var needsH = measure.scrollWidth > measure.clientWidth + 1\n    var needsV = measure.scrollHeight > measure.clientHeight + 1\n    var sWidth = measure.nativeBarWidth\n\n    if (needsV) {\n      this.vert.style.display = \"block\"\n      this.vert.style.bottom = needsH ? sWidth + \"px\" : \"0\"\n      var totalHeight = measure.viewHeight - (needsH ? sWidth : 0)\n      // A bug in IE8 can cause this value to be negative, so guard it.\n      this.vert.firstChild.style.height =\n        Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + \"px\"\n    } else {\n      this.vert.style.display = \"\"\n      this.vert.firstChild.style.height = \"0\"\n    }\n\n    if (needsH) {\n      this.horiz.style.display = \"block\"\n      this.horiz.style.right = needsV ? sWidth + \"px\" : \"0\"\n      this.horiz.style.left = measure.barLeft + \"px\"\n      var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0)\n      this.horiz.firstChild.style.width =\n        (measure.scrollWidth - measure.clientWidth + totalWidth) + \"px\"\n    } else {\n      this.horiz.style.display = \"\"\n      this.horiz.firstChild.style.width = \"0\"\n    }\n\n    if (!this.checkedZeroWidth && measure.clientHeight > 0) {\n      if (sWidth == 0) { this.zeroWidthHack() }\n      this.checkedZeroWidth = true\n    }\n\n    return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}\n  },\n  setScrollLeft: function(pos) {\n    if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos }\n    if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz) }\n  },\n  setScrollTop: function(pos) {\n    if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos }\n    if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert) }\n  },\n  zeroWidthHack: function() {\n    var w = mac && !mac_geMountainLion ? \"12px\" : \"18px\"\n    this.horiz.style.height = this.vert.style.width = w\n    this.horiz.style.pointerEvents = this.vert.style.pointerEvents = \"none\"\n    this.disableHoriz = new Delayed\n    this.disableVert = new Delayed\n  },\n  enableZeroWidthBar: function(bar, delay) {\n    bar.style.pointerEvents = \"auto\"\n    function maybeDisable() {\n      // To find out whether the scrollbar is still visible, we\n      // check whether the element under the pixel in the bottom\n      // left corner of the scrollbar box is the scrollbar box\n      // itself (when the bar is still visible) or its filler child\n      // (when the bar is hidden). If it is still visible, we keep\n      // it enabled, if it's hidden, we disable pointer events.\n      var box = bar.getBoundingClientRect()\n      var elt$$1 = document.elementFromPoint(box.left + 1, box.bottom - 1)\n      if (elt$$1 != bar) { bar.style.pointerEvents = \"none\" }\n      else { delay.set(1000, maybeDisable) }\n    }\n    delay.set(1000, maybeDisable)\n  },\n  clear: function() {\n    var parent = this.horiz.parentNode\n    parent.removeChild(this.horiz)\n    parent.removeChild(this.vert)\n  }\n}, NativeScrollbars.prototype)\n\nfunction NullScrollbars() {}\n\nNullScrollbars.prototype = copyObj({\n  update: function() { return {bottom: 0, right: 0} },\n  setScrollLeft: function() {},\n  setScrollTop: function() {},\n  clear: function() {}\n}, NullScrollbars.prototype)\n\nfunction updateScrollbars(cm, measure) {\n  if (!measure) { measure = measureForScrollbars(cm) }\n  var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight\n  updateScrollbarsInner(cm, measure)\n  for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) {\n    if (startWidth != cm.display.barWidth && cm.options.lineWrapping)\n      { updateHeightsInViewport(cm) }\n    updateScrollbarsInner(cm, measureForScrollbars(cm))\n    startWidth = cm.display.barWidth; startHeight = cm.display.barHeight\n  }\n}\n\n// Re-synchronize the fake scrollbars with the actual size of the\n// content.\nfunction updateScrollbarsInner(cm, measure) {\n  var d = cm.display\n  var sizes = d.scrollbars.update(measure)\n\n  d.sizer.style.paddingRight = (d.barWidth = sizes.right) + \"px\"\n  d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + \"px\"\n  d.heightForcer.style.borderBottom = sizes.bottom + \"px solid transparent\"\n\n  if (sizes.right && sizes.bottom) {\n    d.scrollbarFiller.style.display = \"block\"\n    d.scrollbarFiller.style.height = sizes.bottom + \"px\"\n    d.scrollbarFiller.style.width = sizes.right + \"px\"\n  } else { d.scrollbarFiller.style.display = \"\" }\n  if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {\n    d.gutterFiller.style.display = \"block\"\n    d.gutterFiller.style.height = sizes.bottom + \"px\"\n    d.gutterFiller.style.width = measure.gutterWidth + \"px\"\n  } else { d.gutterFiller.style.display = \"\" }\n}\n\nvar scrollbarModel = {\"native\": NativeScrollbars, \"null\": NullScrollbars}\n\nfunction initScrollbars(cm) {\n  if (cm.display.scrollbars) {\n    cm.display.scrollbars.clear()\n    if (cm.display.scrollbars.addClass)\n      { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass) }\n  }\n\n  cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) {\n    cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller)\n    // Prevent clicks in the scrollbars from killing focus\n    on(node, \"mousedown\", function () {\n      if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0) }\n    })\n    node.setAttribute(\"cm-not-content\", \"true\")\n  }, function (pos, axis) {\n    if (axis == \"horizontal\") { setScrollLeft(cm, pos) }\n    else { setScrollTop(cm, pos) }\n  }, cm)\n  if (cm.display.scrollbars.addClass)\n    { addClass(cm.display.wrapper, cm.display.scrollbars.addClass) }\n}\n\n// SCROLLING THINGS INTO VIEW\n\n// If an editor sits on the top or bottom of the window, partially\n// scrolled out of view, this ensures that the cursor is visible.\nfunction maybeScrollWindow(cm, coords) {\n  if (signalDOMEvent(cm, \"scrollCursorIntoView\")) { return }\n\n  var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null\n  if (coords.top + box.top < 0) { doScroll = true }\n  else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false }\n  if (doScroll != null && !phantom) {\n    var scrollNode = elt(\"div\", \"\\u200b\", null, (\"position: absolute;\\n                         top: \" + (coords.top - display.viewOffset - paddingTop(cm.display)) + \"px;\\n                         height: \" + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + \"px;\\n                         left: \" + (coords.left) + \"px; width: 2px;\"))\n    cm.display.lineSpace.appendChild(scrollNode)\n    scrollNode.scrollIntoView(doScroll)\n    cm.display.lineSpace.removeChild(scrollNode)\n  }\n}\n\n// Scroll a given position into view (immediately), verifying that\n// it actually became visible (as line heights are accurately\n// measured, the position of something may 'drift' during drawing).\nfunction scrollPosIntoView(cm, pos, end, margin) {\n  if (margin == null) { margin = 0 }\n  var coords\n  for (var limit = 0; limit < 5; limit++) {\n    var changed = false\n    coords = cursorCoords(cm, pos)\n    var endCoords = !end || end == pos ? coords : cursorCoords(cm, end)\n    var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left),\n                                       Math.min(coords.top, endCoords.top) - margin,\n                                       Math.max(coords.left, endCoords.left),\n                                       Math.max(coords.bottom, endCoords.bottom) + margin)\n    var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft\n    if (scrollPos.scrollTop != null) {\n      setScrollTop(cm, scrollPos.scrollTop)\n      if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true }\n    }\n    if (scrollPos.scrollLeft != null) {\n      setScrollLeft(cm, scrollPos.scrollLeft)\n      if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true }\n    }\n    if (!changed) { break }\n  }\n  return coords\n}\n\n// Scroll a given set of coordinates into view (immediately).\nfunction scrollIntoView(cm, x1, y1, x2, y2) {\n  var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2)\n  if (scrollPos.scrollTop != null) { setScrollTop(cm, scrollPos.scrollTop) }\n  if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft) }\n}\n\n// Calculate a new scroll position needed to scroll the given\n// rectangle into view. Returns an object with scrollTop and\n// scrollLeft properties. When these are undefined, the\n// vertical/horizontal position does not need to be adjusted.\nfunction calculateScrollPos(cm, x1, y1, x2, y2) {\n  var display = cm.display, snapMargin = textHeight(cm.display)\n  if (y1 < 0) { y1 = 0 }\n  var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop\n  var screen = displayHeight(cm), result = {}\n  if (y2 - y1 > screen) { y2 = y1 + screen }\n  var docBottom = cm.doc.height + paddingVert(display)\n  var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin\n  if (y1 < screentop) {\n    result.scrollTop = atTop ? 0 : y1\n  } else if (y2 > screentop + screen) {\n    var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen)\n    if (newTop != screentop) { result.scrollTop = newTop }\n  }\n\n  var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft\n  var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0)\n  var tooWide = x2 - x1 > screenw\n  if (tooWide) { x2 = x1 + screenw }\n  if (x1 < 10)\n    { result.scrollLeft = 0 }\n  else if (x1 < screenleft)\n    { result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)) }\n  else if (x2 > screenw + screenleft - 3)\n    { result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw }\n  return result\n}\n\n// Store a relative adjustment to the scroll position in the current\n// operation (to be applied when the operation finishes).\nfunction addToScrollPos(cm, left, top) {\n  if (left != null || top != null) { resolveScrollToPos(cm) }\n  if (left != null)\n    { cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left }\n  if (top != null)\n    { cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top }\n}\n\n// Make sure that at the end of the operation the current cursor is\n// shown.\nfunction ensureCursorVisible(cm) {\n  resolveScrollToPos(cm)\n  var cur = cm.getCursor(), from = cur, to = cur\n  if (!cm.options.lineWrapping) {\n    from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur\n    to = Pos(cur.line, cur.ch + 1)\n  }\n  cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}\n}\n\n// When an operation has its scrollToPos property set, and another\n// scroll action is applied before the end of the operation, this\n// 'simulates' scrolling that position into view in a cheap way, so\n// that the effect of intermediate scroll commands is not ignored.\nfunction resolveScrollToPos(cm) {\n  var range$$1 = cm.curOp.scrollToPos\n  if (range$$1) {\n    cm.curOp.scrollToPos = null\n    var from = estimateCoords(cm, range$$1.from), to = estimateCoords(cm, range$$1.to)\n    var sPos = calculateScrollPos(cm, Math.min(from.left, to.left),\n                                  Math.min(from.top, to.top) - range$$1.margin,\n                                  Math.max(from.right, to.right),\n                                  Math.max(from.bottom, to.bottom) + range$$1.margin)\n    cm.scrollTo(sPos.scrollLeft, sPos.scrollTop)\n  }\n}\n\n// Operations are used to wrap a series of changes to the editor\n// state in such a way that each change won't have to update the\n// cursor and display (which would be awkward, slow, and\n// error-prone). Instead, display updates are batched and then all\n// combined and executed at once.\n\nvar nextOpId = 0\n// Start a new operation.\nfunction startOperation(cm) {\n  cm.curOp = {\n    cm: cm,\n    viewChanged: false,      // Flag that indicates that lines might need to be redrawn\n    startHeight: cm.doc.height, // Used to detect need to update scrollbar\n    forceUpdate: false,      // Used to force a redraw\n    updateInput: null,       // Whether to reset the input textarea\n    typing: false,           // Whether this reset should be careful to leave existing text (for compositing)\n    changeObjs: null,        // Accumulated changes, for firing change events\n    cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on\n    cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already\n    selectionChanged: false, // Whether the selection needs to be redrawn\n    updateMaxLine: false,    // Set when the widest line needs to be determined anew\n    scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet\n    scrollToPos: null,       // Used to scroll to a specific position\n    focus: false,\n    id: ++nextOpId           // Unique ID\n  }\n  pushOperation(cm.curOp)\n}\n\n// Finish an operation, updating the display and signalling delayed events\nfunction endOperation(cm) {\n  var op = cm.curOp\n  finishOperation(op, function (group) {\n    for (var i = 0; i < group.ops.length; i++)\n      { group.ops[i].cm.curOp = null }\n    endOperations(group)\n  })\n}\n\n// The DOM updates done when an operation finishes are batched so\n// that the minimum number of relayouts are required.\nfunction endOperations(group) {\n  var ops = group.ops\n  for (var i = 0; i < ops.length; i++) // Read DOM\n    { endOperation_R1(ops[i]) }\n  for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe)\n    { endOperation_W1(ops[i$1]) }\n  for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM\n    { endOperation_R2(ops[i$2]) }\n  for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe)\n    { endOperation_W2(ops[i$3]) }\n  for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM\n    { endOperation_finish(ops[i$4]) }\n}\n\nfunction endOperation_R1(op) {\n  var cm = op.cm, display = cm.display\n  maybeClipScrollbars(cm)\n  if (op.updateMaxLine) { findMaxLine(cm) }\n\n  op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||\n    op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||\n                       op.scrollToPos.to.line >= display.viewTo) ||\n    display.maxLineChanged && cm.options.lineWrapping\n  op.update = op.mustUpdate &&\n    new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate)\n}\n\nfunction endOperation_W1(op) {\n  op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update)\n}\n\nfunction endOperation_R2(op) {\n  var cm = op.cm, display = cm.display\n  if (op.updatedDisplay) { updateHeightsInViewport(cm) }\n\n  op.barMeasure = measureForScrollbars(cm)\n\n  // If the max line changed since it was last measured, measure it,\n  // and ensure the document's width matches it.\n  // updateDisplay_W2 will use these properties to do the actual resizing\n  if (display.maxLineChanged && !cm.options.lineWrapping) {\n    op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3\n    cm.display.sizerWidth = op.adjustWidthTo\n    op.barMeasure.scrollWidth =\n      Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth)\n    op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm))\n  }\n\n  if (op.updatedDisplay || op.selectionChanged)\n    { op.preparedSelection = display.input.prepareSelection(op.focus) }\n}\n\nfunction endOperation_W2(op) {\n  var cm = op.cm\n\n  if (op.adjustWidthTo != null) {\n    cm.display.sizer.style.minWidth = op.adjustWidthTo + \"px\"\n    if (op.maxScrollLeft < cm.doc.scrollLeft)\n      { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true) }\n    cm.display.maxLineChanged = false\n  }\n\n  var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())\n  if (op.preparedSelection)\n    { cm.display.input.showSelection(op.preparedSelection, takeFocus) }\n  if (op.updatedDisplay || op.startHeight != cm.doc.height)\n    { updateScrollbars(cm, op.barMeasure) }\n  if (op.updatedDisplay)\n    { setDocumentHeight(cm, op.barMeasure) }\n\n  if (op.selectionChanged) { restartBlink(cm) }\n\n  if (cm.state.focused && op.updateInput)\n    { cm.display.input.reset(op.typing) }\n  if (takeFocus) { ensureFocus(op.cm) }\n}\n\nfunction endOperation_finish(op) {\n  var cm = op.cm, display = cm.display, doc = cm.doc\n\n  if (op.updatedDisplay) { postUpdateDisplay(cm, op.update) }\n\n  // Abort mouse wheel delta measurement, when scrolling explicitly\n  if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))\n    { display.wheelStartX = display.wheelStartY = null }\n\n  // Propagate the scroll position to the actual DOM scroller\n  if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) {\n    doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop))\n    display.scrollbars.setScrollTop(doc.scrollTop)\n    display.scroller.scrollTop = doc.scrollTop\n  }\n  if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) {\n    doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft))\n    display.scrollbars.setScrollLeft(doc.scrollLeft)\n    display.scroller.scrollLeft = doc.scrollLeft\n    alignHorizontally(cm)\n  }\n  // If we need to scroll a specific position into view, do so.\n  if (op.scrollToPos) {\n    var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),\n                                   clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin)\n    if (op.scrollToPos.isCursor && cm.state.focused) { maybeScrollWindow(cm, coords) }\n  }\n\n  // Fire events for markers that are hidden/unidden by editing or\n  // undoing\n  var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers\n  if (hidden) { for (var i = 0; i < hidden.length; ++i)\n    { if (!hidden[i].lines.length) { signal(hidden[i], \"hide\") } } }\n  if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1)\n    { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], \"unhide\") } } }\n\n  if (display.wrapper.offsetHeight)\n    { doc.scrollTop = cm.display.scroller.scrollTop }\n\n  // Fire change events, and delayed event handlers\n  if (op.changeObjs)\n    { signal(cm, \"changes\", cm, op.changeObjs) }\n  if (op.update)\n    { op.update.finish() }\n}\n\n// Run the given function in an operation\nfunction runInOp(cm, f) {\n  if (cm.curOp) { return f() }\n  startOperation(cm)\n  try { return f() }\n  finally { endOperation(cm) }\n}\n// Wraps a function in an operation. Returns the wrapped function.\nfunction operation(cm, f) {\n  return function() {\n    if (cm.curOp) { return f.apply(cm, arguments) }\n    startOperation(cm)\n    try { return f.apply(cm, arguments) }\n    finally { endOperation(cm) }\n  }\n}\n// Used to add methods to editor and doc instances, wrapping them in\n// operations.\nfunction methodOp(f) {\n  return function() {\n    if (this.curOp) { return f.apply(this, arguments) }\n    startOperation(this)\n    try { return f.apply(this, arguments) }\n    finally { endOperation(this) }\n  }\n}\nfunction docMethodOp(f) {\n  return function() {\n    var cm = this.cm\n    if (!cm || cm.curOp) { return f.apply(this, arguments) }\n    startOperation(cm)\n    try { return f.apply(this, arguments) }\n    finally { endOperation(cm) }\n  }\n}\n\n// Updates the display.view data structure for a given change to the\n// document. From and to are in pre-change coordinates. Lendiff is\n// the amount of lines added or subtracted by the change. This is\n// used for changes that span multiple lines, or change the way\n// lines are divided into visual lines. regLineChange (below)\n// registers single-line changes.\nfunction regChange(cm, from, to, lendiff) {\n  if (from == null) { from = cm.doc.first }\n  if (to == null) { to = cm.doc.first + cm.doc.size }\n  if (!lendiff) { lendiff = 0 }\n\n  var display = cm.display\n  if (lendiff && to < display.viewTo &&\n      (display.updateLineNumbers == null || display.updateLineNumbers > from))\n    { display.updateLineNumbers = from }\n\n  cm.curOp.viewChanged = true\n\n  if (from >= display.viewTo) { // Change after\n    if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo)\n      { resetView(cm) }\n  } else if (to <= display.viewFrom) { // Change before\n    if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) {\n      resetView(cm)\n    } else {\n      display.viewFrom += lendiff\n      display.viewTo += lendiff\n    }\n  } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap\n    resetView(cm)\n  } else if (from <= display.viewFrom) { // Top overlap\n    var cut = viewCuttingPoint(cm, to, to + lendiff, 1)\n    if (cut) {\n      display.view = display.view.slice(cut.index)\n      display.viewFrom = cut.lineN\n      display.viewTo += lendiff\n    } else {\n      resetView(cm)\n    }\n  } else if (to >= display.viewTo) { // Bottom overlap\n    var cut$1 = viewCuttingPoint(cm, from, from, -1)\n    if (cut$1) {\n      display.view = display.view.slice(0, cut$1.index)\n      display.viewTo = cut$1.lineN\n    } else {\n      resetView(cm)\n    }\n  } else { // Gap in the middle\n    var cutTop = viewCuttingPoint(cm, from, from, -1)\n    var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1)\n    if (cutTop && cutBot) {\n      display.view = display.view.slice(0, cutTop.index)\n        .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN))\n        .concat(display.view.slice(cutBot.index))\n      display.viewTo += lendiff\n    } else {\n      resetView(cm)\n    }\n  }\n\n  var ext = display.externalMeasured\n  if (ext) {\n    if (to < ext.lineN)\n      { ext.lineN += lendiff }\n    else if (from < ext.lineN + ext.size)\n      { display.externalMeasured = null }\n  }\n}\n\n// Register a change to a single line. Type must be one of \"text\",\n// \"gutter\", \"class\", \"widget\"\nfunction regLineChange(cm, line, type) {\n  cm.curOp.viewChanged = true\n  var display = cm.display, ext = cm.display.externalMeasured\n  if (ext && line >= ext.lineN && line < ext.lineN + ext.size)\n    { display.externalMeasured = null }\n\n  if (line < display.viewFrom || line >= display.viewTo) { return }\n  var lineView = display.view[findViewIndex(cm, line)]\n  if (lineView.node == null) { return }\n  var arr = lineView.changes || (lineView.changes = [])\n  if (indexOf(arr, type) == -1) { arr.push(type) }\n}\n\n// Clear the view.\nfunction resetView(cm) {\n  cm.display.viewFrom = cm.display.viewTo = cm.doc.first\n  cm.display.view = []\n  cm.display.viewOffset = 0\n}\n\nfunction viewCuttingPoint(cm, oldN, newN, dir) {\n  var index = findViewIndex(cm, oldN), diff, view = cm.display.view\n  if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size)\n    { return {index: index, lineN: newN} }\n  var n = cm.display.viewFrom\n  for (var i = 0; i < index; i++)\n    { n += view[i].size }\n  if (n != oldN) {\n    if (dir > 0) {\n      if (index == view.length - 1) { return null }\n      diff = (n + view[index].size) - oldN\n      index++\n    } else {\n      diff = n - oldN\n    }\n    oldN += diff; newN += diff\n  }\n  while (visualLineNo(cm.doc, newN) != newN) {\n    if (index == (dir < 0 ? 0 : view.length - 1)) { return null }\n    newN += dir * view[index - (dir < 0 ? 1 : 0)].size\n    index += dir\n  }\n  return {index: index, lineN: newN}\n}\n\n// Force the view to cover a given range, adding empty view element\n// or clipping off existing ones as needed.\nfunction adjustView(cm, from, to) {\n  var display = cm.display, view = display.view\n  if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) {\n    display.view = buildViewArray(cm, from, to)\n    display.viewFrom = from\n  } else {\n    if (display.viewFrom > from)\n      { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view) }\n    else if (display.viewFrom < from)\n      { display.view = display.view.slice(findViewIndex(cm, from)) }\n    display.viewFrom = from\n    if (display.viewTo < to)\n      { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)) }\n    else if (display.viewTo > to)\n      { display.view = display.view.slice(0, findViewIndex(cm, to)) }\n  }\n  display.viewTo = to\n}\n\n// Count the number of lines in the view whose DOM representation is\n// out of date (or nonexistent).\nfunction countDirtyView(cm) {\n  var view = cm.display.view, dirty = 0\n  for (var i = 0; i < view.length; i++) {\n    var lineView = view[i]\n    if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty }\n  }\n  return dirty\n}\n\n// HIGHLIGHT WORKER\n\nfunction startWorker(cm, time) {\n  if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo)\n    { cm.state.highlight.set(time, bind(highlightWorker, cm)) }\n}\n\nfunction highlightWorker(cm) {\n  var doc = cm.doc\n  if (doc.frontier < doc.first) { doc.frontier = doc.first }\n  if (doc.frontier >= cm.display.viewTo) { return }\n  var end = +new Date + cm.options.workTime\n  var state = copyState(doc.mode, getStateBefore(cm, doc.frontier))\n  var changedLines = []\n\n  doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) {\n    if (doc.frontier >= cm.display.viewFrom) { // Visible\n      var oldStyles = line.styles, tooLong = line.text.length > cm.options.maxHighlightLength\n      var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true)\n      line.styles = highlighted.styles\n      var oldCls = line.styleClasses, newCls = highlighted.classes\n      if (newCls) { line.styleClasses = newCls }\n      else if (oldCls) { line.styleClasses = null }\n      var ischange = !oldStyles || oldStyles.length != line.styles.length ||\n        oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass)\n      for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i] }\n      if (ischange) { changedLines.push(doc.frontier) }\n      line.stateAfter = tooLong ? state : copyState(doc.mode, state)\n    } else {\n      if (line.text.length <= cm.options.maxHighlightLength)\n        { processLine(cm, line.text, state) }\n      line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null\n    }\n    ++doc.frontier\n    if (+new Date > end) {\n      startWorker(cm, cm.options.workDelay)\n      return true\n    }\n  })\n  if (changedLines.length) { runInOp(cm, function () {\n    for (var i = 0; i < changedLines.length; i++)\n      { regLineChange(cm, changedLines[i], \"text\") }\n  }) }\n}\n\n// DISPLAY DRAWING\n\nfunction DisplayUpdate(cm, viewport, force) {\n  var display = cm.display\n\n  this.viewport = viewport\n  // Store some values that we'll need later (but don't want to force a relayout for)\n  this.visible = visibleLines(display, cm.doc, viewport)\n  this.editorIsHidden = !display.wrapper.offsetWidth\n  this.wrapperHeight = display.wrapper.clientHeight\n  this.wrapperWidth = display.wrapper.clientWidth\n  this.oldDisplayWidth = displayWidth(cm)\n  this.force = force\n  this.dims = getDimensions(cm)\n  this.events = []\n}\n\nDisplayUpdate.prototype.signal = function(emitter, type) {\n  if (hasHandler(emitter, type))\n    { this.events.push(arguments) }\n}\nDisplayUpdate.prototype.finish = function() {\n  var this$1 = this;\n\n  for (var i = 0; i < this.events.length; i++)\n    { signal.apply(null, this$1.events[i]) }\n}\n\nfunction maybeClipScrollbars(cm) {\n  var display = cm.display\n  if (!display.scrollbarsClipped && display.scroller.offsetWidth) {\n    display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth\n    display.heightForcer.style.height = scrollGap(cm) + \"px\"\n    display.sizer.style.marginBottom = -display.nativeBarWidth + \"px\"\n    display.sizer.style.borderRightWidth = scrollGap(cm) + \"px\"\n    display.scrollbarsClipped = true\n  }\n}\n\n// Does the actual updating of the line display. Bails out\n// (returning false) when there is nothing to be done and forced is\n// false.\nfunction updateDisplayIfNeeded(cm, update) {\n  var display = cm.display, doc = cm.doc\n\n  if (update.editorIsHidden) {\n    resetView(cm)\n    return false\n  }\n\n  // Bail out if the visible area is already rendered and nothing changed.\n  if (!update.force &&\n      update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&\n      (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&\n      display.renderedView == display.view && countDirtyView(cm) == 0)\n    { return false }\n\n  if (maybeUpdateLineNumberWidth(cm)) {\n    resetView(cm)\n    update.dims = getDimensions(cm)\n  }\n\n  // Compute a suitable new viewport (from & to)\n  var end = doc.first + doc.size\n  var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first)\n  var to = Math.min(end, update.visible.to + cm.options.viewportMargin)\n  if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom) }\n  if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo) }\n  if (sawCollapsedSpans) {\n    from = visualLineNo(cm.doc, from)\n    to = visualLineEndNo(cm.doc, to)\n  }\n\n  var different = from != display.viewFrom || to != display.viewTo ||\n    display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth\n  adjustView(cm, from, to)\n\n  display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom))\n  // Position the mover div to align with the current scroll position\n  cm.display.mover.style.top = display.viewOffset + \"px\"\n\n  var toUpdate = countDirtyView(cm)\n  if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&\n      (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))\n    { return false }\n\n  // For big changes, we hide the enclosing element during the\n  // update, since that speeds up the operations on most browsers.\n  var focused = activeElt()\n  if (toUpdate > 4) { display.lineDiv.style.display = \"none\" }\n  patchDisplay(cm, display.updateLineNumbers, update.dims)\n  if (toUpdate > 4) { display.lineDiv.style.display = \"\" }\n  display.renderedView = display.view\n  // There might have been a widget with a focused element that got\n  // hidden or updated, if so re-focus it.\n  if (focused && activeElt() != focused && focused.offsetHeight) { focused.focus() }\n\n  // Prevent selection and cursors from interfering with the scroll\n  // width and height.\n  removeChildren(display.cursorDiv)\n  removeChildren(display.selectionDiv)\n  display.gutters.style.height = display.sizer.style.minHeight = 0\n\n  if (different) {\n    display.lastWrapHeight = update.wrapperHeight\n    display.lastWrapWidth = update.wrapperWidth\n    startWorker(cm, 400)\n  }\n\n  display.updateLineNumbers = null\n\n  return true\n}\n\nfunction postUpdateDisplay(cm, update) {\n  var viewport = update.viewport\n\n  for (var first = true;; first = false) {\n    if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {\n      // Clip forced viewport to actual scrollable area.\n      if (viewport && viewport.top != null)\n        { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)} }\n      // Updated line heights might result in the drawn area not\n      // actually covering the viewport. Keep looping until it does.\n      update.visible = visibleLines(cm.display, cm.doc, viewport)\n      if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)\n        { break }\n    }\n    if (!updateDisplayIfNeeded(cm, update)) { break }\n    updateHeightsInViewport(cm)\n    var barMeasure = measureForScrollbars(cm)\n    updateSelection(cm)\n    updateScrollbars(cm, barMeasure)\n    setDocumentHeight(cm, barMeasure)\n  }\n\n  update.signal(cm, \"update\", cm)\n  if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {\n    update.signal(cm, \"viewportChange\", cm, cm.display.viewFrom, cm.display.viewTo)\n    cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo\n  }\n}\n\nfunction updateDisplaySimple(cm, viewport) {\n  var update = new DisplayUpdate(cm, viewport)\n  if (updateDisplayIfNeeded(cm, update)) {\n    updateHeightsInViewport(cm)\n    postUpdateDisplay(cm, update)\n    var barMeasure = measureForScrollbars(cm)\n    updateSelection(cm)\n    updateScrollbars(cm, barMeasure)\n    setDocumentHeight(cm, barMeasure)\n    update.finish()\n  }\n}\n\n// Sync the actual display DOM structure with display.view, removing\n// nodes for lines that are no longer in view, and creating the ones\n// that are not there yet, and updating the ones that are out of\n// date.\nfunction patchDisplay(cm, updateNumbersFrom, dims) {\n  var display = cm.display, lineNumbers = cm.options.lineNumbers\n  var container = display.lineDiv, cur = container.firstChild\n\n  function rm(node) {\n    var next = node.nextSibling\n    // Works around a throw-scroll bug in OS X Webkit\n    if (webkit && mac && cm.display.currentWheelTarget == node)\n      { node.style.display = \"none\" }\n    else\n      { node.parentNode.removeChild(node) }\n    return next\n  }\n\n  var view = display.view, lineN = display.viewFrom\n  // Loop over the elements in the view, syncing cur (the DOM nodes\n  // in display.lineDiv) with the view as we go.\n  for (var i = 0; i < view.length; i++) {\n    var lineView = view[i]\n    if (lineView.hidden) {\n    } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet\n      var node = buildLineElement(cm, lineView, lineN, dims)\n      container.insertBefore(node, cur)\n    } else { // Already drawn\n      while (cur != lineView.node) { cur = rm(cur) }\n      var updateNumber = lineNumbers && updateNumbersFrom != null &&\n        updateNumbersFrom <= lineN && lineView.lineNumber\n      if (lineView.changes) {\n        if (indexOf(lineView.changes, \"gutter\") > -1) { updateNumber = false }\n        updateLineForChanges(cm, lineView, lineN, dims)\n      }\n      if (updateNumber) {\n        removeChildren(lineView.lineNumber)\n        lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)))\n      }\n      cur = lineView.node.nextSibling\n    }\n    lineN += lineView.size\n  }\n  while (cur) { cur = rm(cur) }\n}\n\nfunction updateGutterSpace(cm) {\n  var width = cm.display.gutters.offsetWidth\n  cm.display.sizer.style.marginLeft = width + \"px\"\n}\n\nfunction setDocumentHeight(cm, measure) {\n  cm.display.sizer.style.minHeight = measure.docHeight + \"px\"\n  cm.display.heightForcer.style.top = measure.docHeight + \"px\"\n  cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + \"px\"\n}\n\n// Rebuild the gutter elements, ensure the margin to the left of the\n// code matches their width.\nfunction updateGutters(cm) {\n  var gutters = cm.display.gutters, specs = cm.options.gutters\n  removeChildren(gutters)\n  var i = 0\n  for (; i < specs.length; ++i) {\n    var gutterClass = specs[i]\n    var gElt = gutters.appendChild(elt(\"div\", null, \"CodeMirror-gutter \" + gutterClass))\n    if (gutterClass == \"CodeMirror-linenumbers\") {\n      cm.display.lineGutter = gElt\n      gElt.style.width = (cm.display.lineNumWidth || 1) + \"px\"\n    }\n  }\n  gutters.style.display = i ? \"\" : \"none\"\n  updateGutterSpace(cm)\n}\n\n// Make sure the gutters options contains the element\n// \"CodeMirror-linenumbers\" when the lineNumbers option is true.\nfunction setGuttersForLineNumbers(options) {\n  var found = indexOf(options.gutters, \"CodeMirror-linenumbers\")\n  if (found == -1 && options.lineNumbers) {\n    options.gutters = options.gutters.concat([\"CodeMirror-linenumbers\"])\n  } else if (found > -1 && !options.lineNumbers) {\n    options.gutters = options.gutters.slice(0)\n    options.gutters.splice(found, 1)\n  }\n}\n\n// Selection objects are immutable. A new one is created every time\n// the selection changes. A selection is one or more non-overlapping\n// (and non-touching) ranges, sorted, and an integer that indicates\n// which one is the primary selection (the one that's scrolled into\n// view, that getCursor returns, etc).\nfunction Selection(ranges, primIndex) {\n  this.ranges = ranges\n  this.primIndex = primIndex\n}\n\nSelection.prototype = {\n  primary: function() { return this.ranges[this.primIndex] },\n  equals: function(other) {\n    var this$1 = this;\n\n    if (other == this) { return true }\n    if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false }\n    for (var i = 0; i < this.ranges.length; i++) {\n      var here = this$1.ranges[i], there = other.ranges[i]\n      if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) { return false }\n    }\n    return true\n  },\n  deepCopy: function() {\n    var this$1 = this;\n\n    var out = []\n    for (var i = 0; i < this.ranges.length; i++)\n      { out[i] = new Range(copyPos(this$1.ranges[i].anchor), copyPos(this$1.ranges[i].head)) }\n    return new Selection(out, this.primIndex)\n  },\n  somethingSelected: function() {\n    var this$1 = this;\n\n    for (var i = 0; i < this.ranges.length; i++)\n      { if (!this$1.ranges[i].empty()) { return true } }\n    return false\n  },\n  contains: function(pos, end) {\n    var this$1 = this;\n\n    if (!end) { end = pos }\n    for (var i = 0; i < this.ranges.length; i++) {\n      var range = this$1.ranges[i]\n      if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0)\n        { return i }\n    }\n    return -1\n  }\n}\n\nfunction Range(anchor, head) {\n  this.anchor = anchor; this.head = head\n}\n\nRange.prototype = {\n  from: function() { return minPos(this.anchor, this.head) },\n  to: function() { return maxPos(this.anchor, this.head) },\n  empty: function() {\n    return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch\n  }\n}\n\n// Take an unsorted, potentially overlapping set of ranges, and\n// build a selection out of it. 'Consumes' ranges array (modifying\n// it).\nfunction normalizeSelection(ranges, primIndex) {\n  var prim = ranges[primIndex]\n  ranges.sort(function (a, b) { return cmp(a.from(), b.from()); })\n  primIndex = indexOf(ranges, prim)\n  for (var i = 1; i < ranges.length; i++) {\n    var cur = ranges[i], prev = ranges[i - 1]\n    if (cmp(prev.to(), cur.from()) >= 0) {\n      var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to())\n      var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head\n      if (i <= primIndex) { --primIndex }\n      ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to))\n    }\n  }\n  return new Selection(ranges, primIndex)\n}\n\nfunction simpleSelection(anchor, head) {\n  return new Selection([new Range(anchor, head || anchor)], 0)\n}\n\n// Compute the position of the end of a change (its 'to' property\n// refers to the pre-change end).\nfunction changeEnd(change) {\n  if (!change.text) { return change.to }\n  return Pos(change.from.line + change.text.length - 1,\n             lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0))\n}\n\n// Adjust a position to refer to the post-change position of the\n// same text, or the end of the change if the change covers it.\nfunction adjustForChange(pos, change) {\n  if (cmp(pos, change.from) < 0) { return pos }\n  if (cmp(pos, change.to) <= 0) { return changeEnd(change) }\n\n  var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch\n  if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch }\n  return Pos(line, ch)\n}\n\nfunction computeSelAfterChange(doc, change) {\n  var out = []\n  for (var i = 0; i < doc.sel.ranges.length; i++) {\n    var range = doc.sel.ranges[i]\n    out.push(new Range(adjustForChange(range.anchor, change),\n                       adjustForChange(range.head, change)))\n  }\n  return normalizeSelection(out, doc.sel.primIndex)\n}\n\nfunction offsetPos(pos, old, nw) {\n  if (pos.line == old.line)\n    { return Pos(nw.line, pos.ch - old.ch + nw.ch) }\n  else\n    { return Pos(nw.line + (pos.line - old.line), pos.ch) }\n}\n\n// Used by replaceSelections to allow moving the selection to the\n// start or around the replaced test. Hint may be \"start\" or \"around\".\nfunction computeReplacedSel(doc, changes, hint) {\n  var out = []\n  var oldPrev = Pos(doc.first, 0), newPrev = oldPrev\n  for (var i = 0; i < changes.length; i++) {\n    var change = changes[i]\n    var from = offsetPos(change.from, oldPrev, newPrev)\n    var to = offsetPos(changeEnd(change), oldPrev, newPrev)\n    oldPrev = change.to\n    newPrev = to\n    if (hint == \"around\") {\n      var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0\n      out[i] = new Range(inv ? to : from, inv ? from : to)\n    } else {\n      out[i] = new Range(from, from)\n    }\n  }\n  return new Selection(out, doc.sel.primIndex)\n}\n\n// Used to get the editor into a consistent state again when options change.\n\nfunction loadMode(cm) {\n  cm.doc.mode = getMode(cm.options, cm.doc.modeOption)\n  resetModeState(cm)\n}\n\nfunction resetModeState(cm) {\n  cm.doc.iter(function (line) {\n    if (line.stateAfter) { line.stateAfter = null }\n    if (line.styles) { line.styles = null }\n  })\n  cm.doc.frontier = cm.doc.first\n  startWorker(cm, 100)\n  cm.state.modeGen++\n  if (cm.curOp) { regChange(cm) }\n}\n\n// DOCUMENT DATA STRUCTURE\n\n// By default, updates that start and end at the beginning of a line\n// are treated specially, in order to make the association of line\n// widgets and marker elements with the text behave more intuitive.\nfunction isWholeLineUpdate(doc, change) {\n  return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == \"\" &&\n    (!doc.cm || doc.cm.options.wholeLineUpdateBefore)\n}\n\n// Perform a change on the document data structure.\nfunction updateDoc(doc, change, markedSpans, estimateHeight$$1) {\n  function spansFor(n) {return markedSpans ? markedSpans[n] : null}\n  function update(line, text, spans) {\n    updateLine(line, text, spans, estimateHeight$$1)\n    signalLater(line, \"change\", line, change)\n  }\n  function linesFor(start, end) {\n    var result = []\n    for (var i = start; i < end; ++i)\n      { result.push(new Line(text[i], spansFor(i), estimateHeight$$1)) }\n    return result\n  }\n\n  var from = change.from, to = change.to, text = change.text\n  var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line)\n  var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line\n\n  // Adjust the line structure\n  if (change.full) {\n    doc.insert(0, linesFor(0, text.length))\n    doc.remove(text.length, doc.size - text.length)\n  } else if (isWholeLineUpdate(doc, change)) {\n    // This is a whole-line replace. Treated specially to make\n    // sure line objects move the way they are supposed to.\n    var added = linesFor(0, text.length - 1)\n    update(lastLine, lastLine.text, lastSpans)\n    if (nlines) { doc.remove(from.line, nlines) }\n    if (added.length) { doc.insert(from.line, added) }\n  } else if (firstLine == lastLine) {\n    if (text.length == 1) {\n      update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans)\n    } else {\n      var added$1 = linesFor(1, text.length - 1)\n      added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight$$1))\n      update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0))\n      doc.insert(from.line + 1, added$1)\n    }\n  } else if (text.length == 1) {\n    update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0))\n    doc.remove(from.line + 1, nlines)\n  } else {\n    update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0))\n    update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans)\n    var added$2 = linesFor(1, text.length - 1)\n    if (nlines > 1) { doc.remove(from.line + 1, nlines - 1) }\n    doc.insert(from.line + 1, added$2)\n  }\n\n  signalLater(doc, \"change\", doc, change)\n}\n\n// Call f for all linked documents.\nfunction linkedDocs(doc, f, sharedHistOnly) {\n  function propagate(doc, skip, sharedHist) {\n    if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) {\n      var rel = doc.linked[i]\n      if (rel.doc == skip) { continue }\n      var shared = sharedHist && rel.sharedHist\n      if (sharedHistOnly && !shared) { continue }\n      f(rel.doc, shared)\n      propagate(rel.doc, doc, shared)\n    } }\n  }\n  propagate(doc, null, true)\n}\n\n// Attach a document to an editor.\nfunction attachDoc(cm, doc) {\n  if (doc.cm) { throw new Error(\"This document is already in use.\") }\n  cm.doc = doc\n  doc.cm = cm\n  estimateLineHeights(cm)\n  loadMode(cm)\n  if (!cm.options.lineWrapping) { findMaxLine(cm) }\n  cm.options.mode = doc.modeOption\n  regChange(cm)\n}\n\nfunction History(startGen) {\n  // Arrays of change events and selections. Doing something adds an\n  // event to done and clears undo. Undoing moves events from done\n  // to undone, redoing moves them in the other direction.\n  this.done = []; this.undone = []\n  this.undoDepth = Infinity\n  // Used to track when changes can be merged into a single undo\n  // event\n  this.lastModTime = this.lastSelTime = 0\n  this.lastOp = this.lastSelOp = null\n  this.lastOrigin = this.lastSelOrigin = null\n  // Used by the isClean() method\n  this.generation = this.maxGeneration = startGen || 1\n}\n\n// Create a history change event from an updateDoc-style change\n// object.\nfunction historyChangeFromChange(doc, change) {\n  var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}\n  attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1)\n  linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true)\n  return histChange\n}\n\n// Pop all selection events off the end of a history array. Stop at\n// a change event.\nfunction clearSelectionEvents(array) {\n  while (array.length) {\n    var last = lst(array)\n    if (last.ranges) { array.pop() }\n    else { break }\n  }\n}\n\n// Find the top change event in the history. Pop off selection\n// events that are in the way.\nfunction lastChangeEvent(hist, force) {\n  if (force) {\n    clearSelectionEvents(hist.done)\n    return lst(hist.done)\n  } else if (hist.done.length && !lst(hist.done).ranges) {\n    return lst(hist.done)\n  } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) {\n    hist.done.pop()\n    return lst(hist.done)\n  }\n}\n\n// Register a change in the history. Merges changes that are within\n// a single operation, or are close together with an origin that\n// allows merging (starting with \"+\") into a single event.\nfunction addChangeToHistory(doc, change, selAfter, opId) {\n  var hist = doc.history\n  hist.undone.length = 0\n  var time = +new Date, cur\n  var last\n\n  if ((hist.lastOp == opId ||\n       hist.lastOrigin == change.origin && change.origin &&\n       ((change.origin.charAt(0) == \"+\" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) ||\n        change.origin.charAt(0) == \"*\")) &&\n      (cur = lastChangeEvent(hist, hist.lastOp == opId))) {\n    // Merge this change into the last event\n    last = lst(cur.changes)\n    if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) {\n      // Optimized case for simple insertion -- don't want to add\n      // new changesets for every character typed\n      last.to = changeEnd(change)\n    } else {\n      // Add new sub-event\n      cur.changes.push(historyChangeFromChange(doc, change))\n    }\n  } else {\n    // Can not be merged, start a new event.\n    var before = lst(hist.done)\n    if (!before || !before.ranges)\n      { pushSelectionToHistory(doc.sel, hist.done) }\n    cur = {changes: [historyChangeFromChange(doc, change)],\n           generation: hist.generation}\n    hist.done.push(cur)\n    while (hist.done.length > hist.undoDepth) {\n      hist.done.shift()\n      if (!hist.done[0].ranges) { hist.done.shift() }\n    }\n  }\n  hist.done.push(selAfter)\n  hist.generation = ++hist.maxGeneration\n  hist.lastModTime = hist.lastSelTime = time\n  hist.lastOp = hist.lastSelOp = opId\n  hist.lastOrigin = hist.lastSelOrigin = change.origin\n\n  if (!last) { signal(doc, \"historyAdded\") }\n}\n\nfunction selectionEventCanBeMerged(doc, origin, prev, sel) {\n  var ch = origin.charAt(0)\n  return ch == \"*\" ||\n    ch == \"+\" &&\n    prev.ranges.length == sel.ranges.length &&\n    prev.somethingSelected() == sel.somethingSelected() &&\n    new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500)\n}\n\n// Called whenever the selection changes, sets the new selection as\n// the pending selection in the history, and pushes the old pending\n// selection into the 'done' array when it was significantly\n// different (in number of selected ranges, emptiness, or time).\nfunction addSelectionToHistory(doc, sel, opId, options) {\n  var hist = doc.history, origin = options && options.origin\n\n  // A new event is started when the previous origin does not match\n  // the current, or the origins don't allow matching. Origins\n  // starting with * are always merged, those starting with + are\n  // merged when similar and close together in time.\n  if (opId == hist.lastSelOp ||\n      (origin && hist.lastSelOrigin == origin &&\n       (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin ||\n        selectionEventCanBeMerged(doc, origin, lst(hist.done), sel))))\n    { hist.done[hist.done.length - 1] = sel }\n  else\n    { pushSelectionToHistory(sel, hist.done) }\n\n  hist.lastSelTime = +new Date\n  hist.lastSelOrigin = origin\n  hist.lastSelOp = opId\n  if (options && options.clearRedo !== false)\n    { clearSelectionEvents(hist.undone) }\n}\n\nfunction pushSelectionToHistory(sel, dest) {\n  var top = lst(dest)\n  if (!(top && top.ranges && top.equals(sel)))\n    { dest.push(sel) }\n}\n\n// Used to store marked span information in the history.\nfunction attachLocalSpans(doc, change, from, to) {\n  var existing = change[\"spans_\" + doc.id], n = 0\n  doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) {\n    if (line.markedSpans)\n      { (existing || (existing = change[\"spans_\" + doc.id] = {}))[n] = line.markedSpans }\n    ++n\n  })\n}\n\n// When un/re-doing restores text containing marked spans, those\n// that have been explicitly cleared should not be restored.\nfunction removeClearedSpans(spans) {\n  if (!spans) { return null }\n  var out\n  for (var i = 0; i < spans.length; ++i) {\n    if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i) } }\n    else if (out) { out.push(spans[i]) }\n  }\n  return !out ? spans : out.length ? out : null\n}\n\n// Retrieve and filter the old marked spans stored in a change event.\nfunction getOldSpans(doc, change) {\n  var found = change[\"spans_\" + doc.id]\n  if (!found) { return null }\n  var nw = []\n  for (var i = 0; i < change.text.length; ++i)\n    { nw.push(removeClearedSpans(found[i])) }\n  return nw\n}\n\n// Used for un/re-doing changes from the history. Combines the\n// result of computing the existing spans with the set of spans that\n// existed in the history (so that deleting around a span and then\n// undoing brings back the span).\nfunction mergeOldSpans(doc, change) {\n  var old = getOldSpans(doc, change)\n  var stretched = stretchSpansOverChange(doc, change)\n  if (!old) { return stretched }\n  if (!stretched) { return old }\n\n  for (var i = 0; i < old.length; ++i) {\n    var oldCur = old[i], stretchCur = stretched[i]\n    if (oldCur && stretchCur) {\n      spans: for (var j = 0; j < stretchCur.length; ++j) {\n        var span = stretchCur[j]\n        for (var k = 0; k < oldCur.length; ++k)\n          { if (oldCur[k].marker == span.marker) { continue spans } }\n        oldCur.push(span)\n      }\n    } else if (stretchCur) {\n      old[i] = stretchCur\n    }\n  }\n  return old\n}\n\n// Used both to provide a JSON-safe object in .getHistory, and, when\n// detaching a document, to split the history in two\nfunction copyHistoryArray(events, newGroup, instantiateSel) {\n  var copy = []\n  for (var i = 0; i < events.length; ++i) {\n    var event = events[i]\n    if (event.ranges) {\n      copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event)\n      continue\n    }\n    var changes = event.changes, newChanges = []\n    copy.push({changes: newChanges})\n    for (var j = 0; j < changes.length; ++j) {\n      var change = changes[j], m = void 0\n      newChanges.push({from: change.from, to: change.to, text: change.text})\n      if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\\d+)$/)) {\n        if (indexOf(newGroup, Number(m[1])) > -1) {\n          lst(newChanges)[prop] = change[prop]\n          delete change[prop]\n        }\n      } } }\n    }\n  }\n  return copy\n}\n\n// The 'scroll' parameter given to many of these indicated whether\n// the new cursor position should be scrolled into view after\n// modifying the selection.\n\n// If shift is held or the extend flag is set, extends a range to\n// include a given position (and optionally a second position).\n// Otherwise, simply returns the range between the given positions.\n// Used for cursor motion and such.\nfunction extendRange(doc, range, head, other) {\n  if (doc.cm && doc.cm.display.shift || doc.extend) {\n    var anchor = range.anchor\n    if (other) {\n      var posBefore = cmp(head, anchor) < 0\n      if (posBefore != (cmp(other, anchor) < 0)) {\n        anchor = head\n        head = other\n      } else if (posBefore != (cmp(head, other) < 0)) {\n        head = other\n      }\n    }\n    return new Range(anchor, head)\n  } else {\n    return new Range(other || head, head)\n  }\n}\n\n// Extend the primary selection range, discard the rest.\nfunction extendSelection(doc, head, other, options) {\n  setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options)\n}\n\n// Extend all selections (pos is an array of selections with length\n// equal the number of selections)\nfunction extendSelections(doc, heads, options) {\n  var out = []\n  for (var i = 0; i < doc.sel.ranges.length; i++)\n    { out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null) }\n  var newSel = normalizeSelection(out, doc.sel.primIndex)\n  setSelection(doc, newSel, options)\n}\n\n// Updates a single range in the selection.\nfunction replaceOneSelection(doc, i, range, options) {\n  var ranges = doc.sel.ranges.slice(0)\n  ranges[i] = range\n  setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options)\n}\n\n// Reset the selection to a single range.\nfunction setSimpleSelection(doc, anchor, head, options) {\n  setSelection(doc, simpleSelection(anchor, head), options)\n}\n\n// Give beforeSelectionChange handlers a change to influence a\n// selection update.\nfunction filterSelectionChange(doc, sel, options) {\n  var obj = {\n    ranges: sel.ranges,\n    update: function(ranges) {\n      var this$1 = this;\n\n      this.ranges = []\n      for (var i = 0; i < ranges.length; i++)\n        { this$1.ranges[i] = new Range(clipPos(doc, ranges[i].anchor),\n                                   clipPos(doc, ranges[i].head)) }\n    },\n    origin: options && options.origin\n  }\n  signal(doc, \"beforeSelectionChange\", doc, obj)\n  if (doc.cm) { signal(doc.cm, \"beforeSelectionChange\", doc.cm, obj) }\n  if (obj.ranges != sel.ranges) { return normalizeSelection(obj.ranges, obj.ranges.length - 1) }\n  else { return sel }\n}\n\nfunction setSelectionReplaceHistory(doc, sel, options) {\n  var done = doc.history.done, last = lst(done)\n  if (last && last.ranges) {\n    done[done.length - 1] = sel\n    setSelectionNoUndo(doc, sel, options)\n  } else {\n    setSelection(doc, sel, options)\n  }\n}\n\n// Set a new selection.\nfunction setSelection(doc, sel, options) {\n  setSelectionNoUndo(doc, sel, options)\n  addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options)\n}\n\nfunction setSelectionNoUndo(doc, sel, options) {\n  if (hasHandler(doc, \"beforeSelectionChange\") || doc.cm && hasHandler(doc.cm, \"beforeSelectionChange\"))\n    { sel = filterSelectionChange(doc, sel, options) }\n\n  var bias = options && options.bias ||\n    (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1)\n  setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true))\n\n  if (!(options && options.scroll === false) && doc.cm)\n    { ensureCursorVisible(doc.cm) }\n}\n\nfunction setSelectionInner(doc, sel) {\n  if (sel.equals(doc.sel)) { return }\n\n  doc.sel = sel\n\n  if (doc.cm) {\n    doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true\n    signalCursorActivity(doc.cm)\n  }\n  signalLater(doc, \"cursorActivity\", doc)\n}\n\n// Verify that the selection does not partially select any atomic\n// marked ranges.\nfunction reCheckSelection(doc) {\n  setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll)\n}\n\n// Return a selection that does not partially select any atomic\n// ranges.\nfunction skipAtomicInSelection(doc, sel, bias, mayClear) {\n  var out\n  for (var i = 0; i < sel.ranges.length; i++) {\n    var range = sel.ranges[i]\n    var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]\n    var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear)\n    var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear)\n    if (out || newAnchor != range.anchor || newHead != range.head) {\n      if (!out) { out = sel.ranges.slice(0, i) }\n      out[i] = new Range(newAnchor, newHead)\n    }\n  }\n  return out ? normalizeSelection(out, sel.primIndex) : sel\n}\n\nfunction skipAtomicInner(doc, pos, oldPos, dir, mayClear) {\n  var line = getLine(doc, pos.line)\n  if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) {\n    var sp = line.markedSpans[i], m = sp.marker\n    if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) &&\n        (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) {\n      if (mayClear) {\n        signal(m, \"beforeCursorEnter\")\n        if (m.explicitlyCleared) {\n          if (!line.markedSpans) { break }\n          else {--i; continue}\n        }\n      }\n      if (!m.atomic) { continue }\n\n      if (oldPos) {\n        var near = m.find(dir < 0 ? 1 : -1), diff = void 0\n        if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft)\n          { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null) }\n        if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0))\n          { return skipAtomicInner(doc, near, pos, dir, mayClear) }\n      }\n\n      var far = m.find(dir < 0 ? -1 : 1)\n      if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight)\n        { far = movePos(doc, far, dir, far.line == pos.line ? line : null) }\n      return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null\n    }\n  } }\n  return pos\n}\n\n// Ensure a given position is not inside an atomic range.\nfunction skipAtomic(doc, pos, oldPos, bias, mayClear) {\n  var dir = bias || 1\n  var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) ||\n      (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) ||\n      skipAtomicInner(doc, pos, oldPos, -dir, mayClear) ||\n      (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true))\n  if (!found) {\n    doc.cantEdit = true\n    return Pos(doc.first, 0)\n  }\n  return found\n}\n\nfunction movePos(doc, pos, dir, line) {\n  if (dir < 0 && pos.ch == 0) {\n    if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) }\n    else { return null }\n  } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) {\n    if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) }\n    else { return null }\n  } else {\n    return new Pos(pos.line, pos.ch + dir)\n  }\n}\n\nfunction selectAll(cm) {\n  cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll)\n}\n\n// UPDATING\n\n// Allow \"beforeChange\" event handlers to influence a change\nfunction filterChange(doc, change, update) {\n  var obj = {\n    canceled: false,\n    from: change.from,\n    to: change.to,\n    text: change.text,\n    origin: change.origin,\n    cancel: function () { return obj.canceled = true; }\n  }\n  if (update) { obj.update = function (from, to, text, origin) {\n    if (from) { obj.from = clipPos(doc, from) }\n    if (to) { obj.to = clipPos(doc, to) }\n    if (text) { obj.text = text }\n    if (origin !== undefined) { obj.origin = origin }\n  } }\n  signal(doc, \"beforeChange\", doc, obj)\n  if (doc.cm) { signal(doc.cm, \"beforeChange\", doc.cm, obj) }\n\n  if (obj.canceled) { return null }\n  return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}\n}\n\n// Apply a change to a document, and add it to the document's\n// history, and propagating it to all linked documents.\nfunction makeChange(doc, change, ignoreReadOnly) {\n  if (doc.cm) {\n    if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) }\n    if (doc.cm.state.suppressEdits) { return }\n  }\n\n  if (hasHandler(doc, \"beforeChange\") || doc.cm && hasHandler(doc.cm, \"beforeChange\")) {\n    change = filterChange(doc, change, true)\n    if (!change) { return }\n  }\n\n  // Possibly split or suppress the update based on the presence\n  // of read-only spans in its range.\n  var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to)\n  if (split) {\n    for (var i = split.length - 1; i >= 0; --i)\n      { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [\"\"] : change.text}) }\n  } else {\n    makeChangeInner(doc, change)\n  }\n}\n\nfunction makeChangeInner(doc, change) {\n  if (change.text.length == 1 && change.text[0] == \"\" && cmp(change.from, change.to) == 0) { return }\n  var selAfter = computeSelAfterChange(doc, change)\n  addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN)\n\n  makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change))\n  var rebased = []\n\n  linkedDocs(doc, function (doc, sharedHist) {\n    if (!sharedHist && indexOf(rebased, doc.history) == -1) {\n      rebaseHist(doc.history, change)\n      rebased.push(doc.history)\n    }\n    makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change))\n  })\n}\n\n// Revert a change stored in a document's history.\nfunction makeChangeFromHistory(doc, type, allowSelectionOnly) {\n  if (doc.cm && doc.cm.state.suppressEdits && !allowSelectionOnly) { return }\n\n  var hist = doc.history, event, selAfter = doc.sel\n  var source = type == \"undo\" ? hist.done : hist.undone, dest = type == \"undo\" ? hist.undone : hist.done\n\n  // Verify that there is a useable event (so that ctrl-z won't\n  // needlessly clear selection events)\n  var i = 0\n  for (; i < source.length; i++) {\n    event = source[i]\n    if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges)\n      { break }\n  }\n  if (i == source.length) { return }\n  hist.lastOrigin = hist.lastSelOrigin = null\n\n  for (;;) {\n    event = source.pop()\n    if (event.ranges) {\n      pushSelectionToHistory(event, dest)\n      if (allowSelectionOnly && !event.equals(doc.sel)) {\n        setSelection(doc, event, {clearRedo: false})\n        return\n      }\n      selAfter = event\n    }\n    else { break }\n  }\n\n  // Build up a reverse change object to add to the opposite history\n  // stack (redo when undoing, and vice versa).\n  var antiChanges = []\n  pushSelectionToHistory(selAfter, dest)\n  dest.push({changes: antiChanges, generation: hist.generation})\n  hist.generation = event.generation || ++hist.maxGeneration\n\n  var filter = hasHandler(doc, \"beforeChange\") || doc.cm && hasHandler(doc.cm, \"beforeChange\")\n\n  var loop = function ( i ) {\n    var change = event.changes[i]\n    change.origin = type\n    if (filter && !filterChange(doc, change, false)) {\n      source.length = 0\n      return {}\n    }\n\n    antiChanges.push(historyChangeFromChange(doc, change))\n\n    var after = i ? computeSelAfterChange(doc, change) : lst(source)\n    makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change))\n    if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}) }\n    var rebased = []\n\n    // Propagate to the linked documents\n    linkedDocs(doc, function (doc, sharedHist) {\n      if (!sharedHist && indexOf(rebased, doc.history) == -1) {\n        rebaseHist(doc.history, change)\n        rebased.push(doc.history)\n      }\n      makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change))\n    })\n  };\n\n  for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) {\n    var returned = loop( i$1 );\n\n    if ( returned ) return returned.v;\n  }\n}\n\n// Sub-views need their line numbers shifted when text is added\n// above or below them in the parent document.\nfunction shiftDoc(doc, distance) {\n  if (distance == 0) { return }\n  doc.first += distance\n  doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range(\n    Pos(range.anchor.line + distance, range.anchor.ch),\n    Pos(range.head.line + distance, range.head.ch)\n  ); }), doc.sel.primIndex)\n  if (doc.cm) {\n    regChange(doc.cm, doc.first, doc.first - distance, distance)\n    for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)\n      { regLineChange(doc.cm, l, \"gutter\") }\n  }\n}\n\n// More lower-level change function, handling only a single document\n// (not linked ones).\nfunction makeChangeSingleDoc(doc, change, selAfter, spans) {\n  if (doc.cm && !doc.cm.curOp)\n    { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) }\n\n  if (change.to.line < doc.first) {\n    shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line))\n    return\n  }\n  if (change.from.line > doc.lastLine()) { return }\n\n  // Clip the change to the size of this doc\n  if (change.from.line < doc.first) {\n    var shift = change.text.length - 1 - (doc.first - change.from.line)\n    shiftDoc(doc, shift)\n    change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),\n              text: [lst(change.text)], origin: change.origin}\n  }\n  var last = doc.lastLine()\n  if (change.to.line > last) {\n    change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),\n              text: [change.text[0]], origin: change.origin}\n  }\n\n  change.removed = getBetween(doc, change.from, change.to)\n\n  if (!selAfter) { selAfter = computeSelAfterChange(doc, change) }\n  if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans) }\n  else { updateDoc(doc, change, spans) }\n  setSelectionNoUndo(doc, selAfter, sel_dontScroll)\n}\n\n// Handle the interaction of a change to a document with the editor\n// that this document is part of.\nfunction makeChangeSingleDocInEditor(cm, change, spans) {\n  var doc = cm.doc, display = cm.display, from = change.from, to = change.to\n\n  var recomputeMaxLength = false, checkWidthStart = from.line\n  if (!cm.options.lineWrapping) {\n    checkWidthStart = lineNo(visualLine(getLine(doc, from.line)))\n    doc.iter(checkWidthStart, to.line + 1, function (line) {\n      if (line == display.maxLine) {\n        recomputeMaxLength = true\n        return true\n      }\n    })\n  }\n\n  if (doc.sel.contains(change.from, change.to) > -1)\n    { signalCursorActivity(cm) }\n\n  updateDoc(doc, change, spans, estimateHeight(cm))\n\n  if (!cm.options.lineWrapping) {\n    doc.iter(checkWidthStart, from.line + change.text.length, function (line) {\n      var len = lineLength(line)\n      if (len > display.maxLineLength) {\n        display.maxLine = line\n        display.maxLineLength = len\n        display.maxLineChanged = true\n        recomputeMaxLength = false\n      }\n    })\n    if (recomputeMaxLength) { cm.curOp.updateMaxLine = true }\n  }\n\n  // Adjust frontier, schedule worker\n  doc.frontier = Math.min(doc.frontier, from.line)\n  startWorker(cm, 400)\n\n  var lendiff = change.text.length - (to.line - from.line) - 1\n  // Remember that these lines changed, for updating the display\n  if (change.full)\n    { regChange(cm) }\n  else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change))\n    { regLineChange(cm, from.line, \"text\") }\n  else\n    { regChange(cm, from.line, to.line + 1, lendiff) }\n\n  var changesHandler = hasHandler(cm, \"changes\"), changeHandler = hasHandler(cm, \"change\")\n  if (changeHandler || changesHandler) {\n    var obj = {\n      from: from, to: to,\n      text: change.text,\n      removed: change.removed,\n      origin: change.origin\n    }\n    if (changeHandler) { signalLater(cm, \"change\", cm, obj) }\n    if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj) }\n  }\n  cm.display.selForContextMenu = null\n}\n\nfunction replaceRange(doc, code, from, to, origin) {\n  if (!to) { to = from }\n  if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp }\n  if (typeof code == \"string\") { code = doc.splitLines(code) }\n  makeChange(doc, {from: from, to: to, text: code, origin: origin})\n}\n\n// Rebasing/resetting history to deal with externally-sourced changes\n\nfunction rebaseHistSelSingle(pos, from, to, diff) {\n  if (to < pos.line) {\n    pos.line += diff\n  } else if (from < pos.line) {\n    pos.line = from\n    pos.ch = 0\n  }\n}\n\n// Tries to rebase an array of history events given a change in the\n// document. If the change touches the same lines as the event, the\n// event, and everything 'behind' it, is discarded. If the change is\n// before the event, the event's positions are updated. Uses a\n// copy-on-write scheme for the positions, to avoid having to\n// reallocate them all on every rebase, but also avoid problems with\n// shared position objects being unsafely updated.\nfunction rebaseHistArray(array, from, to, diff) {\n  for (var i = 0; i < array.length; ++i) {\n    var sub = array[i], ok = true\n    if (sub.ranges) {\n      if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true }\n      for (var j = 0; j < sub.ranges.length; j++) {\n        rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff)\n        rebaseHistSelSingle(sub.ranges[j].head, from, to, diff)\n      }\n      continue\n    }\n    for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) {\n      var cur = sub.changes[j$1]\n      if (to < cur.from.line) {\n        cur.from = Pos(cur.from.line + diff, cur.from.ch)\n        cur.to = Pos(cur.to.line + diff, cur.to.ch)\n      } else if (from <= cur.to.line) {\n        ok = false\n        break\n      }\n    }\n    if (!ok) {\n      array.splice(0, i + 1)\n      i = 0\n    }\n  }\n}\n\nfunction rebaseHist(hist, change) {\n  var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1\n  rebaseHistArray(hist.done, from, to, diff)\n  rebaseHistArray(hist.undone, from, to, diff)\n}\n\n// Utility for applying a change to a line by handle or number,\n// returning the number and optionally registering the line as\n// changed.\nfunction changeLine(doc, handle, changeType, op) {\n  var no = handle, line = handle\n  if (typeof handle == \"number\") { line = getLine(doc, clipLine(doc, handle)) }\n  else { no = lineNo(handle) }\n  if (no == null) { return null }\n  if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType) }\n  return line\n}\n\n// The document is represented as a BTree consisting of leaves, with\n// chunk of lines in them, and branches, with up to ten leaves or\n// other branch nodes below them. The top node is always a branch\n// node, and is the document object itself (meaning it has\n// additional methods and properties).\n//\n// All nodes have parent links. The tree is used both to go from\n// line numbers to line objects, and to go from objects to numbers.\n// It also indexes by height, and is used to convert between height\n// and line object, and to find the total height of the document.\n//\n// See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html\n\nfunction LeafChunk(lines) {\n  var this$1 = this;\n\n  this.lines = lines\n  this.parent = null\n  var height = 0\n  for (var i = 0; i < lines.length; ++i) {\n    lines[i].parent = this$1\n    height += lines[i].height\n  }\n  this.height = height\n}\n\nLeafChunk.prototype = {\n  chunkSize: function() { return this.lines.length },\n  // Remove the n lines at offset 'at'.\n  removeInner: function(at, n) {\n    var this$1 = this;\n\n    for (var i = at, e = at + n; i < e; ++i) {\n      var line = this$1.lines[i]\n      this$1.height -= line.height\n      cleanUpLine(line)\n      signalLater(line, \"delete\")\n    }\n    this.lines.splice(at, n)\n  },\n  // Helper used to collapse a small branch into a single leaf.\n  collapse: function(lines) {\n    lines.push.apply(lines, this.lines)\n  },\n  // Insert the given array of lines at offset 'at', count them as\n  // having the given height.\n  insertInner: function(at, lines, height) {\n    var this$1 = this;\n\n    this.height += height\n    this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at))\n    for (var i = 0; i < lines.length; ++i) { lines[i].parent = this$1 }\n  },\n  // Used to iterate over a part of the tree.\n  iterN: function(at, n, op) {\n    var this$1 = this;\n\n    for (var e = at + n; at < e; ++at)\n      { if (op(this$1.lines[at])) { return true } }\n  }\n}\n\nfunction BranchChunk(children) {\n  var this$1 = this;\n\n  this.children = children\n  var size = 0, height = 0\n  for (var i = 0; i < children.length; ++i) {\n    var ch = children[i]\n    size += ch.chunkSize(); height += ch.height\n    ch.parent = this$1\n  }\n  this.size = size\n  this.height = height\n  this.parent = null\n}\n\nBranchChunk.prototype = {\n  chunkSize: function() { return this.size },\n  removeInner: function(at, n) {\n    var this$1 = this;\n\n    this.size -= n\n    for (var i = 0; i < this.children.length; ++i) {\n      var child = this$1.children[i], sz = child.chunkSize()\n      if (at < sz) {\n        var rm = Math.min(n, sz - at), oldHeight = child.height\n        child.removeInner(at, rm)\n        this$1.height -= oldHeight - child.height\n        if (sz == rm) { this$1.children.splice(i--, 1); child.parent = null }\n        if ((n -= rm) == 0) { break }\n        at = 0\n      } else { at -= sz }\n    }\n    // If the result is smaller than 25 lines, ensure that it is a\n    // single leaf node.\n    if (this.size - n < 25 &&\n        (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) {\n      var lines = []\n      this.collapse(lines)\n      this.children = [new LeafChunk(lines)]\n      this.children[0].parent = this\n    }\n  },\n  collapse: function(lines) {\n    var this$1 = this;\n\n    for (var i = 0; i < this.children.length; ++i) { this$1.children[i].collapse(lines) }\n  },\n  insertInner: function(at, lines, height) {\n    var this$1 = this;\n\n    this.size += lines.length\n    this.height += height\n    for (var i = 0; i < this.children.length; ++i) {\n      var child = this$1.children[i], sz = child.chunkSize()\n      if (at <= sz) {\n        child.insertInner(at, lines, height)\n        if (child.lines && child.lines.length > 50) {\n          // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced.\n          // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest.\n          var remaining = child.lines.length % 25 + 25\n          for (var pos = remaining; pos < child.lines.length;) {\n            var leaf = new LeafChunk(child.lines.slice(pos, pos += 25))\n            child.height -= leaf.height\n            this$1.children.splice(++i, 0, leaf)\n            leaf.parent = this$1\n          }\n          child.lines = child.lines.slice(0, remaining)\n          this$1.maybeSpill()\n        }\n        break\n      }\n      at -= sz\n    }\n  },\n  // When a node has grown, check whether it should be split.\n  maybeSpill: function() {\n    if (this.children.length <= 10) { return }\n    var me = this\n    do {\n      var spilled = me.children.splice(me.children.length - 5, 5)\n      var sibling = new BranchChunk(spilled)\n      if (!me.parent) { // Become the parent node\n        var copy = new BranchChunk(me.children)\n        copy.parent = me\n        me.children = [copy, sibling]\n        me = copy\n     } else {\n        me.size -= sibling.size\n        me.height -= sibling.height\n        var myIndex = indexOf(me.parent.children, me)\n        me.parent.children.splice(myIndex + 1, 0, sibling)\n      }\n      sibling.parent = me.parent\n    } while (me.children.length > 10)\n    me.parent.maybeSpill()\n  },\n  iterN: function(at, n, op) {\n    var this$1 = this;\n\n    for (var i = 0; i < this.children.length; ++i) {\n      var child = this$1.children[i], sz = child.chunkSize()\n      if (at < sz) {\n        var used = Math.min(n, sz - at)\n        if (child.iterN(at, used, op)) { return true }\n        if ((n -= used) == 0) { break }\n        at = 0\n      } else { at -= sz }\n    }\n  }\n}\n\n// Line widgets are block elements displayed above or below a line.\n\nfunction LineWidget(doc, node, options) {\n  var this$1 = this;\n\n  if (options) { for (var opt in options) { if (options.hasOwnProperty(opt))\n    { this$1[opt] = options[opt] } } }\n  this.doc = doc\n  this.node = node\n}\neventMixin(LineWidget)\n\nfunction adjustScrollWhenAboveVisible(cm, line, diff) {\n  if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop))\n    { addToScrollPos(cm, null, diff) }\n}\n\nLineWidget.prototype.clear = function() {\n  var this$1 = this;\n\n  var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line)\n  if (no == null || !ws) { return }\n  for (var i = 0; i < ws.length; ++i) { if (ws[i] == this$1) { ws.splice(i--, 1) } }\n  if (!ws.length) { line.widgets = null }\n  var height = widgetHeight(this)\n  updateLineHeight(line, Math.max(0, line.height - height))\n  if (cm) { runInOp(cm, function () {\n    adjustScrollWhenAboveVisible(cm, line, -height)\n    regLineChange(cm, no, \"widget\")\n  }) }\n}\nLineWidget.prototype.changed = function() {\n  var oldH = this.height, cm = this.doc.cm, line = this.line\n  this.height = null\n  var diff = widgetHeight(this) - oldH\n  if (!diff) { return }\n  updateLineHeight(line, line.height + diff)\n  if (cm) { runInOp(cm, function () {\n    cm.curOp.forceUpdate = true\n    adjustScrollWhenAboveVisible(cm, line, diff)\n  }) }\n}\n\nfunction addLineWidget(doc, handle, node, options) {\n  var widget = new LineWidget(doc, node, options)\n  var cm = doc.cm\n  if (cm && widget.noHScroll) { cm.display.alignWidgets = true }\n  changeLine(doc, handle, \"widget\", function (line) {\n    var widgets = line.widgets || (line.widgets = [])\n    if (widget.insertAt == null) { widgets.push(widget) }\n    else { widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget) }\n    widget.line = line\n    if (cm && !lineIsHidden(doc, line)) {\n      var aboveVisible = heightAtLine(line) < doc.scrollTop\n      updateLineHeight(line, line.height + widgetHeight(widget))\n      if (aboveVisible) { addToScrollPos(cm, null, widget.height) }\n      cm.curOp.forceUpdate = true\n    }\n    return true\n  })\n  return widget\n}\n\n// TEXTMARKERS\n\n// Created with markText and setBookmark methods. A TextMarker is a\n// handle that can be used to clear or find a marked position in the\n// document. Line objects hold arrays (markedSpans) containing\n// {from, to, marker} object pointing to such marker objects, and\n// indicating that such a marker is present on that line. Multiple\n// lines may point to the same marker when it spans across lines.\n// The spans will have null for their from/to properties when the\n// marker continues beyond the start/end of the line. Markers have\n// links back to the lines they currently touch.\n\n// Collapsed markers have unique ids, in order to be able to order\n// them, which is needed for uniquely determining an outer marker\n// when they overlap (they may nest, but not partially overlap).\nvar nextMarkerId = 0\n\nfunction TextMarker(doc, type) {\n  this.lines = []\n  this.type = type\n  this.doc = doc\n  this.id = ++nextMarkerId\n}\neventMixin(TextMarker)\n\n// Clear the marker.\nTextMarker.prototype.clear = function() {\n  var this$1 = this;\n\n  if (this.explicitlyCleared) { return }\n  var cm = this.doc.cm, withOp = cm && !cm.curOp\n  if (withOp) { startOperation(cm) }\n  if (hasHandler(this, \"clear\")) {\n    var found = this.find()\n    if (found) { signalLater(this, \"clear\", found.from, found.to) }\n  }\n  var min = null, max = null\n  for (var i = 0; i < this.lines.length; ++i) {\n    var line = this$1.lines[i]\n    var span = getMarkedSpanFor(line.markedSpans, this$1)\n    if (cm && !this$1.collapsed) { regLineChange(cm, lineNo(line), \"text\") }\n    else if (cm) {\n      if (span.to != null) { max = lineNo(line) }\n      if (span.from != null) { min = lineNo(line) }\n    }\n    line.markedSpans = removeMarkedSpan(line.markedSpans, span)\n    if (span.from == null && this$1.collapsed && !lineIsHidden(this$1.doc, line) && cm)\n      { updateLineHeight(line, textHeight(cm.display)) }\n  }\n  if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) {\n    var visual = visualLine(this$1.lines[i$1]), len = lineLength(visual)\n    if (len > cm.display.maxLineLength) {\n      cm.display.maxLine = visual\n      cm.display.maxLineLength = len\n      cm.display.maxLineChanged = true\n    }\n  } }\n\n  if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1) }\n  this.lines.length = 0\n  this.explicitlyCleared = true\n  if (this.atomic && this.doc.cantEdit) {\n    this.doc.cantEdit = false\n    if (cm) { reCheckSelection(cm.doc) }\n  }\n  if (cm) { signalLater(cm, \"markerCleared\", cm, this) }\n  if (withOp) { endOperation(cm) }\n  if (this.parent) { this.parent.clear() }\n}\n\n// Find the position of the marker in the document. Returns a {from,\n// to} object by default. Side can be passed to get a specific side\n// -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the\n// Pos objects returned contain a line object, rather than a line\n// number (used to prevent looking up the same line twice).\nTextMarker.prototype.find = function(side, lineObj) {\n  var this$1 = this;\n\n  if (side == null && this.type == \"bookmark\") { side = 1 }\n  var from, to\n  for (var i = 0; i < this.lines.length; ++i) {\n    var line = this$1.lines[i]\n    var span = getMarkedSpanFor(line.markedSpans, this$1)\n    if (span.from != null) {\n      from = Pos(lineObj ? line : lineNo(line), span.from)\n      if (side == -1) { return from }\n    }\n    if (span.to != null) {\n      to = Pos(lineObj ? line : lineNo(line), span.to)\n      if (side == 1) { return to }\n    }\n  }\n  return from && {from: from, to: to}\n}\n\n// Signals that the marker's widget changed, and surrounding layout\n// should be recomputed.\nTextMarker.prototype.changed = function() {\n  var pos = this.find(-1, true), widget = this, cm = this.doc.cm\n  if (!pos || !cm) { return }\n  runInOp(cm, function () {\n    var line = pos.line, lineN = lineNo(pos.line)\n    var view = findViewForLine(cm, lineN)\n    if (view) {\n      clearLineMeasurementCacheFor(view)\n      cm.curOp.selectionChanged = cm.curOp.forceUpdate = true\n    }\n    cm.curOp.updateMaxLine = true\n    if (!lineIsHidden(widget.doc, line) && widget.height != null) {\n      var oldHeight = widget.height\n      widget.height = null\n      var dHeight = widgetHeight(widget) - oldHeight\n      if (dHeight)\n        { updateLineHeight(line, line.height + dHeight) }\n    }\n  })\n}\n\nTextMarker.prototype.attachLine = function(line) {\n  if (!this.lines.length && this.doc.cm) {\n    var op = this.doc.cm.curOp\n    if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)\n      { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this) }\n  }\n  this.lines.push(line)\n}\nTextMarker.prototype.detachLine = function(line) {\n  this.lines.splice(indexOf(this.lines, line), 1)\n  if (!this.lines.length && this.doc.cm) {\n    var op = this.doc.cm.curOp;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this)\n  }\n}\n\n// Create a marker, wire it up to the right lines, and\nfunction markText(doc, from, to, options, type) {\n  // Shared markers (across linked documents) are handled separately\n  // (markTextShared will call out to this again, once per\n  // document).\n  if (options && options.shared) { return markTextShared(doc, from, to, options, type) }\n  // Ensure we are in an operation.\n  if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) }\n\n  var marker = new TextMarker(doc, type), diff = cmp(from, to)\n  if (options) { copyObj(options, marker, false) }\n  // Don't connect empty markers unless clearWhenEmpty is false\n  if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false)\n    { return marker }\n  if (marker.replacedWith) {\n    // Showing up as a widget implies collapsed (widget replaces text)\n    marker.collapsed = true\n    marker.widgetNode = elt(\"span\", [marker.replacedWith], \"CodeMirror-widget\")\n    if (!options.handleMouseEvents) { marker.widgetNode.setAttribute(\"cm-ignore-events\", \"true\") }\n    if (options.insertLeft) { marker.widgetNode.insertLeft = true }\n  }\n  if (marker.collapsed) {\n    if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||\n        from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))\n      { throw new Error(\"Inserting collapsed marker partially overlapping an existing one\") }\n    seeCollapsedSpans()\n  }\n\n  if (marker.addToHistory)\n    { addChangeToHistory(doc, {from: from, to: to, origin: \"markText\"}, doc.sel, NaN) }\n\n  var curLine = from.line, cm = doc.cm, updateMaxLine\n  doc.iter(curLine, to.line + 1, function (line) {\n    if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine)\n      { updateMaxLine = true }\n    if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0) }\n    addMarkedSpan(line, new MarkedSpan(marker,\n                                       curLine == from.line ? from.ch : null,\n                                       curLine == to.line ? to.ch : null))\n    ++curLine\n  })\n  // lineIsHidden depends on the presence of the spans, so needs a second pass\n  if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) {\n    if (lineIsHidden(doc, line)) { updateLineHeight(line, 0) }\n  }) }\n\n  if (marker.clearOnEnter) { on(marker, \"beforeCursorEnter\", function () { return marker.clear(); }) }\n\n  if (marker.readOnly) {\n    seeReadOnlySpans()\n    if (doc.history.done.length || doc.history.undone.length)\n      { doc.clearHistory() }\n  }\n  if (marker.collapsed) {\n    marker.id = ++nextMarkerId\n    marker.atomic = true\n  }\n  if (cm) {\n    // Sync editor state\n    if (updateMaxLine) { cm.curOp.updateMaxLine = true }\n    if (marker.collapsed)\n      { regChange(cm, from.line, to.line + 1) }\n    else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css)\n      { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, \"text\") } }\n    if (marker.atomic) { reCheckSelection(cm.doc) }\n    signalLater(cm, \"markerAdded\", cm, marker)\n  }\n  return marker\n}\n\n// SHARED TEXTMARKERS\n\n// A shared marker spans multiple linked documents. It is\n// implemented as a meta-marker-object controlling multiple normal\n// markers.\nfunction SharedTextMarker(markers, primary) {\n  var this$1 = this;\n\n  this.markers = markers\n  this.primary = primary\n  for (var i = 0; i < markers.length; ++i)\n    { markers[i].parent = this$1 }\n}\neventMixin(SharedTextMarker)\n\nSharedTextMarker.prototype.clear = function() {\n  var this$1 = this;\n\n  if (this.explicitlyCleared) { return }\n  this.explicitlyCleared = true\n  for (var i = 0; i < this.markers.length; ++i)\n    { this$1.markers[i].clear() }\n  signalLater(this, \"clear\")\n}\nSharedTextMarker.prototype.find = function(side, lineObj) {\n  return this.primary.find(side, lineObj)\n}\n\nfunction markTextShared(doc, from, to, options, type) {\n  options = copyObj(options)\n  options.shared = false\n  var markers = [markText(doc, from, to, options, type)], primary = markers[0]\n  var widget = options.widgetNode\n  linkedDocs(doc, function (doc) {\n    if (widget) { options.widgetNode = widget.cloneNode(true) }\n    markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type))\n    for (var i = 0; i < doc.linked.length; ++i)\n      { if (doc.linked[i].isParent) { return } }\n    primary = lst(markers)\n  })\n  return new SharedTextMarker(markers, primary)\n}\n\nfunction findSharedMarkers(doc) {\n  return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; })\n}\n\nfunction copySharedMarkers(doc, markers) {\n  for (var i = 0; i < markers.length; i++) {\n    var marker = markers[i], pos = marker.find()\n    var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to)\n    if (cmp(mFrom, mTo)) {\n      var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type)\n      marker.markers.push(subMark)\n      subMark.parent = marker\n    }\n  }\n}\n\nfunction detachSharedMarkers(markers) {\n  var loop = function ( i ) {\n    var marker = markers[i], linked = [marker.primary.doc]\n    linkedDocs(marker.primary.doc, function (d) { return linked.push(d); })\n    for (var j = 0; j < marker.markers.length; j++) {\n      var subMarker = marker.markers[j]\n      if (indexOf(linked, subMarker.doc) == -1) {\n        subMarker.parent = null\n        marker.markers.splice(j--, 1)\n      }\n    }\n  };\n\n  for (var i = 0; i < markers.length; i++) loop( i );\n}\n\nvar nextDocId = 0\nvar Doc = function(text, mode, firstLine, lineSep) {\n  if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep) }\n  if (firstLine == null) { firstLine = 0 }\n\n  BranchChunk.call(this, [new LeafChunk([new Line(\"\", null)])])\n  this.first = firstLine\n  this.scrollTop = this.scrollLeft = 0\n  this.cantEdit = false\n  this.cleanGeneration = 1\n  this.frontier = firstLine\n  var start = Pos(firstLine, 0)\n  this.sel = simpleSelection(start)\n  this.history = new History(null)\n  this.id = ++nextDocId\n  this.modeOption = mode\n  this.lineSep = lineSep\n  this.extend = false\n\n  if (typeof text == \"string\") { text = this.splitLines(text) }\n  updateDoc(this, {from: start, to: start, text: text})\n  setSelection(this, simpleSelection(start), sel_dontScroll)\n}\n\nDoc.prototype = createObj(BranchChunk.prototype, {\n  constructor: Doc,\n  // Iterate over the document. Supports two forms -- with only one\n  // argument, it calls that for each line in the document. With\n  // three, it iterates over the range given by the first two (with\n  // the second being non-inclusive).\n  iter: function(from, to, op) {\n    if (op) { this.iterN(from - this.first, to - from, op) }\n    else { this.iterN(this.first, this.first + this.size, from) }\n  },\n\n  // Non-public interface for adding and removing lines.\n  insert: function(at, lines) {\n    var height = 0\n    for (var i = 0; i < lines.length; ++i) { height += lines[i].height }\n    this.insertInner(at - this.first, lines, height)\n  },\n  remove: function(at, n) { this.removeInner(at - this.first, n) },\n\n  // From here, the methods are part of the public interface. Most\n  // are also available from CodeMirror (editor) instances.\n\n  getValue: function(lineSep) {\n    var lines = getLines(this, this.first, this.first + this.size)\n    if (lineSep === false) { return lines }\n    return lines.join(lineSep || this.lineSeparator())\n  },\n  setValue: docMethodOp(function(code) {\n    var top = Pos(this.first, 0), last = this.first + this.size - 1\n    makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),\n                      text: this.splitLines(code), origin: \"setValue\", full: true}, true)\n    setSelection(this, simpleSelection(top))\n  }),\n  replaceRange: function(code, from, to, origin) {\n    from = clipPos(this, from)\n    to = to ? clipPos(this, to) : from\n    replaceRange(this, code, from, to, origin)\n  },\n  getRange: function(from, to, lineSep) {\n    var lines = getBetween(this, clipPos(this, from), clipPos(this, to))\n    if (lineSep === false) { return lines }\n    return lines.join(lineSep || this.lineSeparator())\n  },\n\n  getLine: function(line) {var l = this.getLineHandle(line); return l && l.text},\n\n  getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }},\n  getLineNumber: function(line) {return lineNo(line)},\n\n  getLineHandleVisualStart: function(line) {\n    if (typeof line == \"number\") { line = getLine(this, line) }\n    return visualLine(line)\n  },\n\n  lineCount: function() {return this.size},\n  firstLine: function() {return this.first},\n  lastLine: function() {return this.first + this.size - 1},\n\n  clipPos: function(pos) {return clipPos(this, pos)},\n\n  getCursor: function(start) {\n    var range$$1 = this.sel.primary(), pos\n    if (start == null || start == \"head\") { pos = range$$1.head }\n    else if (start == \"anchor\") { pos = range$$1.anchor }\n    else if (start == \"end\" || start == \"to\" || start === false) { pos = range$$1.to() }\n    else { pos = range$$1.from() }\n    return pos\n  },\n  listSelections: function() { return this.sel.ranges },\n  somethingSelected: function() {return this.sel.somethingSelected()},\n\n  setCursor: docMethodOp(function(line, ch, options) {\n    setSimpleSelection(this, clipPos(this, typeof line == \"number\" ? Pos(line, ch || 0) : line), null, options)\n  }),\n  setSelection: docMethodOp(function(anchor, head, options) {\n    setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options)\n  }),\n  extendSelection: docMethodOp(function(head, other, options) {\n    extendSelection(this, clipPos(this, head), other && clipPos(this, other), options)\n  }),\n  extendSelections: docMethodOp(function(heads, options) {\n    extendSelections(this, clipPosArray(this, heads), options)\n  }),\n  extendSelectionsBy: docMethodOp(function(f, options) {\n    var heads = map(this.sel.ranges, f)\n    extendSelections(this, clipPosArray(this, heads), options)\n  }),\n  setSelections: docMethodOp(function(ranges, primary, options) {\n    var this$1 = this;\n\n    if (!ranges.length) { return }\n    var out = []\n    for (var i = 0; i < ranges.length; i++)\n      { out[i] = new Range(clipPos(this$1, ranges[i].anchor),\n                         clipPos(this$1, ranges[i].head)) }\n    if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex) }\n    setSelection(this, normalizeSelection(out, primary), options)\n  }),\n  addSelection: docMethodOp(function(anchor, head, options) {\n    var ranges = this.sel.ranges.slice(0)\n    ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor)))\n    setSelection(this, normalizeSelection(ranges, ranges.length - 1), options)\n  }),\n\n  getSelection: function(lineSep) {\n    var this$1 = this;\n\n    var ranges = this.sel.ranges, lines\n    for (var i = 0; i < ranges.length; i++) {\n      var sel = getBetween(this$1, ranges[i].from(), ranges[i].to())\n      lines = lines ? lines.concat(sel) : sel\n    }\n    if (lineSep === false) { return lines }\n    else { return lines.join(lineSep || this.lineSeparator()) }\n  },\n  getSelections: function(lineSep) {\n    var this$1 = this;\n\n    var parts = [], ranges = this.sel.ranges\n    for (var i = 0; i < ranges.length; i++) {\n      var sel = getBetween(this$1, ranges[i].from(), ranges[i].to())\n      if (lineSep !== false) { sel = sel.join(lineSep || this$1.lineSeparator()) }\n      parts[i] = sel\n    }\n    return parts\n  },\n  replaceSelection: function(code, collapse, origin) {\n    var dup = []\n    for (var i = 0; i < this.sel.ranges.length; i++)\n      { dup[i] = code }\n    this.replaceSelections(dup, collapse, origin || \"+input\")\n  },\n  replaceSelections: docMethodOp(function(code, collapse, origin) {\n    var this$1 = this;\n\n    var changes = [], sel = this.sel\n    for (var i = 0; i < sel.ranges.length; i++) {\n      var range$$1 = sel.ranges[i]\n      changes[i] = {from: range$$1.from(), to: range$$1.to(), text: this$1.splitLines(code[i]), origin: origin}\n    }\n    var newSel = collapse && collapse != \"end\" && computeReplacedSel(this, changes, collapse)\n    for (var i$1 = changes.length - 1; i$1 >= 0; i$1--)\n      { makeChange(this$1, changes[i$1]) }\n    if (newSel) { setSelectionReplaceHistory(this, newSel) }\n    else if (this.cm) { ensureCursorVisible(this.cm) }\n  }),\n  undo: docMethodOp(function() {makeChangeFromHistory(this, \"undo\")}),\n  redo: docMethodOp(function() {makeChangeFromHistory(this, \"redo\")}),\n  undoSelection: docMethodOp(function() {makeChangeFromHistory(this, \"undo\", true)}),\n  redoSelection: docMethodOp(function() {makeChangeFromHistory(this, \"redo\", true)}),\n\n  setExtending: function(val) {this.extend = val},\n  getExtending: function() {return this.extend},\n\n  historySize: function() {\n    var hist = this.history, done = 0, undone = 0\n    for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done } }\n    for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone } }\n    return {undo: done, redo: undone}\n  },\n  clearHistory: function() {this.history = new History(this.history.maxGeneration)},\n\n  markClean: function() {\n    this.cleanGeneration = this.changeGeneration(true)\n  },\n  changeGeneration: function(forceSplit) {\n    if (forceSplit)\n      { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null }\n    return this.history.generation\n  },\n  isClean: function (gen) {\n    return this.history.generation == (gen || this.cleanGeneration)\n  },\n\n  getHistory: function() {\n    return {done: copyHistoryArray(this.history.done),\n            undone: copyHistoryArray(this.history.undone)}\n  },\n  setHistory: function(histData) {\n    var hist = this.history = new History(this.history.maxGeneration)\n    hist.done = copyHistoryArray(histData.done.slice(0), null, true)\n    hist.undone = copyHistoryArray(histData.undone.slice(0), null, true)\n  },\n\n  addLineClass: docMethodOp(function(handle, where, cls) {\n    return changeLine(this, handle, where == \"gutter\" ? \"gutter\" : \"class\", function (line) {\n      var prop = where == \"text\" ? \"textClass\"\n               : where == \"background\" ? \"bgClass\"\n               : where == \"gutter\" ? \"gutterClass\" : \"wrapClass\"\n      if (!line[prop]) { line[prop] = cls }\n      else if (classTest(cls).test(line[prop])) { return false }\n      else { line[prop] += \" \" + cls }\n      return true\n    })\n  }),\n  removeLineClass: docMethodOp(function(handle, where, cls) {\n    return changeLine(this, handle, where == \"gutter\" ? \"gutter\" : \"class\", function (line) {\n      var prop = where == \"text\" ? \"textClass\"\n               : where == \"background\" ? \"bgClass\"\n               : where == \"gutter\" ? \"gutterClass\" : \"wrapClass\"\n      var cur = line[prop]\n      if (!cur) { return false }\n      else if (cls == null) { line[prop] = null }\n      else {\n        var found = cur.match(classTest(cls))\n        if (!found) { return false }\n        var end = found.index + found[0].length\n        line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? \"\" : \" \") + cur.slice(end) || null\n      }\n      return true\n    })\n  }),\n\n  addLineWidget: docMethodOp(function(handle, node, options) {\n    return addLineWidget(this, handle, node, options)\n  }),\n  removeLineWidget: function(widget) { widget.clear() },\n\n  markText: function(from, to, options) {\n    return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || \"range\")\n  },\n  setBookmark: function(pos, options) {\n    var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),\n                    insertLeft: options && options.insertLeft,\n                    clearWhenEmpty: false, shared: options && options.shared,\n                    handleMouseEvents: options && options.handleMouseEvents}\n    pos = clipPos(this, pos)\n    return markText(this, pos, pos, realOpts, \"bookmark\")\n  },\n  findMarksAt: function(pos) {\n    pos = clipPos(this, pos)\n    var markers = [], spans = getLine(this, pos.line).markedSpans\n    if (spans) { for (var i = 0; i < spans.length; ++i) {\n      var span = spans[i]\n      if ((span.from == null || span.from <= pos.ch) &&\n          (span.to == null || span.to >= pos.ch))\n        { markers.push(span.marker.parent || span.marker) }\n    } }\n    return markers\n  },\n  findMarks: function(from, to, filter) {\n    from = clipPos(this, from); to = clipPos(this, to)\n    var found = [], lineNo$$1 = from.line\n    this.iter(from.line, to.line + 1, function (line) {\n      var spans = line.markedSpans\n      if (spans) { for (var i = 0; i < spans.length; i++) {\n        var span = spans[i]\n        if (!(span.to != null && lineNo$$1 == from.line && from.ch >= span.to ||\n              span.from == null && lineNo$$1 != from.line ||\n              span.from != null && lineNo$$1 == to.line && span.from >= to.ch) &&\n            (!filter || filter(span.marker)))\n          { found.push(span.marker.parent || span.marker) }\n      } }\n      ++lineNo$$1\n    })\n    return found\n  },\n  getAllMarks: function() {\n    var markers = []\n    this.iter(function (line) {\n      var sps = line.markedSpans\n      if (sps) { for (var i = 0; i < sps.length; ++i)\n        { if (sps[i].from != null) { markers.push(sps[i].marker) } } }\n    })\n    return markers\n  },\n\n  posFromIndex: function(off) {\n    var ch, lineNo$$1 = this.first, sepSize = this.lineSeparator().length\n    this.iter(function (line) {\n      var sz = line.text.length + sepSize\n      if (sz > off) { ch = off; return true }\n      off -= sz\n      ++lineNo$$1\n    })\n    return clipPos(this, Pos(lineNo$$1, ch))\n  },\n  indexFromPos: function (coords) {\n    coords = clipPos(this, coords)\n    var index = coords.ch\n    if (coords.line < this.first || coords.ch < 0) { return 0 }\n    var sepSize = this.lineSeparator().length\n    this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value\n      index += line.text.length + sepSize\n    })\n    return index\n  },\n\n  copy: function(copyHistory) {\n    var doc = new Doc(getLines(this, this.first, this.first + this.size),\n                      this.modeOption, this.first, this.lineSep)\n    doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft\n    doc.sel = this.sel\n    doc.extend = false\n    if (copyHistory) {\n      doc.history.undoDepth = this.history.undoDepth\n      doc.setHistory(this.getHistory())\n    }\n    return doc\n  },\n\n  linkedDoc: function(options) {\n    if (!options) { options = {} }\n    var from = this.first, to = this.first + this.size\n    if (options.from != null && options.from > from) { from = options.from }\n    if (options.to != null && options.to < to) { to = options.to }\n    var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep)\n    if (options.sharedHist) { copy.history = this.history\n    ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist})\n    copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]\n    copySharedMarkers(copy, findSharedMarkers(this))\n    return copy\n  },\n  unlinkDoc: function(other) {\n    var this$1 = this;\n\n    if (other instanceof CodeMirror$1) { other = other.doc }\n    if (this.linked) { for (var i = 0; i < this.linked.length; ++i) {\n      var link = this$1.linked[i]\n      if (link.doc != other) { continue }\n      this$1.linked.splice(i, 1)\n      other.unlinkDoc(this$1)\n      detachSharedMarkers(findSharedMarkers(this$1))\n      break\n    } }\n    // If the histories were shared, split them again\n    if (other.history == this.history) {\n      var splitIds = [other.id]\n      linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true)\n      other.history = new History(null)\n      other.history.done = copyHistoryArray(this.history.done, splitIds)\n      other.history.undone = copyHistoryArray(this.history.undone, splitIds)\n    }\n  },\n  iterLinkedDocs: function(f) {linkedDocs(this, f)},\n\n  getMode: function() {return this.mode},\n  getEditor: function() {return this.cm},\n\n  splitLines: function(str) {\n    if (this.lineSep) { return str.split(this.lineSep) }\n    return splitLinesAuto(str)\n  },\n  lineSeparator: function() { return this.lineSep || \"\\n\" }\n})\n\n// Public alias.\nDoc.prototype.eachLine = Doc.prototype.iter\n\n// Kludge to work around strange IE behavior where it'll sometimes\n// re-fire a series of drag-related events right after the drop (#1551)\nvar lastDrop = 0\n\nfunction onDrop(e) {\n  var cm = this\n  clearDragCursor(cm)\n  if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e))\n    { return }\n  e_preventDefault(e)\n  if (ie) { lastDrop = +new Date }\n  var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files\n  if (!pos || cm.isReadOnly()) { return }\n  // Might be a file drop, in which case we simply extract the text\n  // and insert it.\n  if (files && files.length && window.FileReader && window.File) {\n    var n = files.length, text = Array(n), read = 0\n    var loadFile = function (file, i) {\n      if (cm.options.allowDropFileTypes &&\n          indexOf(cm.options.allowDropFileTypes, file.type) == -1)\n        { return }\n\n      var reader = new FileReader\n      reader.onload = operation(cm, function () {\n        var content = reader.result\n        if (/[\\x00-\\x08\\x0e-\\x1f]{2}/.test(content)) { content = \"\" }\n        text[i] = content\n        if (++read == n) {\n          pos = clipPos(cm.doc, pos)\n          var change = {from: pos, to: pos,\n                        text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())),\n                        origin: \"paste\"}\n          makeChange(cm.doc, change)\n          setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change)))\n        }\n      })\n      reader.readAsText(file)\n    }\n    for (var i = 0; i < n; ++i) { loadFile(files[i], i) }\n  } else { // Normal drop\n    // Don't do a replace if the drop happened inside of the selected text.\n    if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) {\n      cm.state.draggingText(e)\n      // Ensure the editor is re-focused\n      setTimeout(function () { return cm.display.input.focus(); }, 20)\n      return\n    }\n    try {\n      var text$1 = e.dataTransfer.getData(\"Text\")\n      if (text$1) {\n        var selected\n        if (cm.state.draggingText && !cm.state.draggingText.copy)\n          { selected = cm.listSelections() }\n        setSelectionNoUndo(cm.doc, simpleSelection(pos, pos))\n        if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1)\n          { replaceRange(cm.doc, \"\", selected[i$1].anchor, selected[i$1].head, \"drag\") } }\n        cm.replaceSelection(text$1, \"around\", \"paste\")\n        cm.display.input.focus()\n      }\n    }\n    catch(e){}\n  }\n}\n\nfunction onDragStart(cm, e) {\n  if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return }\n  if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return }\n\n  e.dataTransfer.setData(\"Text\", cm.getSelection())\n  e.dataTransfer.effectAllowed = \"copyMove\"\n\n  // Use dummy image instead of default browsers image.\n  // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there.\n  if (e.dataTransfer.setDragImage && !safari) {\n    var img = elt(\"img\", null, null, \"position: fixed; left: 0; top: 0;\")\n    img.src = \"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\"\n    if (presto) {\n      img.width = img.height = 1\n      cm.display.wrapper.appendChild(img)\n      // Force a relayout, or Opera won't use our image for some obscure reason\n      img._top = img.offsetTop\n    }\n    e.dataTransfer.setDragImage(img, 0, 0)\n    if (presto) { img.parentNode.removeChild(img) }\n  }\n}\n\nfunction onDragOver(cm, e) {\n  var pos = posFromMouse(cm, e)\n  if (!pos) { return }\n  var frag = document.createDocumentFragment()\n  drawSelectionCursor(cm, pos, frag)\n  if (!cm.display.dragCursor) {\n    cm.display.dragCursor = elt(\"div\", null, \"CodeMirror-cursors CodeMirror-dragcursors\")\n    cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv)\n  }\n  removeChildrenAndAdd(cm.display.dragCursor, frag)\n}\n\nfunction clearDragCursor(cm) {\n  if (cm.display.dragCursor) {\n    cm.display.lineSpace.removeChild(cm.display.dragCursor)\n    cm.display.dragCursor = null\n  }\n}\n\n// These must be handled carefully, because naively registering a\n// handler for each editor will cause the editors to never be\n// garbage collected.\n\nfunction forEachCodeMirror(f) {\n  if (!document.body.getElementsByClassName) { return }\n  var byClass = document.body.getElementsByClassName(\"CodeMirror\")\n  for (var i = 0; i < byClass.length; i++) {\n    var cm = byClass[i].CodeMirror\n    if (cm) { f(cm) }\n  }\n}\n\nvar globalsRegistered = false\nfunction ensureGlobalHandlers() {\n  if (globalsRegistered) { return }\n  registerGlobalHandlers()\n  globalsRegistered = true\n}\nfunction registerGlobalHandlers() {\n  // When the window resizes, we need to refresh active editors.\n  var resizeTimer\n  on(window, \"resize\", function () {\n    if (resizeTimer == null) { resizeTimer = setTimeout(function () {\n      resizeTimer = null\n      forEachCodeMirror(onResize)\n    }, 100) }\n  })\n  // When the window loses focus, we want to show the editor as blurred\n  on(window, \"blur\", function () { return forEachCodeMirror(onBlur); })\n}\n// Called when the window resizes\nfunction onResize(cm) {\n  var d = cm.display\n  if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth)\n    { return }\n  // Might be a text scaling operation, clear size caches.\n  d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null\n  d.scrollbarsClipped = false\n  cm.setSize()\n}\n\nvar keyNames = {\n  3: \"Enter\", 8: \"Backspace\", 9: \"Tab\", 13: \"Enter\", 16: \"Shift\", 17: \"Ctrl\", 18: \"Alt\",\n  19: \"Pause\", 20: \"CapsLock\", 27: \"Esc\", 32: \"Space\", 33: \"PageUp\", 34: \"PageDown\", 35: \"End\",\n  36: \"Home\", 37: \"Left\", 38: \"Up\", 39: \"Right\", 40: \"Down\", 44: \"PrintScrn\", 45: \"Insert\",\n  46: \"Delete\", 59: \";\", 61: \"=\", 91: \"Mod\", 92: \"Mod\", 93: \"Mod\",\n  106: \"*\", 107: \"=\", 109: \"-\", 110: \".\", 111: \"/\", 127: \"Delete\",\n  173: \"-\", 186: \";\", 187: \"=\", 188: \",\", 189: \"-\", 190: \".\", 191: \"/\", 192: \"`\", 219: \"[\", 220: \"\\\\\",\n  221: \"]\", 222: \"'\", 63232: \"Up\", 63233: \"Down\", 63234: \"Left\", 63235: \"Right\", 63272: \"Delete\",\n  63273: \"Home\", 63275: \"End\", 63276: \"PageUp\", 63277: \"PageDown\", 63302: \"Insert\"\n}\n\n// Number keys\nfor (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i) }\n// Alphabetic keys\nfor (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1) }\n// Function keys\nfor (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = \"F\" + i$2 }\n\nvar keyMap = {}\n\nkeyMap.basic = {\n  \"Left\": \"goCharLeft\", \"Right\": \"goCharRight\", \"Up\": \"goLineUp\", \"Down\": \"goLineDown\",\n  \"End\": \"goLineEnd\", \"Home\": \"goLineStartSmart\", \"PageUp\": \"goPageUp\", \"PageDown\": \"goPageDown\",\n  \"Delete\": \"delCharAfter\", \"Backspace\": \"delCharBefore\", \"Shift-Backspace\": \"delCharBefore\",\n  \"Tab\": \"defaultTab\", \"Shift-Tab\": \"indentAuto\",\n  \"Enter\": \"newlineAndIndent\", \"Insert\": \"toggleOverwrite\",\n  \"Esc\": \"singleSelection\"\n}\n// Note that the save and find-related commands aren't defined by\n// default. User code or addons can define them. Unknown commands\n// are simply ignored.\nkeyMap.pcDefault = {\n  \"Ctrl-A\": \"selectAll\", \"Ctrl-D\": \"deleteLine\", \"Ctrl-Z\": \"undo\", \"Shift-Ctrl-Z\": \"redo\", \"Ctrl-Y\": \"redo\",\n  \"Ctrl-Home\": \"goDocStart\", \"Ctrl-End\": \"goDocEnd\", \"Ctrl-Up\": \"goLineUp\", \"Ctrl-Down\": \"goLineDown\",\n  \"Ctrl-Left\": \"goGroupLeft\", \"Ctrl-Right\": \"goGroupRight\", \"Alt-Left\": \"goLineStart\", \"Alt-Right\": \"goLineEnd\",\n  \"Ctrl-Backspace\": \"delGroupBefore\", \"Ctrl-Delete\": \"delGroupAfter\", \"Ctrl-S\": \"save\", \"Ctrl-F\": \"find\",\n  \"Ctrl-G\": \"findNext\", \"Shift-Ctrl-G\": \"findPrev\", \"Shift-Ctrl-F\": \"replace\", \"Shift-Ctrl-R\": \"replaceAll\",\n  \"Ctrl-[\": \"indentLess\", \"Ctrl-]\": \"indentMore\",\n  \"Ctrl-U\": \"undoSelection\", \"Shift-Ctrl-U\": \"redoSelection\", \"Alt-U\": \"redoSelection\",\n  fallthrough: \"basic\"\n}\n// Very basic readline/emacs-style bindings, which are standard on Mac.\nkeyMap.emacsy = {\n  \"Ctrl-F\": \"goCharRight\", \"Ctrl-B\": \"goCharLeft\", \"Ctrl-P\": \"goLineUp\", \"Ctrl-N\": \"goLineDown\",\n  \"Alt-F\": \"goWordRight\", \"Alt-B\": \"goWordLeft\", \"Ctrl-A\": \"goLineStart\", \"Ctrl-E\": \"goLineEnd\",\n  \"Ctrl-V\": \"goPageDown\", \"Shift-Ctrl-V\": \"goPageUp\", \"Ctrl-D\": \"delCharAfter\", \"Ctrl-H\": \"delCharBefore\",\n  \"Alt-D\": \"delWordAfter\", \"Alt-Backspace\": \"delWordBefore\", \"Ctrl-K\": \"killLine\", \"Ctrl-T\": \"transposeChars\",\n  \"Ctrl-O\": \"openLine\"\n}\nkeyMap.macDefault = {\n  \"Cmd-A\": \"selectAll\", \"Cmd-D\": \"deleteLine\", \"Cmd-Z\": \"undo\", \"Shift-Cmd-Z\": \"redo\", \"Cmd-Y\": \"redo\",\n  \"Cmd-Home\": \"goDocStart\", \"Cmd-Up\": \"goDocStart\", \"Cmd-End\": \"goDocEnd\", \"Cmd-Down\": \"goDocEnd\", \"Alt-Left\": \"goGroupLeft\",\n  \"Alt-Right\": \"goGroupRight\", \"Cmd-Left\": \"goLineLeft\", \"Cmd-Right\": \"goLineRight\", \"Alt-Backspace\": \"delGroupBefore\",\n  \"Ctrl-Alt-Backspace\": \"delGroupAfter\", \"Alt-Delete\": \"delGroupAfter\", \"Cmd-S\": \"save\", \"Cmd-F\": \"find\",\n  \"Cmd-G\": \"findNext\", \"Shift-Cmd-G\": \"findPrev\", \"Cmd-Alt-F\": \"replace\", \"Shift-Cmd-Alt-F\": \"replaceAll\",\n  \"Cmd-[\": \"indentLess\", \"Cmd-]\": \"indentMore\", \"Cmd-Backspace\": \"delWrappedLineLeft\", \"Cmd-Delete\": \"delWrappedLineRight\",\n  \"Cmd-U\": \"undoSelection\", \"Shift-Cmd-U\": \"redoSelection\", \"Ctrl-Up\": \"goDocStart\", \"Ctrl-Down\": \"goDocEnd\",\n  fallthrough: [\"basic\", \"emacsy\"]\n}\nkeyMap[\"default\"] = mac ? keyMap.macDefault : keyMap.pcDefault\n\n// KEYMAP DISPATCH\n\nfunction normalizeKeyName(name) {\n  var parts = name.split(/-(?!$)/)\n  name = parts[parts.length - 1]\n  var alt, ctrl, shift, cmd\n  for (var i = 0; i < parts.length - 1; i++) {\n    var mod = parts[i]\n    if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true }\n    else if (/^a(lt)?$/i.test(mod)) { alt = true }\n    else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true }\n    else if (/^s(hift)?$/i.test(mod)) { shift = true }\n    else { throw new Error(\"Unrecognized modifier name: \" + mod) }\n  }\n  if (alt) { name = \"Alt-\" + name }\n  if (ctrl) { name = \"Ctrl-\" + name }\n  if (cmd) { name = \"Cmd-\" + name }\n  if (shift) { name = \"Shift-\" + name }\n  return name\n}\n\n// This is a kludge to keep keymaps mostly working as raw objects\n// (backwards compatibility) while at the same time support features\n// like normalization and multi-stroke key bindings. It compiles a\n// new normalized keymap, and then updates the old object to reflect\n// this.\nfunction normalizeKeyMap(keymap) {\n  var copy = {}\n  for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) {\n    var value = keymap[keyname]\n    if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue }\n    if (value == \"...\") { delete keymap[keyname]; continue }\n\n    var keys = map(keyname.split(\" \"), normalizeKeyName)\n    for (var i = 0; i < keys.length; i++) {\n      var val = void 0, name = void 0\n      if (i == keys.length - 1) {\n        name = keys.join(\" \")\n        val = value\n      } else {\n        name = keys.slice(0, i + 1).join(\" \")\n        val = \"...\"\n      }\n      var prev = copy[name]\n      if (!prev) { copy[name] = val }\n      else if (prev != val) { throw new Error(\"Inconsistent bindings for \" + name) }\n    }\n    delete keymap[keyname]\n  } }\n  for (var prop in copy) { keymap[prop] = copy[prop] }\n  return keymap\n}\n\nfunction lookupKey(key, map$$1, handle, context) {\n  map$$1 = getKeyMap(map$$1)\n  var found = map$$1.call ? map$$1.call(key, context) : map$$1[key]\n  if (found === false) { return \"nothing\" }\n  if (found === \"...\") { return \"multi\" }\n  if (found != null && handle(found)) { return \"handled\" }\n\n  if (map$$1.fallthrough) {\n    if (Object.prototype.toString.call(map$$1.fallthrough) != \"[object Array]\")\n      { return lookupKey(key, map$$1.fallthrough, handle, context) }\n    for (var i = 0; i < map$$1.fallthrough.length; i++) {\n      var result = lookupKey(key, map$$1.fallthrough[i], handle, context)\n      if (result) { return result }\n    }\n  }\n}\n\n// Modifier key presses don't count as 'real' key presses for the\n// purpose of keymap fallthrough.\nfunction isModifierKey(value) {\n  var name = typeof value == \"string\" ? value : keyNames[value.keyCode]\n  return name == \"Ctrl\" || name == \"Alt\" || name == \"Shift\" || name == \"Mod\"\n}\n\n// Look up the name of a key as indicated by an event object.\nfunction keyName(event, noShift) {\n  if (presto && event.keyCode == 34 && event[\"char\"]) { return false }\n  var base = keyNames[event.keyCode], name = base\n  if (name == null || event.altGraphKey) { return false }\n  if (event.altKey && base != \"Alt\") { name = \"Alt-\" + name }\n  if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != \"Ctrl\") { name = \"Ctrl-\" + name }\n  if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != \"Cmd\") { name = \"Cmd-\" + name }\n  if (!noShift && event.shiftKey && base != \"Shift\") { name = \"Shift-\" + name }\n  return name\n}\n\nfunction getKeyMap(val) {\n  return typeof val == \"string\" ? keyMap[val] : val\n}\n\n// Helper for deleting text near the selection(s), used to implement\n// backspace, delete, and similar functionality.\nfunction deleteNearSelection(cm, compute) {\n  var ranges = cm.doc.sel.ranges, kill = []\n  // Build up a set of ranges to kill first, merging overlapping\n  // ranges.\n  for (var i = 0; i < ranges.length; i++) {\n    var toKill = compute(ranges[i])\n    while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) {\n      var replaced = kill.pop()\n      if (cmp(replaced.from, toKill.from) < 0) {\n        toKill.from = replaced.from\n        break\n      }\n    }\n    kill.push(toKill)\n  }\n  // Next, remove those actual ranges.\n  runInOp(cm, function () {\n    for (var i = kill.length - 1; i >= 0; i--)\n      { replaceRange(cm.doc, \"\", kill[i].from, kill[i].to, \"+delete\") }\n    ensureCursorVisible(cm)\n  })\n}\n\n// Commands are parameter-less actions that can be performed on an\n// editor, mostly used for keybindings.\nvar commands = {\n  selectAll: selectAll,\n  singleSelection: function (cm) { return cm.setSelection(cm.getCursor(\"anchor\"), cm.getCursor(\"head\"), sel_dontScroll); },\n  killLine: function (cm) { return deleteNearSelection(cm, function (range) {\n    if (range.empty()) {\n      var len = getLine(cm.doc, range.head.line).text.length\n      if (range.head.ch == len && range.head.line < cm.lastLine())\n        { return {from: range.head, to: Pos(range.head.line + 1, 0)} }\n      else\n        { return {from: range.head, to: Pos(range.head.line, len)} }\n    } else {\n      return {from: range.from(), to: range.to()}\n    }\n  }); },\n  deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({\n    from: Pos(range.from().line, 0),\n    to: clipPos(cm.doc, Pos(range.to().line + 1, 0))\n  }); }); },\n  delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({\n    from: Pos(range.from().line, 0), to: range.from()\n  }); }); },\n  delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) {\n    var top = cm.charCoords(range.head, \"div\").top + 5\n    var leftPos = cm.coordsChar({left: 0, top: top}, \"div\")\n    return {from: leftPos, to: range.from()}\n  }); },\n  delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) {\n    var top = cm.charCoords(range.head, \"div\").top + 5\n    var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, \"div\")\n    return {from: range.from(), to: rightPos }\n  }); },\n  undo: function (cm) { return cm.undo(); },\n  redo: function (cm) { return cm.redo(); },\n  undoSelection: function (cm) { return cm.undoSelection(); },\n  redoSelection: function (cm) { return cm.redoSelection(); },\n  goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); },\n  goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); },\n  goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); },\n    {origin: \"+move\", bias: 1}\n  ); },\n  goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); },\n    {origin: \"+move\", bias: 1}\n  ); },\n  goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); },\n    {origin: \"+move\", bias: -1}\n  ); },\n  goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) {\n    var top = cm.charCoords(range.head, \"div\").top + 5\n    return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, \"div\")\n  }, sel_move); },\n  goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) {\n    var top = cm.charCoords(range.head, \"div\").top + 5\n    return cm.coordsChar({left: 0, top: top}, \"div\")\n  }, sel_move); },\n  goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) {\n    var top = cm.charCoords(range.head, \"div\").top + 5\n    var pos = cm.coordsChar({left: 0, top: top}, \"div\")\n    if (pos.ch < cm.getLine(pos.line).search(/\\S/)) { return lineStartSmart(cm, range.head) }\n    return pos\n  }, sel_move); },\n  goLineUp: function (cm) { return cm.moveV(-1, \"line\"); },\n  goLineDown: function (cm) { return cm.moveV(1, \"line\"); },\n  goPageUp: function (cm) { return cm.moveV(-1, \"page\"); },\n  goPageDown: function (cm) { return cm.moveV(1, \"page\"); },\n  goCharLeft: function (cm) { return cm.moveH(-1, \"char\"); },\n  goCharRight: function (cm) { return cm.moveH(1, \"char\"); },\n  goColumnLeft: function (cm) { return cm.moveH(-1, \"column\"); },\n  goColumnRight: function (cm) { return cm.moveH(1, \"column\"); },\n  goWordLeft: function (cm) { return cm.moveH(-1, \"word\"); },\n  goGroupRight: function (cm) { return cm.moveH(1, \"group\"); },\n  goGroupLeft: function (cm) { return cm.moveH(-1, \"group\"); },\n  goWordRight: function (cm) { return cm.moveH(1, \"word\"); },\n  delCharBefore: function (cm) { return cm.deleteH(-1, \"char\"); },\n  delCharAfter: function (cm) { return cm.deleteH(1, \"char\"); },\n  delWordBefore: function (cm) { return cm.deleteH(-1, \"word\"); },\n  delWordAfter: function (cm) { return cm.deleteH(1, \"word\"); },\n  delGroupBefore: function (cm) { return cm.deleteH(-1, \"group\"); },\n  delGroupAfter: function (cm) { return cm.deleteH(1, \"group\"); },\n  indentAuto: function (cm) { return cm.indentSelection(\"smart\"); },\n  indentMore: function (cm) { return cm.indentSelection(\"add\"); },\n  indentLess: function (cm) { return cm.indentSelection(\"subtract\"); },\n  insertTab: function (cm) { return cm.replaceSelection(\"\\t\"); },\n  insertSoftTab: function (cm) {\n    var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize\n    for (var i = 0; i < ranges.length; i++) {\n      var pos = ranges[i].from()\n      var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize)\n      spaces.push(spaceStr(tabSize - col % tabSize))\n    }\n    cm.replaceSelections(spaces)\n  },\n  defaultTab: function (cm) {\n    if (cm.somethingSelected()) { cm.indentSelection(\"add\") }\n    else { cm.execCommand(\"insertTab\") }\n  },\n  // Swap the two chars left and right of each selection's head.\n  // Move cursor behind the two swapped characters afterwards.\n  //\n  // Doesn't consider line feeds a character.\n  // Doesn't scan more than one line above to find a character.\n  // Doesn't do anything on an empty line.\n  // Doesn't do anything with non-empty selections.\n  transposeChars: function (cm) { return runInOp(cm, function () {\n    var ranges = cm.listSelections(), newSel = []\n    for (var i = 0; i < ranges.length; i++) {\n      if (!ranges[i].empty()) { continue }\n      var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text\n      if (line) {\n        if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1) }\n        if (cur.ch > 0) {\n          cur = new Pos(cur.line, cur.ch + 1)\n          cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),\n                          Pos(cur.line, cur.ch - 2), cur, \"+transpose\")\n        } else if (cur.line > cm.doc.first) {\n          var prev = getLine(cm.doc, cur.line - 1).text\n          if (prev) {\n            cur = new Pos(cur.line, 1)\n            cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() +\n                            prev.charAt(prev.length - 1),\n                            Pos(cur.line - 1, prev.length - 1), cur, \"+transpose\")\n          }\n        }\n      }\n      newSel.push(new Range(cur, cur))\n    }\n    cm.setSelections(newSel)\n  }); },\n  newlineAndIndent: function (cm) { return runInOp(cm, function () {\n    var sels = cm.listSelections()\n    for (var i = sels.length - 1; i >= 0; i--)\n      { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, \"+input\") }\n    sels = cm.listSelections()\n    for (var i$1 = 0; i$1 < sels.length; i$1++)\n      { cm.indentLine(sels[i$1].from().line, null, true) }\n    ensureCursorVisible(cm)\n  }); },\n  openLine: function (cm) { return cm.replaceSelection(\"\\n\", \"start\"); },\n  toggleOverwrite: function (cm) { return cm.toggleOverwrite(); }\n}\n\n\nfunction lineStart(cm, lineN) {\n  var line = getLine(cm.doc, lineN)\n  var visual = visualLine(line)\n  if (visual != line) { lineN = lineNo(visual) }\n  var order = getOrder(visual)\n  var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual)\n  return Pos(lineN, ch)\n}\nfunction lineEnd(cm, lineN) {\n  var merged, line = getLine(cm.doc, lineN)\n  while (merged = collapsedSpanAtEnd(line)) {\n    line = merged.find(1, true).line\n    lineN = null\n  }\n  var order = getOrder(line)\n  var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line)\n  return Pos(lineN == null ? lineNo(line) : lineN, ch)\n}\nfunction lineStartSmart(cm, pos) {\n  var start = lineStart(cm, pos.line)\n  var line = getLine(cm.doc, start.line)\n  var order = getOrder(line)\n  if (!order || order[0].level == 0) {\n    var firstNonWS = Math.max(0, line.text.search(/\\S/))\n    var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch\n    return Pos(start.line, inWS ? 0 : firstNonWS)\n  }\n  return start\n}\n\n// Run a handler that was bound to a key.\nfunction doHandleBinding(cm, bound, dropShift) {\n  if (typeof bound == \"string\") {\n    bound = commands[bound]\n    if (!bound) { return false }\n  }\n  // Ensure previous input has been read, so that the handler sees a\n  // consistent view of the document\n  cm.display.input.ensurePolled()\n  var prevShift = cm.display.shift, done = false\n  try {\n    if (cm.isReadOnly()) { cm.state.suppressEdits = true }\n    if (dropShift) { cm.display.shift = false }\n    done = bound(cm) != Pass\n  } finally {\n    cm.display.shift = prevShift\n    cm.state.suppressEdits = false\n  }\n  return done\n}\n\nfunction lookupKeyForEditor(cm, name, handle) {\n  for (var i = 0; i < cm.state.keyMaps.length; i++) {\n    var result = lookupKey(name, cm.state.keyMaps[i], handle, cm)\n    if (result) { return result }\n  }\n  return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm))\n    || lookupKey(name, cm.options.keyMap, handle, cm)\n}\n\nvar stopSeq = new Delayed\nfunction dispatchKey(cm, name, e, handle) {\n  var seq = cm.state.keySeq\n  if (seq) {\n    if (isModifierKey(name)) { return \"handled\" }\n    stopSeq.set(50, function () {\n      if (cm.state.keySeq == seq) {\n        cm.state.keySeq = null\n        cm.display.input.reset()\n      }\n    })\n    name = seq + \" \" + name\n  }\n  var result = lookupKeyForEditor(cm, name, handle)\n\n  if (result == \"multi\")\n    { cm.state.keySeq = name }\n  if (result == \"handled\")\n    { signalLater(cm, \"keyHandled\", cm, name, e) }\n\n  if (result == \"handled\" || result == \"multi\") {\n    e_preventDefault(e)\n    restartBlink(cm)\n  }\n\n  if (seq && !result && /\\'$/.test(name)) {\n    e_preventDefault(e)\n    return true\n  }\n  return !!result\n}\n\n// Handle a key from the keydown event.\nfunction handleKeyBinding(cm, e) {\n  var name = keyName(e, true)\n  if (!name) { return false }\n\n  if (e.shiftKey && !cm.state.keySeq) {\n    // First try to resolve full name (including 'Shift-'). Failing\n    // that, see if there is a cursor-motion command (starting with\n    // 'go') bound to the keyname without 'Shift-'.\n    return dispatchKey(cm, \"Shift-\" + name, e, function (b) { return doHandleBinding(cm, b, true); })\n        || dispatchKey(cm, name, e, function (b) {\n             if (typeof b == \"string\" ? /^go[A-Z]/.test(b) : b.motion)\n               { return doHandleBinding(cm, b) }\n           })\n  } else {\n    return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); })\n  }\n}\n\n// Handle a key from the keypress event\nfunction handleCharBinding(cm, e, ch) {\n  return dispatchKey(cm, \"'\" + ch + \"'\", e, function (b) { return doHandleBinding(cm, b, true); })\n}\n\nvar lastStoppedKey = null\nfunction onKeyDown(e) {\n  var cm = this\n  cm.curOp.focus = activeElt()\n  if (signalDOMEvent(cm, e)) { return }\n  // IE does strange things with escape.\n  if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false }\n  var code = e.keyCode\n  cm.display.shift = code == 16 || e.shiftKey\n  var handled = handleKeyBinding(cm, e)\n  if (presto) {\n    lastStoppedKey = handled ? code : null\n    // Opera has no cut event... we try to at least catch the key combo\n    if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey))\n      { cm.replaceSelection(\"\", null, \"cut\") }\n  }\n\n  // Turn mouse into crosshair when Alt is held on Mac.\n  if (code == 18 && !/\\bCodeMirror-crosshair\\b/.test(cm.display.lineDiv.className))\n    { showCrossHair(cm) }\n}\n\nfunction showCrossHair(cm) {\n  var lineDiv = cm.display.lineDiv\n  addClass(lineDiv, \"CodeMirror-crosshair\")\n\n  function up(e) {\n    if (e.keyCode == 18 || !e.altKey) {\n      rmClass(lineDiv, \"CodeMirror-crosshair\")\n      off(document, \"keyup\", up)\n      off(document, \"mouseover\", up)\n    }\n  }\n  on(document, \"keyup\", up)\n  on(document, \"mouseover\", up)\n}\n\nfunction onKeyUp(e) {\n  if (e.keyCode == 16) { this.doc.sel.shift = false }\n  signalDOMEvent(this, e)\n}\n\nfunction onKeyPress(e) {\n  var cm = this\n  if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return }\n  var keyCode = e.keyCode, charCode = e.charCode\n  if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return}\n  if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return }\n  var ch = String.fromCharCode(charCode == null ? keyCode : charCode)\n  // Some browsers fire keypress events for backspace\n  if (ch == \"\\x08\") { return }\n  if (handleCharBinding(cm, e, ch)) { return }\n  cm.display.input.onKeyPress(e)\n}\n\n// A mouse down can be a single click, double click, triple click,\n// start of selection drag, start of text drag, new cursor\n// (ctrl-click), rectangle drag (alt-drag), or xwin\n// middle-click-paste. Or it might be a click on something we should\n// not interfere with, such as a scrollbar or widget.\nfunction onMouseDown(e) {\n  var cm = this, display = cm.display\n  if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return }\n  display.shift = e.shiftKey\n\n  if (eventInWidget(display, e)) {\n    if (!webkit) {\n      // Briefly turn off draggability, to allow widgets to do\n      // normal dragging things.\n      display.scroller.draggable = false\n      setTimeout(function () { return display.scroller.draggable = true; }, 100)\n    }\n    return\n  }\n  if (clickInGutter(cm, e)) { return }\n  var start = posFromMouse(cm, e)\n  window.focus()\n\n  switch (e_button(e)) {\n  case 1:\n    // #3261: make sure, that we're not starting a second selection\n    if (cm.state.selectingText)\n      { cm.state.selectingText(e) }\n    else if (start)\n      { leftButtonDown(cm, e, start) }\n    else if (e_target(e) == display.scroller)\n      { e_preventDefault(e) }\n    break\n  case 2:\n    if (webkit) { cm.state.lastMiddleDown = +new Date }\n    if (start) { extendSelection(cm.doc, start) }\n    setTimeout(function () { return display.input.focus(); }, 20)\n    e_preventDefault(e)\n    break\n  case 3:\n    if (captureRightClick) { onContextMenu(cm, e) }\n    else { delayBlurEvent(cm) }\n    break\n  }\n}\n\nvar lastClick;\nvar lastDoubleClick\nfunction leftButtonDown(cm, e, start) {\n  if (ie) { setTimeout(bind(ensureFocus, cm), 0) }\n  else { cm.curOp.focus = activeElt() }\n\n  var now = +new Date, type\n  if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) {\n    type = \"triple\"\n  } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) {\n    type = \"double\"\n    lastDoubleClick = {time: now, pos: start}\n  } else {\n    type = \"single\"\n    lastClick = {time: now, pos: start}\n  }\n\n  var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained\n  if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() &&\n      type == \"single\" && (contained = sel.contains(start)) > -1 &&\n      (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) &&\n      (cmp(contained.to(), start) > 0 || start.xRel < 0))\n    { leftButtonStartDrag(cm, e, start, modifier) }\n  else\n    { leftButtonSelect(cm, e, start, type, modifier) }\n}\n\n// Start a text drag. When it ends, see if any dragging actually\n// happen, and treat as a click if it didn't.\nfunction leftButtonStartDrag(cm, e, start, modifier) {\n  var display = cm.display, startTime = +new Date\n  var dragEnd = operation(cm, function (e2) {\n    if (webkit) { display.scroller.draggable = false }\n    cm.state.draggingText = false\n    off(document, \"mouseup\", dragEnd)\n    off(display.scroller, \"drop\", dragEnd)\n    if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {\n      e_preventDefault(e2)\n      if (!modifier && +new Date - 200 < startTime)\n        { extendSelection(cm.doc, start) }\n      // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081)\n      if (webkit || ie && ie_version == 9)\n        { setTimeout(function () {document.body.focus(); display.input.focus()}, 20) }\n      else\n        { display.input.focus() }\n    }\n  })\n  // Let the drag handler handle this.\n  if (webkit) { display.scroller.draggable = true }\n  cm.state.draggingText = dragEnd\n  dragEnd.copy = mac ? e.altKey : e.ctrlKey\n  // IE's approach to draggable\n  if (display.scroller.dragDrop) { display.scroller.dragDrop() }\n  on(document, \"mouseup\", dragEnd)\n  on(display.scroller, \"drop\", dragEnd)\n}\n\n// Normal selection, as opposed to text dragging.\nfunction leftButtonSelect(cm, e, start, type, addNew) {\n  var display = cm.display, doc = cm.doc\n  e_preventDefault(e)\n\n  var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges\n  if (addNew && !e.shiftKey) {\n    ourIndex = doc.sel.contains(start)\n    if (ourIndex > -1)\n      { ourRange = ranges[ourIndex] }\n    else\n      { ourRange = new Range(start, start) }\n  } else {\n    ourRange = doc.sel.primary()\n    ourIndex = doc.sel.primIndex\n  }\n\n  if (chromeOS ? e.shiftKey && e.metaKey : e.altKey) {\n    type = \"rect\"\n    if (!addNew) { ourRange = new Range(start, start) }\n    start = posFromMouse(cm, e, true, true)\n    ourIndex = -1\n  } else if (type == \"double\") {\n    var word = cm.findWordAt(start)\n    if (cm.display.shift || doc.extend)\n      { ourRange = extendRange(doc, ourRange, word.anchor, word.head) }\n    else\n      { ourRange = word }\n  } else if (type == \"triple\") {\n    var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0)))\n    if (cm.display.shift || doc.extend)\n      { ourRange = extendRange(doc, ourRange, line.anchor, line.head) }\n    else\n      { ourRange = line }\n  } else {\n    ourRange = extendRange(doc, ourRange, start)\n  }\n\n  if (!addNew) {\n    ourIndex = 0\n    setSelection(doc, new Selection([ourRange], 0), sel_mouse)\n    startSel = doc.sel\n  } else if (ourIndex == -1) {\n    ourIndex = ranges.length\n    setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex),\n                 {scroll: false, origin: \"*mouse\"})\n  } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == \"single\" && !e.shiftKey) {\n    setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0),\n                 {scroll: false, origin: \"*mouse\"})\n    startSel = doc.sel\n  } else {\n    replaceOneSelection(doc, ourIndex, ourRange, sel_mouse)\n  }\n\n  var lastPos = start\n  function extendTo(pos) {\n    if (cmp(lastPos, pos) == 0) { return }\n    lastPos = pos\n\n    if (type == \"rect\") {\n      var ranges = [], tabSize = cm.options.tabSize\n      var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize)\n      var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize)\n      var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol)\n      for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line));\n           line <= end; line++) {\n        var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize)\n        if (left == right)\n          { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))) }\n        else if (text.length > leftPos)\n          { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))) }\n      }\n      if (!ranges.length) { ranges.push(new Range(start, start)) }\n      setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex),\n                   {origin: \"*mouse\", scroll: false})\n      cm.scrollIntoView(pos)\n    } else {\n      var oldRange = ourRange\n      var anchor = oldRange.anchor, head = pos\n      if (type != \"single\") {\n        var range$$1\n        if (type == \"double\")\n          { range$$1 = cm.findWordAt(pos) }\n        else\n          { range$$1 = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))) }\n        if (cmp(range$$1.anchor, anchor) > 0) {\n          head = range$$1.head\n          anchor = minPos(oldRange.from(), range$$1.anchor)\n        } else {\n          head = range$$1.anchor\n          anchor = maxPos(oldRange.to(), range$$1.head)\n        }\n      }\n      var ranges$1 = startSel.ranges.slice(0)\n      ranges$1[ourIndex] = new Range(clipPos(doc, anchor), head)\n      setSelection(doc, normalizeSelection(ranges$1, ourIndex), sel_mouse)\n    }\n  }\n\n  var editorSize = display.wrapper.getBoundingClientRect()\n  // Used to ensure timeout re-tries don't fire when another extend\n  // happened in the meantime (clearTimeout isn't reliable -- at\n  // least on Chrome, the timeouts still happen even when cleared,\n  // if the clear happens after their scheduled firing time).\n  var counter = 0\n\n  function extend(e) {\n    var curCount = ++counter\n    var cur = posFromMouse(cm, e, true, type == \"rect\")\n    if (!cur) { return }\n    if (cmp(cur, lastPos) != 0) {\n      cm.curOp.focus = activeElt()\n      extendTo(cur)\n      var visible = visibleLines(display, doc)\n      if (cur.line >= visible.to || cur.line < visible.from)\n        { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e) }}), 150) }\n    } else {\n      var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0\n      if (outside) { setTimeout(operation(cm, function () {\n        if (counter != curCount) { return }\n        display.scroller.scrollTop += outside\n        extend(e)\n      }), 50) }\n    }\n  }\n\n  function done(e) {\n    cm.state.selectingText = false\n    counter = Infinity\n    e_preventDefault(e)\n    display.input.focus()\n    off(document, \"mousemove\", move)\n    off(document, \"mouseup\", up)\n    doc.history.lastSelOrigin = null\n  }\n\n  var move = operation(cm, function (e) {\n    if (!e_button(e)) { done(e) }\n    else { extend(e) }\n  })\n  var up = operation(cm, done)\n  cm.state.selectingText = up\n  on(document, \"mousemove\", move)\n  on(document, \"mouseup\", up)\n}\n\n\n// Determines whether an event happened in the gutter, and fires the\n// handlers for the corresponding event.\nfunction gutterEvent(cm, e, type, prevent) {\n  var mX, mY\n  try { mX = e.clientX; mY = e.clientY }\n  catch(e) { return false }\n  if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false }\n  if (prevent) { e_preventDefault(e) }\n\n  var display = cm.display\n  var lineBox = display.lineDiv.getBoundingClientRect()\n\n  if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) }\n  mY -= lineBox.top - display.viewOffset\n\n  for (var i = 0; i < cm.options.gutters.length; ++i) {\n    var g = display.gutters.childNodes[i]\n    if (g && g.getBoundingClientRect().right >= mX) {\n      var line = lineAtHeight(cm.doc, mY)\n      var gutter = cm.options.gutters[i]\n      signal(cm, type, cm, line, gutter, e)\n      return e_defaultPrevented(e)\n    }\n  }\n}\n\nfunction clickInGutter(cm, e) {\n  return gutterEvent(cm, e, \"gutterClick\", true)\n}\n\n// CONTEXT MENU HANDLING\n\n// To make the context menu work, we need to briefly unhide the\n// textarea (making it as unobtrusive as possible) to let the\n// right-click take effect on it.\nfunction onContextMenu(cm, e) {\n  if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return }\n  if (signalDOMEvent(cm, e, \"contextmenu\")) { return }\n  cm.display.input.onContextMenu(e)\n}\n\nfunction contextMenuInGutter(cm, e) {\n  if (!hasHandler(cm, \"gutterContextMenu\")) { return false }\n  return gutterEvent(cm, e, \"gutterContextMenu\", false)\n}\n\nfunction themeChanged(cm) {\n  cm.display.wrapper.className = cm.display.wrapper.className.replace(/\\s*cm-s-\\S+/g, \"\") +\n    cm.options.theme.replace(/(^|\\s)\\s*/g, \" cm-s-\")\n  clearCaches(cm)\n}\n\nvar Init = {toString: function(){return \"CodeMirror.Init\"}}\n\nvar defaults = {}\nvar optionHandlers = {}\n\nfunction defineOptions(CodeMirror) {\n  var optionHandlers = CodeMirror.optionHandlers\n\n  function option(name, deflt, handle, notOnInit) {\n    CodeMirror.defaults[name] = deflt\n    if (handle) { optionHandlers[name] =\n      notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old) }} : handle }\n  }\n\n  CodeMirror.defineOption = option\n\n  // Passed to option handlers when there is no old value.\n  CodeMirror.Init = Init\n\n  // These two are, on init, called from the constructor because they\n  // have to be initialized before the editor can start at all.\n  option(\"value\", \"\", function (cm, val) { return cm.setValue(val); }, true)\n  option(\"mode\", null, function (cm, val) {\n    cm.doc.modeOption = val\n    loadMode(cm)\n  }, true)\n\n  option(\"indentUnit\", 2, loadMode, true)\n  option(\"indentWithTabs\", false)\n  option(\"smartIndent\", true)\n  option(\"tabSize\", 4, function (cm) {\n    resetModeState(cm)\n    clearCaches(cm)\n    regChange(cm)\n  }, true)\n  option(\"lineSeparator\", null, function (cm, val) {\n    cm.doc.lineSep = val\n    if (!val) { return }\n    var newBreaks = [], lineNo = cm.doc.first\n    cm.doc.iter(function (line) {\n      for (var pos = 0;;) {\n        var found = line.text.indexOf(val, pos)\n        if (found == -1) { break }\n        pos = found + val.length\n        newBreaks.push(Pos(lineNo, found))\n      }\n      lineNo++\n    })\n    for (var i = newBreaks.length - 1; i >= 0; i--)\n      { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) }\n  })\n  option(\"specialChars\", /[\\u0000-\\u001f\\u007f\\u00ad\\u200b-\\u200f\\u2028\\u2029\\ufeff]/g, function (cm, val, old) {\n    cm.state.specialChars = new RegExp(val.source + (val.test(\"\\t\") ? \"\" : \"|\\t\"), \"g\")\n    if (old != Init) { cm.refresh() }\n  })\n  option(\"specialCharPlaceholder\", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true)\n  option(\"electricChars\", true)\n  option(\"inputStyle\", mobile ? \"contenteditable\" : \"textarea\", function () {\n    throw new Error(\"inputStyle can not (yet) be changed in a running editor\") // FIXME\n  }, true)\n  option(\"spellcheck\", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true)\n  option(\"rtlMoveVisually\", !windows)\n  option(\"wholeLineUpdateBefore\", true)\n\n  option(\"theme\", \"default\", function (cm) {\n    themeChanged(cm)\n    guttersChanged(cm)\n  }, true)\n  option(\"keyMap\", \"default\", function (cm, val, old) {\n    var next = getKeyMap(val)\n    var prev = old != Init && getKeyMap(old)\n    if (prev && prev.detach) { prev.detach(cm, next) }\n    if (next.attach) { next.attach(cm, prev || null) }\n  })\n  option(\"extraKeys\", null)\n\n  option(\"lineWrapping\", false, wrappingChanged, true)\n  option(\"gutters\", [], function (cm) {\n    setGuttersForLineNumbers(cm.options)\n    guttersChanged(cm)\n  }, true)\n  option(\"fixedGutter\", true, function (cm, val) {\n    cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + \"px\" : \"0\"\n    cm.refresh()\n  }, true)\n  option(\"coverGutterNextToScrollbar\", false, function (cm) { return updateScrollbars(cm); }, true)\n  option(\"scrollbarStyle\", \"native\", function (cm) {\n    initScrollbars(cm)\n    updateScrollbars(cm)\n    cm.display.scrollbars.setScrollTop(cm.doc.scrollTop)\n    cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft)\n  }, true)\n  option(\"lineNumbers\", false, function (cm) {\n    setGuttersForLineNumbers(cm.options)\n    guttersChanged(cm)\n  }, true)\n  option(\"firstLineNumber\", 1, guttersChanged, true)\n  option(\"lineNumberFormatter\", function (integer) { return integer; }, guttersChanged, true)\n  option(\"showCursorWhenSelecting\", false, updateSelection, true)\n\n  option(\"resetSelectionOnContextMenu\", true)\n  option(\"lineWiseCopyCut\", true)\n\n  option(\"readOnly\", false, function (cm, val) {\n    if (val == \"nocursor\") {\n      onBlur(cm)\n      cm.display.input.blur()\n      cm.display.disabled = true\n    } else {\n      cm.display.disabled = false\n    }\n    cm.display.input.readOnlyChanged(val)\n  })\n  option(\"disableInput\", false, function (cm, val) {if (!val) { cm.display.input.reset() }}, true)\n  option(\"dragDrop\", true, dragDropChanged)\n  option(\"allowDropFileTypes\", null)\n\n  option(\"cursorBlinkRate\", 530)\n  option(\"cursorScrollMargin\", 0)\n  option(\"cursorHeight\", 1, updateSelection, true)\n  option(\"singleCursorHeightPerLine\", true, updateSelection, true)\n  option(\"workTime\", 100)\n  option(\"workDelay\", 100)\n  option(\"flattenSpans\", true, resetModeState, true)\n  option(\"addModeClass\", false, resetModeState, true)\n  option(\"pollInterval\", 100)\n  option(\"undoDepth\", 200, function (cm, val) { return cm.doc.history.undoDepth = val; })\n  option(\"historyEventDelay\", 1250)\n  option(\"viewportMargin\", 10, function (cm) { return cm.refresh(); }, true)\n  option(\"maxHighlightLength\", 10000, resetModeState, true)\n  option(\"moveInputWithCursor\", true, function (cm, val) {\n    if (!val) { cm.display.input.resetPosition() }\n  })\n\n  option(\"tabindex\", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || \"\"; })\n  option(\"autofocus\", null)\n}\n\nfunction guttersChanged(cm) {\n  updateGutters(cm)\n  regChange(cm)\n  setTimeout(function () { return alignHorizontally(cm); }, 20)\n}\n\nfunction dragDropChanged(cm, value, old) {\n  var wasOn = old && old != Init\n  if (!value != !wasOn) {\n    var funcs = cm.display.dragFunctions\n    var toggle = value ? on : off\n    toggle(cm.display.scroller, \"dragstart\", funcs.start)\n    toggle(cm.display.scroller, \"dragenter\", funcs.enter)\n    toggle(cm.display.scroller, \"dragover\", funcs.over)\n    toggle(cm.display.scroller, \"dragleave\", funcs.leave)\n    toggle(cm.display.scroller, \"drop\", funcs.drop)\n  }\n}\n\nfunction wrappingChanged(cm) {\n  if (cm.options.lineWrapping) {\n    addClass(cm.display.wrapper, \"CodeMirror-wrap\")\n    cm.display.sizer.style.minWidth = \"\"\n    cm.display.sizerWidth = null\n  } else {\n    rmClass(cm.display.wrapper, \"CodeMirror-wrap\")\n    findMaxLine(cm)\n  }\n  estimateLineHeights(cm)\n  regChange(cm)\n  clearCaches(cm)\n  setTimeout(function () { return updateScrollbars(cm); }, 100)\n}\n\n// A CodeMirror instance represents an editor. This is the object\n// that user code is usually dealing with.\n\nfunction CodeMirror$1(place, options) {\n  var this$1 = this;\n\n  if (!(this instanceof CodeMirror$1)) { return new CodeMirror$1(place, options) }\n\n  this.options = options = options ? copyObj(options) : {}\n  // Determine effective options based on given values and defaults.\n  copyObj(defaults, options, false)\n  setGuttersForLineNumbers(options)\n\n  var doc = options.value\n  if (typeof doc == \"string\") { doc = new Doc(doc, options.mode, null, options.lineSeparator) }\n  this.doc = doc\n\n  var input = new CodeMirror$1.inputStyles[options.inputStyle](this)\n  var display = this.display = new Display(place, doc, input)\n  display.wrapper.CodeMirror = this\n  updateGutters(this)\n  themeChanged(this)\n  if (options.lineWrapping)\n    { this.display.wrapper.className += \" CodeMirror-wrap\" }\n  if (options.autofocus && !mobile) { display.input.focus() }\n  initScrollbars(this)\n\n  this.state = {\n    keyMaps: [],  // stores maps added by addKeyMap\n    overlays: [], // highlighting overlays, as added by addOverlay\n    modeGen: 0,   // bumped when mode/overlay changes, used to invalidate highlighting info\n    overwrite: false,\n    delayingBlurEvent: false,\n    focused: false,\n    suppressEdits: false, // used to disable editing during key handlers when in readOnly mode\n    pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll\n    selectingText: false,\n    draggingText: false,\n    highlight: new Delayed(), // stores highlight worker timeout\n    keySeq: null,  // Unfinished key sequence\n    specialChars: null\n  }\n\n  // Override magic textarea content restore that IE sometimes does\n  // on our hidden textarea on reload\n  if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20) }\n\n  registerEventHandlers(this)\n  ensureGlobalHandlers()\n\n  startOperation(this)\n  this.curOp.forceUpdate = true\n  attachDoc(this, doc)\n\n  if ((options.autofocus && !mobile) || this.hasFocus())\n    { setTimeout(bind(onFocus, this), 20) }\n  else\n    { onBlur(this) }\n\n  for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt))\n    { optionHandlers[opt](this$1, options[opt], Init) } }\n  maybeUpdateLineNumberWidth(this)\n  if (options.finishInit) { options.finishInit(this) }\n  for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this$1) }\n  endOperation(this)\n  // Suppress optimizelegibility in Webkit, since it breaks text\n  // measuring on line wrapping boundaries.\n  if (webkit && options.lineWrapping &&\n      getComputedStyle(display.lineDiv).textRendering == \"optimizelegibility\")\n    { display.lineDiv.style.textRendering = \"auto\" }\n}\n\n// The default configuration options.\nCodeMirror$1.defaults = defaults\n// Functions to run when options are changed.\nCodeMirror$1.optionHandlers = optionHandlers\n\n// Attach the necessary event handlers when initializing the editor\nfunction registerEventHandlers(cm) {\n  var d = cm.display\n  on(d.scroller, \"mousedown\", operation(cm, onMouseDown))\n  // Older IE's will not fire a second mousedown for a double click\n  if (ie && ie_version < 11)\n    { on(d.scroller, \"dblclick\", operation(cm, function (e) {\n      if (signalDOMEvent(cm, e)) { return }\n      var pos = posFromMouse(cm, e)\n      if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return }\n      e_preventDefault(e)\n      var word = cm.findWordAt(pos)\n      extendSelection(cm.doc, word.anchor, word.head)\n    })) }\n  else\n    { on(d.scroller, \"dblclick\", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }) }\n  // Some browsers fire contextmenu *after* opening the menu, at\n  // which point we can't mess with it anymore. Context menu is\n  // handled in onMouseDown for these browsers.\n  if (!captureRightClick) { on(d.scroller, \"contextmenu\", function (e) { return onContextMenu(cm, e); }) }\n\n  // Used to suppress mouse event handling when a touch happens\n  var touchFinished, prevTouch = {end: 0}\n  function finishTouch() {\n    if (d.activeTouch) {\n      touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000)\n      prevTouch = d.activeTouch\n      prevTouch.end = +new Date\n    }\n  }\n  function isMouseLikeTouchEvent(e) {\n    if (e.touches.length != 1) { return false }\n    var touch = e.touches[0]\n    return touch.radiusX <= 1 && touch.radiusY <= 1\n  }\n  function farAway(touch, other) {\n    if (other.left == null) { return true }\n    var dx = other.left - touch.left, dy = other.top - touch.top\n    return dx * dx + dy * dy > 20 * 20\n  }\n  on(d.scroller, \"touchstart\", function (e) {\n    if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) {\n      clearTimeout(touchFinished)\n      var now = +new Date\n      d.activeTouch = {start: now, moved: false,\n                       prev: now - prevTouch.end <= 300 ? prevTouch : null}\n      if (e.touches.length == 1) {\n        d.activeTouch.left = e.touches[0].pageX\n        d.activeTouch.top = e.touches[0].pageY\n      }\n    }\n  })\n  on(d.scroller, \"touchmove\", function () {\n    if (d.activeTouch) { d.activeTouch.moved = true }\n  })\n  on(d.scroller, \"touchend\", function (e) {\n    var touch = d.activeTouch\n    if (touch && !eventInWidget(d, e) && touch.left != null &&\n        !touch.moved && new Date - touch.start < 300) {\n      var pos = cm.coordsChar(d.activeTouch, \"page\"), range\n      if (!touch.prev || farAway(touch, touch.prev)) // Single tap\n        { range = new Range(pos, pos) }\n      else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap\n        { range = cm.findWordAt(pos) }\n      else // Triple tap\n        { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) }\n      cm.setSelection(range.anchor, range.head)\n      cm.focus()\n      e_preventDefault(e)\n    }\n    finishTouch()\n  })\n  on(d.scroller, \"touchcancel\", finishTouch)\n\n  // Sync scrolling between fake scrollbars and real scrollable\n  // area, ensure viewport is updated when scrolling.\n  on(d.scroller, \"scroll\", function () {\n    if (d.scroller.clientHeight) {\n      setScrollTop(cm, d.scroller.scrollTop)\n      setScrollLeft(cm, d.scroller.scrollLeft, true)\n      signal(cm, \"scroll\", cm)\n    }\n  })\n\n  // Listen to wheel events in order to try and update the viewport on time.\n  on(d.scroller, \"mousewheel\", function (e) { return onScrollWheel(cm, e); })\n  on(d.scroller, \"DOMMouseScroll\", function (e) { return onScrollWheel(cm, e); })\n\n  // Prevent wrapper from ever scrolling\n  on(d.wrapper, \"scroll\", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; })\n\n  d.dragFunctions = {\n    enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e) }},\n    over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e) }},\n    start: function (e) { return onDragStart(cm, e); },\n    drop: operation(cm, onDrop),\n    leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm) }}\n  }\n\n  var inp = d.input.getField()\n  on(inp, \"keyup\", function (e) { return onKeyUp.call(cm, e); })\n  on(inp, \"keydown\", operation(cm, onKeyDown))\n  on(inp, \"keypress\", operation(cm, onKeyPress))\n  on(inp, \"focus\", function (e) { return onFocus(cm, e); })\n  on(inp, \"blur\", function (e) { return onBlur(cm, e); })\n}\n\nvar initHooks = []\nCodeMirror$1.defineInitHook = function (f) { return initHooks.push(f); }\n\n// Indent the given line. The how parameter can be \"smart\",\n// \"add\"/null, \"subtract\", or \"prev\". When aggressive is false\n// (typically set to true for forced single-line indents), empty\n// lines are not indented, and places where the mode returns Pass\n// are left alone.\nfunction indentLine(cm, n, how, aggressive) {\n  var doc = cm.doc, state\n  if (how == null) { how = \"add\" }\n  if (how == \"smart\") {\n    // Fall back to \"prev\" when the mode doesn't have an indentation\n    // method.\n    if (!doc.mode.indent) { how = \"prev\" }\n    else { state = getStateBefore(cm, n) }\n  }\n\n  var tabSize = cm.options.tabSize\n  var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize)\n  if (line.stateAfter) { line.stateAfter = null }\n  var curSpaceString = line.text.match(/^\\s*/)[0], indentation\n  if (!aggressive && !/\\S/.test(line.text)) {\n    indentation = 0\n    how = \"not\"\n  } else if (how == \"smart\") {\n    indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text)\n    if (indentation == Pass || indentation > 150) {\n      if (!aggressive) { return }\n      how = \"prev\"\n    }\n  }\n  if (how == \"prev\") {\n    if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize) }\n    else { indentation = 0 }\n  } else if (how == \"add\") {\n    indentation = curSpace + cm.options.indentUnit\n  } else if (how == \"subtract\") {\n    indentation = curSpace - cm.options.indentUnit\n  } else if (typeof how == \"number\") {\n    indentation = curSpace + how\n  }\n  indentation = Math.max(0, indentation)\n\n  var indentString = \"\", pos = 0\n  if (cm.options.indentWithTabs)\n    { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += \"\\t\"} }\n  if (pos < indentation) { indentString += spaceStr(indentation - pos) }\n\n  if (indentString != curSpaceString) {\n    replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), \"+input\")\n    line.stateAfter = null\n    return true\n  } else {\n    // Ensure that, if the cursor was in the whitespace at the start\n    // of the line, it is moved to the end of that space.\n    for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) {\n      var range = doc.sel.ranges[i$1]\n      if (range.head.line == n && range.head.ch < curSpaceString.length) {\n        var pos$1 = Pos(n, curSpaceString.length)\n        replaceOneSelection(doc, i$1, new Range(pos$1, pos$1))\n        break\n      }\n    }\n  }\n}\n\n// This will be set to a {lineWise: bool, text: [string]} object, so\n// that, when pasting, we know what kind of selections the copied\n// text was made out of.\nvar lastCopied = null\n\nfunction setLastCopied(newLastCopied) {\n  lastCopied = newLastCopied\n}\n\nfunction applyTextInput(cm, inserted, deleted, sel, origin) {\n  var doc = cm.doc\n  cm.display.shift = false\n  if (!sel) { sel = doc.sel }\n\n  var paste = cm.state.pasteIncoming || origin == \"paste\"\n  var textLines = splitLinesAuto(inserted), multiPaste = null\n  // When pasing N lines into N selections, insert one line per selection\n  if (paste && sel.ranges.length > 1) {\n    if (lastCopied && lastCopied.text.join(\"\\n\") == inserted) {\n      if (sel.ranges.length % lastCopied.text.length == 0) {\n        multiPaste = []\n        for (var i = 0; i < lastCopied.text.length; i++)\n          { multiPaste.push(doc.splitLines(lastCopied.text[i])) }\n      }\n    } else if (textLines.length == sel.ranges.length) {\n      multiPaste = map(textLines, function (l) { return [l]; })\n    }\n  }\n\n  var updateInput\n  // Normal behavior is to insert the new text into every selection\n  for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) {\n    var range$$1 = sel.ranges[i$1]\n    var from = range$$1.from(), to = range$$1.to()\n    if (range$$1.empty()) {\n      if (deleted && deleted > 0) // Handle deletion\n        { from = Pos(from.line, from.ch - deleted) }\n      else if (cm.state.overwrite && !paste) // Handle overwrite\n        { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)) }\n      else if (lastCopied && lastCopied.lineWise && lastCopied.text.join(\"\\n\") == inserted)\n        { from = to = Pos(from.line, 0) }\n    }\n    updateInput = cm.curOp.updateInput\n    var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines,\n                       origin: origin || (paste ? \"paste\" : cm.state.cutIncoming ? \"cut\" : \"+input\")}\n    makeChange(cm.doc, changeEvent)\n    signalLater(cm, \"inputRead\", cm, changeEvent)\n  }\n  if (inserted && !paste)\n    { triggerElectric(cm, inserted) }\n\n  ensureCursorVisible(cm)\n  cm.curOp.updateInput = updateInput\n  cm.curOp.typing = true\n  cm.state.pasteIncoming = cm.state.cutIncoming = false\n}\n\nfunction handlePaste(e, cm) {\n  var pasted = e.clipboardData && e.clipboardData.getData(\"Text\")\n  if (pasted) {\n    e.preventDefault()\n    if (!cm.isReadOnly() && !cm.options.disableInput)\n      { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, \"paste\"); }) }\n    return true\n  }\n}\n\nfunction triggerElectric(cm, inserted) {\n  // When an 'electric' character is inserted, immediately trigger a reindent\n  if (!cm.options.electricChars || !cm.options.smartIndent) { return }\n  var sel = cm.doc.sel\n\n  for (var i = sel.ranges.length - 1; i >= 0; i--) {\n    var range$$1 = sel.ranges[i]\n    if (range$$1.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range$$1.head.line)) { continue }\n    var mode = cm.getModeAt(range$$1.head)\n    var indented = false\n    if (mode.electricChars) {\n      for (var j = 0; j < mode.electricChars.length; j++)\n        { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) {\n          indented = indentLine(cm, range$$1.head.line, \"smart\")\n          break\n        } }\n    } else if (mode.electricInput) {\n      if (mode.electricInput.test(getLine(cm.doc, range$$1.head.line).text.slice(0, range$$1.head.ch)))\n        { indented = indentLine(cm, range$$1.head.line, \"smart\") }\n    }\n    if (indented) { signalLater(cm, \"electricInput\", cm, range$$1.head.line) }\n  }\n}\n\nfunction copyableRanges(cm) {\n  var text = [], ranges = []\n  for (var i = 0; i < cm.doc.sel.ranges.length; i++) {\n    var line = cm.doc.sel.ranges[i].head.line\n    var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}\n    ranges.push(lineRange)\n    text.push(cm.getRange(lineRange.anchor, lineRange.head))\n  }\n  return {text: text, ranges: ranges}\n}\n\nfunction disableBrowserMagic(field, spellcheck) {\n  field.setAttribute(\"autocorrect\", \"off\")\n  field.setAttribute(\"autocapitalize\", \"off\")\n  field.setAttribute(\"spellcheck\", !!spellcheck)\n}\n\nfunction hiddenTextarea() {\n  var te = elt(\"textarea\", null, null, \"position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none\")\n  var div = elt(\"div\", [te], null, \"overflow: hidden; position: relative; width: 3px; height: 0px;\")\n  // The textarea is kept positioned near the cursor to prevent the\n  // fact that it'll be scrolled into view on input from scrolling\n  // our fake cursor out of view. On webkit, when wrap=off, paste is\n  // very slow. So make the area wide instead.\n  if (webkit) { te.style.width = \"1000px\" }\n  else { te.setAttribute(\"wrap\", \"off\") }\n  // If border: 0; -- iOS fails to open keyboard (issue #1287)\n  if (ios) { te.style.border = \"1px solid black\" }\n  disableBrowserMagic(te)\n  return div\n}\n\n// The publicly visible API. Note that methodOp(f) means\n// 'wrap f in an operation, performed on its `this` parameter'.\n\n// This is not the complete set of editor methods. Most of the\n// methods defined on the Doc type are also injected into\n// CodeMirror.prototype, for backwards compatibility and\n// convenience.\n\nvar addEditorMethods = function(CodeMirror) {\n  var optionHandlers = CodeMirror.optionHandlers\n\n  var helpers = CodeMirror.helpers = {}\n\n  CodeMirror.prototype = {\n    constructor: CodeMirror,\n    focus: function(){window.focus(); this.display.input.focus()},\n\n    setOption: function(option, value) {\n      var options = this.options, old = options[option]\n      if (options[option] == value && option != \"mode\") { return }\n      options[option] = value\n      if (optionHandlers.hasOwnProperty(option))\n        { operation(this, optionHandlers[option])(this, value, old) }\n    },\n\n    getOption: function(option) {return this.options[option]},\n    getDoc: function() {return this.doc},\n\n    addKeyMap: function(map$$1, bottom) {\n      this.state.keyMaps[bottom ? \"push\" : \"unshift\"](getKeyMap(map$$1))\n    },\n    removeKeyMap: function(map$$1) {\n      var maps = this.state.keyMaps\n      for (var i = 0; i < maps.length; ++i)\n        { if (maps[i] == map$$1 || maps[i].name == map$$1) {\n          maps.splice(i, 1)\n          return true\n        } }\n    },\n\n    addOverlay: methodOp(function(spec, options) {\n      var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec)\n      if (mode.startState) { throw new Error(\"Overlays may not be stateful.\") }\n      insertSorted(this.state.overlays,\n                   {mode: mode, modeSpec: spec, opaque: options && options.opaque,\n                    priority: (options && options.priority) || 0},\n                   function (overlay) { return overlay.priority; })\n      this.state.modeGen++\n      regChange(this)\n    }),\n    removeOverlay: methodOp(function(spec) {\n      var this$1 = this;\n\n      var overlays = this.state.overlays\n      for (var i = 0; i < overlays.length; ++i) {\n        var cur = overlays[i].modeSpec\n        if (cur == spec || typeof spec == \"string\" && cur.name == spec) {\n          overlays.splice(i, 1)\n          this$1.state.modeGen++\n          regChange(this$1)\n          return\n        }\n      }\n    }),\n\n    indentLine: methodOp(function(n, dir, aggressive) {\n      if (typeof dir != \"string\" && typeof dir != \"number\") {\n        if (dir == null) { dir = this.options.smartIndent ? \"smart\" : \"prev\" }\n        else { dir = dir ? \"add\" : \"subtract\" }\n      }\n      if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive) }\n    }),\n    indentSelection: methodOp(function(how) {\n      var this$1 = this;\n\n      var ranges = this.doc.sel.ranges, end = -1\n      for (var i = 0; i < ranges.length; i++) {\n        var range$$1 = ranges[i]\n        if (!range$$1.empty()) {\n          var from = range$$1.from(), to = range$$1.to()\n          var start = Math.max(end, from.line)\n          end = Math.min(this$1.lastLine(), to.line - (to.ch ? 0 : 1)) + 1\n          for (var j = start; j < end; ++j)\n            { indentLine(this$1, j, how) }\n          var newRanges = this$1.doc.sel.ranges\n          if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0)\n            { replaceOneSelection(this$1.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll) }\n        } else if (range$$1.head.line > end) {\n          indentLine(this$1, range$$1.head.line, how, true)\n          end = range$$1.head.line\n          if (i == this$1.doc.sel.primIndex) { ensureCursorVisible(this$1) }\n        }\n      }\n    }),\n\n    // Fetch the parser token for a given character. Useful for hacks\n    // that want to inspect the mode state (say, for completion).\n    getTokenAt: function(pos, precise) {\n      return takeToken(this, pos, precise)\n    },\n\n    getLineTokens: function(line, precise) {\n      return takeToken(this, Pos(line), precise, true)\n    },\n\n    getTokenTypeAt: function(pos) {\n      pos = clipPos(this.doc, pos)\n      var styles = getLineStyles(this, getLine(this.doc, pos.line))\n      var before = 0, after = (styles.length - 1) / 2, ch = pos.ch\n      var type\n      if (ch == 0) { type = styles[2] }\n      else { for (;;) {\n        var mid = (before + after) >> 1\n        if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid }\n        else if (styles[mid * 2 + 1] < ch) { before = mid + 1 }\n        else { type = styles[mid * 2 + 2]; break }\n      } }\n      var cut = type ? type.indexOf(\"overlay \") : -1\n      return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1)\n    },\n\n    getModeAt: function(pos) {\n      var mode = this.doc.mode\n      if (!mode.innerMode) { return mode }\n      return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode\n    },\n\n    getHelper: function(pos, type) {\n      return this.getHelpers(pos, type)[0]\n    },\n\n    getHelpers: function(pos, type) {\n      var this$1 = this;\n\n      var found = []\n      if (!helpers.hasOwnProperty(type)) { return found }\n      var help = helpers[type], mode = this.getModeAt(pos)\n      if (typeof mode[type] == \"string\") {\n        if (help[mode[type]]) { found.push(help[mode[type]]) }\n      } else if (mode[type]) {\n        for (var i = 0; i < mode[type].length; i++) {\n          var val = help[mode[type][i]]\n          if (val) { found.push(val) }\n        }\n      } else if (mode.helperType && help[mode.helperType]) {\n        found.push(help[mode.helperType])\n      } else if (help[mode.name]) {\n        found.push(help[mode.name])\n      }\n      for (var i$1 = 0; i$1 < help._global.length; i$1++) {\n        var cur = help._global[i$1]\n        if (cur.pred(mode, this$1) && indexOf(found, cur.val) == -1)\n          { found.push(cur.val) }\n      }\n      return found\n    },\n\n    getStateAfter: function(line, precise) {\n      var doc = this.doc\n      line = clipLine(doc, line == null ? doc.first + doc.size - 1: line)\n      return getStateBefore(this, line + 1, precise)\n    },\n\n    cursorCoords: function(start, mode) {\n      var pos, range$$1 = this.doc.sel.primary()\n      if (start == null) { pos = range$$1.head }\n      else if (typeof start == \"object\") { pos = clipPos(this.doc, start) }\n      else { pos = start ? range$$1.from() : range$$1.to() }\n      return cursorCoords(this, pos, mode || \"page\")\n    },\n\n    charCoords: function(pos, mode) {\n      return charCoords(this, clipPos(this.doc, pos), mode || \"page\")\n    },\n\n    coordsChar: function(coords, mode) {\n      coords = fromCoordSystem(this, coords, mode || \"page\")\n      return coordsChar(this, coords.left, coords.top)\n    },\n\n    lineAtHeight: function(height, mode) {\n      height = fromCoordSystem(this, {top: height, left: 0}, mode || \"page\").top\n      return lineAtHeight(this.doc, height + this.display.viewOffset)\n    },\n    heightAtLine: function(line, mode) {\n      var end = false, lineObj\n      if (typeof line == \"number\") {\n        var last = this.doc.first + this.doc.size - 1\n        if (line < this.doc.first) { line = this.doc.first }\n        else if (line > last) { line = last; end = true }\n        lineObj = getLine(this.doc, line)\n      } else {\n        lineObj = line\n      }\n      return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || \"page\").top +\n        (end ? this.doc.height - heightAtLine(lineObj) : 0)\n    },\n\n    defaultTextHeight: function() { return textHeight(this.display) },\n    defaultCharWidth: function() { return charWidth(this.display) },\n\n    setGutterMarker: methodOp(function(line, gutterID, value) {\n      return changeLine(this.doc, line, \"gutter\", function (line) {\n        var markers = line.gutterMarkers || (line.gutterMarkers = {})\n        markers[gutterID] = value\n        if (!value && isEmpty(markers)) { line.gutterMarkers = null }\n        return true\n      })\n    }),\n\n    clearGutter: methodOp(function(gutterID) {\n      var this$1 = this;\n\n      var doc = this.doc, i = doc.first\n      doc.iter(function (line) {\n        if (line.gutterMarkers && line.gutterMarkers[gutterID]) {\n          line.gutterMarkers[gutterID] = null\n          regLineChange(this$1, i, \"gutter\")\n          if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null }\n        }\n        ++i\n      })\n    }),\n\n    lineInfo: function(line) {\n      var n\n      if (typeof line == \"number\") {\n        if (!isLine(this.doc, line)) { return null }\n        n = line\n        line = getLine(this.doc, line)\n        if (!line) { return null }\n      } else {\n        n = lineNo(line)\n        if (n == null) { return null }\n      }\n      return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,\n              textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,\n              widgets: line.widgets}\n    },\n\n    getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}},\n\n    addWidget: function(pos, node, scroll, vert, horiz) {\n      var display = this.display\n      pos = cursorCoords(this, clipPos(this.doc, pos))\n      var top = pos.bottom, left = pos.left\n      node.style.position = \"absolute\"\n      node.setAttribute(\"cm-ignore-events\", \"true\")\n      this.display.input.setUneditable(node)\n      display.sizer.appendChild(node)\n      if (vert == \"over\") {\n        top = pos.top\n      } else if (vert == \"above\" || vert == \"near\") {\n        var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),\n        hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth)\n        // Default to positioning above (if specified and possible); otherwise default to positioning below\n        if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)\n          { top = pos.top - node.offsetHeight }\n        else if (pos.bottom + node.offsetHeight <= vspace)\n          { top = pos.bottom }\n        if (left + node.offsetWidth > hspace)\n          { left = hspace - node.offsetWidth }\n      }\n      node.style.top = top + \"px\"\n      node.style.left = node.style.right = \"\"\n      if (horiz == \"right\") {\n        left = display.sizer.clientWidth - node.offsetWidth\n        node.style.right = \"0px\"\n      } else {\n        if (horiz == \"left\") { left = 0 }\n        else if (horiz == \"middle\") { left = (display.sizer.clientWidth - node.offsetWidth) / 2 }\n        node.style.left = left + \"px\"\n      }\n      if (scroll)\n        { scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight) }\n    },\n\n    triggerOnKeyDown: methodOp(onKeyDown),\n    triggerOnKeyPress: methodOp(onKeyPress),\n    triggerOnKeyUp: onKeyUp,\n\n    execCommand: function(cmd) {\n      if (commands.hasOwnProperty(cmd))\n        { return commands[cmd].call(null, this) }\n    },\n\n    triggerElectric: methodOp(function(text) { triggerElectric(this, text) }),\n\n    findPosH: function(from, amount, unit, visually) {\n      var this$1 = this;\n\n      var dir = 1\n      if (amount < 0) { dir = -1; amount = -amount }\n      var cur = clipPos(this.doc, from)\n      for (var i = 0; i < amount; ++i) {\n        cur = findPosH(this$1.doc, cur, dir, unit, visually)\n        if (cur.hitSide) { break }\n      }\n      return cur\n    },\n\n    moveH: methodOp(function(dir, unit) {\n      var this$1 = this;\n\n      this.extendSelectionsBy(function (range$$1) {\n        if (this$1.display.shift || this$1.doc.extend || range$$1.empty())\n          { return findPosH(this$1.doc, range$$1.head, dir, unit, this$1.options.rtlMoveVisually) }\n        else\n          { return dir < 0 ? range$$1.from() : range$$1.to() }\n      }, sel_move)\n    }),\n\n    deleteH: methodOp(function(dir, unit) {\n      var sel = this.doc.sel, doc = this.doc\n      if (sel.somethingSelected())\n        { doc.replaceSelection(\"\", null, \"+delete\") }\n      else\n        { deleteNearSelection(this, function (range$$1) {\n          var other = findPosH(doc, range$$1.head, dir, unit, false)\n          return dir < 0 ? {from: other, to: range$$1.head} : {from: range$$1.head, to: other}\n        }) }\n    }),\n\n    findPosV: function(from, amount, unit, goalColumn) {\n      var this$1 = this;\n\n      var dir = 1, x = goalColumn\n      if (amount < 0) { dir = -1; amount = -amount }\n      var cur = clipPos(this.doc, from)\n      for (var i = 0; i < amount; ++i) {\n        var coords = cursorCoords(this$1, cur, \"div\")\n        if (x == null) { x = coords.left }\n        else { coords.left = x }\n        cur = findPosV(this$1, coords, dir, unit)\n        if (cur.hitSide) { break }\n      }\n      return cur\n    },\n\n    moveV: methodOp(function(dir, unit) {\n      var this$1 = this;\n\n      var doc = this.doc, goals = []\n      var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected()\n      doc.extendSelectionsBy(function (range$$1) {\n        if (collapse)\n          { return dir < 0 ? range$$1.from() : range$$1.to() }\n        var headPos = cursorCoords(this$1, range$$1.head, \"div\")\n        if (range$$1.goalColumn != null) { headPos.left = range$$1.goalColumn }\n        goals.push(headPos.left)\n        var pos = findPosV(this$1, headPos, dir, unit)\n        if (unit == \"page\" && range$$1 == doc.sel.primary())\n          { addToScrollPos(this$1, null, charCoords(this$1, pos, \"div\").top - headPos.top) }\n        return pos\n      }, sel_move)\n      if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++)\n        { doc.sel.ranges[i].goalColumn = goals[i] } }\n    }),\n\n    // Find the word at the given position (as returned by coordsChar).\n    findWordAt: function(pos) {\n      var doc = this.doc, line = getLine(doc, pos.line).text\n      var start = pos.ch, end = pos.ch\n      if (line) {\n        var helper = this.getHelper(pos, \"wordChars\")\n        if ((pos.xRel < 0 || end == line.length) && start) { --start; } else { ++end }\n        var startChar = line.charAt(start)\n        var check = isWordChar(startChar, helper)\n          ? function (ch) { return isWordChar(ch, helper); }\n          : /\\s/.test(startChar) ? function (ch) { return /\\s/.test(ch); }\n          : function (ch) { return (!/\\s/.test(ch) && !isWordChar(ch)); }\n        while (start > 0 && check(line.charAt(start - 1))) { --start }\n        while (end < line.length && check(line.charAt(end))) { ++end }\n      }\n      return new Range(Pos(pos.line, start), Pos(pos.line, end))\n    },\n\n    toggleOverwrite: function(value) {\n      if (value != null && value == this.state.overwrite) { return }\n      if (this.state.overwrite = !this.state.overwrite)\n        { addClass(this.display.cursorDiv, \"CodeMirror-overwrite\") }\n      else\n        { rmClass(this.display.cursorDiv, \"CodeMirror-overwrite\") }\n\n      signal(this, \"overwriteToggle\", this, this.state.overwrite)\n    },\n    hasFocus: function() { return this.display.input.getField() == activeElt() },\n    isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) },\n\n    scrollTo: methodOp(function(x, y) {\n      if (x != null || y != null) { resolveScrollToPos(this) }\n      if (x != null) { this.curOp.scrollLeft = x }\n      if (y != null) { this.curOp.scrollTop = y }\n    }),\n    getScrollInfo: function() {\n      var scroller = this.display.scroller\n      return {left: scroller.scrollLeft, top: scroller.scrollTop,\n              height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight,\n              width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth,\n              clientHeight: displayHeight(this), clientWidth: displayWidth(this)}\n    },\n\n    scrollIntoView: methodOp(function(range$$1, margin) {\n      if (range$$1 == null) {\n        range$$1 = {from: this.doc.sel.primary().head, to: null}\n        if (margin == null) { margin = this.options.cursorScrollMargin }\n      } else if (typeof range$$1 == \"number\") {\n        range$$1 = {from: Pos(range$$1, 0), to: null}\n      } else if (range$$1.from == null) {\n        range$$1 = {from: range$$1, to: null}\n      }\n      if (!range$$1.to) { range$$1.to = range$$1.from }\n      range$$1.margin = margin || 0\n\n      if (range$$1.from.line != null) {\n        resolveScrollToPos(this)\n        this.curOp.scrollToPos = range$$1\n      } else {\n        var sPos = calculateScrollPos(this, Math.min(range$$1.from.left, range$$1.to.left),\n                                      Math.min(range$$1.from.top, range$$1.to.top) - range$$1.margin,\n                                      Math.max(range$$1.from.right, range$$1.to.right),\n                                      Math.max(range$$1.from.bottom, range$$1.to.bottom) + range$$1.margin)\n        this.scrollTo(sPos.scrollLeft, sPos.scrollTop)\n      }\n    }),\n\n    setSize: methodOp(function(width, height) {\n      var this$1 = this;\n\n      var interpret = function (val) { return typeof val == \"number\" || /^\\d+$/.test(String(val)) ? val + \"px\" : val; }\n      if (width != null) { this.display.wrapper.style.width = interpret(width) }\n      if (height != null) { this.display.wrapper.style.height = interpret(height) }\n      if (this.options.lineWrapping) { clearLineMeasurementCache(this) }\n      var lineNo$$1 = this.display.viewFrom\n      this.doc.iter(lineNo$$1, this.display.viewTo, function (line) {\n        if (line.widgets) { for (var i = 0; i < line.widgets.length; i++)\n          { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo$$1, \"widget\"); break } } }\n        ++lineNo$$1\n      })\n      this.curOp.forceUpdate = true\n      signal(this, \"refresh\", this)\n    }),\n\n    operation: function(f){return runInOp(this, f)},\n\n    refresh: methodOp(function() {\n      var oldHeight = this.display.cachedTextHeight\n      regChange(this)\n      this.curOp.forceUpdate = true\n      clearCaches(this)\n      this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop)\n      updateGutterSpace(this)\n      if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5)\n        { estimateLineHeights(this) }\n      signal(this, \"refresh\", this)\n    }),\n\n    swapDoc: methodOp(function(doc) {\n      var old = this.doc\n      old.cm = null\n      attachDoc(this, doc)\n      clearCaches(this)\n      this.display.input.reset()\n      this.scrollTo(doc.scrollLeft, doc.scrollTop)\n      this.curOp.forceScroll = true\n      signalLater(this, \"swapDoc\", this, old)\n      return old\n    }),\n\n    getInputField: function(){return this.display.input.getField()},\n    getWrapperElement: function(){return this.display.wrapper},\n    getScrollerElement: function(){return this.display.scroller},\n    getGutterElement: function(){return this.display.gutters}\n  }\n  eventMixin(CodeMirror)\n\n  CodeMirror.registerHelper = function(type, name, value) {\n    if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []} }\n    helpers[type][name] = value\n  }\n  CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {\n    CodeMirror.registerHelper(type, name, value)\n    helpers[type]._global.push({pred: predicate, val: value})\n  }\n}\n\n// Used for horizontal relative motion. Dir is -1 or 1 (left or\n// right), unit can be \"char\", \"column\" (like char, but doesn't\n// cross line boundaries), \"word\" (across next word), or \"group\" (to\n// the start of next group of word or non-word-non-whitespace\n// chars). The visually param controls whether, in right-to-left\n// text, direction 1 means to move towards the next index in the\n// string, or towards the character to the right of the current\n// position. The resulting position will have a hitSide=true\n// property if it reached the end of the document.\nfunction findPosH(doc, pos, dir, unit, visually) {\n  var line = pos.line, ch = pos.ch, origDir = dir\n  var lineObj = getLine(doc, line)\n  function findNextLine() {\n    var l = line + dir\n    if (l < doc.first || l >= doc.first + doc.size) { return false }\n    line = l\n    return lineObj = getLine(doc, l)\n  }\n  function moveOnce(boundToLine) {\n    var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true)\n    if (next == null) {\n      if (!boundToLine && findNextLine()) {\n        if (visually) { ch = (dir < 0 ? lineRight : lineLeft)(lineObj) }\n        else { ch = dir < 0 ? lineObj.text.length : 0 }\n      } else { return false }\n    } else { ch = next }\n    return true\n  }\n\n  if (unit == \"char\") {\n    moveOnce()\n  } else if (unit == \"column\") {\n    moveOnce(true)\n  } else if (unit == \"word\" || unit == \"group\") {\n    var sawType = null, group = unit == \"group\"\n    var helper = doc.cm && doc.cm.getHelper(pos, \"wordChars\")\n    for (var first = true;; first = false) {\n      if (dir < 0 && !moveOnce(!first)) { break }\n      var cur = lineObj.text.charAt(ch) || \"\\n\"\n      var type = isWordChar(cur, helper) ? \"w\"\n        : group && cur == \"\\n\" ? \"n\"\n        : !group || /\\s/.test(cur) ? null\n        : \"p\"\n      if (group && !first && !type) { type = \"s\" }\n      if (sawType && sawType != type) {\n        if (dir < 0) {dir = 1; moveOnce()}\n        break\n      }\n\n      if (type) { sawType = type }\n      if (dir > 0 && !moveOnce(!first)) { break }\n    }\n  }\n  var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true)\n  if (!cmp(pos, result)) { result.hitSide = true }\n  return result\n}\n\n// For relative vertical movement. Dir may be -1 or 1. Unit can be\n// \"page\" or \"line\". The resulting position will have a hitSide=true\n// property if it reached the end of the document.\nfunction findPosV(cm, pos, dir, unit) {\n  var doc = cm.doc, x = pos.left, y\n  if (unit == \"page\") {\n    var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight)\n    var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3)\n    y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount\n\n  } else if (unit == \"line\") {\n    y = dir > 0 ? pos.bottom + 3 : pos.top - 3\n  }\n  var target\n  for (;;) {\n    target = coordsChar(cm, x, y)\n    if (!target.outside) { break }\n    if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break }\n    y += dir * 5\n  }\n  return target\n}\n\n// CONTENTEDITABLE INPUT STYLE\n\nfunction ContentEditableInput(cm) {\n  this.cm = cm\n  this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null\n  this.polling = new Delayed()\n  this.gracePeriod = false\n}\n\nContentEditableInput.prototype = copyObj({\n  init: function(display) {\n    var input = this, cm = input.cm\n    var div = input.div = display.lineDiv\n    disableBrowserMagic(div, cm.options.spellcheck)\n\n    on(div, \"paste\", function (e) {\n      if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return }\n      // IE doesn't fire input events, so we schedule a read for the pasted content in this way\n      if (ie_version <= 11) { setTimeout(operation(cm, function () {\n        if (!input.pollContent()) { regChange(cm) }\n      }), 20) }\n    })\n\n    on(div, \"compositionstart\", function (e) {\n      var data = e.data\n      input.composing = {sel: cm.doc.sel, data: data, startData: data}\n      if (!data) { return }\n      var prim = cm.doc.sel.primary()\n      var line = cm.getLine(prim.head.line)\n      var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length))\n      if (found > -1 && found <= prim.head.ch)\n        { input.composing.sel = simpleSelection(Pos(prim.head.line, found),\n                                              Pos(prim.head.line, found + data.length)) }\n    })\n    on(div, \"compositionupdate\", function (e) { return input.composing.data = e.data; })\n    on(div, \"compositionend\", function (e) {\n      var ours = input.composing\n      if (!ours) { return }\n      if (e.data != ours.startData && !/\\u200b/.test(e.data))\n        { ours.data = e.data }\n      // Need a small delay to prevent other code (input event,\n      // selection polling) from doing damage when fired right after\n      // compositionend.\n      setTimeout(function () {\n        if (!ours.handled)\n          { input.applyComposition(ours) }\n        if (input.composing == ours)\n          { input.composing = null }\n      }, 50)\n    })\n\n    on(div, \"touchstart\", function () { return input.forceCompositionEnd(); })\n\n    on(div, \"input\", function () {\n      if (input.composing) { return }\n      if (cm.isReadOnly() || !input.pollContent())\n        { runInOp(input.cm, function () { return regChange(cm); }) }\n    })\n\n    function onCopyCut(e) {\n      if (signalDOMEvent(cm, e)) { return }\n      if (cm.somethingSelected()) {\n        setLastCopied({lineWise: false, text: cm.getSelections()})\n        if (e.type == \"cut\") { cm.replaceSelection(\"\", null, \"cut\") }\n      } else if (!cm.options.lineWiseCopyCut) {\n        return\n      } else {\n        var ranges = copyableRanges(cm)\n        setLastCopied({lineWise: true, text: ranges.text})\n        if (e.type == \"cut\") {\n          cm.operation(function () {\n            cm.setSelections(ranges.ranges, 0, sel_dontScroll)\n            cm.replaceSelection(\"\", null, \"cut\")\n          })\n        }\n      }\n      if (e.clipboardData) {\n        e.clipboardData.clearData()\n        var content = lastCopied.text.join(\"\\n\")\n        // iOS exposes the clipboard API, but seems to discard content inserted into it\n        e.clipboardData.setData(\"Text\", content)\n        if (e.clipboardData.getData(\"Text\") == content) {\n          e.preventDefault()\n          return\n        }\n      }\n      // Old-fashioned briefly-focus-a-textarea hack\n      var kludge = hiddenTextarea(), te = kludge.firstChild\n      cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild)\n      te.value = lastCopied.text.join(\"\\n\")\n      var hadFocus = document.activeElement\n      selectInput(te)\n      setTimeout(function () {\n        cm.display.lineSpace.removeChild(kludge)\n        hadFocus.focus()\n        if (hadFocus == div) { input.showPrimarySelection() }\n      }, 50)\n    }\n    on(div, \"copy\", onCopyCut)\n    on(div, \"cut\", onCopyCut)\n  },\n\n  prepareSelection: function() {\n    var result = prepareSelection(this.cm, false)\n    result.focus = this.cm.state.focused\n    return result\n  },\n\n  showSelection: function(info, takeFocus) {\n    if (!info || !this.cm.display.view.length) { return }\n    if (info.focus || takeFocus) { this.showPrimarySelection() }\n    this.showMultipleSelections(info)\n  },\n\n  showPrimarySelection: function() {\n    var sel = window.getSelection(), prim = this.cm.doc.sel.primary()\n    var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset)\n    var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset)\n    if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad &&\n        cmp(minPos(curAnchor, curFocus), prim.from()) == 0 &&\n        cmp(maxPos(curAnchor, curFocus), prim.to()) == 0)\n      { return }\n\n    var start = posToDOM(this.cm, prim.from())\n    var end = posToDOM(this.cm, prim.to())\n    if (!start && !end) { return }\n\n    var view = this.cm.display.view\n    var old = sel.rangeCount && sel.getRangeAt(0)\n    if (!start) {\n      start = {node: view[0].measure.map[2], offset: 0}\n    } else if (!end) { // FIXME dangerously hacky\n      var measure = view[view.length - 1].measure\n      var map$$1 = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map\n      end = {node: map$$1[map$$1.length - 1], offset: map$$1[map$$1.length - 2] - map$$1[map$$1.length - 3]}\n    }\n\n    var rng\n    try { rng = range(start.node, start.offset, end.offset, end.node) }\n    catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible\n    if (rng) {\n      if (!gecko && this.cm.state.focused) {\n        sel.collapse(start.node, start.offset)\n        if (!rng.collapsed) {\n          sel.removeAllRanges()\n          sel.addRange(rng)\n        }\n      } else {\n        sel.removeAllRanges()\n        sel.addRange(rng)\n      }\n      if (old && sel.anchorNode == null) { sel.addRange(old) }\n      else if (gecko) { this.startGracePeriod() }\n    }\n    this.rememberSelection()\n  },\n\n  startGracePeriod: function() {\n    var this$1 = this;\n\n    clearTimeout(this.gracePeriod)\n    this.gracePeriod = setTimeout(function () {\n      this$1.gracePeriod = false\n      if (this$1.selectionChanged())\n        { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }) }\n    }, 20)\n  },\n\n  showMultipleSelections: function(info) {\n    removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors)\n    removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection)\n  },\n\n  rememberSelection: function() {\n    var sel = window.getSelection()\n    this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset\n    this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset\n  },\n\n  selectionInEditor: function() {\n    var sel = window.getSelection()\n    if (!sel.rangeCount) { return false }\n    var node = sel.getRangeAt(0).commonAncestorContainer\n    return contains(this.div, node)\n  },\n\n  focus: function() {\n    if (this.cm.options.readOnly != \"nocursor\") { this.div.focus() }\n  },\n  blur: function() { this.div.blur() },\n  getField: function() { return this.div },\n\n  supportsTouch: function() { return true },\n\n  receivedFocus: function() {\n    var input = this\n    if (this.selectionInEditor())\n      { this.pollSelection() }\n    else\n      { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }) }\n\n    function poll() {\n      if (input.cm.state.focused) {\n        input.pollSelection()\n        input.polling.set(input.cm.options.pollInterval, poll)\n      }\n    }\n    this.polling.set(this.cm.options.pollInterval, poll)\n  },\n\n  selectionChanged: function() {\n    var sel = window.getSelection()\n    return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||\n      sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset\n  },\n\n  pollSelection: function() {\n    if (!this.composing && !this.gracePeriod && this.selectionChanged()) {\n      var sel = window.getSelection(), cm = this.cm\n      this.rememberSelection()\n      var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset)\n      var head = domToPos(cm, sel.focusNode, sel.focusOffset)\n      if (anchor && head) { runInOp(cm, function () {\n        setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll)\n        if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true }\n      }) }\n    }\n  },\n\n  pollContent: function() {\n    var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary()\n    var from = sel.from(), to = sel.to()\n    if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false }\n\n    var fromIndex, fromLine, fromNode\n    if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) {\n      fromLine = lineNo(display.view[0].line)\n      fromNode = display.view[0].node\n    } else {\n      fromLine = lineNo(display.view[fromIndex].line)\n      fromNode = display.view[fromIndex - 1].node.nextSibling\n    }\n    var toIndex = findViewIndex(cm, to.line)\n    var toLine, toNode\n    if (toIndex == display.view.length - 1) {\n      toLine = display.viewTo - 1\n      toNode = display.lineDiv.lastChild\n    } else {\n      toLine = lineNo(display.view[toIndex + 1].line) - 1\n      toNode = display.view[toIndex + 1].node.previousSibling\n    }\n\n    var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine))\n    var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length))\n    while (newText.length > 1 && oldText.length > 1) {\n      if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine-- }\n      else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++ }\n      else { break }\n    }\n\n    var cutFront = 0, cutEnd = 0\n    var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length)\n    while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))\n      { ++cutFront }\n    var newBot = lst(newText), oldBot = lst(oldText)\n    var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0),\n                             oldBot.length - (oldText.length == 1 ? cutFront : 0))\n    while (cutEnd < maxCutEnd &&\n           newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1))\n      { ++cutEnd }\n\n    newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd)\n    newText[0] = newText[0].slice(cutFront)\n\n    var chFrom = Pos(fromLine, cutFront)\n    var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0)\n    if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {\n      replaceRange(cm.doc, newText, chFrom, chTo, \"+input\")\n      return true\n    }\n  },\n\n  ensurePolled: function() {\n    this.forceCompositionEnd()\n  },\n  reset: function() {\n    this.forceCompositionEnd()\n  },\n  forceCompositionEnd: function() {\n    if (!this.composing || this.composing.handled) { return }\n    this.applyComposition(this.composing)\n    this.composing.handled = true\n    this.div.blur()\n    this.div.focus()\n  },\n  applyComposition: function(composing) {\n    if (this.cm.isReadOnly())\n      { operation(this.cm, regChange)(this.cm) }\n    else if (composing.data && composing.data != composing.startData)\n      { operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel) }\n  },\n\n  setUneditable: function(node) {\n    node.contentEditable = \"false\"\n  },\n\n  onKeyPress: function(e) {\n    e.preventDefault()\n    if (!this.cm.isReadOnly())\n      { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0) }\n  },\n\n  readOnlyChanged: function(val) {\n    this.div.contentEditable = String(val != \"nocursor\")\n  },\n\n  onContextMenu: nothing,\n  resetPosition: nothing,\n\n  needsContentAttribute: true\n  }, ContentEditableInput.prototype)\n\nfunction posToDOM(cm, pos) {\n  var view = findViewForLine(cm, pos.line)\n  if (!view || view.hidden) { return null }\n  var line = getLine(cm.doc, pos.line)\n  var info = mapFromLineView(view, line, pos.line)\n\n  var order = getOrder(line), side = \"left\"\n  if (order) {\n    var partPos = getBidiPartAt(order, pos.ch)\n    side = partPos % 2 ? \"right\" : \"left\"\n  }\n  var result = nodeAndOffsetInLineMap(info.map, pos.ch, side)\n  result.offset = result.collapse == \"right\" ? result.end : result.start\n  return result\n}\n\nfunction badPos(pos, bad) { if (bad) { pos.bad = true; } return pos }\n\nfunction domTextBetween(cm, from, to, fromLine, toLine) {\n  var text = \"\", closing = false, lineSep = cm.doc.lineSeparator()\n  function recognizeMarker(id) { return function (marker) { return marker.id == id; } }\n  function walk(node) {\n    if (node.nodeType == 1) {\n      var cmText = node.getAttribute(\"cm-text\")\n      if (cmText != null) {\n        if (cmText == \"\") { cmText = node.textContent.replace(/\\u200b/g, \"\") }\n        text += cmText\n        return\n      }\n      var markerID = node.getAttribute(\"cm-marker\"), range$$1\n      if (markerID) {\n        var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID))\n        if (found.length && (range$$1 = found[0].find()))\n          { text += getBetween(cm.doc, range$$1.from, range$$1.to).join(lineSep) }\n        return\n      }\n      if (node.getAttribute(\"contenteditable\") == \"false\") { return }\n      for (var i = 0; i < node.childNodes.length; i++)\n        { walk(node.childNodes[i]) }\n      if (/^(pre|div|p)$/i.test(node.nodeName))\n        { closing = true }\n    } else if (node.nodeType == 3) {\n      var val = node.nodeValue\n      if (!val) { return }\n      if (closing) {\n        text += lineSep\n        closing = false\n      }\n      text += val\n    }\n  }\n  for (;;) {\n    walk(from)\n    if (from == to) { break }\n    from = from.nextSibling\n  }\n  return text\n}\n\nfunction domToPos(cm, node, offset) {\n  var lineNode\n  if (node == cm.display.lineDiv) {\n    lineNode = cm.display.lineDiv.childNodes[offset]\n    if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) }\n    node = null; offset = 0\n  } else {\n    for (lineNode = node;; lineNode = lineNode.parentNode) {\n      if (!lineNode || lineNode == cm.display.lineDiv) { return null }\n      if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break }\n    }\n  }\n  for (var i = 0; i < cm.display.view.length; i++) {\n    var lineView = cm.display.view[i]\n    if (lineView.node == lineNode)\n      { return locateNodeInLineView(lineView, node, offset) }\n  }\n}\n\nfunction locateNodeInLineView(lineView, node, offset) {\n  var wrapper = lineView.text.firstChild, bad = false\n  if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) }\n  if (node == wrapper) {\n    bad = true\n    node = wrapper.childNodes[offset]\n    offset = 0\n    if (!node) {\n      var line = lineView.rest ? lst(lineView.rest) : lineView.line\n      return badPos(Pos(lineNo(line), line.text.length), bad)\n    }\n  }\n\n  var textNode = node.nodeType == 3 ? node : null, topNode = node\n  if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {\n    textNode = node.firstChild\n    if (offset) { offset = textNode.nodeValue.length }\n  }\n  while (topNode.parentNode != wrapper) { topNode = topNode.parentNode }\n  var measure = lineView.measure, maps = measure.maps\n\n  function find(textNode, topNode, offset) {\n    for (var i = -1; i < (maps ? maps.length : 0); i++) {\n      var map$$1 = i < 0 ? measure.map : maps[i]\n      for (var j = 0; j < map$$1.length; j += 3) {\n        var curNode = map$$1[j + 2]\n        if (curNode == textNode || curNode == topNode) {\n          var line = lineNo(i < 0 ? lineView.line : lineView.rest[i])\n          var ch = map$$1[j] + offset\n          if (offset < 0 || curNode != textNode) { ch = map$$1[j + (offset ? 1 : 0)] }\n          return Pos(line, ch)\n        }\n      }\n    }\n  }\n  var found = find(textNode, topNode, offset)\n  if (found) { return badPos(found, bad) }\n\n  // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems\n  for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) {\n    found = find(after, after.firstChild, 0)\n    if (found)\n      { return badPos(Pos(found.line, found.ch - dist), bad) }\n    else\n      { dist += after.textContent.length }\n  }\n  for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) {\n    found = find(before, before.firstChild, -1)\n    if (found)\n      { return badPos(Pos(found.line, found.ch + dist$1), bad) }\n    else\n      { dist$1 += before.textContent.length }\n  }\n}\n\n// TEXTAREA INPUT STYLE\n\nfunction TextareaInput(cm) {\n  this.cm = cm\n  // See input.poll and input.reset\n  this.prevInput = \"\"\n\n  // Flag that indicates whether we expect input to appear real soon\n  // now (after some event like 'keypress' or 'input') and are\n  // polling intensively.\n  this.pollingFast = false\n  // Self-resetting timeout for the poller\n  this.polling = new Delayed()\n  // Tracks when input.reset has punted to just putting a short\n  // string into the textarea instead of the full selection.\n  this.inaccurateSelection = false\n  // Used to work around IE issue with selection being forgotten when focus moves away from textarea\n  this.hasSelection = false\n  this.composing = null\n}\n\nTextareaInput.prototype = copyObj({\n  init: function(display) {\n    var this$1 = this;\n\n    var input = this, cm = this.cm\n\n    // Wraps and hides input textarea\n    var div = this.wrapper = hiddenTextarea()\n    // The semihidden textarea that is focused when the editor is\n    // focused, and receives input.\n    var te = this.textarea = div.firstChild\n    display.wrapper.insertBefore(div, display.wrapper.firstChild)\n\n    // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore)\n    if (ios) { te.style.width = \"0px\" }\n\n    on(te, \"input\", function () {\n      if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null }\n      input.poll()\n    })\n\n    on(te, \"paste\", function (e) {\n      if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return }\n\n      cm.state.pasteIncoming = true\n      input.fastPoll()\n    })\n\n    function prepareCopyCut(e) {\n      if (signalDOMEvent(cm, e)) { return }\n      if (cm.somethingSelected()) {\n        setLastCopied({lineWise: false, text: cm.getSelections()})\n        if (input.inaccurateSelection) {\n          input.prevInput = \"\"\n          input.inaccurateSelection = false\n          te.value = lastCopied.text.join(\"\\n\")\n          selectInput(te)\n        }\n      } else if (!cm.options.lineWiseCopyCut) {\n        return\n      } else {\n        var ranges = copyableRanges(cm)\n        setLastCopied({lineWise: true, text: ranges.text})\n        if (e.type == \"cut\") {\n          cm.setSelections(ranges.ranges, null, sel_dontScroll)\n        } else {\n          input.prevInput = \"\"\n          te.value = ranges.text.join(\"\\n\")\n          selectInput(te)\n        }\n      }\n      if (e.type == \"cut\") { cm.state.cutIncoming = true }\n    }\n    on(te, \"cut\", prepareCopyCut)\n    on(te, \"copy\", prepareCopyCut)\n\n    on(display.scroller, \"paste\", function (e) {\n      if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return }\n      cm.state.pasteIncoming = true\n      input.focus()\n    })\n\n    // Prevent normal selection in the editor (we handle our own)\n    on(display.lineSpace, \"selectstart\", function (e) {\n      if (!eventInWidget(display, e)) { e_preventDefault(e) }\n    })\n\n    on(te, \"compositionstart\", function () {\n      var start = cm.getCursor(\"from\")\n      if (input.composing) { input.composing.range.clear() }\n      input.composing = {\n        start: start,\n        range: cm.markText(start, cm.getCursor(\"to\"), {className: \"CodeMirror-composing\"})\n      }\n    })\n    on(te, \"compositionend\", function () {\n      if (input.composing) {\n        input.poll()\n        input.composing.range.clear()\n        input.composing = null\n      }\n    })\n  },\n\n  prepareSelection: function() {\n    // Redraw the selection and/or cursor\n    var cm = this.cm, display = cm.display, doc = cm.doc\n    var result = prepareSelection(cm)\n\n    // Move the hidden textarea near the cursor to prevent scrolling artifacts\n    if (cm.options.moveInputWithCursor) {\n      var headPos = cursorCoords(cm, doc.sel.primary().head, \"div\")\n      var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect()\n      result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,\n                                          headPos.top + lineOff.top - wrapOff.top))\n      result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,\n                                           headPos.left + lineOff.left - wrapOff.left))\n    }\n\n    return result\n  },\n\n  showSelection: function(drawn) {\n    var cm = this.cm, display = cm.display\n    removeChildrenAndAdd(display.cursorDiv, drawn.cursors)\n    removeChildrenAndAdd(display.selectionDiv, drawn.selection)\n    if (drawn.teTop != null) {\n      this.wrapper.style.top = drawn.teTop + \"px\"\n      this.wrapper.style.left = drawn.teLeft + \"px\"\n    }\n  },\n\n  // Reset the input to correspond to the selection (or to be empty,\n  // when not typing and nothing is selected)\n  reset: function(typing) {\n    if (this.contextMenuPending) { return }\n    var minimal, selected, cm = this.cm, doc = cm.doc\n    if (cm.somethingSelected()) {\n      this.prevInput = \"\"\n      var range$$1 = doc.sel.primary()\n      minimal = hasCopyEvent &&\n        (range$$1.to().line - range$$1.from().line > 100 || (selected = cm.getSelection()).length > 1000)\n      var content = minimal ? \"-\" : selected || cm.getSelection()\n      this.textarea.value = content\n      if (cm.state.focused) { selectInput(this.textarea) }\n      if (ie && ie_version >= 9) { this.hasSelection = content }\n    } else if (!typing) {\n      this.prevInput = this.textarea.value = \"\"\n      if (ie && ie_version >= 9) { this.hasSelection = null }\n    }\n    this.inaccurateSelection = minimal\n  },\n\n  getField: function() { return this.textarea },\n\n  supportsTouch: function() { return false },\n\n  focus: function() {\n    if (this.cm.options.readOnly != \"nocursor\" && (!mobile || activeElt() != this.textarea)) {\n      try { this.textarea.focus() }\n      catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM\n    }\n  },\n\n  blur: function() { this.textarea.blur() },\n\n  resetPosition: function() {\n    this.wrapper.style.top = this.wrapper.style.left = 0\n  },\n\n  receivedFocus: function() { this.slowPoll() },\n\n  // Poll for input changes, using the normal rate of polling. This\n  // runs as long as the editor is focused.\n  slowPoll: function() {\n    var this$1 = this;\n\n    if (this.pollingFast) { return }\n    this.polling.set(this.cm.options.pollInterval, function () {\n      this$1.poll()\n      if (this$1.cm.state.focused) { this$1.slowPoll() }\n    })\n  },\n\n  // When an event has just come in that is likely to add or change\n  // something in the input textarea, we poll faster, to ensure that\n  // the change appears on the screen quickly.\n  fastPoll: function() {\n    var missed = false, input = this\n    input.pollingFast = true\n    function p() {\n      var changed = input.poll()\n      if (!changed && !missed) {missed = true; input.polling.set(60, p)}\n      else {input.pollingFast = false; input.slowPoll()}\n    }\n    input.polling.set(20, p)\n  },\n\n  // Read input from the textarea, and update the document to match.\n  // When something is selected, it is present in the textarea, and\n  // selected (unless it is huge, in which case a placeholder is\n  // used). When nothing is selected, the cursor sits after previously\n  // seen text (can be empty), which is stored in prevInput (we must\n  // not reset the textarea when typing, because that breaks IME).\n  poll: function() {\n    var this$1 = this;\n\n    var cm = this.cm, input = this.textarea, prevInput = this.prevInput\n    // Since this is called a *lot*, try to bail out as cheaply as\n    // possible when it is clear that nothing happened. hasSelection\n    // will be the case when there is a lot of text in the textarea,\n    // in which case reading its value would be expensive.\n    if (this.contextMenuPending || !cm.state.focused ||\n        (hasSelection(input) && !prevInput && !this.composing) ||\n        cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)\n      { return false }\n\n    var text = input.value\n    // If nothing changed, bail.\n    if (text == prevInput && !cm.somethingSelected()) { return false }\n    // Work around nonsensical selection resetting in IE9/10, and\n    // inexplicable appearance of private area unicode characters on\n    // some key combos in Mac (#2689).\n    if (ie && ie_version >= 9 && this.hasSelection === text ||\n        mac && /[\\uf700-\\uf7ff]/.test(text)) {\n      cm.display.input.reset()\n      return false\n    }\n\n    if (cm.doc.sel == cm.display.selForContextMenu) {\n      var first = text.charCodeAt(0)\n      if (first == 0x200b && !prevInput) { prevInput = \"\\u200b\" }\n      if (first == 0x21da) { this.reset(); return this.cm.execCommand(\"undo\") }\n    }\n    // Find the part of the input that is actually new\n    var same = 0, l = Math.min(prevInput.length, text.length)\n    while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same }\n\n    runInOp(cm, function () {\n      applyTextInput(cm, text.slice(same), prevInput.length - same,\n                     null, this$1.composing ? \"*compose\" : null)\n\n      // Don't leave long text in the textarea, since it makes further polling slow\n      if (text.length > 1000 || text.indexOf(\"\\n\") > -1) { input.value = this$1.prevInput = \"\" }\n      else { this$1.prevInput = text }\n\n      if (this$1.composing) {\n        this$1.composing.range.clear()\n        this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor(\"to\"),\n                                           {className: \"CodeMirror-composing\"})\n      }\n    })\n    return true\n  },\n\n  ensurePolled: function() {\n    if (this.pollingFast && this.poll()) { this.pollingFast = false }\n  },\n\n  onKeyPress: function() {\n    if (ie && ie_version >= 9) { this.hasSelection = null }\n    this.fastPoll()\n  },\n\n  onContextMenu: function(e) {\n    var input = this, cm = input.cm, display = cm.display, te = input.textarea\n    var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop\n    if (!pos || presto) { return } // Opera is difficult.\n\n    // Reset the current text selection only if the click is done outside of the selection\n    // and 'resetSelectionOnContextMenu' option is true.\n    var reset = cm.options.resetSelectionOnContextMenu\n    if (reset && cm.doc.sel.contains(pos) == -1)\n      { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll) }\n\n    var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText\n    input.wrapper.style.cssText = \"position: absolute\"\n    var wrapperBox = input.wrapper.getBoundingClientRect()\n    te.style.cssText = \"position: absolute; width: 30px; height: 30px;\\n      top: \" + (e.clientY - wrapperBox.top - 5) + \"px; left: \" + (e.clientX - wrapperBox.left - 5) + \"px;\\n      z-index: 1000; background: \" + (ie ? \"rgba(255, 255, 255, .05)\" : \"transparent\") + \";\\n      outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);\"\n    var oldScrollY\n    if (webkit) { oldScrollY = window.scrollY } // Work around Chrome issue (#2712)\n    display.input.focus()\n    if (webkit) { window.scrollTo(null, oldScrollY) }\n    display.input.reset()\n    // Adds \"Select all\" to context menu in FF\n    if (!cm.somethingSelected()) { te.value = input.prevInput = \" \" }\n    input.contextMenuPending = true\n    display.selForContextMenu = cm.doc.sel\n    clearTimeout(display.detectingSelectAll)\n\n    // Select-all will be greyed out if there's nothing to select, so\n    // this adds a zero-width space so that we can later check whether\n    // it got selected.\n    function prepareSelectAllHack() {\n      if (te.selectionStart != null) {\n        var selected = cm.somethingSelected()\n        var extval = \"\\u200b\" + (selected ? te.value : \"\")\n        te.value = \"\\u21da\" // Used to catch context-menu undo\n        te.value = extval\n        input.prevInput = selected ? \"\" : \"\\u200b\"\n        te.selectionStart = 1; te.selectionEnd = extval.length\n        // Re-set this, in case some other handler touched the\n        // selection in the meantime.\n        display.selForContextMenu = cm.doc.sel\n      }\n    }\n    function rehide() {\n      input.contextMenuPending = false\n      input.wrapper.style.cssText = oldWrapperCSS\n      te.style.cssText = oldCSS\n      if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos) }\n\n      // Try to detect the user choosing select-all\n      if (te.selectionStart != null) {\n        if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack() }\n        var i = 0, poll = function () {\n          if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 &&\n              te.selectionEnd > 0 && input.prevInput == \"\\u200b\")\n            { operation(cm, selectAll)(cm) }\n          else if (i++ < 10) { display.detectingSelectAll = setTimeout(poll, 500) }\n          else { display.input.reset() }\n        }\n        display.detectingSelectAll = setTimeout(poll, 200)\n      }\n    }\n\n    if (ie && ie_version >= 9) { prepareSelectAllHack() }\n    if (captureRightClick) {\n      e_stop(e)\n      var mouseup = function () {\n        off(window, \"mouseup\", mouseup)\n        setTimeout(rehide, 20)\n      }\n      on(window, \"mouseup\", mouseup)\n    } else {\n      setTimeout(rehide, 50)\n    }\n  },\n\n  readOnlyChanged: function(val) {\n    if (!val) { this.reset() }\n  },\n\n  setUneditable: nothing,\n\n  needsContentAttribute: false\n}, TextareaInput.prototype)\n\nfunction fromTextArea(textarea, options) {\n  options = options ? copyObj(options) : {}\n  options.value = textarea.value\n  if (!options.tabindex && textarea.tabIndex)\n    { options.tabindex = textarea.tabIndex }\n  if (!options.placeholder && textarea.placeholder)\n    { options.placeholder = textarea.placeholder }\n  // Set autofocus to true if this textarea is focused, or if it has\n  // autofocus and no other element is focused.\n  if (options.autofocus == null) {\n    var hasFocus = activeElt()\n    options.autofocus = hasFocus == textarea ||\n      textarea.getAttribute(\"autofocus\") != null && hasFocus == document.body\n  }\n\n  function save() {textarea.value = cm.getValue()}\n\n  var realSubmit\n  if (textarea.form) {\n    on(textarea.form, \"submit\", save)\n    // Deplorable hack to make the submit method do the right thing.\n    if (!options.leaveSubmitMethodAlone) {\n      var form = textarea.form\n      realSubmit = form.submit\n      try {\n        var wrappedSubmit = form.submit = function () {\n          save()\n          form.submit = realSubmit\n          form.submit()\n          form.submit = wrappedSubmit\n        }\n      } catch(e) {}\n    }\n  }\n\n  options.finishInit = function (cm) {\n    cm.save = save\n    cm.getTextArea = function () { return textarea; }\n    cm.toTextArea = function () {\n      cm.toTextArea = isNaN // Prevent this from being ran twice\n      save()\n      textarea.parentNode.removeChild(cm.getWrapperElement())\n      textarea.style.display = \"\"\n      if (textarea.form) {\n        off(textarea.form, \"submit\", save)\n        if (typeof textarea.form.submit == \"function\")\n          { textarea.form.submit = realSubmit }\n      }\n    }\n  }\n\n  textarea.style.display = \"none\"\n  var cm = CodeMirror$1(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); },\n    options)\n  return cm\n}\n\nfunction addLegacyProps(CodeMirror) {\n  CodeMirror.off = off\n  CodeMirror.on = on\n  CodeMirror.wheelEventPixels = wheelEventPixels\n  CodeMirror.Doc = Doc\n  CodeMirror.splitLines = splitLinesAuto\n  CodeMirror.countColumn = countColumn\n  CodeMirror.findColumn = findColumn\n  CodeMirror.isWordChar = isWordCharBasic\n  CodeMirror.Pass = Pass\n  CodeMirror.signal = signal\n  CodeMirror.Line = Line\n  CodeMirror.changeEnd = changeEnd\n  CodeMirror.scrollbarModel = scrollbarModel\n  CodeMirror.Pos = Pos\n  CodeMirror.cmpPos = cmp\n  CodeMirror.modes = modes\n  CodeMirror.mimeModes = mimeModes\n  CodeMirror.resolveMode = resolveMode\n  CodeMirror.getMode = getMode\n  CodeMirror.modeExtensions = modeExtensions\n  CodeMirror.extendMode = extendMode\n  CodeMirror.copyState = copyState\n  CodeMirror.startState = startState\n  CodeMirror.innerMode = innerMode\n  CodeMirror.commands = commands\n  CodeMirror.keyMap = keyMap\n  CodeMirror.keyName = keyName\n  CodeMirror.isModifierKey = isModifierKey\n  CodeMirror.lookupKey = lookupKey\n  CodeMirror.normalizeKeyMap = normalizeKeyMap\n  CodeMirror.StringStream = StringStream\n  CodeMirror.SharedTextMarker = SharedTextMarker\n  CodeMirror.TextMarker = TextMarker\n  CodeMirror.LineWidget = LineWidget\n  CodeMirror.e_preventDefault = e_preventDefault\n  CodeMirror.e_stopPropagation = e_stopPropagation\n  CodeMirror.e_stop = e_stop\n  CodeMirror.addClass = addClass\n  CodeMirror.contains = contains\n  CodeMirror.rmClass = rmClass\n  CodeMirror.keyNames = keyNames\n}\n\n// EDITOR CONSTRUCTOR\n\ndefineOptions(CodeMirror$1)\n\naddEditorMethods(CodeMirror$1)\n\n// Set up methods on CodeMirror's prototype to redirect to the editor's document.\nvar dontDelegate = \"iter insert remove copy getEditor constructor\".split(\" \")\nfor (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)\n  { CodeMirror$1.prototype[prop] = (function(method) {\n    return function() {return method.apply(this.doc, arguments)}\n  })(Doc.prototype[prop]) } }\n\neventMixin(Doc)\n\n// INPUT HANDLING\n\nCodeMirror$1.inputStyles = {\"textarea\": TextareaInput, \"contenteditable\": ContentEditableInput}\n\n// MODE DEFINITION AND QUERYING\n\n// Extra arguments are stored as the mode's dependencies, which is\n// used by (legacy) mechanisms like loadmode.js to automatically\n// load a mode. (Preferred mechanism is the require/define calls.)\nCodeMirror$1.defineMode = function(name/*, mode, …*/) {\n  if (!CodeMirror$1.defaults.mode && name != \"null\") { CodeMirror$1.defaults.mode = name }\n  defineMode.apply(this, arguments)\n}\n\nCodeMirror$1.defineMIME = defineMIME\n\n// Minimal default mode.\nCodeMirror$1.defineMode(\"null\", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); })\nCodeMirror$1.defineMIME(\"text/plain\", \"null\")\n\n// EXTENSIONS\n\nCodeMirror$1.defineExtension = function (name, func) {\n  CodeMirror$1.prototype[name] = func\n}\nCodeMirror$1.defineDocExtension = function (name, func) {\n  Doc.prototype[name] = func\n}\n\nCodeMirror$1.fromTextArea = fromTextArea\n\naddLegacyProps(CodeMirror$1)\n\nCodeMirror$1.version = \"5.20.2\"\n\nreturn CodeMirror$1;\n\n})));\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/css.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\nimport CodeMirror from 'codemirror';\n\n(function(mod) {\n    mod(CodeMirror);\n})(function(CodeMirror) {\n\"use strict\";\n\nCodeMirror.defineMode(\"css\", function(config, parserConfig) {\n  var inline = parserConfig.inline\n  if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode(\"text/css\");\n\n  var indentUnit = config.indentUnit,\n      tokenHooks = parserConfig.tokenHooks,\n      documentTypes = parserConfig.documentTypes || {},\n      mediaTypes = parserConfig.mediaTypes || {},\n      mediaFeatures = parserConfig.mediaFeatures || {},\n      mediaValueKeywords = parserConfig.mediaValueKeywords || {},\n      propertyKeywords = parserConfig.propertyKeywords || {},\n      nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {},\n      fontProperties = parserConfig.fontProperties || {},\n      counterDescriptors = parserConfig.counterDescriptors || {},\n      colorKeywords = parserConfig.colorKeywords || {},\n      valueKeywords = parserConfig.valueKeywords || {},\n      allowNested = parserConfig.allowNested,\n      supportsAtComponent = parserConfig.supportsAtComponent === true;\n\n  var type, override;\n  function ret(style, tp) { type = tp; return style; }\n\n  // Tokenizers\n\n  function tokenBase(stream, state) {\n    var ch = stream.next();\n    if (tokenHooks[ch]) {\n      var result = tokenHooks[ch](stream, state);\n      if (result !== false) return result;\n    }\n    if (ch == \"@\") {\n      stream.eatWhile(/[\\w\\\\\\-]/);\n      return ret(\"def\", stream.current());\n    } else if (ch == \"=\" || (ch == \"~\" || ch == \"|\") && stream.eat(\"=\")) {\n      return ret(null, \"compare\");\n    } else if (ch == \"\\\"\" || ch == \"'\") {\n      state.tokenize = tokenString(ch);\n      return state.tokenize(stream, state);\n    } else if (ch == \"#\") {\n      stream.eatWhile(/[\\w\\\\\\-]/);\n      return ret(\"atom\", \"hash\");\n    } else if (ch == \"!\") {\n      stream.match(/^\\s*\\w*/);\n      return ret(\"keyword\", \"important\");\n    } else if (/\\d/.test(ch) || ch == \".\" && stream.eat(/\\d/)) {\n      stream.eatWhile(/[\\w.%]/);\n      return ret(\"number\", \"unit\");\n    } else if (ch === \"-\") {\n      if (/[\\d.]/.test(stream.peek())) {\n        stream.eatWhile(/[\\w.%]/);\n        return ret(\"number\", \"unit\");\n      } else if (stream.match(/^-[\\w\\\\\\-]+/)) {\n        stream.eatWhile(/[\\w\\\\\\-]/);\n        if (stream.match(/^\\s*:/, false))\n          return ret(\"variable-2\", \"variable-definition\");\n        return ret(\"variable-2\", \"variable\");\n      } else if (stream.match(/^\\w+-/)) {\n        return ret(\"meta\", \"meta\");\n      }\n    } else if (/[,+>*\\/]/.test(ch)) {\n      return ret(null, \"select-op\");\n    } else if (ch == \".\" && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {\n      return ret(\"qualifier\", \"qualifier\");\n    } else if (/[:;{}\\[\\]\\(\\)]/.test(ch)) {\n      return ret(null, ch);\n    } else if ((ch == \"u\" && stream.match(/rl(-prefix)?\\(/)) ||\n               (ch == \"d\" && stream.match(\"omain(\")) ||\n               (ch == \"r\" && stream.match(\"egexp(\"))) {\n      stream.backUp(1);\n      state.tokenize = tokenParenthesized;\n      return ret(\"property\", \"word\");\n    } else if (/[\\w\\\\\\-]/.test(ch)) {\n      stream.eatWhile(/[\\w\\\\\\-]/);\n      return ret(\"property\", \"word\");\n    } else {\n      return ret(null, null);\n    }\n  }\n\n  function tokenString(quote) {\n    return function(stream, state) {\n      var escaped = false, ch;\n      while ((ch = stream.next()) != null) {\n        if (ch == quote && !escaped) {\n          if (quote == \")\") stream.backUp(1);\n          break;\n        }\n        escaped = !escaped && ch == \"\\\\\";\n      }\n      if (ch == quote || !escaped && quote != \")\") state.tokenize = null;\n      return ret(\"string\", \"string\");\n    };\n  }\n\n  function tokenParenthesized(stream, state) {\n    stream.next(); // Must be '('\n    if (!stream.match(/\\s*[\\\"\\')]/, false))\n      state.tokenize = tokenString(\")\");\n    else\n      state.tokenize = null;\n    return ret(null, \"(\");\n  }\n\n  // Context management\n\n  function Context(type, indent, prev) {\n    this.type = type;\n    this.indent = indent;\n    this.prev = prev;\n  }\n\n  function pushContext(state, stream, type, indent) {\n    state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context);\n    return type;\n  }\n\n  function popContext(state) {\n    if (state.context.prev)\n      state.context = state.context.prev;\n    return state.context.type;\n  }\n\n  function pass(type, stream, state) {\n    return states[state.context.type](type, stream, state);\n  }\n  function popAndPass(type, stream, state, n) {\n    for (var i = n || 1; i > 0; i--)\n      state.context = state.context.prev;\n    return pass(type, stream, state);\n  }\n\n  // Parser\n\n  function wordAsValue(stream) {\n    var word = stream.current().toLowerCase();\n    if (valueKeywords.hasOwnProperty(word))\n      override = \"atom\";\n    else if (colorKeywords.hasOwnProperty(word))\n      override = \"keyword\";\n    else\n      override = \"variable\";\n  }\n\n  var states = {};\n\n  states.top = function(type, stream, state) {\n    if (type == \"{\") {\n      return pushContext(state, stream, \"block\");\n    } else if (type == \"}\" && state.context.prev) {\n      return popContext(state);\n    } else if (supportsAtComponent && /@component/.test(type)) {\n      return pushContext(state, stream, \"atComponentBlock\");\n    } else if (/^@(-moz-)?document$/.test(type)) {\n      return pushContext(state, stream, \"documentTypes\");\n    } else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) {\n      return pushContext(state, stream, \"atBlock\");\n    } else if (/^@(font-face|counter-style)/.test(type)) {\n      state.stateArg = type;\n      return \"restricted_atBlock_before\";\n    } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) {\n      return \"keyframes\";\n    } else if (type && type.charAt(0) == \"@\") {\n      return pushContext(state, stream, \"at\");\n    } else if (type == \"hash\") {\n      override = \"builtin\";\n    } else if (type == \"word\") {\n      override = \"tag\";\n    } else if (type == \"variable-definition\") {\n      return \"maybeprop\";\n    } else if (type == \"interpolation\") {\n      return pushContext(state, stream, \"interpolation\");\n    } else if (type == \":\") {\n      return \"pseudo\";\n    } else if (allowNested && type == \"(\") {\n      return pushContext(state, stream, \"parens\");\n    }\n    return state.context.type;\n  };\n\n  states.block = function(type, stream, state) {\n    if (type == \"word\") {\n      var word = stream.current().toLowerCase();\n      if (propertyKeywords.hasOwnProperty(word)) {\n        override = \"property\";\n        return \"maybeprop\";\n      } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {\n        override = \"string-2\";\n        return \"maybeprop\";\n      } else if (allowNested) {\n        override = stream.match(/^\\s*:(?:\\s|$)/, false) ? \"property\" : \"tag\";\n        return \"block\";\n      } else {\n        override += \" error\";\n        return \"maybeprop\";\n      }\n    } else if (type == \"meta\") {\n      return \"block\";\n    } else if (!allowNested && (type == \"hash\" || type == \"qualifier\")) {\n      override = \"error\";\n      return \"block\";\n    } else {\n      return states.top(type, stream, state);\n    }\n  };\n\n  states.maybeprop = function(type, stream, state) {\n    if (type == \":\") return pushContext(state, stream, \"prop\");\n    return pass(type, stream, state);\n  };\n\n  states.prop = function(type, stream, state) {\n    if (type == \";\") return popContext(state);\n    if (type == \"{\" && allowNested) return pushContext(state, stream, \"propBlock\");\n    if (type == \"}\" || type == \"{\") return popAndPass(type, stream, state);\n    if (type == \"(\") return pushContext(state, stream, \"parens\");\n\n    if (type == \"hash\" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) {\n      override += \" error\";\n    } else if (type == \"word\") {\n      wordAsValue(stream);\n    } else if (type == \"interpolation\") {\n      return pushContext(state, stream, \"interpolation\");\n    }\n    return \"prop\";\n  };\n\n  states.propBlock = function(type, _stream, state) {\n    if (type == \"}\") return popContext(state);\n    if (type == \"word\") { override = \"property\"; return \"maybeprop\"; }\n    return state.context.type;\n  };\n\n  states.parens = function(type, stream, state) {\n    if (type == \"{\" || type == \"}\") return popAndPass(type, stream, state);\n    if (type == \")\") return popContext(state);\n    if (type == \"(\") return pushContext(state, stream, \"parens\");\n    if (type == \"interpolation\") return pushContext(state, stream, \"interpolation\");\n    if (type == \"word\") wordAsValue(stream);\n    return \"parens\";\n  };\n\n  states.pseudo = function(type, stream, state) {\n    if (type == \"word\") {\n      override = \"variable-3\";\n      return state.context.type;\n    }\n    return pass(type, stream, state);\n  };\n\n  states.documentTypes = function(type, stream, state) {\n    if (type == \"word\" && documentTypes.hasOwnProperty(stream.current())) {\n      override = \"tag\";\n      return state.context.type;\n    } else {\n      return states.atBlock(type, stream, state);\n    }\n  };\n\n  states.atBlock = function(type, stream, state) {\n    if (type == \"(\") return pushContext(state, stream, \"atBlock_parens\");\n    if (type == \"}\" || type == \";\") return popAndPass(type, stream, state);\n    if (type == \"{\") return popContext(state) && pushContext(state, stream, allowNested ? \"block\" : \"top\");\n\n    if (type == \"interpolation\") return pushContext(state, stream, \"interpolation\");\n\n    if (type == \"word\") {\n      var word = stream.current().toLowerCase();\n      if (word == \"only\" || word == \"not\" || word == \"and\" || word == \"or\")\n        override = \"keyword\";\n      else if (mediaTypes.hasOwnProperty(word))\n        override = \"attribute\";\n      else if (mediaFeatures.hasOwnProperty(word))\n        override = \"property\";\n      else if (mediaValueKeywords.hasOwnProperty(word))\n        override = \"keyword\";\n      else if (propertyKeywords.hasOwnProperty(word))\n        override = \"property\";\n      else if (nonStandardPropertyKeywords.hasOwnProperty(word))\n        override = \"string-2\";\n      else if (valueKeywords.hasOwnProperty(word))\n        override = \"atom\";\n      else if (colorKeywords.hasOwnProperty(word))\n        override = \"keyword\";\n      else\n        override = \"error\";\n    }\n    return state.context.type;\n  };\n\n  states.atComponentBlock = function(type, stream, state) {\n    if (type == \"}\")\n      return popAndPass(type, stream, state);\n    if (type == \"{\")\n      return popContext(state) && pushContext(state, stream, allowNested ? \"block\" : \"top\", false);\n    if (type == \"word\")\n      override = \"error\";\n    return state.context.type;\n  };\n\n  states.atBlock_parens = function(type, stream, state) {\n    if (type == \")\") return popContext(state);\n    if (type == \"{\" || type == \"}\") return popAndPass(type, stream, state, 2);\n    return states.atBlock(type, stream, state);\n  };\n\n  states.restricted_atBlock_before = function(type, stream, state) {\n    if (type == \"{\")\n      return pushContext(state, stream, \"restricted_atBlock\");\n    if (type == \"word\" && state.stateArg == \"@counter-style\") {\n      override = \"variable\";\n      return \"restricted_atBlock_before\";\n    }\n    return pass(type, stream, state);\n  };\n\n  states.restricted_atBlock = function(type, stream, state) {\n    if (type == \"}\") {\n      state.stateArg = null;\n      return popContext(state);\n    }\n    if (type == \"word\") {\n      if ((state.stateArg == \"@font-face\" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) ||\n          (state.stateArg == \"@counter-style\" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase())))\n        override = \"error\";\n      else\n        override = \"property\";\n      return \"maybeprop\";\n    }\n    return \"restricted_atBlock\";\n  };\n\n  states.keyframes = function(type, stream, state) {\n    if (type == \"word\") { override = \"variable\"; return \"keyframes\"; }\n    if (type == \"{\") return pushContext(state, stream, \"top\");\n    return pass(type, stream, state);\n  };\n\n  states.at = function(type, stream, state) {\n    if (type == \";\") return popContext(state);\n    if (type == \"{\" || type == \"}\") return popAndPass(type, stream, state);\n    if (type == \"word\") override = \"tag\";\n    else if (type == \"hash\") override = \"builtin\";\n    return \"at\";\n  };\n\n  states.interpolation = function(type, stream, state) {\n    if (type == \"}\") return popContext(state);\n    if (type == \"{\" || type == \";\") return popAndPass(type, stream, state);\n    if (type == \"word\") override = \"variable\";\n    else if (type != \"variable\" && type != \"(\" && type != \")\") override = \"error\";\n    return \"interpolation\";\n  };\n\n  return {\n    startState: function(base) {\n      return {tokenize: null,\n              state: inline ? \"block\" : \"top\",\n              stateArg: null,\n              context: new Context(inline ? \"block\" : \"top\", base || 0, null)};\n    },\n\n    token: function(stream, state) {\n      if (!state.tokenize && stream.eatSpace()) return null;\n      var style = (state.tokenize || tokenBase)(stream, state);\n      if (style && typeof style == \"object\") {\n        type = style[1];\n        style = style[0];\n      }\n      override = style;\n      state.state = states[state.state](type, stream, state);\n      return override;\n    },\n\n    indent: function(state, textAfter) {\n      var cx = state.context, ch = textAfter && textAfter.charAt(0);\n      var indent = cx.indent;\n      if (cx.type == \"prop\" && (ch == \"}\" || ch == \")\")) cx = cx.prev;\n      if (cx.prev) {\n        if (ch == \"}\" && (cx.type == \"block\" || cx.type == \"top\" ||\n                          cx.type == \"interpolation\" || cx.type == \"restricted_atBlock\")) {\n          // Resume indentation from parent context.\n          cx = cx.prev;\n          indent = cx.indent;\n        } else if (ch == \")\" && (cx.type == \"parens\" || cx.type == \"atBlock_parens\") ||\n            ch == \"{\" && (cx.type == \"at\" || cx.type == \"atBlock\")) {\n          // Dedent relative to current context.\n          indent = Math.max(0, cx.indent - indentUnit);\n          cx = cx.prev;\n        }\n      }\n      return indent;\n    },\n\n    electricChars: \"}\",\n    blockCommentStart: \"/*\",\n    blockCommentEnd: \"*/\",\n    fold: \"brace\"\n  };\n});\n\n  function keySet(array) {\n    var keys = {};\n    for (var i = 0; i < array.length; ++i) {\n      keys[array[i].toLowerCase()] = true;\n    }\n    return keys;\n  }\n\n  var documentTypes_ = [\n    \"domain\", \"regexp\", \"url\", \"url-prefix\"\n  ], documentTypes = keySet(documentTypes_);\n\n  var mediaTypes_ = [\n    \"all\", \"aural\", \"braille\", \"handheld\", \"print\", \"projection\", \"screen\",\n    \"tty\", \"tv\", \"embossed\"\n  ], mediaTypes = keySet(mediaTypes_);\n\n  var mediaFeatures_ = [\n    \"width\", \"min-width\", \"max-width\", \"height\", \"min-height\", \"max-height\",\n    \"device-width\", \"min-device-width\", \"max-device-width\", \"device-height\",\n    \"min-device-height\", \"max-device-height\", \"aspect-ratio\",\n    \"min-aspect-ratio\", \"max-aspect-ratio\", \"device-aspect-ratio\",\n    \"min-device-aspect-ratio\", \"max-device-aspect-ratio\", \"color\", \"min-color\",\n    \"max-color\", \"color-index\", \"min-color-index\", \"max-color-index\",\n    \"monochrome\", \"min-monochrome\", \"max-monochrome\", \"resolution\",\n    \"min-resolution\", \"max-resolution\", \"scan\", \"grid\", \"orientation\",\n    \"device-pixel-ratio\", \"min-device-pixel-ratio\", \"max-device-pixel-ratio\",\n    \"pointer\", \"any-pointer\", \"hover\", \"any-hover\"\n  ], mediaFeatures = keySet(mediaFeatures_);\n\n  var mediaValueKeywords_ = [\n    \"landscape\", \"portrait\", \"none\", \"coarse\", \"fine\", \"on-demand\", \"hover\",\n    \"interlace\", \"progressive\"\n  ], mediaValueKeywords = keySet(mediaValueKeywords_);\n\n  var propertyKeywords_ = [\n    \"align-content\", \"align-items\", \"align-self\", \"alignment-adjust\",\n    \"alignment-baseline\", \"anchor-point\", \"animation\", \"animation-delay\",\n    \"animation-direction\", \"animation-duration\", \"animation-fill-mode\",\n    \"animation-iteration-count\", \"animation-name\", \"animation-play-state\",\n    \"animation-timing-function\", \"appearance\", \"azimuth\", \"backface-visibility\",\n    \"background\", \"background-attachment\", \"background-blend-mode\", \"background-clip\",\n    \"background-color\", \"background-image\", \"background-origin\", \"background-position\",\n    \"background-repeat\", \"background-size\", \"baseline-shift\", \"binding\",\n    \"bleed\", \"bookmark-label\", \"bookmark-level\", \"bookmark-state\",\n    \"bookmark-target\", \"border\", \"border-bottom\", \"border-bottom-color\",\n    \"border-bottom-left-radius\", \"border-bottom-right-radius\",\n    \"border-bottom-style\", \"border-bottom-width\", \"border-collapse\",\n    \"border-color\", \"border-image\", \"border-image-outset\",\n    \"border-image-repeat\", \"border-image-slice\", \"border-image-source\",\n    \"border-image-width\", \"border-left\", \"border-left-color\",\n    \"border-left-style\", \"border-left-width\", \"border-radius\", \"border-right\",\n    \"border-right-color\", \"border-right-style\", \"border-right-width\",\n    \"border-spacing\", \"border-style\", \"border-top\", \"border-top-color\",\n    \"border-top-left-radius\", \"border-top-right-radius\", \"border-top-style\",\n    \"border-top-width\", \"border-width\", \"bottom\", \"box-decoration-break\",\n    \"box-shadow\", \"box-sizing\", \"break-after\", \"break-before\", \"break-inside\",\n    \"caption-side\", \"clear\", \"clip\", \"color\", \"color-profile\", \"column-count\",\n    \"column-fill\", \"column-gap\", \"column-rule\", \"column-rule-color\",\n    \"column-rule-style\", \"column-rule-width\", \"column-span\", \"column-width\",\n    \"columns\", \"content\", \"counter-increment\", \"counter-reset\", \"crop\", \"cue\",\n    \"cue-after\", \"cue-before\", \"cursor\", \"direction\", \"display\",\n    \"dominant-baseline\", \"drop-initial-after-adjust\",\n    \"drop-initial-after-align\", \"drop-initial-before-adjust\",\n    \"drop-initial-before-align\", \"drop-initial-size\", \"drop-initial-value\",\n    \"elevation\", \"empty-cells\", \"fit\", \"fit-position\", \"flex\", \"flex-basis\",\n    \"flex-direction\", \"flex-flow\", \"flex-grow\", \"flex-shrink\", \"flex-wrap\",\n    \"float\", \"float-offset\", \"flow-from\", \"flow-into\", \"font\", \"font-feature-settings\",\n    \"font-family\", \"font-kerning\", \"font-language-override\", \"font-size\", \"font-size-adjust\",\n    \"font-stretch\", \"font-style\", \"font-synthesis\", \"font-variant\",\n    \"font-variant-alternates\", \"font-variant-caps\", \"font-variant-east-asian\",\n    \"font-variant-ligatures\", \"font-variant-numeric\", \"font-variant-position\",\n    \"font-weight\", \"grid\", \"grid-area\", \"grid-auto-columns\", \"grid-auto-flow\",\n    \"grid-auto-rows\", \"grid-column\", \"grid-column-end\", \"grid-column-gap\",\n    \"grid-column-start\", \"grid-gap\", \"grid-row\", \"grid-row-end\", \"grid-row-gap\",\n    \"grid-row-start\", \"grid-template\", \"grid-template-areas\", \"grid-template-columns\",\n    \"grid-template-rows\", \"hanging-punctuation\", \"height\", \"hyphens\",\n    \"icon\", \"image-orientation\", \"image-rendering\", \"image-resolution\",\n    \"inline-box-align\", \"justify-content\", \"left\", \"letter-spacing\",\n    \"line-break\", \"line-height\", \"line-stacking\", \"line-stacking-ruby\",\n    \"line-stacking-shift\", \"line-stacking-strategy\", \"list-style\",\n    \"list-style-image\", \"list-style-position\", \"list-style-type\", \"margin\",\n    \"margin-bottom\", \"margin-left\", \"margin-right\", \"margin-top\",\n    \"marker-offset\", \"marks\", \"marquee-direction\", \"marquee-loop\",\n    \"marquee-play-count\", \"marquee-speed\", \"marquee-style\", \"max-height\",\n    \"max-width\", \"min-height\", \"min-width\", \"move-to\", \"nav-down\", \"nav-index\",\n    \"nav-left\", \"nav-right\", \"nav-up\", \"object-fit\", \"object-position\",\n    \"opacity\", \"order\", \"orphans\", \"outline\",\n    \"outline-color\", \"outline-offset\", \"outline-style\", \"outline-width\",\n    \"overflow\", \"overflow-style\", \"overflow-wrap\", \"overflow-x\", \"overflow-y\",\n    \"padding\", \"padding-bottom\", \"padding-left\", \"padding-right\", \"padding-top\",\n    \"page\", \"page-break-after\", \"page-break-before\", \"page-break-inside\",\n    \"page-policy\", \"pause\", \"pause-after\", \"pause-before\", \"perspective\",\n    \"perspective-origin\", \"pitch\", \"pitch-range\", \"play-during\", \"position\",\n    \"presentation-level\", \"punctuation-trim\", \"quotes\", \"region-break-after\",\n    \"region-break-before\", \"region-break-inside\", \"region-fragment\",\n    \"rendering-intent\", \"resize\", \"rest\", \"rest-after\", \"rest-before\", \"richness\",\n    \"right\", \"rotation\", \"rotation-point\", \"ruby-align\", \"ruby-overhang\",\n    \"ruby-position\", \"ruby-span\", \"shape-image-threshold\", \"shape-inside\", \"shape-margin\",\n    \"shape-outside\", \"size\", \"speak\", \"speak-as\", \"speak-header\",\n    \"speak-numeral\", \"speak-punctuation\", \"speech-rate\", \"stress\", \"string-set\",\n    \"tab-size\", \"table-layout\", \"target\", \"target-name\", \"target-new\",\n    \"target-position\", \"text-align\", \"text-align-last\", \"text-decoration\",\n    \"text-decoration-color\", \"text-decoration-line\", \"text-decoration-skip\",\n    \"text-decoration-style\", \"text-emphasis\", \"text-emphasis-color\",\n    \"text-emphasis-position\", \"text-emphasis-style\", \"text-height\",\n    \"text-indent\", \"text-justify\", \"text-outline\", \"text-overflow\", \"text-shadow\",\n    \"text-size-adjust\", \"text-space-collapse\", \"text-transform\", \"text-underline-position\",\n    \"text-wrap\", \"top\", \"transform\", \"transform-origin\", \"transform-style\",\n    \"transition\", \"transition-delay\", \"transition-duration\",\n    \"transition-property\", \"transition-timing-function\", \"unicode-bidi\",\n    \"vertical-align\", \"visibility\", \"voice-balance\", \"voice-duration\",\n    \"voice-family\", \"voice-pitch\", \"voice-range\", \"voice-rate\", \"voice-stress\",\n    \"voice-volume\", \"volume\", \"white-space\", \"widows\", \"width\", \"word-break\",\n    \"word-spacing\", \"word-wrap\", \"z-index\",\n    // SVG-specific\n    \"clip-path\", \"clip-rule\", \"mask\", \"enable-background\", \"filter\", \"flood-color\",\n    \"flood-opacity\", \"lighting-color\", \"stop-color\", \"stop-opacity\", \"pointer-events\",\n    \"color-interpolation\", \"color-interpolation-filters\",\n    \"color-rendering\", \"fill\", \"fill-opacity\", \"fill-rule\", \"image-rendering\",\n    \"marker\", \"marker-end\", \"marker-mid\", \"marker-start\", \"shape-rendering\", \"stroke\",\n    \"stroke-dasharray\", \"stroke-dashoffset\", \"stroke-linecap\", \"stroke-linejoin\",\n    \"stroke-miterlimit\", \"stroke-opacity\", \"stroke-width\", \"text-rendering\",\n    \"baseline-shift\", \"dominant-baseline\", \"glyph-orientation-horizontal\",\n    \"glyph-orientation-vertical\", \"text-anchor\", \"writing-mode\"\n  ], propertyKeywords = keySet(propertyKeywords_);\n\n  var nonStandardPropertyKeywords_ = [\n    \"scrollbar-arrow-color\", \"scrollbar-base-color\", \"scrollbar-dark-shadow-color\",\n    \"scrollbar-face-color\", \"scrollbar-highlight-color\", \"scrollbar-shadow-color\",\n    \"scrollbar-3d-light-color\", \"scrollbar-track-color\", \"shape-inside\",\n    \"searchfield-cancel-button\", \"searchfield-decoration\", \"searchfield-results-button\",\n    \"searchfield-results-decoration\", \"zoom\"\n  ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);\n\n  var fontProperties_ = [\n    \"font-family\", \"src\", \"unicode-range\", \"font-variant\", \"font-feature-settings\",\n    \"font-stretch\", \"font-weight\", \"font-style\"\n  ], fontProperties = keySet(fontProperties_);\n\n  var counterDescriptors_ = [\n    \"additive-symbols\", \"fallback\", \"negative\", \"pad\", \"prefix\", \"range\",\n    \"speak-as\", \"suffix\", \"symbols\", \"system\"\n  ], counterDescriptors = keySet(counterDescriptors_);\n\n  var colorKeywords_ = [\n    \"aliceblue\", \"antiquewhite\", \"aqua\", \"aquamarine\", \"azure\", \"beige\",\n    \"bisque\", \"black\", \"blanchedalmond\", \"blue\", \"blueviolet\", \"brown\",\n    \"burlywood\", \"cadetblue\", \"chartreuse\", \"chocolate\", \"coral\", \"cornflowerblue\",\n    \"cornsilk\", \"crimson\", \"cyan\", \"darkblue\", \"darkcyan\", \"darkgoldenrod\",\n    \"darkgray\", \"darkgreen\", \"darkkhaki\", \"darkmagenta\", \"darkolivegreen\",\n    \"darkorange\", \"darkorchid\", \"darkred\", \"darksalmon\", \"darkseagreen\",\n    \"darkslateblue\", \"darkslategray\", \"darkturquoise\", \"darkviolet\",\n    \"deeppink\", \"deepskyblue\", \"dimgray\", \"dodgerblue\", \"firebrick\",\n    \"floralwhite\", \"forestgreen\", \"fuchsia\", \"gainsboro\", \"ghostwhite\",\n    \"gold\", \"goldenrod\", \"gray\", \"grey\", \"green\", \"greenyellow\", \"honeydew\",\n    \"hotpink\", \"indianred\", \"indigo\", \"ivory\", \"khaki\", \"lavender\",\n    \"lavenderblush\", \"lawngreen\", \"lemonchiffon\", \"lightblue\", \"lightcoral\",\n    \"lightcyan\", \"lightgoldenrodyellow\", \"lightgray\", \"lightgreen\", \"lightpink\",\n    \"lightsalmon\", \"lightseagreen\", \"lightskyblue\", \"lightslategray\",\n    \"lightsteelblue\", \"lightyellow\", \"lime\", \"limegreen\", \"linen\", \"magenta\",\n    \"maroon\", \"mediumaquamarine\", \"mediumblue\", \"mediumorchid\", \"mediumpurple\",\n    \"mediumseagreen\", \"mediumslateblue\", \"mediumspringgreen\", \"mediumturquoise\",\n    \"mediumvioletred\", \"midnightblue\", \"mintcream\", \"mistyrose\", \"moccasin\",\n    \"navajowhite\", \"navy\", \"oldlace\", \"olive\", \"olivedrab\", \"orange\", \"orangered\",\n    \"orchid\", \"palegoldenrod\", \"palegreen\", \"paleturquoise\", \"palevioletred\",\n    \"papayawhip\", \"peachpuff\", \"peru\", \"pink\", \"plum\", \"powderblue\",\n    \"purple\", \"rebeccapurple\", \"red\", \"rosybrown\", \"royalblue\", \"saddlebrown\",\n    \"salmon\", \"sandybrown\", \"seagreen\", \"seashell\", \"sienna\", \"silver\", \"skyblue\",\n    \"slateblue\", \"slategray\", \"snow\", \"springgreen\", \"steelblue\", \"tan\",\n    \"teal\", \"thistle\", \"tomato\", \"turquoise\", \"violet\", \"wheat\", \"white\",\n    \"whitesmoke\", \"yellow\", \"yellowgreen\"\n  ], colorKeywords = keySet(colorKeywords_);\n\n  var valueKeywords_ = [\n    \"above\", \"absolute\", \"activeborder\", \"additive\", \"activecaption\", \"afar\",\n    \"after-white-space\", \"ahead\", \"alias\", \"all\", \"all-scroll\", \"alphabetic\", \"alternate\",\n    \"always\", \"amharic\", \"amharic-abegede\", \"antialiased\", \"appworkspace\",\n    \"arabic-indic\", \"armenian\", \"asterisks\", \"attr\", \"auto\", \"avoid\", \"avoid-column\", \"avoid-page\",\n    \"avoid-region\", \"background\", \"backwards\", \"baseline\", \"below\", \"bidi-override\", \"binary\",\n    \"bengali\", \"blink\", \"block\", \"block-axis\", \"bold\", \"bolder\", \"border\", \"border-box\",\n    \"both\", \"bottom\", \"break\", \"break-all\", \"break-word\", \"bullets\", \"button\", \"button-bevel\",\n    \"buttonface\", \"buttonhighlight\", \"buttonshadow\", \"buttontext\", \"calc\", \"cambodian\",\n    \"capitalize\", \"caps-lock-indicator\", \"caption\", \"captiontext\", \"caret\",\n    \"cell\", \"center\", \"checkbox\", \"circle\", \"cjk-decimal\", \"cjk-earthly-branch\",\n    \"cjk-heavenly-stem\", \"cjk-ideographic\", \"clear\", \"clip\", \"close-quote\",\n    \"col-resize\", \"collapse\", \"color\", \"color-burn\", \"color-dodge\", \"column\", \"column-reverse\",\n    \"compact\", \"condensed\", \"contain\", \"content\",\n    \"content-box\", \"context-menu\", \"continuous\", \"copy\", \"counter\", \"counters\", \"cover\", \"crop\",\n    \"cross\", \"crosshair\", \"currentcolor\", \"cursive\", \"cyclic\", \"darken\", \"dashed\", \"decimal\",\n    \"decimal-leading-zero\", \"default\", \"default-button\", \"dense\", \"destination-atop\",\n    \"destination-in\", \"destination-out\", \"destination-over\", \"devanagari\", \"difference\",\n    \"disc\", \"discard\", \"disclosure-closed\", \"disclosure-open\", \"document\",\n    \"dot-dash\", \"dot-dot-dash\",\n    \"dotted\", \"double\", \"down\", \"e-resize\", \"ease\", \"ease-in\", \"ease-in-out\", \"ease-out\",\n    \"element\", \"ellipse\", \"ellipsis\", \"embed\", \"end\", \"ethiopic\", \"ethiopic-abegede\",\n    \"ethiopic-abegede-am-et\", \"ethiopic-abegede-gez\", \"ethiopic-abegede-ti-er\",\n    \"ethiopic-abegede-ti-et\", \"ethiopic-halehame-aa-er\",\n    \"ethiopic-halehame-aa-et\", \"ethiopic-halehame-am-et\",\n    \"ethiopic-halehame-gez\", \"ethiopic-halehame-om-et\",\n    \"ethiopic-halehame-sid-et\", \"ethiopic-halehame-so-et\",\n    \"ethiopic-halehame-ti-er\", \"ethiopic-halehame-ti-et\", \"ethiopic-halehame-tig\",\n    \"ethiopic-numeric\", \"ew-resize\", \"exclusion\", \"expanded\", \"extends\", \"extra-condensed\",\n    \"extra-expanded\", \"fantasy\", \"fast\", \"fill\", \"fixed\", \"flat\", \"flex\", \"flex-end\", \"flex-start\", \"footnotes\",\n    \"forwards\", \"from\", \"geometricPrecision\", \"georgian\", \"graytext\", \"grid\", \"groove\",\n    \"gujarati\", \"gurmukhi\", \"hand\", \"hangul\", \"hangul-consonant\", \"hard-light\", \"hebrew\",\n    \"help\", \"hidden\", \"hide\", \"higher\", \"highlight\", \"highlighttext\",\n    \"hiragana\", \"hiragana-iroha\", \"horizontal\", \"hsl\", \"hsla\", \"hue\", \"icon\", \"ignore\",\n    \"inactiveborder\", \"inactivecaption\", \"inactivecaptiontext\", \"infinite\",\n    \"infobackground\", \"infotext\", \"inherit\", \"initial\", \"inline\", \"inline-axis\",\n    \"inline-block\", \"inline-flex\", \"inline-grid\", \"inline-table\", \"inset\", \"inside\", \"intrinsic\", \"invert\",\n    \"italic\", \"japanese-formal\", \"japanese-informal\", \"justify\", \"kannada\",\n    \"katakana\", \"katakana-iroha\", \"keep-all\", \"khmer\",\n    \"korean-hangul-formal\", \"korean-hanja-formal\", \"korean-hanja-informal\",\n    \"landscape\", \"lao\", \"large\", \"larger\", \"left\", \"level\", \"lighter\", \"lighten\",\n    \"line-through\", \"linear\", \"linear-gradient\", \"lines\", \"list-item\", \"listbox\", \"listitem\",\n    \"local\", \"logical\", \"loud\", \"lower\", \"lower-alpha\", \"lower-armenian\",\n    \"lower-greek\", \"lower-hexadecimal\", \"lower-latin\", \"lower-norwegian\",\n    \"lower-roman\", \"lowercase\", \"ltr\", \"luminosity\", \"malayalam\", \"match\", \"matrix\", \"matrix3d\",\n    \"media-controls-background\", \"media-current-time-display\",\n    \"media-fullscreen-button\", \"media-mute-button\", \"media-play-button\",\n    \"media-return-to-realtime-button\", \"media-rewind-button\",\n    \"media-seek-back-button\", \"media-seek-forward-button\", \"media-slider\",\n    \"media-sliderthumb\", \"media-time-remaining-display\", \"media-volume-slider\",\n    \"media-volume-slider-container\", \"media-volume-sliderthumb\", \"medium\",\n    \"menu\", \"menulist\", \"menulist-button\", \"menulist-text\",\n    \"menulist-textfield\", \"menutext\", \"message-box\", \"middle\", \"min-intrinsic\",\n    \"mix\", \"mongolian\", \"monospace\", \"move\", \"multiple\", \"multiply\", \"myanmar\", \"n-resize\",\n    \"narrower\", \"ne-resize\", \"nesw-resize\", \"no-close-quote\", \"no-drop\",\n    \"no-open-quote\", \"no-repeat\", \"none\", \"normal\", \"not-allowed\", \"nowrap\",\n    \"ns-resize\", \"numbers\", \"numeric\", \"nw-resize\", \"nwse-resize\", \"oblique\", \"octal\", \"open-quote\",\n    \"optimizeLegibility\", \"optimizeSpeed\", \"oriya\", \"oromo\", \"outset\",\n    \"outside\", \"outside-shape\", \"overlay\", \"overline\", \"padding\", \"padding-box\",\n    \"painted\", \"page\", \"paused\", \"persian\", \"perspective\", \"plus-darker\", \"plus-lighter\",\n    \"pointer\", \"polygon\", \"portrait\", \"pre\", \"pre-line\", \"pre-wrap\", \"preserve-3d\",\n    \"progress\", \"push-button\", \"radial-gradient\", \"radio\", \"read-only\",\n    \"read-write\", \"read-write-plaintext-only\", \"rectangle\", \"region\",\n    \"relative\", \"repeat\", \"repeating-linear-gradient\",\n    \"repeating-radial-gradient\", \"repeat-x\", \"repeat-y\", \"reset\", \"reverse\",\n    \"rgb\", \"rgba\", \"ridge\", \"right\", \"rotate\", \"rotate3d\", \"rotateX\", \"rotateY\",\n    \"rotateZ\", \"round\", \"row\", \"row-resize\", \"row-reverse\", \"rtl\", \"run-in\", \"running\",\n    \"s-resize\", \"sans-serif\", \"saturation\", \"scale\", \"scale3d\", \"scaleX\", \"scaleY\", \"scaleZ\", \"screen\",\n    \"scroll\", \"scrollbar\", \"se-resize\", \"searchfield\",\n    \"searchfield-cancel-button\", \"searchfield-decoration\",\n    \"searchfield-results-button\", \"searchfield-results-decoration\",\n    \"semi-condensed\", \"semi-expanded\", \"separate\", \"serif\", \"show\", \"sidama\",\n    \"simp-chinese-formal\", \"simp-chinese-informal\", \"single\",\n    \"skew\", \"skewX\", \"skewY\", \"skip-white-space\", \"slide\", \"slider-horizontal\",\n    \"slider-vertical\", \"sliderthumb-horizontal\", \"sliderthumb-vertical\", \"slow\",\n    \"small\", \"small-caps\", \"small-caption\", \"smaller\", \"soft-light\", \"solid\", \"somali\",\n    \"source-atop\", \"source-in\", \"source-out\", \"source-over\", \"space\", \"space-around\", \"space-between\", \"spell-out\", \"square\",\n    \"square-button\", \"start\", \"static\", \"status-bar\", \"stretch\", \"stroke\", \"sub\",\n    \"subpixel-antialiased\", \"super\", \"sw-resize\", \"symbolic\", \"symbols\", \"table\",\n    \"table-caption\", \"table-cell\", \"table-column\", \"table-column-group\",\n    \"table-footer-group\", \"table-header-group\", \"table-row\", \"table-row-group\",\n    \"tamil\",\n    \"telugu\", \"text\", \"text-bottom\", \"text-top\", \"textarea\", \"textfield\", \"thai\",\n    \"thick\", \"thin\", \"threeddarkshadow\", \"threedface\", \"threedhighlight\",\n    \"threedlightshadow\", \"threedshadow\", \"tibetan\", \"tigre\", \"tigrinya-er\",\n    \"tigrinya-er-abegede\", \"tigrinya-et\", \"tigrinya-et-abegede\", \"to\", \"top\",\n    \"trad-chinese-formal\", \"trad-chinese-informal\",\n    \"translate\", \"translate3d\", \"translateX\", \"translateY\", \"translateZ\",\n    \"transparent\", \"ultra-condensed\", \"ultra-expanded\", \"underline\", \"up\",\n    \"upper-alpha\", \"upper-armenian\", \"upper-greek\", \"upper-hexadecimal\",\n    \"upper-latin\", \"upper-norwegian\", \"upper-roman\", \"uppercase\", \"urdu\", \"url\",\n    \"var\", \"vertical\", \"vertical-text\", \"visible\", \"visibleFill\", \"visiblePainted\",\n    \"visibleStroke\", \"visual\", \"w-resize\", \"wait\", \"wave\", \"wider\",\n    \"window\", \"windowframe\", \"windowtext\", \"words\", \"wrap\", \"wrap-reverse\", \"x-large\", \"x-small\", \"xor\",\n    \"xx-large\", \"xx-small\"\n  ], valueKeywords = keySet(valueKeywords_);\n\n  var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_)\n    .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_)\n    .concat(valueKeywords_);\n  CodeMirror.registerHelper(\"hintWords\", \"css\", allWords);\n\n  function tokenCComment(stream, state) {\n    var maybeEnd = false, ch;\n    while ((ch = stream.next()) != null) {\n      if (maybeEnd && ch == \"/\") {\n        state.tokenize = null;\n        break;\n      }\n      maybeEnd = (ch == \"*\");\n    }\n    return [\"comment\", \"comment\"];\n  }\n\n  CodeMirror.defineMIME(\"text/css\", {\n    documentTypes: documentTypes,\n    mediaTypes: mediaTypes,\n    mediaFeatures: mediaFeatures,\n    mediaValueKeywords: mediaValueKeywords,\n    propertyKeywords: propertyKeywords,\n    nonStandardPropertyKeywords: nonStandardPropertyKeywords,\n    fontProperties: fontProperties,\n    counterDescriptors: counterDescriptors,\n    colorKeywords: colorKeywords,\n    valueKeywords: valueKeywords,\n    tokenHooks: {\n      \"/\": function(stream, state) {\n        if (!stream.eat(\"*\")) return false;\n        state.tokenize = tokenCComment;\n        return tokenCComment(stream, state);\n      }\n    },\n    name: \"css\"\n  });\n\n  CodeMirror.defineMIME(\"text/x-scss\", {\n    mediaTypes: mediaTypes,\n    mediaFeatures: mediaFeatures,\n    mediaValueKeywords: mediaValueKeywords,\n    propertyKeywords: propertyKeywords,\n    nonStandardPropertyKeywords: nonStandardPropertyKeywords,\n    colorKeywords: colorKeywords,\n    valueKeywords: valueKeywords,\n    fontProperties: fontProperties,\n    allowNested: true,\n    tokenHooks: {\n      \"/\": function(stream, state) {\n        if (stream.eat(\"/\")) {\n          stream.skipToEnd();\n          return [\"comment\", \"comment\"];\n        } else if (stream.eat(\"*\")) {\n          state.tokenize = tokenCComment;\n          return tokenCComment(stream, state);\n        } else {\n          return [\"operator\", \"operator\"];\n        }\n      },\n      \":\": function(stream) {\n        if (stream.match(/\\s*\\{/))\n          return [null, \"{\"];\n        return false;\n      },\n      \"$\": function(stream) {\n        stream.match(/^[\\w-]+/);\n        if (stream.match(/^\\s*:/, false))\n          return [\"variable-2\", \"variable-definition\"];\n        return [\"variable-2\", \"variable\"];\n      },\n      \"#\": function(stream) {\n        if (!stream.eat(\"{\")) return false;\n        return [null, \"interpolation\"];\n      }\n    },\n    name: \"css\",\n    helperType: \"scss\"\n  });\n\n  CodeMirror.defineMIME(\"text/x-less\", {\n    mediaTypes: mediaTypes,\n    mediaFeatures: mediaFeatures,\n    mediaValueKeywords: mediaValueKeywords,\n    propertyKeywords: propertyKeywords,\n    nonStandardPropertyKeywords: nonStandardPropertyKeywords,\n    colorKeywords: colorKeywords,\n    valueKeywords: valueKeywords,\n    fontProperties: fontProperties,\n    allowNested: true,\n    tokenHooks: {\n      \"/\": function(stream, state) {\n        if (stream.eat(\"/\")) {\n          stream.skipToEnd();\n          return [\"comment\", \"comment\"];\n        } else if (stream.eat(\"*\")) {\n          state.tokenize = tokenCComment;\n          return tokenCComment(stream, state);\n        } else {\n          return [\"operator\", \"operator\"];\n        }\n      },\n      \"@\": function(stream) {\n        if (stream.eat(\"{\")) return [null, \"interpolation\"];\n        if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\\b/, false)) return false;\n        stream.eatWhile(/[\\w\\\\\\-]/);\n        if (stream.match(/^\\s*:/, false))\n          return [\"variable-2\", \"variable-definition\"];\n        return [\"variable-2\", \"variable\"];\n      },\n      \"&\": function() {\n        return [\"atom\", \"atom\"];\n      }\n    },\n    name: \"css\",\n    helperType: \"less\"\n  });\n\n  CodeMirror.defineMIME(\"text/x-gss\", {\n    documentTypes: documentTypes,\n    mediaTypes: mediaTypes,\n    mediaFeatures: mediaFeatures,\n    propertyKeywords: propertyKeywords,\n    nonStandardPropertyKeywords: nonStandardPropertyKeywords,\n    fontProperties: fontProperties,\n    counterDescriptors: counterDescriptors,\n    colorKeywords: colorKeywords,\n    valueKeywords: valueKeywords,\n    supportsAtComponent: true,\n    tokenHooks: {\n      \"/\": function(stream, state) {\n        if (!stream.eat(\"*\")) return false;\n        state.tokenize = tokenCComment;\n        return tokenCComment(stream, state);\n      }\n    },\n    name: \"css\",\n    helperType: \"gss\"\n  });\n\n});\n"
  },
  {
    "path": "app/src/assets/vendor/js/codemirror/xml.js",
    "content": "// CodeMirror, copyright (c) by Marijn Haverbeke and others\n// Distributed under an MIT license: http://codemirror.net/LICENSE\nimport CodeMirror from 'codemirror';\n\n(function(mod) {\n    mod(CodeMirror);\n})(function(CodeMirror) {\n    \"use strict\";\n\n    var htmlConfig = {\n        autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,\n            'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,\n            'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,\n            'track': true, 'wbr': true, 'menuitem': true},\n        implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,\n            'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,\n            'th': true, 'tr': true},\n        contextGrabbers: {\n            'dd': {'dd': true, 'dt': true},\n            'dt': {'dd': true, 'dt': true},\n            'li': {'li': true},\n            'option': {'option': true, 'optgroup': true},\n            'optgroup': {'optgroup': true},\n            'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,\n                'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,\n                'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,\n                'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,\n                'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},\n            'rp': {'rp': true, 'rt': true},\n            'rt': {'rp': true, 'rt': true},\n            'tbody': {'tbody': true, 'tfoot': true},\n            'td': {'td': true, 'th': true},\n            'tfoot': {'tbody': true},\n            'th': {'td': true, 'th': true},\n            'thead': {'tbody': true, 'tfoot': true},\n            'tr': {'tr': true}\n        },\n        doNotIndent: {\"pre\": true},\n        allowUnquoted: true,\n        allowMissing: true,\n        caseFold: true\n    }\n\n    var xmlConfig = {\n        autoSelfClosers: {},\n        implicitlyClosed: {},\n        contextGrabbers: {},\n        doNotIndent: {},\n        allowUnquoted: false,\n        allowMissing: false,\n        caseFold: false\n    }\n\n    CodeMirror.defineMode(\"xml\", function(editorConf, config_) {\n        var indentUnit = editorConf.indentUnit\n        var config = {}\n        var defaults = config_.htmlMode ? htmlConfig : xmlConfig\n        for (var prop in defaults) config[prop] = defaults[prop]\n        for (var prop in config_) config[prop] = config_[prop]\n\n        // Return variables for tokenizers\n        var type, setStyle;\n\n        function inText(stream, state) {\n            function chain(parser) {\n                state.tokenize = parser;\n                return parser(stream, state);\n            }\n\n            var ch = stream.next();\n            if (ch == \"<\") {\n                if (stream.eat(\"!\")) {\n                    if (stream.eat(\"[\")) {\n                        if (stream.match(\"CDATA[\")) return chain(inBlock(\"atom\", \"]]>\"));\n                        else return null;\n                    } else if (stream.match(\"--\")) {\n                        return chain(inBlock(\"comment\", \"-->\"));\n                    } else if (stream.match(\"DOCTYPE\", true, true)) {\n                        stream.eatWhile(/[\\w\\._\\-]/);\n                        return chain(doctype(1));\n                    } else {\n                        return null;\n                    }\n                } else if (stream.eat(\"?\")) {\n                    stream.eatWhile(/[\\w\\._\\-]/);\n                    state.tokenize = inBlock(\"meta\", \"?>\");\n                    return \"meta\";\n                } else {\n                    type = stream.eat(\"/\") ? \"closeTag\" : \"openTag\";\n                    state.tokenize = inTag;\n                    return \"tag bracket\";\n                }\n            } else if (ch == \"&\") {\n                var ok;\n                if (stream.eat(\"#\")) {\n                    if (stream.eat(\"x\")) {\n                        ok = stream.eatWhile(/[a-fA-F\\d]/) && stream.eat(\";\");\n                    } else {\n                        ok = stream.eatWhile(/[\\d]/) && stream.eat(\";\");\n                    }\n                } else {\n                    ok = stream.eatWhile(/[\\w\\.\\-:]/) && stream.eat(\";\");\n                }\n                return ok ? \"atom\" : \"error\";\n            } else {\n                stream.eatWhile(/[^&<]/);\n                return null;\n            }\n        }\n        inText.isInText = true;\n\n        function inTag(stream, state) {\n            var ch = stream.next();\n            if (ch == \">\" || (ch == \"/\" && stream.eat(\">\"))) {\n                state.tokenize = inText;\n                type = ch == \">\" ? \"endTag\" : \"selfcloseTag\";\n                return \"tag bracket\";\n            } else if (ch == \"=\") {\n                type = \"equals\";\n                return null;\n            } else if (ch == \"<\") {\n                state.tokenize = inText;\n                state.state = baseState;\n                state.tagName = state.tagStart = null;\n                var next = state.tokenize(stream, state);\n                return next ? next + \" tag error\" : \"tag error\";\n            } else if (/[\\'\\\"]/.test(ch)) {\n                state.tokenize = inAttribute(ch);\n                state.stringStartCol = stream.column();\n                return state.tokenize(stream, state);\n            } else {\n                stream.match(/^[^\\s\\u00a0=<>\\\"\\']*[^\\s\\u00a0=<>\\\"\\'\\/]/);\n                return \"word\";\n            }\n        }\n\n        function inAttribute(quote) {\n            var closure = function(stream, state) {\n                while (!stream.eol()) {\n                    if (stream.next() == quote) {\n                        state.tokenize = inTag;\n                        break;\n                    }\n                }\n                return \"string\";\n            };\n            closure.isInAttribute = true;\n            return closure;\n        }\n\n        function inBlock(style, terminator) {\n            return function(stream, state) {\n                while (!stream.eol()) {\n                    if (stream.match(terminator)) {\n                        state.tokenize = inText;\n                        break;\n                    }\n                    stream.next();\n                }\n                return style;\n            };\n        }\n        function doctype(depth) {\n            return function(stream, state) {\n                var ch;\n                while ((ch = stream.next()) != null) {\n                    if (ch == \"<\") {\n                        state.tokenize = doctype(depth + 1);\n                        return state.tokenize(stream, state);\n                    } else if (ch == \">\") {\n                        if (depth == 1) {\n                            state.tokenize = inText;\n                            break;\n                        } else {\n                            state.tokenize = doctype(depth - 1);\n                            return state.tokenize(stream, state);\n                        }\n                    }\n                }\n                return \"meta\";\n            };\n        }\n\n        function Context(state, tagName, startOfLine) {\n            this.prev = state.context;\n            this.tagName = tagName;\n            this.indent = state.indented;\n            this.startOfLine = startOfLine;\n            if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))\n                this.noIndent = true;\n        }\n        function popContext(state) {\n            if (state.context) state.context = state.context.prev;\n        }\n        function maybePopContext(state, nextTagName) {\n            var parentTagName;\n            while (true) {\n                if (!state.context) {\n                    return;\n                }\n                parentTagName = state.context.tagName;\n                if (!config.contextGrabbers.hasOwnProperty(parentTagName) ||\n                    !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {\n                    return;\n                }\n                popContext(state);\n            }\n        }\n\n        function baseState(type, stream, state) {\n            if (type == \"openTag\") {\n                state.tagStart = stream.column();\n                return tagNameState;\n            } else if (type == \"closeTag\") {\n                return closeTagNameState;\n            } else {\n                return baseState;\n            }\n        }\n        function tagNameState(type, stream, state) {\n            if (type == \"word\") {\n                state.tagName = stream.current();\n                setStyle = \"tag\";\n                return attrState;\n            } else {\n                setStyle = \"error\";\n                return tagNameState;\n            }\n        }\n        function closeTagNameState(type, stream, state) {\n            if (type == \"word\") {\n                var tagName = stream.current();\n                if (state.context && state.context.tagName != tagName &&\n                    config.implicitlyClosed.hasOwnProperty(state.context.tagName))\n                    popContext(state);\n                if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) {\n                    setStyle = \"tag\";\n                    return closeState;\n                } else {\n                    setStyle = \"tag error\";\n                    return closeStateErr;\n                }\n            } else {\n                setStyle = \"error\";\n                return closeStateErr;\n            }\n        }\n\n        function closeState(type, _stream, state) {\n            if (type != \"endTag\") {\n                setStyle = \"error\";\n                return closeState;\n            }\n            popContext(state);\n            return baseState;\n        }\n        function closeStateErr(type, stream, state) {\n            setStyle = \"error\";\n            return closeState(type, stream, state);\n        }\n\n        function attrState(type, _stream, state) {\n            if (type == \"word\") {\n                setStyle = \"attribute\";\n                return attrEqState;\n            } else if (type == \"endTag\" || type == \"selfcloseTag\") {\n                var tagName = state.tagName, tagStart = state.tagStart;\n                state.tagName = state.tagStart = null;\n                if (type == \"selfcloseTag\" ||\n                    config.autoSelfClosers.hasOwnProperty(tagName)) {\n                    maybePopContext(state, tagName);\n                } else {\n                    maybePopContext(state, tagName);\n                    state.context = new Context(state, tagName, tagStart == state.indented);\n                }\n                return baseState;\n            }\n            setStyle = \"error\";\n            return attrState;\n        }\n        function attrEqState(type, stream, state) {\n            if (type == \"equals\") return attrValueState;\n            if (!config.allowMissing) setStyle = \"error\";\n            return attrState(type, stream, state);\n        }\n        function attrValueState(type, stream, state) {\n            if (type == \"string\") return attrContinuedState;\n            if (type == \"word\" && config.allowUnquoted) {setStyle = \"string\"; return attrState;}\n            setStyle = \"error\";\n            return attrState(type, stream, state);\n        }\n        function attrContinuedState(type, stream, state) {\n            if (type == \"string\") return attrContinuedState;\n            return attrState(type, stream, state);\n        }\n\n        return {\n            startState: function(baseIndent) {\n                var state = {tokenize: inText,\n                    state: baseState,\n                    indented: baseIndent || 0,\n                    tagName: null, tagStart: null,\n                    context: null}\n                if (baseIndent != null) state.baseIndent = baseIndent\n                return state\n            },\n\n            token: function(stream, state) {\n                if (!state.tagName && stream.sol())\n                    state.indented = stream.indentation();\n\n                if (stream.eatSpace()) return null;\n                type = null;\n                var style = state.tokenize(stream, state);\n                if ((style || type) && style != \"comment\") {\n                    setStyle = null;\n                    state.state = state.state(type || style, stream, state);\n                    if (setStyle)\n                        style = setStyle == \"error\" ? style + \" error\" : setStyle;\n                }\n                return style;\n            },\n\n            indent: function(state, textAfter, fullLine) {\n                var context = state.context;\n                // Indent multi-line strings (e.g. css).\n                if (state.tokenize.isInAttribute) {\n                    if (state.tagStart == state.indented)\n                        return state.stringStartCol + 1;\n                    else\n                        return state.indented + indentUnit;\n                }\n                if (context && context.noIndent) return CodeMirror.Pass;\n                if (state.tokenize != inTag && state.tokenize != inText)\n                    return fullLine ? fullLine.match(/^(\\s*)/)[0].length : 0;\n                // Indent the starts of attribute names.\n                if (state.tagName) {\n                    if (config.multilineTagIndentPastTag !== false)\n                        return state.tagStart + state.tagName.length + 2;\n                    else\n                        return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1);\n                }\n                if (config.alignCDATA && /<!\\[CDATA\\[/.test(textAfter)) return 0;\n                var tagAfter = textAfter && /^<(\\/)?([\\w_:\\.-]*)/.exec(textAfter);\n                if (tagAfter && tagAfter[1]) { // Closing tag spotted\n                    while (context) {\n                        if (context.tagName == tagAfter[2]) {\n                            context = context.prev;\n                            break;\n                        } else if (config.implicitlyClosed.hasOwnProperty(context.tagName)) {\n                            context = context.prev;\n                        } else {\n                            break;\n                        }\n                    }\n                } else if (tagAfter) { // Opening tag spotted\n                    while (context) {\n                        var grabbers = config.contextGrabbers[context.tagName];\n                        if (grabbers && grabbers.hasOwnProperty(tagAfter[2]))\n                            context = context.prev;\n                        else\n                            break;\n                    }\n                }\n                while (context && context.prev && !context.startOfLine)\n                    context = context.prev;\n                if (context) return context.indent + indentUnit;\n                else return state.baseIndent || 0;\n            },\n\n            electricInput: /<\\/[\\s\\w:]+>$/,\n            blockCommentStart: \"<!--\",\n            blockCommentEnd: \"-->\",\n\n            configuration: config.htmlMode ? \"html\" : \"xml\",\n            helperType: config.htmlMode ? \"html\" : \"xml\",\n\n            skipAttribute: function(state) {\n                if (state.state == attrValueState)\n                    state.state = attrState\n            }\n        };\n    });\n\n    CodeMirror.defineMIME(\"text/xml\", \"xml\");\n    CodeMirror.defineMIME(\"application/xml\", \"xml\");\n    if (!CodeMirror.mimeModes.hasOwnProperty(\"text/html\"))\n        CodeMirror.defineMIME(\"text/html\", {name: \"xml\", htmlMode: true});\n\n});\n"
  },
  {
    "path": "app/src/components/About.vue",
    "content": "<template>\n    <div class=\"about\" ref=\"content\">\n        <div class=\"about-wrapper\">\n            <p-header :title=\"$t('publii.aboutPublii')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    type=\"clean back\"\n                    slot=\"buttons\">\n                    {{ $t('ui.goBack') }}\n                </p-button>\n            </p-header>\n\n            <p class=\"about-version\">{{ $t('publii.currentPubliiVersion') }}: {{ appVersion.version }} (build {{ appVersion.build }})</p>\n\n            <fields-group>\n                <p class=\"about-copyright\">\n                    <span v-pure-html=\"$t('publii.copyright')\"></span>\n                    <router-link to=\"/about/credits\">{{ $t('publii.openSourceSoftware') }}</router-link>.\n                </p>\n\n                <p v-pure-html=\"$t('publii.dataCollectionInfo')\"></p>\n\n                <p>\n                    <a href=\"https://getpublii.com/license.html\" target=\"_blank\" rel=\"noopener noreferrer\">{{ $t('publii.licensingInformation') }}</a>\n                </p>\n            </fields-group>\n        </div>\n    </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\n\nexport default {\n    name: 'about',\n    mixins: [\n        GoToLastOpenedWebsite\n    ],\n    computed: {\n        ...mapGetters([\n            'appVersion'\n        ])\n    },\n    mounted () {\n        this.$bus.$emit('sites-list-reset');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n\n.about {\n    padding: 3rem 0 4rem;\n    width: 100%;\n\n    &-wrapper {\n        margin: 0 auto;\n        max-width: $wrapper;\n    }\n\n    .heading {\n        margin-bottom: 10 * $spacing;\n        width: 100%;\n\n        @include clearfix;\n\n        .title {\n            float: left;\n            margin: 0;\n        }\n\n        .button {\n            float: right;\n            margin-top: -.5rem;\n        }\n    }\n\n    &-version {\n       \n        margin: -2.5rem 0 4rem 0;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/AboutCredits.vue",
    "content": "<template>\n    <div class=\"credits\">\n        <div class=\"credits-wrapper\">\n            <div class=\"heading\">\n                <h1 class=\"title\">\n                    {{ $t('ui.credits') }}\n                </h1>\n\n                <p-button\n                    :onClick=\"goBack\"\n                    type=\"clean back\"\n                    slot=\"buttons\">\n                    {{ $t('ui.goBack') }}\n                </p-button>\n            </div>\n\n            <p class=\"credits-intro\">{{ $t('publii.creditsIntro') }}</p>\n            \n            <fields-group>\n               <credits-list\n                   :licenses=\"licenses\" />\n            </fields-group>\n        </div>\n    </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex';\nimport licenses from '../../licenses/all-licenses.json';\nimport AboutCreditsList from './AboutCreditsList';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\n\nexport default {\n    name: 'about-credits',\n    mixins: [GoToLastOpenedWebsite],\n    components: {\n        'credits-list': AboutCreditsList\n    },\n    computed: {\n        ...mapGetters([\n            'appVersion'\n        ])\n    },\n    data: function() {\n        return {\n            licenses\n        };\n    },\n    mounted () {\n        this.$bus.$emit('sites-list-reset');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n\n.credits {\n    padding: 3rem 0 4rem;\n    width: 100%;\n\n    &-wrapper {\n        margin: 0 auto;\n        max-width: $wrapper;\n    }\n\n    .heading {\n        margin-bottom: 10 * $spacing;\n        width: 100%;\n\n        @include clearfix;\n\n        .title {\n            float: left;\n            margin: 0;\n        }\n\n        .button {\n            float: right;\n            margin-top: -.5rem;\n        }\n    }\n\n    &-intro {\n        margin: -2.5rem 0 4rem 0;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/AboutCreditsList.vue",
    "content": "<template>\n    <dl\n        class=\"credits-list\"\n        ref=\"content\">\n        <template v-for=\"(licenseData, index) in licensesData\">\n            <dt class=\"credits-item\">\n                {{ licenseData.name }}\n\n                <a\n                    v-if=\"licenseData.target !== '_blank'\"\n                    class=\"credits-toggle\"\n                    @click.prevent=\"itemClicked($event, licenseData.id, licenseData.url)\"\n                    :href=\"licenseData.href\">\n                    {{ $t('publii.license') }}\n                </a>\n\n                <a\n                    v-if=\"licenseData.target === '_blank'\"\n                    class=\"credits-toggle\"\n                    :href=\"licenseData.href\"\n                    target=\"_blank\">\n                    {{ $t('publii.license') }}\n                </a>\n\n                <a\n                    v-if=\"licenseData.homepage\"\n                    :href=\"licenseData.homepage\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\">\n                    {{ $t('publii.homepage') }}\n                </a>\n            </dt>\n\n            <dd :class=\"licenseData.cssClasses\" :data-id=\"licenseData.id\">\n                <pre></pre>\n            </dd>\n        </template>\n    </dl>\n</template>\n\n<script>\nexport default {\n    name: 'about-credits-list',\n    props: [\n        'licenses'\n    ],\n    data: function() {\n        return {\n            openedID: -1\n        };\n    },\n    computed: {\n        licensesData: function() {\n            let licensesData = [];\n            let licensePackages = Object.keys(this.licenses).sort();\n            let licenseID = 1;\n\n            for(let licenseKey of licensePackages) {\n                let licenseObject = this.parseLicense(licenseKey, licenseID);\n                licensesData.push(licenseObject);\n                licenseID++;\n            }\n\n            return licensesData;\n        }\n    },\n    mounted () {\n        (async () => {\n            this.appDirPath = await mainProcessAPI.invoke('app-credits-list:get-app-path');\n        });\n    },\n    methods: {\n        itemClicked: function(e, id, licenseUrl) {\n            if(e.target.getAttribute('href') === '#') {\n                if(this.openedID === id) {\n                    this.openedID = -1;\n                    return;\n                }\n\n                this.openedID = id;\n                this.loadLicense(e.target, licenseUrl);\n            }\n        },\n        loadLicense: function(link, licenseUrl) {\n            mainProcessAPI.send('app-license-load', {\n                url: licenseUrl\n            });\n\n            mainProcessAPI.receiveOnce('app-license-loaded', function(licenseText) {\n                if (licenseText.translation) {\n                    licenseText = this.$t(licenseText);\n                }\n\n                link.parentNode.nextElementSibling.querySelector('pre').innerText = licenseText;\n            });\n        },\n        parseLicense: function(licenseKey, licenseID) {\n            let licenseHomepage = this.licenses[licenseKey].url;\n            let licenseName = licenseKey.split('@')[0];\n            let licenseUrl = `licenses/${licenseName}/license.txt`;\n            let licenseExternal = false;\n            let licenseHref = '#';\n            let licenseTarget = '';\n\n            if(!licenseHomepage) {\n                licenseHomepage = this.licenses[licenseKey].repository;\n            }\n\n            if(this.licenses[licenseKey].licenseFile) {\n                licenseUrl = this.licenses[licenseKey].licenseFile;\n            }\n\n            if(this.licenses[licenseKey]['helper-text']) {\n                let appDirPath = remote.app.getAppPath();\n                licenseUrl = 'file://' + appDirPath + '/' + this.licenses[licenseKey].url;\n                licenseHomepage = false;\n                licenseExternal = true;\n                licenseHref = licenseUrl;\n                licenseTarget = '_blank';\n            }\n\n            return {\n                id: licenseID,\n                name: licenseName,\n                url: licenseUrl,\n                href: licenseHref,\n                homepage: licenseHomepage,\n                isExternal: licenseExternal,\n                target: licenseTarget,\n                cssClasses: {\n                    'credits-content': true,\n                    'is-hidden': parseInt(this.openedID, 10) !== licenseID\n                }\n            };\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.credits {\n    &-list {\n        margin-top: 0;\n    }\n\n    &-item {\n        border-bottom: 1px solid var(--border-light-color);\n        padding: 1.4rem 0;\n\n        & > a {\n            color: var(--link-primary-color);\n            float: right;\n            font-size: 1.4rem;\n            margin-left: 5rem;\n\n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--link-primary-color-hover);\n            }\n        }\n\n        &:last-of-type {\n            border-bottom: none;\n        }\n    }\n\n    &-content {\n        margin: 0;\n        padding: 2rem;\n\n        pre {\n            white-space: pre-line;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/App.vue",
    "content": "<template>\n    <div\n        id=\"app\"\n        :class=\"{ 'app-view': true, 'use-wide-scrollbars': useWideScrollbars }\"\n        :style=\"$root.overridedCssVariables\">\n        <message />\n        <topbar v-if=\"!splashScreenDisplayed && !itemEditorDisplayed\" />\n        <section :class=\"$route.path.replace(/^\\//mi, '').replace(/\\/$/mi, '').replace(/\\//gmi, '-')\">\n            <router-view />\n        </section>\n\n        <confirm />\n        <alert />\n        <rendering-popup />\n        <regenerate-thumbnails-popup />\n        <error-popup />\n        <sites-popup />\n        <sync-popup />\n    </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex';\nimport TopBar from './TopBar';\nimport TopBarAppBar from './TopBarAppBar';\nimport Message from './Message';\nimport RenderingPopup from './RenderingPopup';\nimport RegenerateThumbnailsPopup from './RegenerateThumbnailsPopup';\nimport SitesPopup from './SitesPopup';\nimport SyncPopup from './SyncPopup';\nimport ErrorPopup from './ErrorPopup';\n\nexport default {\n    name: 'app',\n    props: [\n        'initialData'\n    ],\n    components: {\n        'message': Message,\n        'topbar': TopBar,\n        'topbar-appbar': TopBarAppBar,\n        'rendering-popup': RenderingPopup,\n        'regenerate-thumbnails-popup': RegenerateThumbnailsPopup,\n        'error-popup': ErrorPopup,\n        'sites-popup': SitesPopup,\n        'sync-popup': SyncPopup\n    },\n    computed: {\n        ...mapGetters([\n            'siteNames'\n        ]),\n        splashScreenDisplayed () {\n            if(this.$route.path === '/') {\n                return true;\n            }\n\n            return false;\n        },\n        itemEditorDisplayed () {\n            if (this.$route.path.indexOf('/posts/editor/') > -1 || this.$route.path.indexOf('/pages/editor/') > -1) {\n                return true;\n            }\n\n            return false;\n        },\n        useWideScrollbars () {\n            return this.$store.state.app.config.wideScrollbars;\n        }\n    },\n    created () {\n        let notificationsReadStatus = localStorage.getItem('publii-notifications-readed') || '';\n        notificationsReadStatus = notificationsReadStatus.replace(/[^a-z0-9\\-_;\\.]/gmi, '');\n        this.$store.commit('setNotificationsReadStatus', notificationsReadStatus);\n    },\n    async mounted () {\n        // Setup app\n        this.disableDragNDrop();\n        await this.setEnvironmentInfo();\n        this.setState();\n        this.integrateTopBar();\n\n        // Display initial screen after 2sec\n        if(this.$store.state.app.config.licenseAccepted) {\n            setTimeout(() => this.showInitialScreen(), 2000);\n        }\n\n        this.$bus.$on('license-accepted', this.showInitialScreen);\n    },\n    beforeDestroy () {\n        this.$bus.$off('license-accepted');\n        mainProcessAPI.stopReceiveAll('app-license-accepted');\n    },\n    methods: {\n        // Block drag'n'drop redirects\n        disableDragNDrop () {\n            document.addEventListener('dragover', event => event.preventDefault());\n            document.addEventListener('drop', event => event.preventDefault());\n        },\n\n        // Add to <body> additional informations\n        async setEnvironmentInfo () {\n            document.body.setAttribute('data-node-version', mainProcessAPI.getEnv().nodeVersion);\n            document.body.setAttribute('data-chrome-version', mainProcessAPI.getEnv().chromeVersion);\n            document.body.setAttribute('data-electron-version', mainProcessAPI.getEnv().electronVersion);\n            document.body.setAttribute('data-os', mainProcessAPI.getEnv().platformName === 'darwin' ? 'osx' : mainProcessAPI.getEnv().platformName === 'linux' ? 'linux' : 'win');\n            document.documentElement.setAttribute('data-is-osx-11-or-higher', await mainProcessAPI.invoke('app-main-process-is-osx11-or-higher'));\n            document.body.setAttribute('data-env', mainProcessAPI.getEnv().name);\n        },\n\n        // Set initial application state tree\n        setState () {\n            this.$store.commit('init', this.initialData);\n            document.documentElement.style.setProperty('--ui-zoom-level', parseInt(this.$store.state.app.config.uiZoomLevel * 100.0, 10) + '%');\n        },\n\n        // Show site screen when there is only one website\n        // or user wants to load directly specific website\n        showInitialScreen: function() {\n            let startScreen = this.$store.state.app.config.startScreen;\n            let siteNames = this.siteNames;\n            let siteToDisplay = '!';\n            let lastOpenedWebsite = window.localStorage.getItem('publii-last-opened-website');\n\n            if (siteNames.length > 0) {\n                if (startScreen && siteNames.indexOf(startScreen) > -1) {\n                    siteToDisplay = startScreen;\n                } else if (lastOpenedWebsite !== null && siteNames.indexOf(lastOpenedWebsite) > -1) {\n                    siteToDisplay = lastOpenedWebsite;\n                } else {\n                    siteToDisplay = '!';\n                }\n            }\n\n            this.showWebsite(siteToDisplay);\n        },\n\n        // Show specific website\n        showWebsite: function(siteToDisplay) {\n            if(siteToDisplay !== '' && siteToDisplay !== '!') {\n                window.localStorage.setItem('publii-last-opened-website', siteToDisplay);\n            }\n\n            this.$router.push(`/site/${siteToDisplay}`);\n        },\n\n        // Check for helper click events for TopBar\n        integrateTopBar: function() {\n            document.body.addEventListener('click', e => {\n                this.$bus.$emit('document-body-clicked');\n            });\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('license-accepted');\n    }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../scss/vendor/normalize.css';\n@import '../scss/vendor/vue-multiselect.scss';\n@import '../scss/variables.scss';\n@import '../scss/css-variables.scss';\n@import '../scss/mixins.scss';\n@import '../scss/global.scss';\n@import '../scss/forms.scss';\n@import '../scss/scope-fix.scss';\n@import '../scss/codemirror.scss';\n\n/*\n * Main container for the app\n */\n.app {\n    background: var(--bg-primary);\n\n    &-view {\n        background: var(--bg-primary);\n        font-size: $app-font-base;\n        height: 100%;\n        left: 0;\n        position: absolute;\n        top: 0;\n        width: 100%;\n    }\n\n    &-site-sidebar {\n        bottom: 0;\n        font-size: $app-font-base;\n        left: 0;\n        position: absolute;\n        top: var(--topbar-height);\n        width: 35rem;\n        z-index: 1;\n    }\n}\n\n#app {\n    & > .topbar + section {\n        background: var(--bg-site);\n        height: calc(100vh - var(--topbar-height));\n        margin-top: var(--topbar-height);\n        width: 100%;\n\n        & > * {\n            height: calc(100vh - var(--topbar-height));\n            overflow: auto;\n            position: absolute;\n            width: 100%;\n        }\n    }\n\n    a {\n        -webkit-user-select: none;\n        -webkit-user-drag: none;\n        -webkit-app-region: no-drag;\n    }\n}\n\n#app > .app-settings ~ .overlay.is-minimized {\n    display: none;\n}\n\nbody[data-os=\"win\"] {    \n    .app {\n        &-view {\n            border: 1px solid var(--icon-secondary-color);\n            overflow: hidden;\n        }\n    }\n}\n\nbody[data-os=\"linux\"] {\n    #app {\n        & > .topbar + section {\n            height: 100vh;\n            margin-top: 0;\n            top: 0;\n\n            & > * {\n                height: 100vh;\n            }\n        }\n    }\n\n    .app {\n        &-view {\n            border: 1px solid var(--icon-secondary-color);\n            overflow: hidden;\n        }\n\n        &-site-sidebar {\n            top: 0;\n        }\n    }\n}\n    \n/*\n * Responsive improvements\n */\n\n@media (max-width: 1400px) {\n    .app {\n        &-site-sidebar {        \n            width: $app-sidebar;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/AppLanguages.vue",
    "content": "<template>\n    <section class=\"settings site-settings-app\">\n        <div class=\"settings-wrapper\">\n            <p-header :title=\"$t('langs.languages')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    type=\"clean back\"\n                    slot=\"buttons\">\n                    {{ $t('ui.goBack') }}\n                </p-button>\n\n                <p-button\n                    :onClick=\"installLanguage\"\n                    slot=\"buttons\" \n                    type=\"icon\"\n                    icon=\"upload-file\">\n                    {{ $t('langs.installLanguage') }}\n                </p-button>\n            </p-header>\n\n            <div ref=\"content\">\n                <languages-list />\n            </div>\n        </div>\n    </section>\n</template>\n\n<script>\nimport LanguagesList from './LanguagesList';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\n\nexport default {\n    name: 'app-languages',\n    mixins: [\n        GoToLastOpenedWebsite\n    ],\n    components: {\n        'languages-list': LanguagesList\n    },\n    mounted () {\n        this.$bus.$emit('sites-list-reset');\n    },\n    methods: {\n        goBack () {\n            let lastOpened = localStorage.getItem('publii-last-opened-website');\n            let sites = Object.keys(this.$store.state.sites);\n\n            if (sites.indexOf(lastOpened) > -1) {\n                this.$router.push('/site/' + lastOpened + '/posts/');\n            } else {\n                if (sites.length > 0) {\n                    this.$router.push('/site/' + sites[0] + '/posts/');\n                } else {\n                    this.$router.push('/site/!/posts/');\n                }\n            }\n        },\n        async installLanguage () {\n            await mainProcessAPI.invoke('app-main-process-select-file', 'file-select');\n\n            mainProcessAPI.receiveOnce('app-file-selected', (data) => {\n                if (data.path === undefined || !data.path.filePaths.length) {\n                    return;\n                }\n\n                mainProcessAPI.send('app-language-upload', {\n                    sourcePath: data.path.filePaths[0]\n                });\n\n                mainProcessAPI.receiveOnce('app-language-uploaded', this.uploadedLanguage);\n            });\n        },\n        uploadedLanguage (data) {\n            this.$store.commit('replaceAppLanguages', data.languages);\n\n            let messageConfig = {\n                message: '',\n                type: 'success',\n                lifeTime: 3\n            };\n\n            if(data.status === 'added') {\n                messageConfig.message = this.$t('langs.addLanguageSuccessMessage');\n            } else if(data.status === 'updated') {\n                messageConfig.message = this.$t('langs.updatedLanguageSuccessMessage');\n            } else if(data.status === 'wrong-format') {\n                messageConfig.message = this.$t('langs.uploadLanguageErrorMessage');\n                messageConfig.type = 'warning';\n            }\n\n            this.$bus.$emit('message-display', messageConfig);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.settings {\n    padding: 3rem 0 4rem;\n    width: 100%;\n\n    &-wrapper {\n        margin: 0 auto;\n        max-width: $wrapper;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/AppPlugins.vue",
    "content": "<template>\n    <section class=\"settings site-settings-app\">\n        <div class=\"settings-wrapper\">\n            <p-header :title=\"$t('plugins.plugins')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    type=\"clean back\"\n                    slot=\"buttons\">\n                    {{ $t('ui.goBack') }}\n                </p-button>\n\n                <p-button\n                    :onClick=\"installPlugin\"\n                    slot=\"buttons\" \n                    type=\"icon\"\n                    icon=\"upload-file\">\n                    {{ $t('plugins.installPlugin') }}\n                </p-button>\n            </p-header>\n\n            <div ref=\"content\">\n                <plugins-list />\n            </div>\n        </div>\n    </section>\n</template>\n\n<script>\nimport PluginsList from './PluginsList';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\n\nexport default {\n    name: 'app-plugins',\n    mixins: [\n        GoToLastOpenedWebsite\n    ],\n    components: {\n        'plugins-list': PluginsList\n    },\n    mounted () {\n        this.$bus.$emit('sites-list-reset');\n    },\n    methods: {\n        goBack () {\n            let lastOpened = localStorage.getItem('publii-last-opened-website');\n            let sites = Object.keys(this.$store.state.sites);\n\n            if (sites.indexOf(lastOpened) > -1) {\n                this.$router.push('/site/' + lastOpened + '/posts/');\n            } else {\n                if (sites.length > 0) {\n                    this.$router.push('/site/' + sites[0] + '/posts/');\n                } else {\n                    this.$router.push('/site/!/posts/');\n                }\n            }\n        },\n        async installPlugin () {\n            await mainProcessAPI.invoke('app-main-process-select-file', 'file-select');\n\n            mainProcessAPI.receiveOnce('app-file-selected', (data) => {\n                if (data.path === undefined || !data.path.filePaths.length) {\n                    return;\n                }\n\n                mainProcessAPI.send('app-plugin-upload', {\n                    sourcePath: data.path.filePaths[0]\n                });\n\n                mainProcessAPI.receiveOnce('app-plugin-uploaded', this.uploadedPlugin);\n            });\n        },\n        uploadedPlugin (data) {\n            this.$store.commit('replaceAppPlugins', data.plugins);\n\n            let messageConfig = {\n                message: '',\n                type: 'success',\n                lifeTime: 3\n            };\n\n            if(data.status === 'added') {\n                messageConfig.message = this.$t('plugins.addPluginSuccessMessage');\n            } else if(data.status === 'updated') {\n                messageConfig.message = this.$t('plugins.updatedPluginSuccessMessage');\n            } else if(data.status === 'wrong-format') {\n                messageConfig.message = this.$t('plugins.uploadPluginErrorMessage');\n                messageConfig.type = 'warning';\n            }\n\n            this.$bus.$emit('app-update-notifications-counters');\n            this.$bus.$emit('message-display', messageConfig);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.settings {\n    padding: 3rem 0 4rem;\n    width: 100%;\n\n    &-wrapper {\n        margin: 0 auto;\n        max-width: $wrapper;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/AppSettings.vue",
    "content": "<template>\n    <section class=\"settings site-settings-app\">\n        <div class=\"settings-wrapper\">\n            <p-header :title=\"$t('settings.appSettings')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    type=\"clean back\"\n                    slot=\"buttons\">\n                    {{ $t('ui.goBack') }}\n                </p-button>\n\n                <p-button\n                    :onClick=\"checkBeforeSave\"\n                    slot=\"buttons\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n            </p-header>\n\n            <fields-group :title=\"$t('settings.basicSettings')\">\n                <field\n                    id=\"theme\"\n                    :label=\"$t('settings.colorTheme')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"start\"\n                        v-model=\"theme\"\n                        :items=\"availableColorSchemes\"></dropdown>\n                </field>\n\n                <field\n                    id=\"start\"\n                    :label=\"$t('settings.loadAtStart')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"start\"\n                        v-model=\"screensSelected\"\n                        :items=\"screens\"></dropdown>\n                </field>\n\n                <field\n                    id=\"time-format\"\n                    :label=\"$t('settings.timeFormat')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"time-format\"\n                        :items=\"timeFormats\"\n                        v-model=\"timeFormatsSelected\"></dropdown>\n                </field>\n\n                <field\n                    id=\"images-resize-engine\"\n                    :label=\"$t('settings.imagesResizeEngine')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"images-resize-engine\"\n                        :items=\"imageResizeEngines\"\n                        v-model=\"imageResizeEnginesSelected\"></dropdown>\n                    <p\n                        v-if=\"showWebpWarning\"\n                        slot=\"note\" \n                        class=\"msg msg-icon msg-alert\">\n                        <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                        <span>{{ $t('settings.imageResizeEngineWarning') }}</span>\n                    </p>\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('settings.imageResizeEngineInfo') }}\n                    </small>\n                </field>  \n                \n                <field\n                    id=\"app-ui-zoom-level\"\n                    :label=\"$t('settings.appUIZoomLevel')\">\n                    <range-slider\n                        :min=\"0.75\"\n                        :max=\"2.5\"\n                        :step=\"0.05\"\n                        v-model=\"uiZoomLevel\"\n                        :contentModifier=\"(value) => parseInt(value * 100.0, 10) + '%'\"\n                        slot=\"field\"></range-slider>\n                    <span\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('settings.appUIZoomLevelInfo') }}\n                    </span>\n                </field>\n\n                <field\n                    id=\"notifications-center-enabled\"\n                    :label=\"$t('settings.notificationsCenterEnabled')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"notifications-center-enabled\"\n                        v-model=\"notificationsStatus\" />\n                </field>\n\n                <field\n                    id=\"always-save-search-state\"\n                    :label=\"$t('settings.alwaysSaveSearchState')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"always-save-search-state\"\n                        v-model=\"alwaysSaveSearchState\" />\n                    <span\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('settings.saveSearchStateInfo') }}\n                    </span>\n                </field>\n\n                <field\n                    id=\"show-modification-date\"\n                    :label=\"$t('settings.showModificationDate')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"show-modification-date\"\n                        v-model=\"showModificationDate\" />\n                </field>\n                \n                <field\n                    v-if=\"showModificationDate\"\n                    id=\"show-modification-date-as-column\"\n                    :label=\"$t('settings.showModificationDateAsColumn')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"show-modification-date-as-column\"\n                        v-model=\"showModificationDateAsColumn\" />\n                </field>\n\n                <field\n                    id=\"show-post-slugs\"\n                    :label=\"$t('settings.showPostSlugsOnTheListing')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"show-post-slugs\"\n                        v-model=\"showPostSlugs\" />\n                </field>\n\n                <field\n                    id=\"show-post-tags\"\n                    :label=\"$t('settings.showPostTagsOnTheListing')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"show-post-tags\"\n                        v-model=\"showPostTags\" />\n                </field>\n\n                <field\n                    id=\"wide-scrollbars\"\n                    :label=\"$t('settings.useWideScrollbars')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"wide-scrollbars\"\n                        v-model=\"wideScrollbars\" />\n                </field>\n            </fields-group>\n\n            <fields-group :title=\"$t('settings.filesLocation')\">\n                <field\n                    id=\"sites-location\"\n                    :label=\"$t('settings.sitesLocation')\">\n                    <dir-select\n                        id=\"sites-location\"\n                        :placeholder=\"$t('settings.leaveBlankToUseDefaultSitesDir')\"\n                        v-model=\"locations.sites\"\n                        :readonly=\"syncInProgress\"\n                        slot=\"field\" />\n                    <switcher\n                        v-if=\"originalSitesLocation !== locations.sites\"\n                        slot=\"field\"\n                        id=\"site-location-switcher\"\n                        v-model=\"changeSitesLocationWithoutCopying\"\n                        :label=\"$t('settings.changeSitesLocationWithoutCopyingFiles')\" />\n                    <small\n                        v-if=\"syncInProgress\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.duringSyncYouCantChangeFilesLocation') }}\n                    </small>\n                    <small\n                        v-if=\"locations.sites !== '' && !checkSitesCatalog\"\n                        slot=\"note\"\n                        class=\"note is-invalid\">\n                        {{ $t('settings.selectedDirInvalid') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"backups-location\"\n                    :label=\"$t('settings.backupLocation')\">\n                    <dir-select\n                        id=\"backups-location\"\n                        :placeholder=\"$t('settings.leaveBlankForDefaultBackupsDir')\"\n                        v-model=\"locations.backups\"\n                        :readonly=\"syncInProgress\"\n                        slot=\"field\" />\n                    <small\n                        v-if=\"syncInProgress\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.duringSyncYouCantChangeFilesLocation') }}\n                    </small>\n                    <small\n                        v-if=\"locations.backups !== '' && !checkBackupsCatalog\"\n                        slot=\"note\"\n                        class=\"note is-invalid\">\n                        {{ $t('settings.selectedDirInvalid') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"preview-location\"\n                    :label=\"$t('settings.previewLocation')\">\n                    <dir-select\n                        id=\"preview-location\"\n                        :placeholder=\"$t('settings.leaveBlankForDefaultPreviewDirectory')\"\n                        v-model=\"locations.preview\"\n                        :readonly=\"syncInProgress\"\n                        slot=\"field\" />\n                    <small\n                        v-if=\"syncInProgress\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.duringSyncYouCantChangeFilesLocation') }}\n                    </small>\n                    <small\n                        v-if=\"locations.preview !== '' && !checkPreviewCatalog\"\n                        slot=\"note\"\n                        class=\"note is-invalid\">\n                        {{ $t('settings.selectedDirInvalid') }}\n                    </small>\n                </field>\n            </fields-group>\n\n            <fields-group :title=\"$t('settings.defaultOrderingOnLists')\">\n                <field\n                    id=\"posts-ordering\"\n                    :label=\"$t('settings.defaultPostsOrdering')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"posts-ordering\"\n                        :items=\"orderingPostItems\"\n                        v-model=\"postsOrdering\"></dropdown>\n                </field>\n\n                <field\n                    id=\"pages-ordering\"\n                    :label=\"$t('settings.defaultPagesOrdering')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"pages-ordering\"\n                        :items=\"orderingPageItems\"\n                        v-model=\"pagesOrdering\"></dropdown>\n                </field>\n\n                <field\n                    id=\"tags-ordering\"\n                    :label=\"$t('settings.defaultTagsOrdering')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"tags-ordering\"\n                        :items=\"orderingTagItems\"\n                        v-model=\"tagsOrdering\"></dropdown>\n                </field>\n\n                <field\n                    id=\"authors-ordering\"\n                    :label=\"$t('settings.defaultAuthorsOrdering')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"authors-ordering\"\n                        :items=\"orderingAuthorItems\"\n                        v-model=\"authorsOrdering\"></dropdown>\n                </field>\n            </fields-group>\n\n            <fields-group :title=\"$t('settings.optionsForEditors')\">\n                <field\n                    id=\"editor-font-size\"\n                    :label=\"$t('settings.editorFontSize')\">\n                    <range-slider\n                        :min=\"18\"\n                        :max=\"22\"\n                        :step=\"1\"\n                        v-model=\"editorFontSize\"\n                        slot=\"field\"></range-slider>\n                </field>\n\n                <field\n                    id=\"editor-font-family\"\n                    :label=\"$t('settings.editorFontFamily')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"editor-font-family\"\n                        :items=\"editorFontFamilyItems\"\n                        v-model=\"editorFontFamily\"></dropdown>\n                </field>\n\n                 <field\n                    id=\"close-editor-on-save\"\n                    :label=\"$t('settings.closePostEditorOnSave')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"close-editor-on-save\"\n                        v-model=\"closeEditorOnSave\" />\n                </field>\n            </fields-group>\n\n            <fields-group :title=\"$t('settings.optionsForDevelopers')\">\n                <field\n                    id=\"open-devtools-in-main\"\n                    :label=\"$t('settings.openDevtoolsInMainW')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\" \n                        id=\"open-devtools-in-main\"\n                        v-model=\"openDevToolsInMainWindow\" />\n                        <span\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('settings.requiresRestartingApp') }}\n                    </span>\n                </field>\n\n                <field\n                    id=\"enable-advanced-preview\"\n                    :label=\"$t('settings.enableAdvancedPreview')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"enable-advanced-preview\"\n                        v-model=\"enableAdvancedPreview\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('settings.advancedPreviewDesc') }}\n                    </small>\n                </field>\n            </fields-group>\n\n            <fields-group :title=\"$t('settings.optionsForExperimentalFeatures')\">\n                 <div\n                    class=\"msg msg-icon msg-info\">\n                    <icon\n                        name=\"warning\"\n                        customWidth=\"28\"\n                        customHeight=\"28\" />\n                    <div v-pure-html=\"$t('settings.experimentalFeaturesWarning')\"></div>\n                </div>\n                <field\n                    id=\"experimental-feature-app-ui-languages\"\n                    :label=\"$t('settings.experimentalFeatureAppAutoBeautifySourceCode')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"experimental-feature-app-ui-languages\"\n                        v-model=\"experimentalFeatureAppAutoBeautifySourceCode\" />\n                    <small \n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('settings.experimentalFeatureAppAutoBeautifySourceCodeDesc') }}\n                    </small>\n                </field>\n                <field\n                    id=\"experimental-feature-app-alt-ftp\"\n                    :label=\"$t('settings.experimentalFeatureAppFtpAlt')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"experimental-feature-app-alt-ftp\"\n                        v-model=\"experimentalFeatureAppFtpAlt\" />\n                    <small \n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('settings.experimentalFeatureAppFtpAltDesc') }}\n                    </small>\n                </field>\n                <field\n                    id=\"experimental-feature-app-file-manager-in-sidebar\"\n                    :label=\"$t('settings.experimentalFileManagerInSidebar')\"\n                    :labelSeparated=\"false\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"experimental-feature-app-file-manager-in-sidebar\"\n                        v-model=\"experimentalFileManagerInSidebar\" />\n                </field>\n            </fields-group>\n\n            <p-footer>\n                <p-button\n                    :onClick=\"checkBeforeSave\"\n                    slot=\"buttons\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n            </p-footer>\n        </div>\n    </section>\n</template>\n\n<script>\nimport { experimentalFileManagerInSidebar } from '../../config/AST.app.config.js';\nimport Utils from './../helpers/utils.js';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\nimport Vue from 'vue';\n\nexport default {\n    name: 'appsettings',\n    mixins: [\n        GoToLastOpenedWebsite\n    ],\n    data () {\n        return {\n            alwaysSaveSearchState: false,\n            buttonsLocked: false,\n            screensSelected: '',\n            timeFormatsSelected: '12',\n            imageResizeEnginesSelected: 'sharp',\n            uiZoomLevel: 1.0,\n            openDevToolsInMainWindow: false,\n            wideScrollbars: false,\n            notificationsStatus: false,\n            closeEditorOnSave: true,\n            showModificationDate: true,\n            showModificationDateAsColumn: false,\n            showPostSlugs: false,\n            showPostTags: true,\n            postsOrdering: 'id DESC',\n            pagesOrdering: ' DESC',\n            tagsOrdering: 'id DESC',\n            authorsOrdering: 'id DESC',\n            originalSitesLocation: '',\n            theme: 'default',\n            enableAdvancedPreview: false,\n            locations: {\n                sites: '',\n                backups: '',\n                preview: ''\n            },\n            unwatchLocationPreview: null,\n            unwatchBackupsLocation: null,\n            editorFontSize: 18,\n            editorFontFamily: 'serif',\n            experimentalFeatureAppAutoBeautifySourceCode: false,\n            experimentalFeatureAppFtpAlt: false,\n            experimentalFileManagerInSidebar: false,\n            changeSitesLocationWithoutCopying: false,\n            sitesLocationExists: false,\n            backupsLocationExists: false,\n            previewLocationExists: false\n        };\n    },\n    computed: {\n        screens () {\n            let websites = this.$store.getters.siteDisplayNames;\n\n            return {\n                '': this.$t('settings.openLastUsedWebsite'),\n                ...websites\n            };\n        },\n        availableColorSchemes () {\n            return {\n                'system': this.$t('settings.systemColors'),\n                'default': this.$t('settings.lightMode'),\n                'dark': this.$t('settings.darkMode')\n            };\n        },\n        timeFormats () {\n            return {\n                '24': '24h',\n                '12': '12h'\n            };\n        },\n        imageResizeEngines () {\n            return {\n                'sharp': 'Sharp',\n                'jimp': 'Jimp'\n            };\n        },\n        orderingPostItems () {\n            return {\n                'id DESC': this.$t('settings.ordering.idDESC'),\n                'id ASC': this.$t('settings.ordering.idASC'),\n                'title DESC': this.$t('settings.ordering.titleDESC'),\n                'title ASC': this.$t('settings.ordering.titleASC'),\n                'created DESC': this.$t('settings.ordering.createdDESC'),\n                'created ASC': this.$t('settings.ordering.createdASC'),\n                'modified DESC': this.$t('settings.ordering.modifiedDESC'),\n                'modified ASC': this.$t('settings.ordering.modifiedASC'),\n                'author DESC': this.$t('settings.ordering.authorDESC'),\n                'author ASC': this.$t('settings.ordering.authorASC')\n            };\n        },\n        orderingPageItems () {\n            return {\n                ' DESC': this.$t('settings.ordering.hierarchical'),\n                'id DESC': this.$t('settings.ordering.idDESC'),\n                'id ASC': this.$t('settings.ordering.idASC'),\n                'title DESC': this.$t('settings.ordering.titleDESC'),\n                'title ASC': this.$t('settings.ordering.titleASC'),\n                'created DESC': this.$t('settings.ordering.createdDESC'),\n                'created ASC': this.$t('settings.ordering.createdASC'),\n                'modified DESC': this.$t('settings.ordering.modifiedDESC'),\n                'modified ASC': this.$t('settings.ordering.modifiedASC'),\n                'author DESC': this.$t('settings.ordering.authorDESC'),\n                'author ASC': this.$t('settings.ordering.authorASC')\n            };\n        },\n        orderingTagItems () {\n            return {\n                'id DESC': this.$t('settings.ordering.idDESC'),\n                'id ASC': this.$t('settings.ordering.idASC'),\n                'name DESC': this.$t('settings.ordering.nameDESC'),\n                'name ASC': this.$t('settings.ordering.nameASC'),\n                'postsCounter DESC': this.$t('settings.ordering.postsCounterDESC'),\n                'postsCounter ASC': this.$t('settings.ordering.postsCounterASC')\n            };\n        },\n        orderingAuthorItems () {\n            return {\n                'id DESC': this.$t('settings.ordering.idDESC'),\n                'id ASC': this.$t('settings.ordering.idASC'),\n                'name DESC': this.$t('settings.ordering.nameDESC'),\n                'name ASC': this.$t('settings.ordering.nameASC'),\n                'postsCounter DESC': this.$t('settings.ordering.postsCounterDESC'),\n                'postsCounter ASC': this.$t('settings.ordering.postsCounterASC')\n            };\n        },\n        syncInProgress () {\n            return this.$store.state.components.sidebar.syncInProgress;\n        },\n        editorFontFamilyItems () {\n            return {\n                'var(--font-base)': this.$t('settings.editorFontFamilySansSerif'),\n                'var(--font-serif)': this.$t('settings.editorFontFamilySerif')\n            };\n        },\n        showWebpWarning () {\n            return this.imageResizeEnginesSelected === 'jimp';\n        },\n        isSitesLocationExists () {\n            return this.sitesLocationExists;\n        },\n        isBackupsLocationExists () {\n            return this.backupsLocationExists;\n        },\n        isPreviewLocationExists () {\n            return this.previewLocationExists;\n        }\n    },\n    watch: {\n        'locations.sites': async function (newValue) {\n            this.checkSitesCatalog();\n        },\n        'locations.backups': async function (newValue) {\n            this.checkBackupsCatalog();\n        },\n        'locations.preview': async function (newValue) {\n            this.checkPreviewCatalog();\n        },\n        'uiZoomLevel': async function (newValue) {\n            await this.setUIZoomLevel();\n        }\n    },\n    mounted () {\n        this.locations.sites = this.$store.state.app.config.sitesLocation;\n        this.originalSitesLocation = this.locations.sites;\n        this.locations.backups = this.$store.state.app.config.backupsLocation;\n        this.locations.preview = this.$store.state.app.config.previewLocation;\n        this.alwaysSaveSearchState = this.$store.state.app.config.alwaysSaveSearchState;\n        this.wideScrollbars = this.$store.state.app.config.wideScrollbars;\n        this.notificationsStatus = this.$store.state.app.config.notificationsStatus === 'accepted';\n        this.openDevToolsInMainWindow = this.$store.state.app.config.openDevToolsInMain;\n        this.imageResizeEnginesSelected = this.$store.state.app.config.resizeEngine;\n        this.timeFormatsSelected = (this.$store.state.app.config.timeFormat).toString();\n        this.screensSelected = this.$store.state.app.config.startScreen;\n        this.closeEditorOnSave = this.$store.state.app.config.closeEditorOnSave;\n        this.showModificationDate = this.$store.state.app.config.showModificationDate;\n        this.showModificationDateAsColumn = this.$store.state.app.config.showModificationDateAsColumn;\n        this.showPostSlugs = this.$store.state.app.config.showPostSlugs;\n        this.showPostTags = this.$store.state.app.config.showPostTags;\n        this.postsOrdering = this.$store.state.app.config.postsOrdering;\n        this.pagesOrdering = this.$store.state.app.config.pagesOrdering;\n        this.tagsOrdering = this.$store.state.app.config.tagsOrdering;\n        this.authorsOrdering = this.$store.state.app.config.authorsOrdering;\n        this.enableAdvancedPreview = this.$store.state.app.config.enableAdvancedPreview;\n        this.editorFontSize = this.$store.state.app.config.editorFontSize;\n        this.editorFontFamily = this.$store.state.app.config.editorFontFamily;\n        this.experimentalFeatureAppAutoBeautifySourceCode = this.$store.state.app.config.experimentalFeatureAppAutoBeautifySourceCode;\n        this.experimentalFeatureAppFtpAlt = this.$store.state.app.config.experimentalFeatureAppFtpAlt;\n        this.experimentalFileManagerInSidebar = this.$store.state.app.config.experimentalFileManagerInSidebar;\n        this.uiZoomLevel = this.$store.state.app.config.uiZoomLevel;\n        this.theme = this.getAppTheme();\n\n        Vue.nextTick(() => {\n            this.unwatchLocationPreview = this.$watch('locations.preview', this.detectPreviewLocationChange);\n            this.unwatchBackupsLocation = this.$watch('locations.backups', this.detectBackupLocationChange);\n        });\n    },\n    methods: {\n        goBack () {\n            let lastOpened = localStorage.getItem('publii-last-opened-website');\n            let sites = Object.keys(this.$store.state.sites);\n\n            if (sites.indexOf(lastOpened) > -1) {\n                this.$router.push('/site/' + lastOpened + '/posts/');\n            } else {\n                if (sites.length > 0) {\n                    this.$router.push('/site/' + sites[0] + '/posts/');\n                } else {\n                    this.$router.push('/site/!/posts/');\n                }\n            }\n        },\n        checkBeforeSave () {\n            if (this.originalSitesLocation !== this.locations.sites) {\n                if (!this.changeSitesLocationWithoutCopying) {\n                    this.$bus.$emit('confirm-display', {\n                        hasInput: false,\n                        message: this.$t('settings.sitesLocationChangedConfirmMsg'),\n                        okClick: this.save,\n                        okLabel: this.$t('ui.ok'),\n                        cancelLabel: this.$t('ui.cancel')\n                    });\n\n                    return;\n                }\n            }\n\n            this.save();\n        },\n        save () {\n            this.buttonsLocked = true;\n\n            let newSettings = {\n                licenseAccepted: true,\n                startScreen: this.screensSelected,\n                openDevToolsInMain: this.openDevToolsInMainWindow,\n                timeFormat: this.timeFormatsSelected,\n                resizeEngine: this.imageResizeEnginesSelected,\n                uiZoomLevel: this.uiZoomLevel,\n                sitesLocation: this.locations.sites.trim(),\n                backupsLocation: this.locations.backups.trim(),\n                previewLocation: this.locations.preview.trim(),\n                wideScrollbars: this.wideScrollbars,\n                notificationsStatus: this.notificationsStatus ? 'accepted' : 'rejected',\n                closeEditorOnSave: this.closeEditorOnSave,\n                showModificationDate: this.showModificationDate,\n                showModificationDateAsColumn: this.showModificationDateAsColumn,\n                showPostSlugs: this.showPostSlugs,\n                showPostTags: this.showPostTags,\n                alwaysSaveSearchState: this.alwaysSaveSearchState,\n                postsOrdering: this.postsOrdering,\n                pagesOrdering: this.pagesOrdering,\n                tagsOrdering: this.tagsOrdering,\n                authorsOrdering: this.authorsOrdering,\n                enableAdvancedPreview: this.enableAdvancedPreview,\n                editorFontFamily: this.editorFontFamily,\n                editorFontSize: this.editorFontSize,\n                experimentalFeatureAppAutoBeautifySourceCode: this.experimentalFeatureAppAutoBeautifySourceCode,\n                experimentalFeatureAppFtpAlt: this.experimentalFeatureAppFtpAlt,\n                experimentalFileManagerInSidebar: this.experimentalFileManagerInSidebar,\n                changeSitesLocationWithoutCopying: this.changeSitesLocationWithoutCopying\n            };\n\n            let appConfigCopy = JSON.parse(JSON.stringify(this.$store.state.app.config));\n            newSettings = Utils.deepMerge(appConfigCopy, newSettings);\n\n            mainProcessAPI.send('app-config-save', newSettings);\n\n            mainProcessAPI.receiveOnce('app-config-saved', (data) => {\n                if (data.status === true && data.sites) {\n                    this.$store.commit('setSites', data.sites);\n                }\n\n                this.saved(newSettings, data);\n            });\n\n            this.$bus.$emit('app-settings-saved', newSettings);\n        },\n        saved (newSettings, data) {\n            this.$store.commit('setSiteDir', newSettings.sitesLocation);\n            this.$store.commit('setAppConfig', newSettings);\n            mainProcessAPI.send('app-backup-set-location', newSettings.backupsLocation);\n\n            if (data.status === true) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.appSettingsSavedMsg'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n            } else {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.appSettingsSaveErrorMsg'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n            }\n\n            this.$store.commit('setAppTheme', this.theme);\n            this.$bus.$emit('app-theme-change');\n            this.buttonsLocked = false;\n        },\n        getAppTheme () {\n            let theme = localStorage.getItem('publii-theme');\n\n            if (theme === 'system' || theme === 'dark' || theme === 'default') {\n                return theme;\n            }\n\n            return 'system';\n        },\n        detectPreviewLocationChange (newValue, oldValue) {\n            if (newValue !== oldValue) {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('settings.previewLocationChangedConfirmMsg'),\n                    okLabel: this.$t('ui.iUnderstand'),\n                });\n\n                this.unwatchLocationPreview();\n            }\n        },\n        detectBackupLocationChange (newValue, oldValue) {\n            if (newValue !== oldValue) {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('settings.backupLocationChangedConfirmMsg'),\n                    okLabel: this.$t('ui.iUnderstand'),\n                });\n\n                this.unwatchBackupsLocation();\n            }\n        },\n        async checkSitesCatalog () {\n            return await mainProcessAPI.existsSync(this.locations.sites);\n        },\n        async checkBackupsCatalog () {\n            return await mainProcessAPI.existsSync(this.locations.backups);\n        },\n        async checkPreviewCatalog () {\n            return await mainProcessAPI.existsSync(this.locations.preview);\n        },\n        async setUIZoomLevel () {\n            this.$store.commit('setAppUIZoomLevel', this.uiZoomLevel);\n            document.documentElement.style.setProperty('--ui-zoom-level', parseInt(this.uiZoomLevel * 100.0, 10) + '%');\n            return await mainProcessAPI.send('app-set-ui-zoom-level', this.uiZoomLevel);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/notifications.scss';\n\n.settings {\n    margin: 0 auto;\n    padding: 3rem 0 4rem;\n    user-select: none;\n    width: 100%;\n\n    &-wrapper {\n        margin: 0 auto;\n        max-width: $wrapper;\n    }\n}\n\n.note.is-warning {\n    color: var(--warning);\n}\n\n#site-location-switcher {\n    margin: 1.5rem 0 1rem;\n    display: block;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/AppThemes.vue",
    "content": "<template>\n    <section class=\"settings site-settings-app\">\n        <div class=\"settings-wrapper\">\n            <p-header :title=\"$t('theme.themes')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    type=\"clean back\"\n                    slot=\"buttons\">\n                    {{ $t('ui.goBack') }}\n                </p-button>\n\n                <p-button\n                    :onClick=\"installTheme\"\n                    slot=\"buttons\" \n                    type=\"icon\"\n                    icon=\"upload-file\">\n                    {{ $t('theme.installTheme') }}\n                </p-button>\n            </p-header>\n\n            <div ref=\"content\">\n                <themes-list />\n            </div>\n        </div>\n    </section>\n</template>\n\n<script>\nimport ThemesList from './ThemesList';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\n\nexport default {\n    name: 'app-themes',\n    mixins: [\n        GoToLastOpenedWebsite\n    ],\n    components: {\n        'themes-list': ThemesList\n    },\n    data () {\n        return {};\n    },\n    mounted () {\n        this.$bus.$emit('sites-list-reset');\n    },\n    methods: {\n        goBack () {\n            let lastOpened = localStorage.getItem('publii-last-opened-website');\n            let sites = Object.keys(this.$store.state.sites);\n\n            if (sites.indexOf(lastOpened) > -1) {\n                this.$router.push('/site/' + lastOpened + '/posts/');\n            } else {\n                if (sites.length > 0) {\n                    this.$router.push('/site/' + sites[0] + '/posts/');\n                } else {\n                    this.$router.push('/site/!/posts/');\n                }\n            }\n        },\n        async installTheme () {\n            await mainProcessAPI.invoke('app-main-process-select-file', 'file-select');\n\n            mainProcessAPI.receiveOnce('app-file-selected', (data) => {\n                if (data.path === undefined || !data.path.filePaths.length) {\n                    return;\n                }\n\n                mainProcessAPI.send('app-theme-upload', {\n                    sourcePath: data.path.filePaths[0]\n                });\n\n                mainProcessAPI.receiveOnce('app-theme-uploaded', this.uploadedTheme);\n            });\n        },\n        uploadedTheme (data) {\n            this.$store.commit('replaceAppThemes', data.themes);\n            this.$store.commit('updateSiteThemes');\n\n            let messageConfig = {\n                message: '',\n                type: 'success',\n                lifeTime: 3\n            };\n\n            if (data.status === 'added') {\n                messageConfig.message = this.$t('theme.addThemeSuccessMessage');\n            } else if(data.status === 'updated') {\n                messageConfig.message = this.$t('theme.updateThemeSuccessMessage');\n            } else if(data.status === 'wrong-format') {\n                messageConfig.message = this.$t('theme.uploadThemeErrorMessage');\n                messageConfig.type = 'warning';\n            }\n\n            this.$bus.$emit('app-update-notifications-counters');\n            this.$bus.$emit('message-display', messageConfig);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.settings {\n    padding: 3rem 0 4rem;\n    width: 100%;\n\n    &-wrapper {\n        margin: 0 auto;\n        max-width: $wrapper;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/AuthorForm.vue",
    "content": "<template>\n    <div\n        :key=\"'author-view-' + authorData.id\"\n        :data-animate=\"formAnimation ? 'true' : 'false'\"\n        class=\"options-sidebar-container\">\n        <div class=\"options-sidebar\">\n            <h2>\n                <template v-if=\"authorData.id\">{{ $t('author.editAuthor') }}</template>\n                <template v-if=\"!authorData.id\">{{ $t('author.addNewAuthor') }}</template>\n            </h2>\n\n           <span\n                class=\"options-sidebar-close\"\n                name=\"sidebar-close\"\n                @click.prevent=\"close()\">\n                &times;\n            </span>\n\n            <div\n                v-if=\"!currentThemeHasSupportForAuthorPages\"\n                slot=\"note\"\n                class=\"msg msg-small msg-icon msg-alert\">\n                <icon name=\"warning\" size=\"m\" />\n                <p>{{ $t('settings.themeDoesNotSupportAuthorPages') }}</p>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'basic' }\"\n                    @click=\"openItem('basic')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-status\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.basicInformation') }}</span>\n                </div>\n\n                <div\n                    class=\"author-settings\"\n                    style=\"max-height: none;\"\n                    ref=\"basic-content-wrapper\">\n                    <div\n                        class=\"author-settings-content\"\n                        ref=\"basic-content\">\n                        <label :class=\"{ 'is-invalid': errors.indexOf('name') > -1 }\">\n                            <span>{{ $t('ui.name') }}:</span>\n                            <input\n                                v-model=\"authorData.name\"\n                                @keyup=\"cleanError('name')\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                type=\"text\">\n                        </label>\n\n                        <label>\n                            <span>{{ $t('ui.description') }}:</span>\n                            <text-area\n                                v-model=\"authorData.description\"\n                                :wysiwyg=\"true\"\n                                :miniEditorMode=\"true\"\n                                :simplifiedToolbar=\"true\"\n                                :rows=\"4\"></text-area>\n                        </label>\n\n                        <label :class=\"{ 'is-invalid': errors.indexOf('email') > -1 }\">\n                            <span>{{ $t('author.eMail') }}:</span>\n                            <input\n                                v-model=\"authorData.email\"\n                                @keyup=\"emailChanged\"\n                                spellcheck=\"false\"\n                                type=\"text\">\n                        </label>\n\n                        <label>\n                            <span>{{ $t('author.website') }}:</span>\n                            <input\n                                v-model=\"authorData.website\"\n                                spellcheck=\"false\"\n                                type=\"text\">\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'image' }\"\n                    @click=\"openItem('image')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-image\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('author.avatarAndFeaturedImage') }}</span>\n                </div>\n\n                <div\n                    class=\"author-settings\"\n                    ref=\"image-content-wrapper\">\n                    <div\n                        class=\"author-settings-content\"\n                        ref=\"image-content\">\n                        <label>\n                            <span>{{ $t('author.avatar') }}:</span>\n                            <image-upload\n                                slot=\"field\"\n                                type=\"small\"\n                                id=\"author\"\n                                ref=\"author-avatar\"\n                                :onRemove=\"avatarRemoved\"\n                                imageType=\"authorImages\"\n                                v-model=\"authorData.avatar\" />\n                        </label>\n                        <div                               \n                            slot=\"note\"\n                            class=\"msg msg-small msg-icon msg-info\">\n                            <icon name=\"info\" size=\"m\" />\n                            <p>{{ $t('author.whenYouUseGravatarYourSiteVisitorsWillQueryAThirdPartyServer') }}</p>\n                        </div>\n                        <label class=\"use-gravatar\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"use-gravatar\"\n                                @click.native=\"toggleGravatar\"\n                                v-model=\"authorData.useGravatar\" />\n                            <span\n                                v-pure-html=\"$t('author.useGravatarMessage')\">\n                            </span>\n                        </label>\n                        <div>\n                            <label class=\"no-margin\">{{ $t('ui.featuredImage') }}:</label>\n                            <div\n                                v-if=\"!currentThemeHasSupportForAuthorImages\"\n                                slot=\"note\"\n                                class=\"msg msg-small msg-icon msg-alert\">\n                                <icon name=\"warning\" size=\"m\" />\n                                <p>{{ $t('author.themeDoesNotSupportFeaturedImagesForAuthors') }}</p>\n                            </div>\n                            <label>\n                                <image-upload\n                                    slot=\"field\"\n                                    type=\"small\"\n                                    id=\"featured-image\"\n                                    :item-id=\"authorData.id\"\n                                    ref=\"author-featured-image\"\n                                    imageType=\"authorImages\"\n                                    :onRemove=\"() => { hasFeaturedImage = false }\"\n                                    :onAdd=\"() => { hasFeaturedImage = true } \"\n                                    v-model=\"authorData.additionalData.featuredImage\" />\n\n                                <div\n                                    v-if=\"hasFeaturedImage\"\n                                    class=\"image-uploader-settings-form\">\n                                    <label>{{ $t('ui.alternativeText') }}\n                                        <text-input\n                                            ref=\"featured-image-alt\"\n                                            :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                            v-model=\"authorData.additionalData.featuredImageAlt\" />\n                                    </label>\n\n                                    <label>{{ $t('ui.caption') }}\n                                        <text-input\n                                            ref=\"featured-image-caption\"\n                                            :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                            v-model=\"authorData.additionalData.featuredImageCaption\" />\n                                    </label>\n\n                                    <label>{{ $t('ui.credits') }}\n                                        <text-input\n                                            ref=\"featured-image-credits\"\n                                            :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                            v-model=\"authorData.additionalData.featuredImageCredits\" />\n                                    </label>\n                                </div>\n                            </label>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'seo' }\"\n                    @click=\"openItem('seo')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-seo\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.seo') }}</span>\n                </div>\n\n                <div\n                    class=\"author-settings\"\n                    ref=\"seo-content-wrapper\">\n                    <div\n                        class=\"author-settings-content\"\n                        ref=\"seo-content\">\n                        <label :class=\"{ 'is-invalid': errors.indexOf('slug') > -1 }\">\n                            <span>{{ $t('ui.slug') }}:</span>\n                            <div class=\"options-sidebar-item-slug\">\n                                <input\n                                    v-model=\"authorData.username\"\n                                    @keyup=\"cleanError('slug')\"\n                                    spellcheck=\"false\"\n                                    type=\"text\">\n                                <p-button \n                                    :onClick=\"updateSlug\" \n                                    :title=\"$t('ui.updateSlug')\"\n                                    icon=\"refresh\"\n                                    type=\"secondary icon\">\n                                </p-button>\n                            </div>\n                        </label>\n\n                        <label class=\"with-char-counter\">\n                            <span>{{ $t('ui.pageTitle') }}:</span>\n                            <text-input\n                                v-model=\"authorData.metaTitle\"\n                                type=\"text\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :placeholder=\"$t('ui.leaveBlankToUseDefaultPageTitle')\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                        </label>\n\n                        <label class=\"with-char-counter\">\n                            <span>{{ $t('ui.metaDescription') }}:</span>\n                            <text-area\n                                v-model=\"authorData.metaDescription\"\n                                :placeholder=\"$t('ui.leaveBlankToUseDefaultPageTitle')\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\"></text-area>\n                        </label>\n\n                        <label>\n                            {{ $t('ui.metaRobotsIndex') }}:\n                            <dropdown\n                                v-if=\"!authorData.additionalData.canonicalUrl\"\n                                id=\"tag-meta-robots\"\n                                v-model=\"authorData.additionalData.metaRobots\"\n                                :items=\"metaRobotsOptions\">\n                            </dropdown>\n                            <div v-else>\n                                <small>{{ $t('ui.ifCanonicalUrlIsSetMetaRobotsTagIsIgnored') }}</small>\n                            </div>\n                        </label>\n\n                        <label>\n                            {{ $t('ui.canonicalURL') }}:\n                            <input\n                                type=\"text\"\n                                v-model=\"authorData.additionalData.canonicalUrl\"\n                                spellcheck=\"false\"\n                                :placeholder=\"$t('tag.leaveBlankToUseDefaultTagPageURL')\" />\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'other' }\"\n                    @click=\"openItem('other')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-options\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.otherOptions') }}</span>\n                </div>\n\n                <div\n                    class=\"author-settings\"\n                    ref=\"other-content-wrapper\">\n                    <div\n                        class=\"author-settings-content\"\n                        ref=\"other-content\">\n                        <label>\n                            <span>{{ $t('ui.customTemplate') }}:</span>\n                            <dropdown\n                                v-if=\"currentThemeHasAuthorTemplates\"\n                                ref=\"template\"\n                                id=\"template\"\n                                v-model=\"authorData.template\"\n                                :items=\"authorTemplates\"></dropdown>\n\n                            <text-input\n                                v-if=\"!currentThemeHasAuthorTemplates\"\n                                slot=\"field\"\n                                id=\"template\"\n                                :placeholder=\"$t('ui.notAvailableInYourTheme')\"\n                                :spellcheck=\"false\"\n                                :disabled=\"true\"\n                                :readonly=\"true\" />\n                        </label>\n\n                        <template v-if=\"dataSet\">\n                            <template v-for=\"(field, index) of authorViewThemeSettings\">\n                                <separator \n                                    v-if=\"displayField(field) && field.type === 'separator'\"\n                                    :label=\"field.label\"\n                                    :is-line=\"true\"\n                                    :key=\"'author-view-field-' + index\"\n                                    :note=\"field.note\" />\n\n                                <label\n                                    v-if=\"displayField(field) && field.type !== 'separator'\"\n                                    :key=\"'author-view-field-' + index\">\n                                    {{ field.label }}\n\n                                    <dropdown\n                                        v-if=\"!field.type || field.type === 'select'\"\n                                        :id=\"field.name + '-select'\"\n                                        class=\"author-view-settings\"\n                                        v-model=\"authorData.additionalData.viewConfig[field.name]\"\n                                        :items=\"generateItems(field.options)\">\n                                        <option slot=\"first-choice\" value=\"\">{{ $t('settings.useGlobalConfiguration') }}</option>\n                                    </dropdown>\n\n                                    <text-input\n                                        v-if=\"field.type === 'text' || field.type === 'number'\"\n                                        :type=\"field.type\"\n                                        class=\"author-view-settings\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        :placeholder=\"fieldPlaceholder(field)\"\n                                        v-model=\"authorData.additionalData.viewConfig[field.name]\" />\n\n                                    <text-area\n                                        v-if=\"field.type === 'textarea'\"\n                                        class=\"author-view-settings\"\n                                        :placeholder=\"fieldPlaceholder(field)\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        v-model=\"authorData.additionalData.viewConfig[field.name]\" />\n\n                                    <color-picker\n                                        v-if=\"field.type === 'colorpicker'\"\n                                        class=\"author-view-settings\"\n                                        v-model=\"authorData.additionalData.viewConfig[field.name]\"\n                                        :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\">\n                                    </color-picker>\n\n                                    <image-upload\n                                        v-if=\"field.type === 'image'\"\n                                        class=\"author-view-settings\"\n                                        v-model=\"authorData.additionalData.viewConfig[field.name]\"\n                                        :item-id=\"authorData.id\"\n                                        imageType=\"authorImages\" />\n\n                                    <small\n                                        v-if=\"field.note\"\n                                        class=\"note\">\n                                        {{ field.note }}\n                                    </small>\n                                </label>\n                            </template>\n                        </template>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-buttons\">\n                <p-button\n                    type=\"secondary\"\n                    @click.native=\"save(false)\">\n                    <template v-if=\"authorData.id\">{{ $t('ui.saveChanges') }}</template>\n                    <template v-if=\"!authorData.id\">{{ $t('author.addNewAuthor') }}</template>\n                </p-button>\n\n                <p-button\n                    :disabled=\"!authorData.id || !currentThemeHasSupportForAuthorPages\"\n                    type=\"primary\"\n                    class=\"options-sidebar-preview-button\"\n                    @click.native=\"saveAndPreview\">\n                    {{ $t('ui.saveAndPreview') }}\n                    <span>\n                        <icon\n                            size=\"s\"\n                            name=\"quick-preview\"/>\n                    </span>\n                </p-button>\n\n                <p-button\n                    @click.native=\"close\"\n                    type=\"outline\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nimport Utils from './../helpers/utils';\nimport Vue from 'vue';\n\nexport default {\n    name: 'author-form-sidebar',\n    props: [\n        'formAnimation'\n    ],\n    data () {\n        return {\n            errors: [],\n            hasFeaturedImage: false,\n            openedItem: 'basic',\n            dataSet: false,\n            authorData: {\n                id: 0,\n                name: '',\n                username: '',\n                email: '',\n                avatar: '',\n                useGravatar: false,\n                description: '',\n                metaTitle: '',\n                metaDescription: '',\n                template: '',\n                visibleIndexingOptions: false,\n                additionalData: {\n                    featuredImage: '',\n                    featuredImageAlt: '',\n                    featuredImageCaption: '',\n                    featuredImageCredits: '',\n                    metaRobots: '',\n                    canonicalUrl: ''\n                }\n            }\n        };\n    },\n    computed: {\n        currentThemeHasSupportForAuthorImages () {\n            return this.$store.state.currentSite.themeSettings.supportedFeatures && this.$store.state.currentSite.themeSettings.supportedFeatures.authorImages;\n        },\n        currentThemeHasSupportForAuthorPages () {\n            if (this.$store.state.currentSite.themeSettings.supportedFeatures && this.$store.state.currentSite.themeSettings.supportedFeatures.authorPages === false) {\n                return false;\n            }\n\n            return this.$store.state.currentSite.themeSettings.renderer.createAuthorPages;\n        },\n        authorTemplates: function() {\n            return this.$store.getters.authorTemplates;\n        },\n        currentThemeHasAuthorTemplates: function() {\n            return Object.keys(this.authorTemplates).length > 1;\n        },\n        metaRobotsOptions () {\n            return {\n                '': this.$t('settings.useSiteGlobalSettings'),\n                'index, follow': this.$t('ui.indexFollow'),\n                'index, nofollow': this.$t('ui.indexNofollow'),\n                'index, follow, noarchive': this.$t('ui.indexFollowNoArchive'),\n                'index, nofollow, noarchive': this.$t('ui.indexNofollowNoArchive'),\n                'noindex, follow': this.$t('ui.noindexFollow'),\n                'noindex, nofollow': this.$t('ui.noindexNofollow')\n            };\n        },\n        authorViewThemeSettings () {\n            return this.$store.state.currentSite.themeSettings.authorConfig;\n        }\n    },\n    mounted () {\n        this.$bus.$on('show-author-item-editor', params => {\n            try {\n                if (typeof params.additionalData === 'string' && params.additionalData) {\n                    params.additionalData = JSON.parse(params.additionalData);\n                } else {\n                    params.additionalData = {};\n                }\n            } catch (e) {\n                console.warn(this.$t('author.authorDataParsingErrorMessage') + params.id);\n                params.additionalData = {};\n            }\n\n            this.errors = [];\n            this.authorData.id = params.id || 0;\n            this.authorData.name = params.name || '';\n            this.authorData.username = params.username || '';\n            this.authorData.email = params.email || '';\n            this.authorData.website = params.website || '';\n            this.authorData.avatar = params.avatar || '';\n            this.authorData.useGravatar = params.useGravatar || false;\n            this.authorData.description = params.description || '';\n            this.authorData.metaTitle = params.metaTitle || '';\n            this.authorData.metaDescription = params.metaDescription || '';\n            this.authorData.template = params.template || '';\n            this.authorData.visibleIndexingOptions = params.visibleIndexingOptions || false;\n            this.authorData.additionalData = {};\n\n            if (typeof params.additionalData.viewConfig === 'object') {\n                this.authorData.additionalData.viewConfig = params.additionalData.viewConfig;\n            } else {\n                this.authorData.additionalData.viewConfig = {};\n            }\n\n            this.authorData.additionalData.featuredImage = params.additionalData.featuredImage || '';\n            this.authorData.additionalData.featuredImageAlt = params.additionalData.featuredImageAlt || '';\n            this.authorData.additionalData.featuredImageCaption = params.additionalData.featuredImageCaption || '';\n            this.authorData.additionalData.featuredImageCredits = params.additionalData.featuredImageCredits || '';\n            this.authorData.additionalData.metaRobots = params.additionalData.metaRobots || '';\n            this.authorData.additionalData.canonicalUrl = params.additionalData.canonicalUrl || '';\n\n            if (this.authorData.additionalData && this.authorData.additionalData.featuredImage) {\n                this.hasFeaturedImage = true;\n            }\n\n            Vue.nextTick(() => {\n                this.dataSet = true;\n            });\n        });\n    },\n    methods: {\n        async save (showPreview = false) {\n            if (this.authorData.username === '') {\n                this.authorData.username = await mainProcessAPI.invoke('app-main-process-create-slug', this.authorData.name);\n            } else {\n                this.authorData.username = await mainProcessAPI.invoke('app-main-process-create-slug', this.authorData.username);\n            }\n\n            this.$bus.$emit('view-settings-before-save');\n\n            setTimeout(() => {\n                let authorData = {\n                    id: this.authorData.id,\n                    site: this.$store.state.currentSite.config.name,\n                    name: this.authorData.name,\n                    username: this.authorData.username,\n                    config: JSON.stringify({\n                        email: this.authorData.email,\n                        website: this.authorData.website,\n                        avatar: this.authorData.avatar,\n                        useGravatar: this.authorData.useGravatar,\n                        description: this.authorData.description,\n                        metaTitle: this.authorData.metaTitle,\n                        metaDescription: this.authorData.metaDescription,\n                        template: this.authorData.template\n                    }),\n                    additionalData: JSON.stringify(this.authorData.additionalData),\n                    imageConfigFields: this.authorViewThemeSettings.filter(field => field.type === 'image').map(field => field.name)\n                };\n\n                this.saveData(authorData, showPreview);\n            }, 500);\n        },\n        async saveAndPreview () {\n            await this.save(true);\n        },\n        saveData(authorData, showPreview = false) {\n            // Send form data to the back-end\n            mainProcessAPI.send('app-author-save', authorData);\n\n            mainProcessAPI.receiveOnce('app-author-saved', (data) => {\n                if(data.status !== false) {\n                    if(authorData.id === 0) {\n                        let newlyAddedAuthor = JSON.parse(JSON.stringify(data.authors.filter(author => author.id === data.authorID)[0]));\n                        this.$bus.$emit('show-author-item-editor', newlyAddedAuthor);\n                        this.$bus.$emit('hide-author-item-editor');\n                        this.dataSet = false;\n                        this.showMessage(data.message);\n                    } else {\n                        if (!showPreview) {\n                            this.$bus.$emit('hide-author-item-editor');\n                            this.dataSet = false;\n                        }\n\n                        this.showMessage('success');\n\n                        if (showPreview) {\n                            this.$bus.$emit('rendering-popup-display', {\n                                authorOnly: true,\n                                itemID: this.authorData.id\n                            });\n                        }\n                    }\n\n                    this.$store.commit('setAuthors', data.authors);\n                    this.$store.commit('setPostAuthors', data.postsAuthors);\n                    this.$store.commit('setPageAuthors', data.pagesAuthors);\n                    this.$bus.$emit('authors-list-updated');\n                    return;\n                }\n\n                this.showMessage(data.message);\n            });\n        },\n        close() {\n            this.$bus.$emit('hide-author-item-editor');\n            this.dataSet = false;\n\n            mainProcessAPI.send('app-author-cancel', {\n                site: this.$store.state.currentSite.config.name,\n                id: this.authorData.id,\n                imageConfigFields: this.authorViewThemeSettings.filter(field => field.type === 'image').map(field => field.name)\n            });\n        },\n        showMessage(message) {\n            let msg = this.$t('author.newAuthorHasBeenCreated');\n\n            if (this.authorData.id > 0) {\n                msg = this.$t('author.authorHasBeenUpdated');\n            }\n\n            let messageConfig = {\n                message: msg,\n                type: 'success',\n                lifeTime: 3\n            };\n\n            if(message !== 'success' && message !== 'author-added') {\n                messageConfig.type = 'warning';\n            }\n\n            if (message === 'author-duplicate-name') {\n                this.displayAdvancedOptions = false;\n                this.errors.push('name');\n                messageConfig.message = this.$t('author.authorNameInUseErrorMessage');\n            } else if (message === 'author-duplicate-username') {\n                this.displayAdvancedOptions = true;\n                this.errors.push('slug');\n                messageConfig.message = this.$t('author.authorNameSimilarInUseErrorMessage');\n            } else if (message === 'author-empty-name') {\n                this.displayAdvancedOptions = false;\n                this.errors.push('name');\n                messageConfig.message = this.$t('author.authorNameCannotBeEmptyErrorMessage');\n            } else if (message === 'author-empty-email') {\n                this.displayAdvancedOptions = false;\n                this.errors.push('email');\n                messageConfig.message = this.$t('author.useGravatarServiceMessage');\n            } else if (message === 'author-empty-username') {\n                this.displayAdvancedOptions = false;\n                this.errors.push('slug');\n                messageConfig.message = this.$t('author.authorSlugCannotBeEmpty');\n            }\n\n            this.$bus.$emit('message-display', messageConfig);\n        },\n        cleanError (field) {\n            let pos = this.errors.indexOf(field);\n\n            if (pos !== -1) {\n                this.errors.splice(pos, 1);\n            }\n        },\n        toggleGravatar () {\n            if (this.authorData.useGravatar === false) {\n                this.authorData.avatar = '';\n            } else {\n                if (this.authorData.email.trim() !== '') {\n                    this.getGravatar();\n                } else {\n                    setTimeout(() => {\n                        this.errors.push('email');\n                        this.authorData.useGravatar = false;\n                        this.$bus.$emit('message-display', {\n                            message: this.$t('author.gravatarEmailWarningMessage'),\n                            type: 'warning',\n                            lifeTime: 3\n                        });\n                    }, 100);\n                }\n            }\n        },\n        emailChanged () {\n            this.cleanError('email');\n\n            if (!this.authorData.useGravatar) {\n                return;\n            }\n\n            this.getGravatar();\n        },\n        getGravatar: Utils.debouncedFunction(async function() {\n            let avatarPath = 'https://www.gravatar.com/avatar/' + await this.md5(this.authorData.email) + '?s=240';\n            this.authorData.avatar = avatarPath;\n        }, 1000),\n        async md5 (value) {\n            return await mainProcessAPI.createMD5(value);\n        },\n        avatarRemoved () {\n            this.authorData.useGravatar = false;\n        },\n        openItem (itemName) {\n            if (this.openedItem === itemName) {\n                this.closeItem();\n                return;\n            }\n\n            this.closeItem();\n            this.openedItem = itemName;\n            let contentWrapper = this.$refs[this.openedItem + '-content-wrapper'];\n            let content = this.$refs[this.openedItem + '-content'];\n            contentWrapper.style.maxHeight = content.clientHeight + \"px\";\n\n            setTimeout(function() {\n                contentWrapper.style.maxHeight = 'none';\n            }, 300);\n        },\n        closeItem () {\n            if (this.openedItem === '') {\n                return;\n            }\n\n            let contentWrapper = this.$refs[this.openedItem + '-content-wrapper'];\n            let content = this.$refs[this.openedItem + '-content'];\n            this.openedItem = '';\n\n            if (content.classList.contains('post-editor-settings-content-tags')) {\n                contentWrapper.style.overflow = 'hidden';\n            }\n\n            contentWrapper.style.maxHeight = content.clientHeight + \"px\";\n\n            setTimeout(function () {\n                contentWrapper.style.maxHeight = 0;\n            }, 50);\n        },\n        displayField (field) {\n            if (!this.dataSet) {\n                return false;\n            }\n\n            if (!field.authorTemplates) {\n                return true;\n            }\n\n            if (field.authorTemplates.indexOf('!') === 0) {\n                return !(field.authorTemplates.replace('!', '').split(',').indexOf(this.authorData.template) > -1);\n            }\n\n            return field.authorTemplates.split(',').indexOf(this.authorData.template) > -1;\n        },\n        generateItems (arrayToConvert) {\n            let options = {};\n\n            for (let i = 0; i < arrayToConvert.length; i++) {\n                options[arrayToConvert[i].value] = arrayToConvert[i].label;\n            }\n\n            return options;\n        },\n        fieldPlaceholder (field) {\n            if (field.placeholder || field.placeholder === '') {\n                return field.placeholder;\n            }\n\n\t\t\treturn this.$t('theme.leaveBlankToUseDefault');\n        },\n        async updateSlug () {\n            if (this.authorData.name.trim() !== '') {\n                this.authorData.username = await mainProcessAPI.invoke('app-main-process-create-slug', this.authorData.name);\n            }\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('show-author-item-editor');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/options-sidebar.scss';\n@import '../scss/notifications.scss';\n\n.options-sidebar {\n    .use-gravatar {\n        font-weight: 400;\n        margin-bottom: 2rem;\n    }\n}\n\n.author-settings {\n    max-height: 0;\n    overflow: hidden;\n    transition: max-height .25s ease-out;\n\n    &-content {\n        padding: 0 0 1rem;\n\n        .image-uploader {\n            margin-top: 0;\n        }\n\n        .msg {\n            margin: 0 0 2rem;\n        }\n    }\n\n}\n\n.note {\n    position: relative;\n    z-index: 1;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Authors.vue",
    "content": "<template>\n    <section :class=\"{ 'content': true, 'authors': true, 'authors-list-view': true, 'no-scroll': editorVisible }\">\n        <p-header :title=\"$t('ui.authors')\">\n            <header-search\n                slot=\"search\"\n                :placeholder=\"$t('author.filterOrSearchAuthors')\"\n                onChangeEventName=\"authors-filter-value-changed\" />\n\n            <p-button\n                :onClick=\"addAuthor\"\n                slot=\"buttons\"\n                type=\"primary icon\"\n                icon=\"add-site-mono\">\n                {{ $t('author.addNewAuthor') }}\n            </p-button>\n        </p-header>\n\n        <collection\n            v-if=\"!emptySearchResults\"\n            :itemsCount=\"4\">\n            <collection-header slot=\"header\">\n                <collection-cell>\n                    <checkbox\n                        value=\"all\"\n                        :checked=\"anyCheckboxIsSelected\"\n                        :onClick=\"toggleAllCheckboxes\" />\n                </collection-cell>\n\n                <collection-cell>\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('name')\">\n                        <template v-if=\"orderBy === 'name'\">\n                            <strong>{{ $t('ui.name') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.name') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'name' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'name' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell \n                    justifyContent=\"center\"\n                    textAlign=\"center\"\n                    min-width=\"100px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('postsCounter')\">\n                        <template v-if=\"orderBy === 'postsCounter'\">\n                            <strong>{{ $t('ui.posts') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.posts') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'postsCounter' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'postsCounter' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell min-width=\"40px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('id')\">\n                        <template v-if=\"orderBy === 'id'\">\n                            <strong>{{ $t('ui.id') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.id') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'id' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'id' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <div\n                    v-if=\"anyCheckboxIsSelected\"\n                    class=\"tools\">\n                    <p-button\n                        icon=\"trash\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkDelete\">\n                        {{ $t('ui.delete') }}\n                    </p-button>\n                </div>\n            </collection-header>\n\n            <collection-row\n                v-for=\"(item, index) in items\"\n                slot=\"content\"\n                :key=\"'collection-row-' + index\"\n                :cssClasses=\"item.id === 1 ? 'is-main-author' : ''\">\n                <collection-cell>\n                    <checkbox\n                        v-if=\"item.id !== 1\"\n                        :value=\"item.id\"\n                        :checked=\"isChecked(item.id)\"\n                        :onClick=\"toggleSelection\"\n                        :key=\"'collection-row-checkbox-' + index\" />\n\n                    <span\n                        v-if=\"item.id === 1\"\n                        class=\"main-author-icon\"\n                        :title=\"$t('author.mainAuthorCannotBeRemoved')\">\n                        <icon\n                            size=\"s\"\n                            name=\"padlock\" />\n                    </span>\n                </collection-cell>\n\n                <collection-cell type=\"titles\">\n                    <h2 class=\"title\">\n                        <a\n                            href=\"#\"\n                            @click.prevent.stop=\"editAuthor(item)\">\n                            {{ item.name }}\n\n                            <em\n                                v-if=\"item.id === 1\"\n                                class=\"is-main-author\">\n                                ({{ $t('author.mainAuthor') }})\n                            </em>\n                        </a>\n                    </h2>\n\n                    <div\n                        v-if=\"showAuthorSlugs\"\n                        class=\"author-slug\">\n                        {{ $t('author.url') }}: /{{ item.username }}<template v-if=\"!$store.state.currentSite.config.advanced.urls.cleanUrls\">.html</template>\n                    </div>\n                </collection-cell>\n\n                <collection-cell\n                    justifyContent=\"center\"\n                    textAlign=\"center\">\n                    <a\n                        @click.prevent.stop=\"showPostsConnectedWithAuthor(item.name)\"\n                        href=\"#\">\n                        {{ item.postsCounter }}\n                    </a>\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.id }}\n                </collection-cell>\n            </collection-row>\n        </collection>\n\n        <transition>\n            <author-form\n                v-if=\"editorVisible\"\n                :form-animation=\"formAnimation\" />\n        </transition>\n\n        <empty-state\n            v-if=\"emptySearchResults\"\n            :description=\"$t('author.noAuthorsMatchingYourCriteria')\"></empty-state>\n    </section>\n</template>\n\n<script>\nimport AuthorForm from './AuthorForm';\nimport CollectionCheckboxes from './mixins/CollectionCheckboxes.js';\n\nexport default {\n    name: 'authors',\n    mixins: [\n        CollectionCheckboxes\n    ],\n    components: {\n        'author-form': AuthorForm\n    },\n    data: function() {\n        return {\n            formAnimation: false,\n            editorVisible: false,\n            filterValue: '',\n            selectedItems: [],\n            orderBy: 'id',\n            order: 'DESC'\n        };\n    },\n    watch: {\n        editorVisible (newValue, oldValue) {\n            if (newValue !== oldValue) {\n                this.formAnimation = true;\n\n                setTimeout(() => {\n                    this.formAnimation = false;\n                }, 500);\n            }\n        }\n    },\n    computed: {\n        items: function() {\n            return this.$store.getters.siteAuthors(this.filterValue, this.orderBy, this.order);\n        },\n        emptySearchResults: function() {\n            return this.filterValue !== '' && !this.items.length;\n        },\n        showAuthorSlugs () {\n            return this.$store.state.app.config.showPostSlugs;\n        }\n    },\n    beforeMount () {\n        mainProcessAPI.send('app-authors-load', {\n            \"site\": this.$store.state.currentSite.config.name\n        });\n\n        mainProcessAPI.receiveOnce('app-authors-loaded', (data) => {\n            this.$store.commit('setAuthors', data.authors);\n            this.$store.commit('setPostsAuthors', data.postsAuthors);\n        });\n    },\n    mounted: function() {\n        this.orderBy = this.$store.state.ordering.authors.orderBy;\n        this.order = this.$store.state.ordering.authors.order;\n\n        this.$bus.$on('authors-filter-value-changed', (newValue) => {\n            this.filterValue = newValue.trim().toLowerCase();\n        });\n\n        this.$bus.$on('hide-author-item-editor', () => {\n            if (document.querySelector('.authors-list-view .item.is-edited')) {\n                document.querySelector('.authors-list-view .item.is-edited').classList.remove('is-edited');\n            }\n\n            this.editorVisible = false;\n        });\n\n        this.$bus.$on('site-switched', () => {\n            setTimeout(() => {\n                this.saveOrdering(this.$store.state.ordering.authors.orderBy, this.$store.state.ordering.authors.order);\n            }, 500);\n        });\n\n        this.$bus.$on('app-settings-saved', newSettings => {\n            if (this.orderBy + ' ' + this.order !== newSettings.authorsOrdering) {\n                let order = newSettings.authorsOrdering.split(' ');\n                this.saveOrdering(order[0], order[1]);\n            }\n        });\n    },\n    methods: {\n        addAuthor () {\n            this.$bus.$on('show-author-item-editor', () => ({\n                id: 0,\n                name: '',\n                username: '',\n                email: '',\n                website: '',\n                avatar: '',\n                useGravatar: false,\n                description: '',\n                postsCounter: 0,\n                metaTitle: '',\n                metaDescription: '',\n                template: '',\n                authorTemplates: [],\n                additionalData: {},\n                visibleIndexingOptions: false\n            }));\n\n            this.editorVisible = true;\n        },\n        editAuthor (item) {\n            if (document.querySelector('.authors-list-view .item.is-edited')) {\n                document.querySelector('.authors-list-view .item.is-edited').classList.remove('is-edited');\n            }\n\n            let input = document.querySelector('.authors-list-view .item input[value=\"' + parseInt(item.id, 10) + '\"]');\n            \n            if (input) {\n                input.parentNode.parentNode.classList.add('is-edited');\n            } else {\n                document.querySelector('.authors-list-view .item.is-main-author').classList.add('is-edited');\n            }\n\n            this.editorVisible = true;\n\n            setTimeout(() => {\n                this.$bus.$emit('show-author-item-editor', item);\n            }, 100);\n        },\n        bulkDelete: function() {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('author.removeAuthorsMessage'),\n                isDanger: true,\n                okClick: this.deleteSelected\n            });\n        },\n        deleteSelected: function() {\n            let itemsToRemove = this.getSelectedItems();\n            itemsToRemove = itemsToRemove.filter(item => item !== 1);\n\n            if(itemsToRemove.length === 0) {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('author.cannotRemoveMainAuthor')\n                });\n\n                return;\n            }\n\n            mainProcessAPI.send('app-author-delete', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToRemove\n            });\n\n            mainProcessAPI.receiveOnce('app-author-deleted', () => {\n                this.$store.commit('removeAuthors', itemsToRemove);\n                this.selectedItems = [];\n\n                this.$bus.$emit('message-display', {\n                    message: this.$t('author.removeAuthorsSuccessMessage'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n            });\n        },\n        showPostsConnectedWithAuthor: function(name) {\n            let siteName = this.$store.state.currentSite.config.name;\n            localStorage.setItem('publii-posts-search-value', 'author:' + name);\n            this.$router.push('/site/' + siteName + '/posts');\n        },\n        ordering (field) {\n            if (field !== this.orderBy) {\n                this.orderBy = field;\n                this.order = 'DESC';\n            } else {\n                if (this.order === 'DESC') {\n                    this.order = 'ASC';\n                } else {\n                    this.order = 'DESC';\n                }\n            }\n\n            this.saveOrdering(this.orderBy, this.order);\n        },\n        saveOrdering (orderBy, order) {\n            this.orderBy = orderBy;\n            this.order = order;\n\n            this.$store.commit('setOrdering', {\n                type: 'authors',\n                orderBy: this.orderBy,\n                order: this.order\n            });\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('authors-filter-value-changed');\n        this.$bus.$off('hide-author-item-editor');\n        this.$bus.$off('show-author-item-editor');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.authors {\n    overflow-x: hidden!important;\n\n    &.no-scroll {\n        overflow: hidden;\n    }\n\n    .is-main-author {\n        color: var(--text-light-color);\n    }\n\n    .main-author-icon {\n        position: relative;\n        top: 2px;\n    }\n\n    .header {\n        .col {\n             align-items: center;\n             display: flex;\n\n             .col-sortable-title {\n                 cursor: pointer;\n             }\n        }\n    }\n\n    .order-ascending,\n    .order-descending {\n        margin-left: 3px;\n        position: relative;\n        &:after {\n             border-top: solid 5px var(--icon-secondary-color);\n             border-left: solid 5px transparent;\n             border-right: solid 5px transparent;\n             content: \"\";\n             cursor: pointer;\n             display: inline-block;\n             height: 4px;\n             left: 0;\n             line-height: 1.1;\n             opacity: 1;\n             padding: 0;\n             position: relative;\n             text-align: center;\n             top: 50%;\n             transform: translateY(-50%);\n             width: 8px;\n        }\n    }\n\n    .order-descending {\n        &:after {\n            border-top-color: transparent;\n            border-bottom: solid 5px var(--icon-secondary-color);\n        }\n    }\n\n    .author-slug {\n        color: var(--gray-4);\n        font-size: 11px;\n        margin-top: .2rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Backups.vue",
    "content": "<template>\n    <section class=\"content backups\">\n        <p-header\n            v-if=\"!noBackups\"\n            :title=\"$t('file.backups')\">\n\n            <p-button\n                :onClick=\"goBack\"\n                slot=\"buttons\"\n                type=\"clean back\">\n                {{ $t('ui.backToTools') }}\n            </p-button>\n\n            <p-button\n                :onClick=\"createBackup\"\n                slot=\"buttons\"\n                :type=\"operationInProgress ? 'disabled preloader' : 'primary icon'\"\n                :disabled=\"operationInProgress\"\n                icon=\"plus\">\n                {{ $t('file.createBackup') }}\n            </p-button>\n\n        </p-header>\n\n        <empty-state\n            v-if=\"noBackups\"\n            imageName=\"backups.svg\"\n            imageWidth=\"344\"\n            imageHeight=\"286\"\n            :title=\"$t('file.noBackupsAvailable')\"\n            :description=\"$t('file.createFirstBackupMsg')\">\n            <p-button\n                slot=\"button\"\n                icon=\"plus\"\n                :type=\"operationInProgress ? 'disabled preloader' : 'icon'\"\n                :onClick=\"createBackup\"\n                :disabled=\"operationInProgress\">\n                {{ $t('file.createBackup') }}\n            </p-button>\n        </empty-state>\n\n        <collection\n            v-if=\"!noBackups\"\n            :itemsCount=\"5\">\n            <collection-header slot=\"header\">\n                <collection-cell>\n                    <checkbox\n                        value=\"all\"\n                        :checked=\"anyCheckboxIsSelected\"\n                        :onClick=\"toggleAllCheckboxes\" />\n                </collection-cell>\n\n                <collection-cell>\n                    {{ $t('file.filename') }}\n                </collection-cell>\n\n                <collection-cell min-width=\"120px\">\n                    {{ $t('file.fileSize') }}\n                </collection-cell>\n\n                <collection-cell min-width=\"160px\">\n                    {{ $t('file.creationDate') }}\n                </collection-cell>\n\n                <collection-cell min-width=\"150px\">\n                    {{ $t('file.operations') }}\n                </collection-cell>\n\n                <div\n                    v-if=\"anyCheckboxIsSelected\"\n                    class=\"tools\">\n                    <p-button\n                        icon=\"trash\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkDelete\">\n                        {{ $t('ui.delete') }}\n                    </p-button>\n                </div>\n            </collection-header>\n\n            <collection-row\n                v-for=\"(item, index) in items\"\n                slot=\"content\"\n                :key=\"'collection-row-' + index\">\n                <collection-cell>\n                    <checkbox\n                        :id=\"item.name\"\n                        :value=\"item.id\"\n                        :checked=\"isChecked(item.id)\"\n                        :onClick=\"toggleSelection\"\n                        :key=\"'collection-row-checkbox-' + index\" />\n                </collection-cell>\n\n                <collection-cell type=\"titles\">\n                    <h2 class=\"title\">\n                        <a\n                            :href=\"item.url\"\n                            @click.prevent.stop=\"showFileInFolder(item.url)\">\n                            {{ item.name }}\n                        </a>\n                    </h2>\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.size }}\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.createdAt }}\n                </collection-cell>\n\n                <collection-cell class=\"col-buttons\">\n                    <p-button\n                        :type=\"operationInProgress ? 'disabled outline small' : 'outline small'\"\n                        :onClick=\"renameFile.bind(this, item.name)\">\n                        {{ $t('file.rename') }}\n                    </p-button>\n\n                    <p-button\n                        :type=\"operationInProgress ? 'disabled secondary small' : 'secondary small'\"\n                        :onClick=\"restoreFile.bind(this, item.name)\"\n                        :disabled=\"operationInProgress\">\n                        {{ $t('file.restore') }}\n                    </p-button>\n                </collection-cell>\n            </collection-row>\n        </collection>\n    </section>\n</template>\n\n<script>\nimport BackToTools from './mixins/BackToTools.js';\nimport CollectionCheckboxes from './mixins/CollectionCheckboxes.js';\n\nexport default {\n    name: 'backups',\n    mixins: [\n        BackToTools,\n        CollectionCheckboxes\n    ],\n    data: function() {\n        return {\n            isLoading: true,\n            items: [],\n            operationInProgress: false,\n            selectedItems: [],\n            fileToRename: '',\n            fileToRestore: ''\n        };\n    },\n    computed: {\n        noBackups () {\n            return this.items.length === 0;\n        }\n    },\n    mounted: function() {\n        mainProcessAPI.send('app-backups-list-load', {\n            site: this.$store.state.currentSite.config.name\n        });\n\n        mainProcessAPI.receiveOnce('app-backups-list-loaded', (data) => {\n            if (data.status) {\n                this.isLoading = false;\n                this.items = data.backups;\n            }\n        });\n    },\n    methods: {\n        goToSettings: function() {\n            this.$router.push('/app-settings');\n        },\n        bulkDelete: function() {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('file.deleteBackupsConfirmMsg'),\n                isDanger: true,\n                okClick: this.deleteSelected\n            });\n        },\n        deleteSelected: function() {\n            let backupsToRemove = this.selectedItems.map(id => {\n                return document.querySelector('input[value=\"' + id + '\"]').getAttribute('id');\n            });\n\n            mainProcessAPI.send('app-backup-remove', {\n                site: this.$store.state.currentSite.config.name,\n                backupsNames: backupsToRemove\n            });\n\n            mainProcessAPI.receiveOnce('app-backup-removed', (data) => {\n                this.items = data.backups;\n                this.selectedItems = [];\n\n                if (!data.status) {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.deleteBackupsErrorMsg'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.deleteBackupsSuccessMsg'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                }\n            });\n        },\n        showFileInFolder: function(url) {\n            mainProcessAPI.shellShowItemInFolder(url);\n        },\n        renameFile: function(originalName) {\n            let oldFilename = originalName.substr(0, originalName.length - 4);\n            this.fileToRename = oldFilename;\n\n            this.$bus.$emit('confirm-display', {\n                hasInput: true,\n                message: this.$t('file.renameBackupConfirmMsg'),\n                okClick: this.rename,\n                okLabel: this.$t('file.renameBackupConfirmLabel'),\n                cancelLabel: this.$t('ui.cancel'),\n                defaultText: oldFilename\n            });\n        },\n        rename: function(newFilename) {\n            if (!newFilename) {\n                return;\n            }\n\n            newFilename = newFilename.replace(/[^a-z0-9\\-\\_]/gmi, '');\n\n            if(newFilename.trim() === '') {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('file.renameBackupNameEmptyMsg'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                return;\n            }\n\n            if(this.fileToRename === newFilename) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('file.renameBackupSameNameMsg'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                return;\n            }\n\n            if(this.filenameIsInUse(newFilename)) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('file.renameBackupNameInUseMsg'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                return;\n            }\n\n            mainProcessAPI.send('app-backup-rename', {\n                site: this.$store.state.currentSite.config.name,\n                oldBackupName: this.fileToRename,\n                newBackupName: newFilename\n            });\n\n            mainProcessAPI.receiveOnce('app-backup-renamed', (data) => {\n                if (!data.status) {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.renameBackupErrorMsg'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.renameBackupSuccessMsg'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                }\n\n                this.items = data.backups;\n                this.fileToRename = '';\n            });\n        },\n        filenameIsInUse(filename) {\n            for(let backupData of this.items) {\n                if(filename + '.tar' === backupData.name) {\n                    return true;\n                }\n            }\n\n            return false;\n        },\n        createBackup: function() {\n            let siteNamePrefix = this.$store.state.currentSite.config.name;\n            let defaultFilename = siteNamePrefix + '-' + this.$moment().format('MM-DD-YYYY-HH-mm-ss');\n\n            this.$bus.$emit('confirm-display', {\n                hasInput: true,\n                message: this.$t('file.createBackupConfirmMsg'),\n                okClick: this.create,\n                okLabel: this.$t('file.createBackup'),\n                cancelLabel: this.$t('ui.cancel'),\n                defaultText: defaultFilename\n            });\n        },\n        create: function(filename) {\n            if (filename === false) {\n                return;\n            }\n\n            filename = filename.replace(/[^a-z0-9\\-\\_]/gmi, '');\n\n            if (filename.trim() === '') {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('file.createBackupNameEmptyMsg'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                return;\n            }\n\n            if (this.filenameIsInUse(filename)) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('file.createBackupNameInUseMsg', { filename }),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                return;\n            }\n\n            this.operationInProgress = true;\n\n            mainProcessAPI.send('app-backup-create', {\n                site: this.$store.state.currentSite.config.name,\n                filename: filename\n            });\n\n            mainProcessAPI.receiveOnce('app-backup-created', (data) => {\n                if (data.status) {\n                    this.items = data.backups;\n\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.createBackupSuccessMsg'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.createBackupErrorMsg'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n\n                    if (data.error) {\n                        this.$bus.$emit('alert-display', {\n                            message: data.error,\n                            buttonStyle: 'danger'\n                        });\n                    }\n                }\n\n                this.operationInProgress = false;\n            });\n        },\n        restoreFile: function(fileName) {\n            this.fileToRestore = fileName;\n\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('file.restoreBackupConfirmMsg'),\n                okClick: this.restore,\n                okLabel: this.$t('file.restoreBackupConfirmLabel'),\n                cancelLabel: this.$t('ui.cancel'),\n            });\n        },\n        restore: function() {\n            this.operationInProgress = true;\n\n            mainProcessAPI.send('app-backup-restore', {\n                site: this.$store.state.currentSite.config.name,\n                backupName: this.fileToRestore\n            });\n\n            mainProcessAPI.receiveOnce('app-backup-restored', (data) => {\n                if (!data.status) {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.restoreBackupErrorMsg') + ' ' + this.$t(data.error),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('file.restoreBackupSuccessMsg'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n\n                    mainProcessAPI.send('app-site-reload', {\n                        siteName: this.$store.state.currentSite.config.name\n                    });\n\n                    mainProcessAPI.receiveOnce('app-site-reloaded', (result) => {\n                        this.$store.commit('setSiteConfig', result);\n                        this.$store.commit('switchSite', result.data);\n                    });\n                }\n\n                this.fileToRestore = '';\n                this.operationInProgress = false;\n            });\n        }\n    }\n}\n</script>\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n</style>\n"
  },
  {
    "path": "app/src/components/CustomCss.vue",
    "content": "<template>\n    <section class=\"content tools-custom-css\">\n        <p-header :title=\"$t('tools.css.customCSS')\">\n            <p-button\n                @click.native=\"goBack\"\n                slot=\"buttons\"\n                type=\"clean back\"\n                :disabled=\"buttonsLocked\">\n                {{ $t('ui.backToTools') }}\n            </p-button>\n\n            <p-button\n                @click.native=\"save(false, false)\"\n                slot=\"buttons\"\n                type=\"secondary\"\n                :disabled=\"buttonsLocked\">\n                {{ $t('ui.saveChanges') }}\n            </p-button>\n\n            <btn-dropdown\n                slot=\"buttons\"\n                buttonColor=\"green\"\n                :items=\"dropdownItems\"\n                :disabled=\"!siteHasTheme || buttonsLocked\"\n                :previewIcon=\"true\"\n                localStorageKey=\"publii-preview-mode\"\n                defaultValue=\"full-site-preview\" />\n        </p-header>\n\n        <div :class=\"{ 'editor-wrapper': true, 'is-active': true }\">\n            <codemirror-editor\n                id=\"custom-css-editor-normal\"\n                ref=\"codemirrorNormal\"\n                mode=\"css\"\n                editorLoadedEventName=\"custom-css-editor-loaded\">\n            </codemirror-editor>\n        </div>\n\n        <small class=\"editor-note\">\n            <span>\n                {{ $t('tools.find') }}\n                <template v-if=\"!isMac\">{{ $t('tools.findShortcut') }}</template>\n                <template v-if=\"isMac\">{{ $t('tools.findShortcutMac') }}</template>\n            </span>\n            <span>\n                {{ $t('tools.findAndReplace') }}\n                <template v-if=\"!isMac\">{{ $t('tools.findAndReplaceShortcut') }}</template>\n                <template v-if=\"isMac\">{{ $t('tools.findAndReplaceShortcutMac') }}</template>\n            </span>\n        </small>\n    </section>\n</template>\n\n<script>\nimport BackToTools from './mixins/BackToTools.js';\n\nexport default {\n    name: 'custom-css',\n    mixins: [\n        BackToTools\n    ],\n    watch: {\n        filterValue (newValue) {\n            setTimeout(() => {\n                if (newValue === 'normal') {\n                    this.$refs.codemirrorNormal.editor.refresh();\n                }\n            }, 0);\n        }\n    },\n    data: function() {\n        return {\n            buttonsLocked: false,\n            filterValue: 'normal',\n            editorValueNormal: `/*\n * ${this.$t('tools.css.putCustomCSSComment')}\n */`\n        };\n    },\n    computed: {\n        siteHasTheme: function() {\n            return !!this.$store.state.currentSite.config.theme;\n        },\n        isMac: function () {\n            return mainProcessAPI.getEnv().platformName === 'darwin';\n        },\n        dropdownItems () {\n            return [\n                {\n                    label: this.$t('ui.previewFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'full-site-preview',\n                    isVisible: () => true,\n                    icon: 'full-preview-monitor',\n                    onClick: this.saveAndPreview.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.renderFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'full-site-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    icon: 'full-render-monitor',\n                    onClick: this.saveAndRender.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.previewFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'homepage-preview',\n                    icon: 'quick-preview',\n                    isVisible: () => true,\n                    onClick: this.saveAndPreview.bind(this, 'homepage')\n                },\n                {\n                    label: this.$t('ui.renderFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'homepage-render',\n                    icon: 'quick-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    onClick: this.saveAndRender.bind(this, 'homepage')\n                }\n            ];\n        }\n    },\n    mounted: function() {\n        this.$bus.$on('custom-css-editor-loaded', () => {\n            this.initEditor();\n        });\n    },\n    methods: {\n        initEditor: function() {\n            mainProcessAPI.send('app-site-css-load', {\n                site: this.$store.state.currentSite.config.name\n            });\n\n            mainProcessAPI.receiveOnce('app-site-css-loaded', (data) => {\n                if (data.normal !== false) {\n                    this.editorValueNormal = data.normal;\n                }\n\n                this.$refs.codemirrorNormal.editor.setValue(this.editorValueNormal);\n                this.$refs.codemirrorNormal.editor.refresh();\n            });\n        },\n        save (showPreview = false, renderingType = false, renderFiles = false) {\n            this.$refs.codemirrorNormal.editor.save();\n            \n            mainProcessAPI.send('app-site-css-save', {\n                site: this.$store.state.currentSite.config.name,\n                code: {\n                    normal: document.getElementById('custom-css-editor-normal').value\n                }\n            });\n\n            mainProcessAPI.receiveOnce('app-site-css-saved', (data) => {\n                this.saved(showPreview, renderingType, renderFiles);\n            });\n        },\n        saveAndPreview (renderingType = false) {\n            this.$bus.$emit('theme-settings-before-save');\n\n            setTimeout(() => {\n                this.save(true, renderingType, false);\n            }, 500);\n        },\n        saveAndRender (renderingType = false) {\n            this.$bus.$emit('theme-settings-before-save');\n\n            setTimeout(() => {\n                this.save(true, renderingType, true);\n            }, 500);\n        },\n        async saved (showPreview, renderingType = false, renderFiles = false) {\n            this.$bus.$emit('message-display', {\n                message: this.$t('tools.css.customCSSSaveSuccessMsg'),\n                type: 'success',\n                lifeTime: 3\n            });\n\n            if (showPreview) {\n                let previewLocationExists = await mainProcessAPI.existsSync(this.$store.state.app.config.previewLocation);\n\n                if (this.$store.state.app.config.previewLocation !== '' && !previewLocationExists) {\n                    this.$bus.$emit('confirm-display', {\n                        message: this.$t('sync.previewCatalogDoesNotExistInfo'),\n                        okLabel: this.$t('sync.goToAppSettings'),\n                        okClick: () => {\n                            this.$router.push(`/app-settings/`);\n                        }\n                    });\n                    return;\n                }\n\n                if (renderingType === 'homepage') {\n                    this.$bus.$emit('rendering-popup-display', {\n                        homepageOnly: true,\n                        showPreview: !renderFiles\n                    });\n                } else {\n                    this.$bus.$emit('rendering-popup-display', {\n                        showPreview: !renderFiles\n                    });\n                }\n            }\n        },\n        filterCssClasses (type) {\n            return {\n                'filter-value': true,\n                'filter-active': this.filterValue === type\n            };\n        },\n        setFilter (newValue) {\n            this.filterValue = newValue;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('custom-css-editor-loaded');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.editor-note {\n    color: var(--gray-4);\n    display: block;\n    margin-top: 2rem;\n\n    span {\n        display: inline-block;\n        margin: .5rem 2rem 0 0;\n    }\n}\n\n.editor-wrapper {\n    display: none;\n\n    &.is-active {\n        display: block;\n    }\n}\n\n.filters {\n    font-size: 1.35rem;\n    list-style-type: none;\n    margin: -2.2rem 0 0 0;\n    padding: 0;\n    position: relative;\n    user-select: none;\n    z-index: 1;\n\n    .label {\n        color: var(--gray-4);\n        float: left;\n        margin-right: 1rem;\n    }\n\n    .filter-value {\n        color: var(--gray-4);\n        cursor: pointer;\n        display: inline-block;\n        margin-right: 1rem;\n\n        &.filter-active {\n            color: var(--link-primary-color);\n            cursor: default;\n        }\n\n        &:hover {\n            color: var(--link-primary-color);\n        }\n\n        &:last-child {\n            border-right: none;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/CustomHtml.vue",
    "content": "<template>\n    <section class=\"content tools-custom-html\">\n        <p-header :title=\"$t('tools.customHTML')\">\n            <p-button\n                @click.native=\"goBack\"\n                slot=\"buttons\"\n                type=\"clean back\"\n                :disabled=\"buttonsLocked\">\n                {{ $t('ui.backToTools') }}\n            </p-button>\n\n            <p-button\n                @click.native=\"save(false, false)\"\n                slot=\"buttons\"\n                type=\"secondary\"\n                :disabled=\"buttonsLocked\">\n                {{ $t('ui.saveChanges') }}\n            </p-button>\n\n            <btn-dropdown\n                slot=\"buttons\"\n                buttonColor=\"green\"\n                :items=\"dropdownItems\"\n                :disabled=\"!siteHasTheme || buttonsLocked\"\n                :previewIcon=\"true\"\n                localStorageKey=\"publii-preview-mode\"\n                defaultValue=\"full-site-preview\" />\n        </p-header>\n\n        <fields-group>\n            <tabs\n                id=\"custom-html-tabs\"\n                :items=\"tabs\"\n                :onToggle=\"refreshEditors\">\n                <supported-features-check\n                    v-for=\"(htmlFieldName, index) in Object.keys(requiredFeatures)\"\n                    :key=\"'supported-features-check-' + index\"\n                    :slot=\"'tab-' + index\"\n                    :featuresToCheck=\"requiredFeatures[htmlFieldName]\" />\n\n                <codemirror-editor\n                    v-for=\"(editor, index) in Object.keys(editors)\"\n                    :slot=\"'tab-' + index\"\n                    :id=\"editor\"\n                    :key=\"editor\"\n                    :ref=\"editor\"\n                    editorLoadedEventName=\"custom-html-editor-loaded\"\n                    mode=\"xml\">\n                </codemirror-editor>\n\n                <small\n                    v-for=\"(editor, index) in Object.keys(editors)\"\n                    class=\"editor-note\"\n                    :slot=\"'tab-' + index\"\n                    :key=\"'note-' + editor\">\n                    <span>\n                        {{ $t('tools.find') }}\n                        <template v-if=\"!isMac\">{{ $t('tools.findShortcut') }}</template>\n                        <template v-if=\"isMac\">{{ $t('tools.findShortcutMac') }}</template>\n                    </span>\n                    <span>\n                        {{ $t('tools.findAndReplace') }}\n                        <template v-if=\"!isMac\">{{ $t('tools.findAndReplaceShortcut') }}</template>\n                        <template v-if=\"isMac\">{{ $t('tools.findAndReplaceShortcutMac') }}</template>\n                    </span>\n                </small>\n            </tabs>\n        </fields-group>\n    </section>\n</template>\n\n<script>\nimport Utils from '../helpers/utils.js';\nimport BackToTools from './mixins/BackToTools.js';\nimport SupportedFeaturesCheck from './basic-elements/SupportedFeaturesCheck.vue';\n\nexport default {\n    name: 'custom-html',\n    mixins: [\n        BackToTools\n    ],\n    components: {\n        'supported-features-check': SupportedFeaturesCheck\n    },\n    data: function() {\n        return {\n            buttonsLocked: false,\n            tabs: [\n                this.$t('customHTML.tabs.head'),\n                this.$t('customHTML.tabs.body'),\n                this.$t('customHTML.tabs.comments'),\n                this.$t('customHTML.tabs.searchInput'),\n                this.$t('customHTML.tabs.searchContent'),\n                this.$t('customHTML.tabs.socialSharing'),\n                this.$t('customHTML.tabs.footer'),\n            ],\n            requiredFeatures: {\n                'custom-head-code': [],\n                'custom-body-code': [],\n                'custom-comments-code': ['customComments'],\n                'custom-search-input': ['customSearch'],\n                'custom-search-content': ['customSearch'],\n                'custom-social-sharing': ['customSharing'],\n                'custom-footer-code': []\n            },\n            editors: {},\n            loadedEditors: 0\n        };\n    },\n    computed: {\n        siteHasTheme: function() {\n            return !!this.$store.state.currentSite.config.theme;\n        },\n        isMac: function () {\n            return mainProcessAPI.getEnv().platformName === 'darwin';\n        },\n        dropdownItems () {\n            return [\n                {\n                    label: this.$t('ui.previewFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'full-site-preview',\n                    isVisible: () => true,\n                    icon: 'full-preview-monitor',\n                    onClick: this.saveAndPreview.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.renderFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'full-site-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    icon: 'full-render-monitor',\n                    onClick: this.saveAndRender.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.previewFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'homepage-preview',\n                    icon: 'quick-preview',\n                    isVisible: () => true,\n                    onClick: this.saveAndPreview.bind(this, 'homepage')\n                },\n                {\n                    label: this.$t('ui.renderFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'homepage-render',\n                    icon: 'quick-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    onClick: this.saveAndRender.bind(this, 'homepage')\n                }\n            ];\n        }\n    },\n    mounted: function() {\n        this.getEditorsList();\n\n        this.$bus.$on('custom-html-editor-loaded', () => {\n            this.loadedEditors++;\n\n            if(this.loadedEditors === Object.keys(this.editors).length) {\n                this.initEditors();\n            }\n        });\n    },\n    methods: {\n        getEditorsList: function() {\n            let customHtml = {\n                'custom-head-code': this.$store.state.currentSite.config.advanced.customHeadCode,\n                'custom-body-code': this.$store.state.currentSite.config.advanced.customBodyCode,\n                'custom-comments-code': this.$store.state.currentSite.config.advanced.customCommentsCode,\n                'custom-search-input': this.$store.state.currentSite.config.advanced.customSearchInput,\n                'custom-search-content': this.$store.state.currentSite.config.advanced.customSearchContent,\n                'custom-social-sharing': this.$store.state.currentSite.config.advanced.customSocialSharing,\n                'custom-footer-code': this.$store.state.currentSite.config.advanced.customFooterCode\n            };\n\n            if(\n                this.siteHasTheme &&\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.customHTML\n            ) {\n                let customHtmlCodes = Object.keys(this.$store.state.currentSite.themeSettings.renderer.customHTML);\n\n                for(let i = 0; i < customHtmlCodes.length; i++) {\n                    let id = customHtmlCodes[i];\n                    customHtml[id] = this.getCustomHtmlCode(id);\n\n                    if (this.$te('customHTML.tabs.' + id)) {\n                        this.tabs.push(this.$t('customHTML.tabs.' + id));\n                    } else {\n                        this.tabs.push(this.$store.state.currentSite.themeSettings.renderer.customHTML[id]);\n                    }\n                }\n            }\n\n            this.editors = customHtml;\n        },\n        getCustomHtmlCode: function(id) {\n            if(\n                this.$store.state.currentSite.config.advanced.customHTML &&\n                this.$store.state.currentSite.config.advanced.customHTML[id]\n            ) {\n                return this.$store.state.currentSite.config.advanced.customHTML[id];\n            }\n\n            return ;\n        },\n        initEditors: function() {\n            let refIDs = Object.keys(this.$refs);\n\n            for(let refID of refIDs) {\n                if(this.editors[refID]) {\n                    this.$refs[refID][0].editor.setValue(this.editors[refID]);\n                }\n\n                this.$refs[refID][0].editor.refresh();\n            }\n        },\n        refreshEditors: function() {\n            setTimeout(() => {\n                let refIDs = Object.keys(this.$refs);\n\n                for(let refID of refIDs) {\n                    this.$refs[refID][0].editor.refresh();\n                }\n            }, 0);\n        },\n        save (showPreview = false, renderingType = false, renderFiles = false) {\n            let customCodeEditors = document.querySelectorAll('.tools-custom-html textarea');\n\n            for(let editor of Object.keys(this.editors)) {\n                this.$refs[editor][0].editor.save();\n            }\n\n            let newSettings = {\n                advanced: {\n                    customHeadCode: document.getElementById('custom-head-code').value,\n                    customBodyCode: document.getElementById('custom-body-code').value,\n                    customFooterCode: document.getElementById('custom-footer-code').value,\n                    customCommentsCode: document.getElementById('custom-comments-code').value,\n                    customSearchInput: document.getElementById('custom-search-input').value,\n                    customSearchContent: document.getElementById('custom-search-content').value,\n                    customSocialSharing: document.getElementById('custom-social-sharing').value,\n                    customHTML: {}\n                }\n            };\n\n            for (let customCodeEditor of customCodeEditors) {\n                let id = customCodeEditor.getAttribute('id');\n                let excludedIDs = [\n                    'custom-head-code',\n                    'custom-body-code',\n                    'custom-footer-code',\n                    'custom-comments-code',\n                    'custom-search-input',\n                    'custom-search-content',\n                    'custom-social-sharing'\n                ];\n\n                if(!id || excludedIDs.indexOf(id) > -1) {\n                    continue;\n                }\n\n                newSettings.advanced.customHTML[id] = customCodeEditor.value;\n            }\n\n            // Merge new settings with existing settings\n            let currentSiteConfigCopy = JSON.parse(JSON.stringify(this.$store.state.currentSite.config));\n            newSettings = Utils.deepMerge(currentSiteConfigCopy, newSettings);\n\n            // Send request to the back-end\n            mainProcessAPI.send('app-site-config-save', {\n                site: this.$store.state.currentSite.config.name,\n                settings: newSettings,\n                source: \"custom-html\"\n            });\n\n            // Settings saved\n            mainProcessAPI.receiveOnce('app-site-config-saved', (data) => {\n                if(data.status === true) {\n                    this.saved (newSettings, showPreview, renderingType, renderFiles);\n\n                    if(data.newThemeConfig) {\n                        this.$store.commit('refreshSiteThemeConfig', data);\n                    }\n                }\n\n                if(data.message === 'success-save') {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('site.siteSettingsSaveSuccessMsg'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                }\n\n                mainProcessAPI.send('app-site-reload', {\n                    siteName: this.$store.state.currentSite.config.name\n                });\n\n                mainProcessAPI.receiveOnce('app-site-reloaded', (result) => {\n                    this.$store.commit('setSiteConfig', result);\n                    this.$store.commit('switchSite', result.data);\n                });\n            });\n        },\n        saveAndPreview (renderingType = false) {\n            this.save(true, renderingType, false);\n        },\n        saveAndRender (renderingType = false) {\n            this.save(true, renderingType, true);\n        },\n        async saved (newSettings, showPreview, renderingType = false, renderFiles = false) {\n            let siteName = this.$store.state.currentSite.config.name;\n\n            this.$store.commit('refreshSiteConfig', {\n                newSettings: newSettings,\n                siteName: siteName\n            });\n\n            if (showPreview) {\n                let previewLocationExists = await mainProcessAPI.existsSync(this.$store.state.app.config.previewLocation);\n\n                if (this.$store.state.app.config.previewLocation !== '' && !previewLocationExists) {\n                    this.$bus.$emit('confirm-display', {\n                        message: this.$t('sync.previewCatalogDoesNotExistInfo'),\n                        okLabel: this.$t('sync.goToAppSettings'),\n                        okClick: () => {\n                            this.$router.push(`/app-settings/`);\n                        }\n                    });\n                    return;\n                }\n\n                if (renderingType === 'homepage') {\n                    this.$bus.$emit('rendering-popup-display', {\n                        homepageOnly: true,\n                        showPreview: !renderFiles\n                    });\n                } else {\n                    this.$bus.$emit('rendering-popup-display', {\n                        showPreview: !renderFiles\n                    });\n                }\n            }\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('custom-html-editor-loaded');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.editor-note {\n   color: var(--gray-4);\n\n    span {\n        display: inline-block;\n        margin: .5rem 2rem 0 0;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/ErrorPopup.vue",
    "content": "<template>\n    <div\n        v-if=\"isVisible\"\n        class=\"overlay\">\n        <div class=\"popup\">\n            <icon\n                size=\"xl\"\n                primaryColor=\"color-3\"\n                name=\"alert\" />\n\n            <div\n                class=\"error-log\"\n                v-pure-html=\"errors\"></div>\n\n            <textarea spellcheck=\"false\" ref=\"error-log\">{{ text }}</textarea>\n\n            <div class=\"buttons\">\n                <p-button\n                    type=\"medium danger no-border-radius half-width\"\n                    :onClick=\"copyToClipboard\">\n                    {{ $t('ui.copyToClipboard') }}\n                </p-button>\n\n                <p-button\n                    type=\"medium no-border-radius half-width cancel-popup\"\n                    :onClick=\"close\">\n                    {{ $t('ui.close') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'error-popup',\n    data: function() {\n        return {\n            isVisible: false,\n            text: '',\n            errors: ''\n        };\n    },\n    mounted: function() {\n        this.$bus.$on('error-popup-display', (config) => {\n            this.isVisible = true;\n            this.text = config.text;\n            this.errors = config.errors;\n        });\n    },\n    methods: {\n        copyToClipboard: function() {\n            this.$refs['error-log'].select();\n            document.execCommand('copy');\n            this.close();\n        },\n        close: function() {\n            this.isVisible = false;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('error-popup-display');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/popup-common.scss';\n\n.popup {\n    padding: 4rem;\n    width: 60rem;\n\n    .error-log {\n        height: 225px;\n        overflow: scroll;\n        text-align: left;\n    }\n}\n\ntextarea {\n    border: none;\n    left: 0;\n    height: 1px;\n    padding: 0!important;\n    position: absolute;\n    top: 0;\n    width: 1px;\n}\n\n.message {\n    padding: 0 0 4rem 0;\n}\n\n.buttons {\n    display: flex;\n    margin: 4rem -4rem -4rem -4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/FileManager.vue",
    "content": "<template>\n    <section class=\"content file-manager\">\n        <p-header\n            :title=\"$t('file.files')\">\n            <header-search\n                slot=\"search\"\n                ref=\"search\"\n                :placeholder=\"$t('file.filterOrSearchFiles')\"\n                onChangeEventName=\"files-filter-value-changed\" />\n\n            <p-button\n                :onClick=\"goBack\"\n                slot=\"buttons\"\n                type=\"clean back\">\n                {{ $t('ui.backToTools') }}\n            </p-button>\n\n            <p-button\n                :onClick=\"addNewFile\"\n                slot=\"buttons\"\n                type=\"secondary icon\"\n                icon=\"plus\">\n                {{ $t('file.addNewFile') }}\n            </p-button>\n\n            <p-button\n                :onClick=\"uploadFiles\"\n                slot=\"buttons\"\n                type=\"icon\"\n                icon=\"upload-file\">\n                {{ $t('file.uploadFiles') }}\n            </p-button>\n        </p-header>\n\n        <div class=\"filters\">\n            <a\n                :class=\"{ 'directory-link': true, 'is-active': isRoot }\"\n                @click=\"changeDirectory('root-files')\">\n                <icon\n                    name=\"folder\"\n                    customWidth=\"22\"\n                    customHeight=\"16\"\n                    :primaryColor=\"isRoot ? 'color-1' : 'color-7'\" />\n\n                {{ $t('file.rootDirectory') }}\n            </a>\n\n            <a\n                :class=\"{ 'directory-link': true, 'is-active': isMedia }\"\n                @click=\"changeDirectory('media/files')\">\n                <icon\n                    name=\"folder\"\n                    customWidth=\"22\"\n                    customHeight=\"16\"\n                    :primaryColor=\"isMedia ? 'color-1' : 'color-7'\" />\n\n                {{ $t('file.mediaFiles') }}\n            </a>\n        </div>\n\n        <collection\n            v-if=\"!emptySearchResults && hasFiles\"\n            :itemsCount=\"5\">\n            <collection-header slot=\"header\">\n                <collection-cell>\n                    <checkbox\n                        value=\"all\"\n                        :checked=\"anyCheckboxIsSelected\"\n                        :onClick=\"toggleAllCheckboxes.bind(this, true)\" />\n                </collection-cell>\n\n                <collection-cell>\n                    {{ $t('file.filename') }}\n                </collection-cell>\n\n                <collection-cell min-width=\"100px\">\n                    {{ $t('file.fileSize') }}\n                </collection-cell>\n\n                <collection-cell min-width=\"130px\">\n                    {{ $t('file.creationDate') }}\n                </collection-cell>\n\n                <collection-cell min-width=\"200px\">\n                    {{ $t('ui.lastModified') }}\n                </collection-cell>\n\n                <div\n                    v-if=\"anyCheckboxIsSelected\"\n                    class=\"tools\">\n                    <p-button\n                        icon=\"trash\"\n                        type=\"small danger icon light\"\n                        :onClick=\"bulkDelete\">\n                        {{ $t('ui.delete') }}\n                    </p-button>\n                </div>\n            </collection-header>\n\n            <collection-row\n                v-for=\"(item, index) in filteredFiles\"\n                slot=\"content\"\n                :key=\"'collection-row-' + index\">\n                <collection-cell>\n                    <checkbox\n                        :value=\"item.name\"\n                        :checked=\"isChecked(index)\"\n                        :onClick=\"toggleSelection.bind(this, index)\"\n                        :key=\"'collection-row-checkbox-' + index\" />\n                </collection-cell>\n\n                <collection-cell\n                    type=\"titles\">\n                    <a\n                        :href=\"item.name\"\n                        class=\"file-link\"\n                        :data-is-binary=\"item.isBinary\"\n                        @click=\"openFile($event, item.fullPath)\">\n                        <icon\n                            size=\"s\"\n                            :name=\"item.icon\"\n                            :customCssClasses=\"'file ' + item.icon\"\n                            iconset=\"svg-map-file-extensions\"\n                            customWidth=\"22\"\n                            customHeight=\"24\" />\n\n                        {{ item.name }}\n                    </a>\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.size }}\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.createdAt }}\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.modifiedAt }}\n                </collection-cell>\n            </collection-row>\n        </collection>\n\n        <empty-state\n            v-if=\"hasFiles && emptySearchResults\"\n            :description=\"$t('file.noFileMatchingCriteriaInfo')\"></empty-state>\n\n        <empty-state\n            v-if=\"!hasFiles && isRoot\"\n            :description=\"$t('file.noFileInRootDirInfo')\"></empty-state>\n\n        <empty-state\n            v-if=\"!hasFiles && isMedia\"\n            :description=\"$t('file.noFileInMediaFilesDirInfo')\"></empty-state>\n    </section>\n</template>\n\n<script>\nimport BackToTools from './mixins/BackToTools.js';\nimport CollectionCheckboxes from './mixins/CollectionCheckboxes.js';\n\nexport default {\n    name: 'file-manager',\n    mixins: [\n        BackToTools,\n        CollectionCheckboxes\n    ],\n    data () {\n        return {\n            filterValue: '',\n            items: [],\n            selectedItems: [],\n            existingItems: [],\n            dirPath: 'root-files',\n            isLoading: true\n        };\n    },\n    computed: {\n        hasFiles () {\n            return !!this.items.length;\n        },\n        emptySearchResults () {\n            return this.filterValue !== '' && !this.items.length;\n        },\n        isRoot () {\n            return this.dirPath === 'root-files';\n        },\n        isMedia () {\n            return this.dirPath !== 'root-files';\n        },\n        filteredFiles () {\n            return this.items.filter(file => {\n                if(this.filterValue.trim() === '') {\n                    return true;\n                }\n\n                return file.name.indexOf(this.filterValue) > -1;\n            });\n        }\n    },\n    mounted () {\n        this.$bus.$on('posts-filter-value-changed', (newValue) => {\n            this.filterValue = newValue.trim().toLowerCase();\n        });\n\n        mainProcessAPI.receive('app-files-selected', (data) => {\n            if (data.paths !== undefined && data.paths.filePaths.length) {\n                this.uploadFile(data.paths.filePaths);\n            }\n        });\n\n        this.loadFiles();\n    },\n    beforeDestroy () {\n        mainProcessAPI.stopReceiveAll('app-files-selected');\n    },\n    methods: {\n        loadFiles () {\n            this.isLoading = true;\n\n            mainProcessAPI.send('app-file-manager-list', {\n                siteName: this.$store.state.currentSite.config.name,\n                dirPath: this.dirPath\n            });\n\n            mainProcessAPI.receiveOnce('app-file-manager-listed', (data) => {\n                this.items = data.map(file => {\n                    file.size = this.formatBytes(file.size, 2);\n                    file.createdAt = this.getFormatedDate(file.createdAt);\n                    file.modifiedAt = this.getFormatedDate(file.modifiedAt);\n\n                    return file;\n                });\n\n                this.isLoading = false;\n            });\n        },\n        getFormatedDate (timestamp) {\n            if(this.$store.state.app.config.timeFormat == 12) {\n                return this.$moment(timestamp).format('MMM DD, YYYY  hh:mm a');\n            } else {\n                return this.$moment(timestamp).format('MMM DD, YYYY  HH:mm');\n            }\n        },\n        bulkDelete () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('file.removeFilesConfirmMsg'),\n                isDanger: true,\n                okClick: this.deleteSelected\n            });\n        },\n        deleteSelected () {\n            mainProcessAPI.send('app-file-manager-delete', {\n                siteName: this.$store.state.currentSite.config.name,\n                dirPath: this.dirPath,\n                filesToDelete: this.getSelectedFiles()\n            });\n\n            mainProcessAPI.receiveOnce('app-file-manager-deleted', (data) => {\n                this.loadFiles();\n\n                this.$bus.$emit('message-display', {\n                    message: this.$t('file.removeFilesSuccessMsg'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n\n                this.selectedItems = [];\n            });\n        },\n        formatBytes (bytes, decimals) {\n            if(bytes == 0) {\n                return '0 bytes';\n            }\n\n            let k = 1024;\n            let dm = decimals || 2;\n            let sizes = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n            let i = Math.floor(Math.log(bytes) / Math.log(k));\n\n            return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n        },\n        openFile (e, filePath) {\n            e.preventDefault();\n            // Uncomment this when file editor will be ready\n            // if(item.attr('data-is-binary') === 'true') {\n                mainProcessAPI.shellOpenPath(filePath);\n            // } else {\n            //    console.log('OPENING: ' + item.attr('href'));\n            // }\n\n        },\n        changeDirectory (dirPath) {\n            if(this.dirPath === dirPath) {\n                return;\n            }\n\n            this.dirPath = dirPath;\n            this.loadFiles();\n            this.$refs.search.isOpen = false;\n            this.$refs.search.value = '';\n            this.$refs.search.updateValue();\n        },\n        addNewFile () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('file.provideNameForNewFile'),\n                hasInput: true,\n                okClick: this.addFile\n            });\n        },\n        addFile (fileName) {\n            if(fileName === false) {\n                return;\n            }\n\n            mainProcessAPI.send('app-file-manager-create', {\n                siteName: this.$store.state.currentSite.config.name,\n                dirPath: this.dirPath,\n                fileToSave: fileName\n            });\n\n            mainProcessAPI.receiveOnce('app-file-manager-created', (data) => {\n                if(data === false) {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('file.selectedFilenameInUseMsg'),\n                    });\n\n                    return;\n                }\n\n                this.loadFiles();\n            });\n        },\n        async uploadFiles () {\n            await mainProcessAPI.invoke('app-main-process-select-files', false);\n        },\n        uploadFile (queue) {\n            if(!queue.length) {\n                if(this.existingItems.length) {\n                    this.$bus.$emit('alert-display', {\n                        'message': this.$t('file.selectedFileExistsMsg') + this.existingItems.join(', ')\n                    });\n\n                    this.existingFiles = [];\n                }\n\n                this.loadFiles();\n\n                return;\n            }\n\n            let fileToMove = queue.pop();\n\n            mainProcessAPI.send('app-file-manager-upload', {\n                siteName: this.$store.state.currentSite.config.name,\n                dirPath: this.dirPath,\n                fileToMove: fileToMove\n            });\n\n            mainProcessAPI.receiveOnce('app-file-manager-uploaded', (data) => {\n                if(data === false) {\n                    let fileName = fileToMove.split('/').pop();\n                    this.existingItems.push(fileName);\n                }\n\n                this.uploadFile(queue);\n            });\n        },\n        filenameIsInUse (filename) {\n            for(let file of this.items) {\n                if(filename === file.name) {\n                    return true;\n                }\n            }\n\n            return false;\n        },\n        getSelectedFiles () {\n            let selectedItems;\n            let visibleNames = this.items.map(item => item.name);\n\n            selectedItems = this.selectedItems.map(\n                item => this.items[item].name\n            ).filter(\n                name => visibleNames.indexOf(name) > -1\n            );\n\n            return selectedItems;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('posts-filter-value-changed');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.file-manager {\n    .filters {\n        margin-top: -1.5rem;\n\n        .directory-link {\n            color: var(--text-light-color);\n            cursor: pointer;\n            font-size: 1.35rem;\n            transition: var(--transition);\n\n            &:hover {\n                color: var(--link-primary-color);\n            }\n\n            & + .directory-link {\n                margin-left: 2rem;\n            }\n\n            &.is-active {\n                color: var(--link-primary-color);\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/LanguagesList.vue",
    "content": "<template>\n    <div\n        @drop.stop.prevent=\"uploadLanguage\"\n        @dragleave.stop.prevent=\"hideOverlay\"\n        @dragenter.stop.prevent=\"showOverlay\"\n        @dragover.stop.prevent=\"showOverlay\"\n        @drag.stop.prevent=\"showOverlay\"\n        @dragstart.stop.prevent\n        @dragend.stop.prevent\n        :class=\"{ 'languages': true, 'language-is-over': languageIsOver }\">\n        <div\n            class=\"add-more-languages\">\n                <a href=\"https://languages.getpublii.com/\" target=\"_blank\" rel=\"noopener noreferrer\">\n                    <icon\n                        customWidth=\"50\"\n                        customHeight=\"46\"\n                        properties=\"not-clickable\"\n                        name=\"add\" />\n\n                    <h3>{{ $t('langs.getMoreLanguages') }}</h3>\n                </a>\n        </div>\n\n        <language-item\n            v-for=\"(language, index) in languages\"\n            :languageData=\"language\"\n            :key=\"'language-item-' + index\" />\n\n        <overlay\n            v-if=\"languageIsOver\"\n            :hasBorder=\"true\"\n            :isBlue=\"true\">\n            <div>{{ $t('file.dropYourFileHere') }}</div>\n        </overlay>\n    </div>\n</template>\n\n<script>\nimport LanguagesListItem from './LanguagesListItem';\n\nexport default {\n    name: 'languages-list',\n    data () {\n        return {\n            languageIsOver: false\n        };\n    },\n    components: {\n        'language-item': LanguagesListItem\n    },\n    computed: {\n        languages () {\n            let languagesList = JSON.parse(JSON.stringify(this.$store.getters.languages));\n            let activeLanguageIndex = languagesList.findIndex(language => language.directory + '-' + language.type === this.activeLanguage);\n            let activeLanguage = languagesList[activeLanguageIndex];\n            languagesList.splice(activeLanguageIndex, 1);\n            languagesList.splice(0, 0, activeLanguage);\n            languagesList = languagesList.filter(language => !!language);\n\n            return languagesList;\n        },\n        activeLanguage () {\n            let language = this.$store.state.app.config.language;\n            let languageType = this.$store.state.app.config.languageType;\n\n            return [language, languageType].join('-');\n        }\n    },\n    methods: {\n        showOverlay (e) {\n            this.languageIsOver = true;\n        },\n        hideOverlay (e) {\n            if (e.target.classList.contains('languages')) {\n                this.languageIsOver = false;\n            }\n        },\n        async uploadLanguage (e) {\n            this.languageIsOver = false;\n\n            mainProcessAPI.send('app-language-upload', {\n                sourcePath: await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.dataTransfer.files[0]))\n            });\n\n            mainProcessAPI.receiveOnce('app-language-uploaded', this.$parent.uploadedLanguage);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.languages {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 3rem;\n    position: relative;\n    user-select: none;\n\n    &.language-is-over {\n        & > * {\n            pointer-events: none;\n        }\n    }\n}\n\n.add-more-languages {\n    background-color: var(--bg-secondary);\n    border: 1px solid transparent;\n    border-radius: var(--border-radius);\n    box-shadow: var(--box-shadow-small);      \n    height: 100%;\n    transition: var(--transition);\n    text-align: center;\n\n    &:hover {\n         background: var(--bg-primary);\n         border-color: var(--color-primary);\n         box-shadow: 0 0 26px rgba(black, .07);\n\n         svg {\n             fill: var(--color-primary);\n         }\n\n         h3 {\n             color: var(--color-primary);\n         }\n    }\n\n    & > a {\n         align-items: center;\n         display: flex;\n         flex-direction: column;\n         height: 100%;\n         justify-content: center;\n         min-height: 29rem;\n         width: 100%;\n    }\n\n    h3 {\n         color: var(--text-primary-color);\n         font-size: $app-font-base;\n         font-weight: var(--font-weight-semibold);\n         margin-bottom: 0;\n         transition: inherit;\n    }\n\n    svg {\n         fill: var(--icon-primary-color);\n         transition: inherit;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/LanguagesListItem.vue",
    "content": "<template>\n    <figure\n        @click=\"activateLanguage(directory, type)\"\n        :class=\"{\n            'language': true,\n            'is-active': isActiveLanguage,\n            'is-outdated': isOutdated\n        }\">\n        <span class=\"language-thumbnail-wrapper\">\n            <img\n                :src=\"thumbnail\"\n                class=\"language-thumbnail\"\n                alt=\"\">\n        </span>\n\n        <figcaption class=\"language-name\">\n            <h3>\n                <span>{{ name }}</span>\n                <span class=\"language-version\">\n                    {{ version }}\n                </span>\n                <span \n                    v-if=\"isOutdated\"\n                    class=\"language-is-outdated\"\n                    :title=\"$t('langs.isOutdatedTitle', { supportedVersion: languageData.publiiSupport, currentVersion: this.$store.state.app.versionInfo.version })\">\n                    {{ $t('langs.isOutdated') }}\n                </span>\n             </h3>\n            <a\n                v-if=\"type === 'installed' && !isActiveLanguage\"\n                href=\"#\"\n                class=\"language-delete\"\n                :title=\"$t('langs.deleteLanguage')\"\n                @click.stop.prevent=\"deleteLanguage(name, directory)\">\n                    <icon\n                        size=\"xs\"\n                        properties=\"not-clickable\"\n                        name=\"trash\" />\n            </a>\n        </figcaption>\n    </figure>\n</template>\n\n<script>\nimport Vue from 'vue';\nimport compare from 'node-version-compare';\n\nexport default {\n    name: 'languages-list-item',\n    props: [\n        'languageData'\n    ],\n    computed: {\n        isActiveLanguage () {\n            let language = this.$store.state.app.config.language;\n            let languageType = this.$store.state.app.config.languageType;\n\n            if (this.languageData.directory === language && this.languageData.type === languageType) {\n                return true;\n            }\n\n            return false;\n        },\n        isOutdated () {\n            /*\n            let publiiSupport = this.languageData.publiiSupport.split('.').slice(0, 2).join('.');\n            let currentMajorVersion = this.$store.state.app.versionInfo.version.split('.').slice(0, 2).join('.');\n           \n            if (compare(publiiSupport, currentMajorVersion) === -1) {\n                return true;\n            }\n            */\n\n            return false;\n        },\n        thumbnail () {\n            return this.languageData.thumbnail;\n        },\n        name () {\n            return this.languageData.name;\n        },\n        directory () {\n            return this.languageData.directory;\n        },\n        version () {\n            return this.languageData.version;\n        },\n        type () {\n            return this.languageData.type;\n        }\n    },\n    methods: {\n        deleteLanguage (languageName, languageDirectory) {\n            let confirmConfig = {\n                message: this.$t('langs.removeLanguageMessage', { languageName }),\n                isDanger: true,\n                okClick: function() {\n                    mainProcessAPI.send('app-language-delete', {\n                        name: languageName,\n                        directory: languageDirectory\n                    });\n\n                    mainProcessAPI.receiveOnce('app-language-deleted', (data) => {\n                        this.$bus.$emit('message-display', {\n                            message: this.$t('langs.removeLanguageSuccessMessage'),\n                            type: 'success',\n                            lifeTime: 3\n                        });\n\n                        this.$store.commit('replaceAppLanguages', data.languages);\n                    });\n                }\n            };\n\n            this.$bus.$emit('confirm-display', confirmConfig);\n        },\n        async activateLanguage (name, type) {\n            if (this.isActiveLanguage) {\n                return;\n            }\n\n            let results = await mainProcessAPI.invoke('app-main-load-language', name, type);\n\n            if (results.languageChanged) {\n                this.$store.commit('setAppLanguage', results.lang);\n                this.$store.commit('setAppLanguageType', results.type);\n                this.$i18n.setLocaleMessage(results.lang, results.translations);\n                this.$i18n.locale = results.lang;\n\n                if (results.momentLocale) {\n                    this.$moment.locale(results.momentLocale);\n                }\n\n                this.$store.commit('setWysiwygTranslation', results.wysiwygTranslation);\n\n                this.$bus.$emit('message-display', {\n                    message: this.$t('langs.languageChangedMsg'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n            } else {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('langs.languageChangeError'),\n                    buttonStyle: 'danger'\n                });\n            }\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.language {\n    background-color: var(--bg-secondary);\n    border: 1px solid transparent;\n    border-radius: var(--border-radius);\n    box-shadow: var(--box-shadow-small); \n    cursor: default;  \n    height: 100%;\n    margin: 0;\n    overflow: hidden;\n    padding: 1rem;\n    position: relative;\n    transition: var(--transition);\n    text-align: center;\n\n    &:hover:not(.is-active) {\n        background: var(--bg-primary);\n        border-color: var(--color-primary);\n        box-shadow: var(--box-shadow-medium);  \n        cursor: pointer;\n\n        a {\n            color: var(--color-primary);\n        }\n    }\n\n    &.is-active {\n        background: var(--button-secondary-bg);\n    }\n\n    &-thumbnail {\n        display: block;\n        max-height: 90%;\n        left: 50%;\n        position: absolute;\n        top: 50%;\n        transform: translateX(-50%) translateY(-50%);\n        max-width: 90%;\n\n        &-wrapper {\n            display: block;\n            padding-bottom: 75%;\n            position: relative;\n            transition: var(--transition);\n            width: 100%;\n        }\n    }\n\n    &-delete {\n        align-items: center;\n        background: var(--bg-primary);\n        border-radius: 50%;\n        height: 3rem;\n        display: inline-flex;\n        justify-content: center;\n        position: absolute;\n        right: 1.4rem;\n        text-align: center;\n        width: 3rem;\n\n        & > svg {\n             fill: var(--icon-secondary-color);\n             transform: scale(.9);\n             transition: var(--transition);\n        }\n\n        &:hover {\n             & > svg {\n                fill: var(--warning);\n                transform: scale(1);\n             }\n        }\n    }\n\n    &-name {\n        align-items: center;\n        background: var(--gray-1);\n        border-radius: 0 0 4px 4px;\n        display: flex;\n        justify-content: space-between;\n        padding: 0 2rem;\n        position: relative;\n        text-align: left;\n\n        & > h3 {\n             font-size: 1.4rem;\n             font-weight: var(--font-weight-semibold);\n             line-height: 1.4;\n             margin: 1.2rem 0;\n\n             span:first-of-type {\n                 display: block;\n             }\n        }\n    }\n\n    &-version,\n    &-is-outdated {\n        color: var(--text-light-color);\n        font-size: 1.2rem;\n        font-weight: var(--font-weight-normnal);\n        \n    }\n\n    &-is-outdated { \n        color: var(--warning);\n        margin: 0 4rem 0 .5rem;\n        text-transform: uppercase;\n    }\n\n    &.is-outdated {\n       .language-version {\n           text-decoration-color: var(--warning);\n           text-decoration-line: line-through;\n       }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/LogViewer.vue",
    "content": "<template>\n    <section class=\"content tools-log-viewer\">\n        <p-header :title=\"$t('tools.logViewer')\">\n            <p-button\n                :onClick=\"goBack\"\n                slot=\"buttons\"\n                type=\"clean back\">\n                {{ $t('ui.backToTools') }}\n            </p-button>\n        </p-header>\n\n        <div class=\"tools-log-viewer-selector\">\n            <dropdown\n                id=\"selectedFile\"\n                ref=\"selectedFile\"\n                :items=\"files\"\n                selected=\"\"\n                :onChange=\"loadFile\"></dropdown>\n            \n            <p-button\n                :onClick=\"loadSelectedFile\"\n                type=\"secondary\">\n                {{ $t('ui.reloadFile') }}\n            </p-button>\n        </div>\n\n        <codemirror-editor\n            id=\"log-viewer\"\n            ref=\"codemirror\"\n            :readonly=\"true\">\n        </codemirror-editor>\n    </section>\n</template>\n\n<script>\nimport BackToTools from './mixins/BackToTools.js';\n\nexport default {\n    name: 'log-viewer',\n    mixins: [\n        BackToTools\n    ],\n    data () {\n        return {\n            files: {}\n        };\n    },\n    mounted () {\n        this.loadFilesList();\n    },\n    methods: {\n        loadFilesList () {\n            mainProcessAPI.send('app-log-files-load');\n\n            mainProcessAPI.receiveOnce('app-log-files-loaded', (data) => {\n                let items = {};\n                items[\"\"] = this.$t('tools.selectFileToLoad');\n\n                if(data.files.length) {\n                    for(let file of data.files) {\n                        items[file] = file;\n                    }\n                }\n\n                this.files = items;\n            });\n        },\n        loadFile (filename) {\n            if (filename === '') {\n                this.$refs.codemirror.editor.setValue('');\n                return;\n            }\n\n            mainProcessAPI.send('app-log-file-load', filename);\n\n            mainProcessAPI.receiveOnce('app-log-file-loaded', (data) => {\n                if(typeof data.fileContent === 'string') {\n                    if(data.fileContent.trim() !== '') {\n                        this.$refs.codemirror.editor.setValue(data.fileContent);\n                    } else {\n                        this.$refs.codemirror.editor.setValue(this.$t('tools.logFileEmpty'));\n                    }\n                }\n\n                this.$refs.codemirror.editor.refresh();\n            });\n        },\n        loadSelectedFile () {\n            let filename = this.$refs['selectedFile'].selectedValue;\n            this.loadFile(filename);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.tools-log-viewer-selector {\n    display: flex;\n\n    .button {\n        margin-left: 1rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/MenuItem.vue",
    "content": "<template>\n    <li\n        :class=\"cssClasses\"\n        :data-id=\"id\"\n        :title=\"titleTooltip\">\n        <div :class=\"{\n            'menu-item-wrapper': true,\n            'is-dnd-disabled': !!selectedItem \n        }\">\n            <span class=\"menu-item-label\">\n                {{ label }}\n\n                <icon\n                    v-if=\"isInvalid\"\n                    name=\"trash\"\n                    size=\"xs\"\n                    class=\"removed-icon\"\n                    primary-color=\"color-3\"\n                    :title=\"$t('menu.likedItemError')\" />\n\n                <icon\n                    v-if=\"isDraft\"\n                    name=\"draft-post\"\n                    size=\"xs\"\n                    class=\"draft-icon\"\n                    strokeColor=\"color-7\"\n                    :title=\"$t('post.thisPostIsADraft')\" />\n\n                <icon\n                    v-if=\"isHidden\"\n                    size=\"xs\"\n                    name=\"hidden-post\"\n                    class=\"hidden-icon\"\n                    strokeColor=\"color-7\"\n                    :title=\"$t('menu.menuItemIsHidden')\" />\n            </span>\n\n            <a\n                v-if=\"!selectedItem\"\n                href=\"#\"\n                class=\"menu-item-duplicate\"\n                :title=\"$t('menu.duplicateThisMenuItem')\"\n                @click.prevent=\"duplicateMenuItem\">\n                <icon\n                    name=\"duplicate\"\n                    size=\"xs\" />\n            </a>\n\n            <a\n                v-if=\"!selectedItem\"\n                href=\"#\"\n                class=\"menu-item-remove\"\n                :title=\"$t('menu.deleteThisMenuItem')\"\n                @click.prevent=\"removeMenuItem\">\n                <icon\n                    name=\"trash\"\n                    size=\"xs\" />\n            </a>\n\n            <a\n                v-if=\"!selectedItem\"\n                href=\"#\"\n                class=\"menu-item-edit\"\n                :title=\"$t('menu.editThisMenuItem')\"\n                @click.prevent=\"editMenuItem\">{{ $t('ui.edit') }}</a>\n\n            <a\n                v-if=\"!isHidden && !selectedItem\"\n                href=\"#\"\n                class=\"menu-item-hide\"\n                :title=\"$t('menu.hideThisMenuItem')\"\n                @click.prevent=\"hideMenuItem\">{{ $t('ui.hide') }}</a>\n\n            <a\n                v-if=\"isHidden && !selectedItem\"\n                href=\"#\"\n                class=\"menu-item-show\"\n                :title=\"$t('menu.showThisMenuItem')\"\n                @click.prevent=\"showMenuItem\">{{ $t('ui.show') }}</a>\n\n            <a\n                v-if=\"!selectedItem\"\n                href=\"#\"\n                class=\"menu-item-submenu\"\n                :title=\"$t('menu.addSubmenuItem')\"\n                @click.prevent=\"addSubmenuItem()\">{{ $t('menu.addSubmenu') }}</a>\n\n            <a\n                v-if=\"!selectedItem\"\n                href=\"#\"\n                class=\"menu-item-select\"\n                :title=\"$t('menu.moveItem')\"\n                @click.prevent=\"selectItem()\">{{ $t('menu.moveItem') }}</a>\n            \n            <a\n                v-if=\"selectedItem && isSelected\"\n                href=\"#\"\n                class=\"menu-item-unselect\"\n                :title=\"$t('menu.unselectItem')\"\n                @click.prevent=\"unselectItem()\">\n                <icon\n                    class=\"menu-item-move-icon\"\n                    customWidth=\"22\"\n                    customHeight=\"22\"\n                    name=\"sidebar-close\"/> {{ $t('menu.unselectItem') }}</a>\n\n            <span \n                v-if=\"selectedItem && !isSelected && !parentIsSelected\"\n                class=\"menu-item-insert-actions\">\n                {{ $t('menu.insertActions') }}\n            </span>\n\n            <a\n                v-if=\"selectedItem && !isSelected && !parentIsSelected\"\n                href=\"#\"\n                class=\"menu-item-insert-before\"\n                :title=\"$t('menu.insertBefore')\"\n                @click.prevent=\"insertSelectedItem('before')\">\n                <icon\n                    class=\"menu-item-move-icon\"\n                    size=\"xs\"\n                    name=\"move-up\"/> {{ $t('menu.insertBefore') }}</a>\n\n            <a\n                v-if=\"selectedItem && !isSelected && !parentIsSelected\"\n                href=\"#\"\n                class=\"menu-item-insert-after\"\n                :title=\"$t('menu.insertAfter')\"\n                @click.prevent=\"insertSelectedItem('after')\">\n                <icon\n                    class=\"menu-item-move-icon\"\n                    size=\"xs\"\n                    name=\"move-down\"/> {{ $t('menu.insertAfter') }}</a>\n\n            <a\n                v-if=\"selectedItem && !isSelected && !parentIsSelected\"\n                href=\"#\"\n                class=\"menu-item-insert-as-child\"\n                :title=\"$t('menu.insertAsChild')\"\n                @click.prevent=\"insertSelectedItem('child')\">{{ $t('menu.insertAsChild') }} </a>\n        </div>\n\n        <draggable\n            tag=\"ol\"\n            group=\"menu-items\"\n            chosenClass=\"is-chosen\"\n            ghostClass=\"is-ghost\"\n            :class=\"{ 'menu-item-list': true }\"\n            v-model=\"itemsList\"\n            v-bind=\"{\n                animation: 0,\n                forceFallback: true,\n                disabled: !!selectedItem\n            }\"\n            :key=\"'draggable-menu-items-' + id\"\n            @update=\"listItemUpdated\"\n            @add=\"listItemAdded\">\n            <menu-item\n                v-for=\"(item, index) in items\"\n                :key=\"'item-' + index + '-' + getUID()\"\n                :itemData=\"item\"\n                :itemMenuID=\"menuID\"\n                :itemOrder=\"index\"\n                :editedID=\"parseInt(editedID, 10)\"\n                :selectedItem=\"selectedItem\"\n                :parentIsSelected=\"isSelected || parentIsSelected\" />\n        </draggable>\n    </li>\n</template>\n\n<script>\nimport Draggable from 'vuedraggable';\n\nexport default {\n    name: 'menu-item',\n    props: {\n        editedID: {\n            type: [Boolean, Number],\n            required: false\n        },\n        itemData: {\n            type: Object,\n            required: true\n        },\n        itemMenuID: {\n            type: Number,\n            required: true\n        },\n        itemOrder: {\n            type: Number,\n            required: true\n        },\n        selectedItem: {\n            required: true\n        },\n        parentIsSelected: {\n            required: false\n        }\n    },\n    components: {\n        'draggable': Draggable\n    },\n    data () {\n        return {\n            menuID: '',\n            id: '',\n            label: '',\n            type: '',\n            link: '',\n            title: '',\n            target: '_self',\n            cssClass: '',\n            rel: '',\n            isHidden: false,\n            items: []\n        };\n    },\n    computed: {\n        cssClasses () {\n            return {\n                'menu-item': true,\n                'is-edited': this.editedID === this.id,\n                'is-invalid': this.isInvalid,\n                'is-draft': this.isDraft,\n                'is-selected': this.isSelected\n            };\n        },\n        isInvalid () {\n            return !this.elementExists();\n        },\n        isDraft () {\n            return this.elementIsDraft();\n        },\n        titleTooltip () {\n            if (this.isInvalid) {\n                return this.$t('menu.likedItemError');\n            }\n\n            if (this.isDraft) {\n                return this.$t('menu.likedItemIsADraft');\n            }\n\n            return false;\n        },\n        itemsList: {\n            get () {\n                return this.findItem(this.$store.state.currentSite.menuStructure[this.itemMenuID].items);\n            },\n            set (newValue) {\n                this.$store.commit('reorderMenuItems', {\n                    menuID: this.menuID,\n                    itemID: this.id,\n                    items: newValue\n                });\n\n                this.$bus.$emit('save-new-menu-structure');\n            }\n        },\n        isSelected () {\n            return this.id === this.selectedItem;\n        }\n    },\n    mounted () {\n        this.menuID = this.itemMenuID;\n        this.id = this.itemData.id;\n        this.label = this.itemData.label;\n        this.type = this.itemData.type;\n        this.link = this.itemData.link;\n        this.title = this.itemData.title;\n        this.target = this.itemData.target;\n        this.rel = this.itemData.rel;\n        this.items = this.itemData.items;\n        this.cssClass = this.itemData.cssClass;\n        this.isHidden = this.itemData.isHidden || false;\n    },\n    methods: {\n        elementExists () {\n            if(this.type !== 'post' && this.type !== 'page' && this.type !== 'tag' && this.type !== 'author') {\n                return true;\n            }\n\n            let validItems = [];\n\n            if (this.type === 'post') {\n                validItems = this.$store.state.currentSite.posts.filter(\n                    post => post.id == this.link && post.status.indexOf('trashed') === -1\n                );\n            }\n\n            if (this.type === 'page') {\n                validItems = this.$store.state.currentSite.pages.filter(\n                    page => page.id == this.link && page.status.indexOf('trashed') === -1\n                );\n            }\n\n            if (this.type === 'tag') {\n                validItems = this.$store.state.currentSite.tags.filter(\n                    tag => tag.id == this.link && tag.additionalData.indexOf('\"isHidden\":true') === -1\n                );\n\n                if(!this.$store.state.currentSite.config.advanced.displayEmptyTags) {\n                    let assignedPosts = this.$store.state.currentSite.postsTags.filter(\n                        postTag => postTag.tagID == this.link\n                    );\n\n                    if(assignedPosts.length === 0) {\n                        return false;\n                    }\n                }\n            }\n\n            if (this.type === 'author') {\n                validItems = this.$store.state.currentSite.authors.filter(\n                    author => author.username === this.link\n                );\n\n                let authorID = validItems[0] ? validItems[0].id : null;\n\n                if(authorID && !this.$store.state.currentSite.config.advanced.displayEmptyAuthors) {\n                    let assignedPosts = this.$store.state.currentSite.postsAuthors.filter(\n                        postAuthor => postAuthor.authorID == authorID\n                    );\n\n                    if (assignedPosts.length === 0) {\n                        return false;\n                    }\n                }\n            }\n\n            return validItems.length > 0;\n        },\n        elementIsDraft () {\n            if (this.type !== 'post' && this.type !== 'page') {\n                return false;\n            }\n\n            let draftItems = [];\n\n            if (this.type === 'post') {\n                draftItems = this.$store.state.currentSite.posts.filter(\n                    post => post.id == this.link && post.status.indexOf('draft') > -1\n                );\n            }\n\n            if (this.type === 'page') {\n                draftItems = this.$store.state.currentSite.pages.filter(\n                    page => page.id == this.link && page.status.indexOf('draft') > -1\n                );\n            }\n\n            return draftItems.length > 0;\n        },\n        getUID () {\n            return Math.random().toString(36).substr(2, 9);\n        },\n        addSubmenuItem () {\n            this.$bus.$emit('show-menu-item-editor-from-submenu');\n\n            setTimeout(() => {\n                this.$bus.$emit('show-menu-item-editor', {\n                    menuID: this.menuID,\n                    parentID: this.id\n                });\n            }, 50);\n        },\n        editMenuItem () {\n            this.$bus.$emit('show-menu-item-editor-from-submenu', this.id);\n            \n            setTimeout(() => {\n                this.$bus.$emit('show-menu-item-editor', {\n                    menuID: this.menuID,\n                    menuItemID: this.id,\n                    label: this.label,\n                    title: this.title,\n                    cssClass: this.cssClass,\n                    type: this.type,\n                    target: this.target,\n                    rel: this.rel,\n                    isHidden: this.isHidden,\n                    link: this.link\n                });\n            }, 50);\n        },\n        duplicateMenuItem () {\n            this.$store.commit('duplicateMenuItem', {\n                menuID: this.menuID,\n                menuItemID: this.id\n            });\n\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        removeMenuItem () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('menu.menuItemsRemoveMessage'),\n                isDanger: true,\n                okClick: this.removeSelectedMenuItem\n            });\n        },\n        removeSelectedMenuItem () {\n            this.$store.commit('deleteMenuItem', {\n                menuID: this.menuID,\n                menuItemID: this.id\n            });\n\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        listItemUpdated (e) {\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        listItemAdded (e) {\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        findItem(items) {\n            if (items) {\n                for (var i = 0; i < items.length; i++) {\n                    if (items[i].id == this.id) {\n                        return items[i].items;\n                    }\n\n                    var found = this.findItem(items[i].items);\n\n                    if (found) {\n                        return found;\n                    }\n                }\n            }\n        },\n        hideMenuItem () {\n            this.isHidden = true;\n\n            this.$store.commit('hideMenuItem', {\n                itemID: this.id,\n                menuID: this.menuID\n            });\n\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        showMenuItem () {\n            this.isHidden = false;\n\n            this.$store.commit('showMenuItem', {\n                itemID: this.id,\n                menuID: this.menuID\n            });\n\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        selectItem () {\n            this.$bus.$emit('menus-manager-selected-item', {\n                id: this.id, \n                menuID: this.menuID\n            });\n        },\n        unselectItem () {\n            this.$bus.$emit('menus-manager-unselect-item');\n        },\n        insertSelectedItem (position) {\n            this.$bus.$emit('menus-manager-move-item', {\n                menuID: this.menuID,\n                position: position,\n                destinationID: this.id\n            });\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\nli {\n    border: 1px solid transparent;\n    margin-bottom: calc((3 * $spacing) - 2px);\n    padding: 0;\n    position: relative;\n    \n    &.is-chosen {\n        opacity: .75;\n    }\n\n    &.is-selected {\n        border: 1px dashed var(--input-border-focus);\n    }\n\n    &.is-ghost {\n        border: 1px dashed var(--input-border-focus);\n\n        & > .menu-item-wrapper {\n            opacity: 0;\n        }\n    }\n\n    &.is-invalid {\n        & > .menu-item-wrapper > .menu-item-label {\n            color: var(--warning);\n            text-decoration: line-through;\n        }\n    }\n\n    &.is-draft {\n        .menu-item-label {\n            color: var(--gray-4);\n            font-size: 1.4rem;\n            font-style: italic;\n\n            svg {\n                fill: var(--gray-4);\n                position: relative;\n                top: 2px;\n            }\n        }\n    }\n\n    .hidden-icon {\n        position: relative;\n        top: 2px;\n    }\n\n    &.is-edited {\n        & > div {\n            background: var(--collection-bg-hover);\n            border-left: 3px solid var(--color-primary);\n        }\n    }\n\n    & > div {\n        background: var(--collection-bg);\n        border: 1px solid var(--border-light-color);\n        border-left: 3px solid var(--input-border-color);\n        border-radius: 3px;\n        cursor: move !important;\n        padding: 4 * $spacing 10 * $spacing;\n        position: relative;\n\n        &.is-dnd-disabled {\n            cursor: auto!important;\n        }\n\n        &:hover {\n            background: var(--collection-bg-hover);\n            border-left: 3px solid var(--color-primary);\n        }\n\n        .menu-item-label {\n            color: var(--text-primary-color);\n            font-size: 1.4rem;\n            margin-right: 2rem;\n        }\n\n        .menu-item-remove,\n        .menu-item-duplicate {\n            align-items: center;\n            background: var(--bg-primary);\n            position: relative;\n            border-radius: 50%;\n            cursor: pointer;\n            display: inline-flex;\n            height: 3rem;\n            justify-content: center;\n            padding: 0;\n            position: absolute;\n            right: 1.5rem;\n            text-align: center;\n            transition: all .3s ease-out;\n            top: 50%;\n            transform: translate(0, -50%);\n            width: 3rem;\n\n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--headings-color)\n            }\n\n            &:hover {\n                & > svg {\n                   fill: var(--warning);\n                   transform: scale(1);\n               }\n            }\n\n            svg {\n                fill: var(--icon-secondary-color);\n                height: 1.6rem;\n                pointer-events: none;\n                transform: scale(.9);\n                transition: var(--transition);\n                width: 1.6rem;\n            }\n        }\n\n        .menu-item-duplicate {\n            right: 5rem;\n\n            &:hover {\n                & > svg {\n                   fill: currentColor;\n                   transform: scale(1);\n               }\n            }\n        }\n\n        .menu-item-edit,\n        .menu-item-hide,\n        .menu-item-show,\n        .menu-item-select,\n        .menu-item-submenu,\n        .menu-item-insert-before,\n        .menu-item-insert-after,\n        .menu-item-insert-as-child,\n        .menu-item-unselect {\n            color: var(--link-primary-color);\n            display: inline-block;\n            font-size: 1.3rem;\n            padding: .25rem .5rem;\n\n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--link-primary-color-hover);\n            }\n        }\n\n        .menu-item-insert-actions {\n            color: var(--text-light-color);\n            font-size: 1.3rem;\n            padding: 1rem .5rem;\n        }\n\n        .menu-item-unselect {\n            color: var(--warning);\n\n            & > svg {\n               fill: var(--warning);\n               position: relative;\n               right: 3px;\n               top: 3px;\n            }\n        }\n\n        .menu-item-edit,\n        .menu-item-hide,\n        .menu-item-show,\n        .menu-item-submenu,\n        .menu-item-insert-before,\n        .menu-item-insert-after {\n            padding-right: 1rem;\n            position: relative;\n\n            &::after {\n                background: var(--input-border-color);\n                content: \"\";\n                display: block;\n                height: 14px;\n                position: absolute;\n                right: 0;\n                top: 50%;\n                transform: translate(0, -50%);\n                width: 1px;\n            }\n        }\n\n        .menu-item-move-icon {\n            vertical-align: text-bottom;\n        }\n\n        select {\n            height: 2.4rem;\n        }\n    }\n\n    ol {\n        list-style-type: none;\n        padding-left: 0;\n        margin-left: 15 * $spacing;\n\n        & > li:first-child {\n            margin-top: 3 * $spacing;\n        }\n\n        &:empty {\n            bottom: -6 * $spacing;\n            left: 0;\n            min-height: 6 * $spacing;\n            position: absolute;\n            width: 100%;\n            z-index: 10;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/MenuItemEditor.vue",
    "content": "<template>\n    <div\n        :key=\"'menu-item-view-' + id\"\n        class=\"options-sidebar-container\">\n        <div class=\"options-sidebar\">\n            <h2>\n                <template v-if=\"menuItemID !== ''\">{{ $t('menu.editMenuItem') }}</template>\n                <template v-if=\"menuItemID === ''\">{{ $t('menu.addNewMenuItem') }}</template>\n            </h2>\n\n            <span\n                class=\"options-sidebar-close\"\n                name=\"sidebar-close\"\n                @click.prevent=\"hide()\">\n                &times;\n            </span>\n\n            <label\n                :class=\"{ 'is-invalid': errors.indexOf('label') > -1 }\"\n                key=\"menu-item-editor-field-label\">\n                <span>{{ $t('menu.label') }}</span>\n\n                 <span\n                    v-if=\"(type === 'tag' && tagPage) || (type === 'author' && authorPage) || (type === 'post' && postPage) || (type === 'page' && pagePage)\" \n                    @click.prevent.stop=\"setLabel(type)\" \n                    class=\"options-sidebar-icon-button-suggestion\"\n                    :title=\"$t('menu.updateLabel.' + type)\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"pen-ai-suggestion\"/>\n                </span>\n\n                <input\n                    v-model=\"label\"\n                    :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                    key=\"menu-item-editor-field-label-value\"\n                    @keyup=\"cleanError('label')\"\n                    type=\"text\">\n            </label>\n\n            <label\n                :class=\"{ 'is-invalid': errors.indexOf('type') > -1 }\"\n                key=\"menu-item-editor-field-type\">\n                <span>{{ $t('menu.type') }}</span>\n                <v-select\n                    v-model=\"type\"\n                    @click.native=\"cleanError('type')\"\n                    :options=\"linkTypes\"\n                    :searchable=\"false\"\n                    :custom-label=\"customTypeLabels\"\n                    :show-labels=\"false\"\n                    :placeholder=\"$t('menu.selectItemType')\"></v-select>\n            </label>\n\n            <label\n                v-if=\"type === 'internal'\"\n                :class=\"{ 'is-invalid': errors.indexOf('internalLink') > -1 }\"\n                key=\"menu-item-editor-field-internal\">\n                <span>{{ $t('menu.internalLink') }}</span>\n                <input\n                    v-model=\"internalLink\"\n                    @keyup=\"cleanError('internalLink')\"\n                    spellcheck=\"false\"\n                    type=\"text\" />\n            </label>\n\n            <label\n                v-if=\"type === 'external'\"\n                :class=\"{ 'is-invalid': errors.indexOf('externalLink') > -1 }\"\n                key=\"menu-item-editor-field-external\">\n                <span>{{ $t('menu.externalURL') }}</span>\n                <input\n                    v-model=\"externalLink\"\n                    @keyup=\"cleanError('externalLink')\"\n                    spellcheck=\"false\"\n                    type=\"text\" />\n            </label>\n\n            <label\n                v-if=\"type === 'tag'\"\n                :class=\"{ 'is-invalid': errors.indexOf('tagPage') > -1 }\"\n                key=\"menu-item-editor-field-tag\">\n                <span>\n                    {{ $t('tag.tagPage') }}\n                </span>\n\n                <v-select\n                    ref=\"tagPagesSelect\"\n                    :options=\"tagPages\"\n                    @click.native=\"cleanError('tagPage')\"\n                    v-model=\"tagPage\"\n                    :custom-label=\"customTagLabels\"\n                    :close-on-select=\"true\"\n                    :show-labels=\"false\"\n                    @select=\"closeDropdown('tagPagesSelect')\"\n                    :placeholder=\"$t('tag.selectTagPage')\"></v-select>\n            </label>\n\n            <label\n                v-if=\"type === 'author'\"\n                :class=\"{ 'is-invalid': errors.indexOf('authorPage') > -1 }\"\n                key=\"menu-item-editor-field-author\">\n                <span>\n                    {{ $t('author.authorPage') }}\n                </span>\n\n                <v-select\n                    ref=\"authorPagesSelect\"\n                    :options=\"authorPages\"\n                    @click.native=\"cleanError('authorPage')\"\n                    v-model=\"authorPage\"\n                    :custom-label=\"customAuthorsLabels\"\n                    :close-on-select=\"true\"\n                    :show-labels=\"false\"\n                    @select=\"closeDropdown('authorPagesSelect')\"\n                    :placeholder=\"$t('author.selectAuthorPage')\"></v-select>\n            </label>\n\n            <label\n                v-if=\"type === 'post'\"\n                :class=\"{ 'is-invalid': errors.indexOf('postPage') > -1 }\"\n                key=\"menu-item-editor-field-post\">\n                <span>\n                    {{ $t('post.postPage') }}\n                </span>\n\n                <v-select\n                    ref=\"postPagesSelect\"\n                    :options=\"postPages\"\n                    @click.native=\"cleanError('postPage')\"\n                    v-model=\"postPage\"\n                    :custom-label=\"customPostLabels\"\n                    :close-on-select=\"true\"\n                    :show-labels=\"false\"\n                    @select=\"closeDropdown('postPagesSelect')\"\n                    :placeholder=\"$t('post.selectPostPage')\"></v-select>\n            </label>\n\n            <label\n                v-if=\"type === 'page'\"\n                :class=\"{ 'is-invalid': errors.indexOf('pagePage') > -1 }\"\n                key=\"menu-item-editor-field-post\">\n                <span>\n                    {{ $t('page.page') }}\n                </span>\n\n                <v-select\n                    ref=\"pagePagesSelect\"\n                    :options=\"pagePages\"\n                    @click.native=\"cleanError('pagePage')\"\n                    v-model=\"pagePage\"\n                    :custom-label=\"customPageLabels\"\n                    :close-on-select=\"true\"\n                    :show-labels=\"false\"\n                    @select=\"closeDropdown('pagePagesSelect')\"\n                    :placeholder=\"$t('page.selectPage')\"></v-select>\n            </label>\n\n            <label key=\"menu-item-editor-field-title\">\n                <span>{{ $t('link.linkTitleAttribute') }}</span>\n                <input\n                    v-model=\"title\"\n                    :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                    type=\"text\" />\n            </label>\n\n            <label key=\"menu-item-editor-field-cssclass\">\n                <span>{{ $t('menu.classCSS') }}</span>\n                <input\n                    v-model=\"cssClass\"\n                    spellcheck=\"false\"\n                    type=\"text\" />\n            </label>\n\n            <label key=\"menu-item-editor-field-target\">\n                <span>{{ $t('ui.linkTarget') }}:</span>\n                <v-select\n                    v-model=\"target\"\n                    :options=\"linkTargets\"\n                    :searchable=\"false\"\n                    :custom-label=\"customTargetLabels\"\n                    :show-labels=\"false\"\n                    :placeholder=\"$t('menu.selectLinkTarget')\"></v-select>\n            </label>\n\n            <label key=\"menu-item-editor-field-rel\">\n                <span>{{ $t('link.linkRelAttribute') }}:</span>\n                <input\n                    v-model=\"rel\"\n                    spellcheck=\"false\"\n                    type=\"text\" />\n            </label>\n\n            <div class=\"options-sidebar-buttons\">\n                <p-button\n                    v-if=\"menuItemID !== ''\"\n                    type=\"primary\"\n                    @click.native=\"editMenuItem\">\n                    {{ $t('ui.saveChanges') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"menuItemID === ''\"\n                    type=\"primary\"\n                    @click.native=\"addMenuItem\">\n                    {{ $t('menu.addMenuItem') }}\n                </p-button>\n\n                <p-button\n                    @click.native=\"hide()\"\n                    type=\"outline\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'menu-item-editor',\n    data () {\n        return {\n            isVisible: false,\n            id: '',\n            menuItemID: '',\n            parentID: '',\n            menuID: '',\n            label: '',\n            title: '',\n            type: '',\n            target: '_self',\n            rel: '',\n            cssClass: '',\n            isHidden: false,\n            internalLink: '',\n            externalLink: '',\n            tagPage: '',\n            authorPage: '',\n            postPage: '',\n            pagePage: '',\n            errors: []\n        };\n    },\n    computed: {\n        linkTypes () {\n            return [\n                'post',\n                'page',\n                'tag',\n                'tags',\n                'author',\n                'frontpage',\n                'blogpage',\n                'internal',\n                'external',\n                'separator'\n            ];\n        },\n        tagPages () {\n            return this.$store.state.currentSite.tags.filter(tag => tag.additionalData.indexOf('\"isHidden\":true') === -1).map(tag => tag.id);\n        },\n        linkTargets () {\n            return [ '_self', '_blank' ];\n        },\n        authorPages () {\n            return this.$store.state.currentSite.authors.map(author => author.username).sort((a, b) => {\n                if (a.toLowerCase() < b.toLowerCase()) {\n                    return -1;\n                }\n\n                if (a.toLowerCase() > b.toLowerCase()) {\n                    return 1;\n                }\n\n                return 0;\n            });\n        },\n        pagePages () {\n            return this.$store.state.currentSite.pages.filter(page => page.status.indexOf('published') > -1).map(page => page.id);\n        },\n        postPages () {\n            return this.$store.state.currentSite.posts.filter(post => post.status.indexOf('published') > -1).map(post => post.id);\n        }\n    },\n    mounted () {\n        this.$bus.$on('show-menu-item-editor', params => {\n            console.log(params);\n            this.reset();\n            this.menuItemID = params.menuItemID || '';\n            this.parentID = params.parentID || '';\n\n            if(params.menuID || params.menuID === 0) {\n                this.menuID = params.menuID;\n            } else {\n                this.menuID = '';\n            }\n\n            this.label = params.label || '';\n            this.title = params.title || '';\n            this.cssClass = params.cssClass || '';\n            this.target = params.target || '_self';\n            this.rel = params.rel || '';\n            this.isHidden = params.isHidden || false;\n\n            setTimeout(() => {\n                this.type = params.type || '';\n                this.setLinkValue(params.link);\n            }, 0);\n        });\n    },\n    methods: {\n        customTypeLabels (value) {\n            switch (value) {\n                case 'post': return this.$t('post.postLink');\n                case 'page': return this.$t('page.pageLink');\n                case 'tag': return this.$t('tag.tagLink');\n                case 'tags': return this.$t('tag.tagsListLink');\n                case 'author': return this.$t('author.authorLink');\n                case 'frontpage': return this.$t('ui.frontpageLink');\n                case 'blogpage': return this.$t('ui.blogIndexLink');\n                case 'internal': return this.$t('menu.internalLink');\n                case 'external': return this.$t('menu.externalLink');\n                case 'separator': return this.$t('menu.textSeparator');\n            }\n        },\n        customTagLabels (value) {\n            return this.$store.state.currentSite.tags.filter(tag => tag.additionalData.indexOf('\"isHidden\":true') === -1 && tag.id === value).map(tag => tag.name)[0];\n        },\n        customAuthorsLabels (value) {\n            return this.$store.state.currentSite.authors.filter(author => author.username === value).map(author => author.name)[0];\n        },\n        customPageLabels (value) {\n            return this.$store.state.currentSite.pages.filter(page => page.id === value).map(page => page.title)[0];\n        },\n        customPostLabels (value) {\n            return this.$store.state.currentSite.posts.filter(post => post.id === value).map(post => post.title)[0];\n        },\n        customTargetLabels (value) {\n            switch (value) {\n                case '_self': return this.$t('ui.sameWindow');\n                case '_blank': return this.$t('ui.newWindow');\n            }\n        },\n        closeDropdown (refID) {\n            this.$refs[refID].isOpen = false;\n        },\n        reset () {\n            this.menuItemID = Date.now();\n            this.parentID = '';\n            this.menuID = '';\n            this.label = '';\n            this.title = '';\n            this.type = '';\n            this.target = '_self';\n            this.rel = '';\n            this.cssClass = '';\n            this.isHidden = false;\n            this.internalLink = '';\n            this.externalLink = '';\n            this.tagPage = '';\n            this.authorPage = '';\n            this.pagePage = '';\n            this.postPage = '';\n            this.errors = [];\n        },\n        hide () {\n            this.reset();\n            this.$bus.$emit('hide-menu-item-editor');\n        },\n        validate() {\n            this.errors = [];\n\n            if(this.label === '') {\n                this.errors.push('label');\n            }\n\n            if(!this.type) {\n                this.errors.push('type');\n            }\n\n            if(this.type === 'post' && !(!!this.postPage)) {\n                this.errors.push('postPage');\n            }\n\n            if(this.type === 'page' && !(!!this.pagePage)) {\n                this.errors.push('pagePage');\n            }\n\n            if(this.type === 'tag' && !(!!this.tagPage)) {\n                this.errors.push('tagPage');\n            }\n\n            if(this.type === 'author' && !(!!this.authorPage)) {\n                this.errors.push('authorPage');\n            }\n\n            if(this.type === 'external' && !(!!this.externalLink)) {\n                this.errors.push('externalLink')\n            }\n\n            if(this.type === 'internal' && !(!!this.internalLink)) {\n                this.errors.push('internalLink');\n            }\n\n            if(this.errors.length) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('ui.pleaseFillAllRequiredFields'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n            }\n\n            return this.errors.length === 0;\n        },\n        cleanError (field) {\n            let pos = this.errors.indexOf(field);\n\n            if (pos !== -1) {\n                this.errors.splice(pos, 1);\n            }\n        },\n        addMenuItem () {\n            if (!this.validate()) {\n                return;\n            }\n\n            let menuItem = {\n                id: new Date().getTime(),\n                label: this.label,\n                title: this.title,\n                type: this.type,\n                target: this.target,\n                rel: this.rel,\n                link: this.getLinkValue(),\n                cssClass: this.cssClass,\n                isHidden: this.isHidden,\n                items: []\n            };\n\n            if(this.parentID === '') {\n                this.$store.commit('addNewMenuItem', {\n                    menuItem: menuItem,\n                    menuID: this.menuID\n                });\n            } else {\n                this.$store.commit('addNewSubmenuItem', {\n                    menuItem: menuItem,\n                    menuID: this.menuID,\n                    parentID: this.parentID\n                });\n            }\n\n            this.hide();\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        editMenuItem () {\n            if (!this.validate()) {\n                return;\n            }\n\n            let menuItem = {\n                id: this.menuItemID,\n                label: this.label,\n                title: this.title,\n                type: this.type,\n                target: this.target,\n                rel: this.rel,\n                link: this.getLinkValue(),\n                cssClass: this.cssClass,\n                isHidden: this.isHidden\n            };\n\n            this.$store.commit('editMenuItem', {\n                menuItem: menuItem,\n                menuID: this.menuID\n            });\n\n            this.hide();\n            this.$bus.$emit('save-new-menu-structure');\n        },\n        getLinkValue () {\n            let type = this.type;\n\n            switch (type) {\n                case 'page':      return parseInt(this.pagePage, 10);\n                case 'post':      return parseInt(this.postPage, 10);\n                case 'tag':       return parseInt(this.tagPage, 10);\n                case 'tags':      return 'empty';\n                case 'author':    return this.authorPage;\n                case 'frontpage': return 'empty';\n                case 'blogpage':  return 'empty';\n                case 'internal':  return this.internalLink;\n                case 'external':  return this.externalLink;\n                case 'separator': return '';\n            }\n        },\n        setLinkValue (value) {\n            this.internalLink = this.type === 'internal' ? value : '';\n            this.externalLink = this.type === 'external' ? value : '';\n            this.tagPage = this.type === 'tag' ? parseInt(value, 10) : '';\n            this.authorPage = this.type === 'author' ? value : '';\n            this.pagePage = this.type === 'page' ? parseInt(value, 10) : '';\n            this.postPage = this.type === 'post' ? parseInt(value, 10) : '';\n        },\n        setLabel (itemType) {\n            if (itemType === 'tag') {\n                this.label = this.customTagLabels(this.tagPage);\n            } else if (itemType === 'author') {\n                this.label = this.customAuthorsLabels(this.authorPage);\n            } else if (itemType === 'post') {\n                this.label = this.customPostLabels(this.postPage);\n            } else if (itemType === 'page') {\n                this.label = this.customPageLabels(this.pagePage);\n            }\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('show-menu-item-editor');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/options-sidebar.scss';\n\n.options-sidebar {\n    h2 {\n        margin-bottom: 1.2rem;\n    }\n\n    &-buttons {\n        border: none;\n        padding-top: 1.8rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/MenuPositionPopup.vue",
    "content": "<template>\n    <div class=\"overlay\">\n        <div \n            class=\"popup\"\n            @click.stop>\n            <h1>{{ $t('menuPositionPopup.title') }}</h1>\n\n            <div class=\"menu-position-items\">\n                <div \n                    v-for=\"(menu, position) of availableMenus\"\n                    :key=\"'menu-position-item-' + position\"\n                    class=\"menu-position-item\">\n                    <div>\n                        <switcher \n                            v-model=\"configuration[position].status\"\n                            :key=\"'menu-position-item-' + position + '-switcher'\"\n                            :disabled=\"!!menu.used\" />\n                    \n                        <span class=\"menu-position-item-name\">\n                            {{ menu.name }}\n                        </span>\n                    \n\n                        <small \n                            v-if=\"menu.desc\"\n                            class=\"menu-position-item-desc\">\n                            {{ menu.desc }}\n                        </small>\n\n                        <small \n                            v-if=\"menu.used\"\n                            class=\"menu-position-item-usedby\">\n                            {{ $t('menuPositionPopup.usedBy') }} {{ getMenuUsingPosition(position) }}\n                        </small>\n\n                    </div> \n\n                    <div \n                        v-if=\"configuration[position].status && !menu.used\"\n                        class=\"menu-position-item-max-levels\"\n                        :key=\"'menu-position-item-' + position + '-input'\">\n                        {{ $t('menuPositionPopup.maxLevels') }}\n                        <div>\n                            <text-input \n                                type=\"number\"\n                                @input=\"validateMaxLevels\"\n                                v-model=\"configuration[position].maxLevels\"\n                                :min=\"((menu.maxLevels === -1) ? -1 : 1).toString()\"\n                                :max=\"((menu.maxLevels === -1) ? 999 : menu.maxLevels).toString()\"\n                                step=\"1\"\n                                properties=\"is-small\"\n                                :class=\"{ 'is-invalid': configuration[position].invalid }\" />\n                            \n                            <small \n                                v-if=\"(configuration[position].maxLevels).toString() !== (menu.maxLevels).toString()\"\n                                class=\"menu-position-item-max-levels-desc\">\n                                {{ $t('menuPositionPopup.themeDefaultValue') }} {{ menu.maxLevels }}\n                            </small>\n                        </div>\n                    </div>\n                    <small \n                        v-if=\"configuration[position].invalid\"\n                        class=\"menu-position-item-max-levels-error\">\n                        {{ $t('menuPositionPopup.invalidValue') }}\n                    </small>\n                </div>\n            </div>\n\n            <div class=\"buttons\">\n                <p-button\n                    @click.native=\"close\"\n                    type=\"medium no-border-radius half-width cancel-popup\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n\n                <p-button\n                    @click.native=\"saveChanges\"\n                    :disabled=\"!configurationIsValid\"\n                    type=\"medium no-border-radius half-width\">\n                    {{ $t('ui.saveChanges') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'menu-position-popup',\n    props: [\n        'editedItem',\n        'editedItemIndex',\n        'menus'\n    ],\n    computed: {\n        configurationIsValid () {\n            let positions = Object.keys(this.configuration);\n\n            for (let i = 0; i < positions.length; i++) {\n                let position = positions[i];\n\n                if (this.menusInUse.indexOf(position) === -1 && this.configuration[position].invalid) {\n                    return false;\n                }\n            }\n            \n            return true;\n        },\n        menusInUse () {\n            return this.menus.filter((item, index) => index !== this.editedItemIndex || item.position === '')\n                                .map(item => item.position)\n                                .join(';')\n                                .split(';');\n        },\n        availableMenus () {\n            let menus = JSON.parse(JSON.stringify(this.$store.state.currentSite.themeSettings.menus));\n            let menuPositions = Object.keys(menus);\n\n            for (let i = 0; i < menuPositions.length; i++) {\n                let menuPosition = menuPositions[i];\n\n                // Check which format uses theme in menus inside config.json\n                if (typeof menus[menuPosition] === 'object') {\n                    menus[menuPosition] = {\n                        desc: menus[menuPosition].desc || '',\n                        name: menus[menuPosition].name,\n                        maxLevels: menus[menuPosition].maxLevels || -1,\n                        used: this.menusInUse.indexOf(menuPosition) > -1\n                    };\n                } else {\n                    menus[menuPosition] = {\n                        desc: '',\n                        name: menus[menuPosition],\n                        maxLevels: -1,\n                        used: this.menusInUse.indexOf(menuPosition) > -1\n                    };\n                }\n            }\n\n            return menus;\n        }\n    },\n    data () {\n        return {\n            configuration: {}\n        };\n    },\n    beforeMount () {\n        let menus = JSON.parse(JSON.stringify(this.$store.state.currentSite.themeSettings.menus));\n        let menuPositions = Object.keys(menus);\n        let currentlyUsedMenuPositions = this.editedItem.position ? this.editedItem.position.split(';') : [];\n        let currentlyUsedMaxLevels = this.editedItem.maxLevels ? this.editedItem.maxLevels.split(';').map(level => parseInt(level, 10)) : []; \n        let maxLevels = {};\n        \n        if (!currentlyUsedMaxLevels && currentlyUsedMenuPositions.length && currentlyUsedMenuPositions[0] !== '') {\n            currentlyUsedMaxLevels = Array.from({length: currentlyUsedMenuPositions.length}, () => -1);\n        }\n\n        if (currentlyUsedMenuPositions.length && currentlyUsedMenuPositions[0] !== '') {\n            for (let i = 0; i < currentlyUsedMenuPositions.length; i++) {\n                let position = currentlyUsedMenuPositions[i];\n\n                if (menus[position]) {\n                    maxLevels[position] = currentlyUsedMaxLevels[i];\n\n                    if (menus[position].maxLevels) {\n                        if (maxLevels[position] === -1 && menus[position].maxLevels !== -1) {\n                            maxLevels[position] = menus[position].maxLevels;\n                        } else if (maxLevels[position] > 0 && menus[position].maxLevels > -1 && maxLevels[position] > menus[position].maxLevels) {\n                            maxLevels[position] = menus[position].maxLevels;\n                        }\n                    }    \n                }\n            }\n        }\n\n        for (let i = 0; i < menuPositions.length; i++) {\n            let position = menuPositions[i];\n            let themeMaxLevel = menus[position].maxLevels;\n\n            if (!themeMaxLevel) {\n                themeMaxLevel = -1;\n            }\n\n            Vue.set(this.configuration, position, {\n                status: currentlyUsedMenuPositions.indexOf(position) > -1,\n                maxLevels: maxLevels[position] ? maxLevels[position] : themeMaxLevel,\n                invalid: false\n            });\n        }\n    },\n    methods: {\n        close () {\n            this.$bus.$emit('hide-menu-position-popup');\n        },\n        getMenuUsingPosition (position) {\n            let foundedMenu = this.menus.filter(menu => menu.position.split(';').indexOf(position) > -1);\n\n            if (foundedMenu[0]) {\n                return foundedMenu[0].name;\n            }\n\n            return '??';\n        },\n        saveChanges () {\n            let itemPositions = [];\n            let itemMaxLevels = [];\n            let positions = Object.keys(this.configuration);\n\n            for (let i = 0; i < positions.length; i++) {\n                let position = positions[i];\n\n                if (this.configuration[position].status) {\n                    itemPositions.push(position);\n                    itemMaxLevels.push(this.configuration[position].maxLevels);\n                }\n            }\n\n            itemPositions = itemPositions.join(';');\n            itemMaxLevels = itemMaxLevels.join(';');\n\n            this.$bus.$emit('menus-manager-save-menu-positions', {\n                index: this.editedItemIndex,\n                position: itemPositions,\n                maxLevels: itemMaxLevels\n            });\n\n            this.close();\n        },\n        validateMaxLevels () {\n            let positions = Object.keys(this.configuration);\n            \n            for (let i = 0; i < positions.length; i++) {\n                let position = positions[i];\n\n                if (this.menusInUse.indexOf(position) === -1) {\n                    let menu = this.availableMenus[position];\n                    \n                    if (\n                        (this.configuration[position].maxLevels).toString() === '' ||\n                        this.configuration[position].maxLevels < -1 ||\n                        this.configuration[position].maxLevels === 0 ||\n                        parseInt(this.configuration[position].maxLevels, 10) === 0 || \n                        (this.configuration[position].maxLevels).toString().indexOf('+') > -1 ||\n                        (this.configuration[position].maxLevels).toString().indexOf('e') > -1 ||\n                        (this.configuration[position].maxLevels).toString().indexOf(',') > -1 ||\n                        (this.configuration[position].maxLevels).toString().indexOf('.') > -1 ||\n                        isNaN(this.configuration[position].maxLevels) ||\n                        (\n                            menu.maxLevels > -1 && \n                            this.configuration[position].maxLevels > menu.maxLevels\n                        )\n                    ) {\n                        Vue.set(this.configuration[position], 'invalid', true);\n                    } else {\n                        Vue.set(this.configuration[position], 'invalid', false);\n                    }\n                }\n            }\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100006;\n}\n\n.popup {\n    padding: 4rem 4rem 1rem 4rem;\n    text-align: left;\n    width: 60rem;\n}\n\n.buttons {\n    display: flex;\n    margin: 0 -4rem -1rem -4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n\n    .button.button-disabled {\n       border-top: 1px solid var(--input-border-color);\n    }\n}\n\n.menu-position-items {\n    padding-bottom: 2rem;\n\n    .menu-position-item {\n        align-items: baseline;\n        display: grid;\n        grid-template-columns: 1fr auto;\n        gap: 0 3rem;\n        margin: 3rem 0;\n        position: relative;\n        \n        & + .menu-position-item {\n            border-top: 1px solid var(--border-light-color);\n            padding-top: 3rem;\n        }\n\n        .menu-position-item-max-levels {\n            align-items: baseline;\n            display: flex;\n            gap: 0 .75rem;\n            text-align: left;\n\n            &-desc,\n            &-error {\n                color: var(--text-light-color);\n                display: block;\n                flex-basis: 100%;\n                margin-top: .5rem;\n            }\n\n            &-error {\n                background-color: var(--popup-bg);\n                bottom: -2rem;\n                color: var(--warning);\n                margin-left: 41px;\n                position: absolute;\n            }\n        }\n\n        .menu-position-item-desc,\n        .menu-position-item-usedby {   \n            color: var(--text-light-color);\n            display: block;\n            flex-basis: 100%;\n            margin-left: 41px;\n        }\n\n        input.is-invalid {\n            border-color: var(--warning);\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Menus.vue",
    "content": "<template>\n    <section :class=\"{ \n        'content': true, \n        'menu': true, \n        'menus-list-view': true, \n        'no-scroll': editorVisible \n    }\">\n        <p-header\n            v-if=\"!showEmptyState\"\n            :title=\"$t('menu.menu')\">\n            <p-button\n                :onClick=\"showAddMenuForm\"\n                slot=\"buttons\"\n                type=\"primary icon\"\n                icon=\"add-site-mono\">\n                {{ $t('menu.addNewMenu') }}\n            </p-button>\n        </p-header>\n\n        <collection\n            v-if=\"!showEmptyState\"\n            :itemsCount=\"4\">\n            <collection-header slot=\"header\">\n                <collection-cell>\n                    <checkbox\n                        value=\"all\"\n                        :checked=\"anyCheckboxIsSelected\"\n                        :onClick=\"toggleAllCheckboxes.bind(this, true)\" />\n                </collection-cell>\n\n                <collection-cell>\n                    {{ $t('ui.name') }}\n                </collection-cell>\n\n                <collection-cell>\n                    {{ $t('menu.assignedMenu') }}\n                </collection-cell>\n\n                <collection-cell\n                    justifyContent=\"center\"\n                    textAlign=\"center\"\n                    min-width=\"80px\">\n                    {{ $t('menu.items') }}\n                </collection-cell>\n\n                <div\n                    v-if=\"anyCheckboxIsSelected\"\n                    class=\"tools\">\n                    <p-button\n                        icon=\"trash\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkDelete\">\n                        {{ $t('ui.delete') }}\n                    </p-button>\n                </div>\n            </collection-header>\n\n            <collection-row\n                v-for=\"(item, index) in items\"\n                slot=\"content\"\n                :key=\"'menu-item-in-menus-list-' + index\">\n                <collection-cell>\n                    <checkbox\n                        :value=\"index\"\n                        :checked=\"isChecked(index)\"\n                        :onClick=\"toggleSelection\" />\n                </collection-cell>\n\n                <collection-cell type=\"titles\">\n                    <h2 class=\"title\">\n                        <a\n                            href=\"#\"\n                            @click.prevent.stop=\"toggleMenu(index)\">\n                            {{ item.name }}\n                        </a>\n                    </h2>\n                </collection-cell>\n\n                <collection-cell\n                    type=\"assignment\">\n                    \n                    <a \n                        href=\"#\"\n                        @click.prevent=\"openMenuPositionPopup(item, index)\"\n                        class=\"menu-assignment-link\">\n                        {{ menuPositions(item.position) }} \n                        <span\n                            class=\"menu-assignment-link-icon\"\n                            name=\"sidebar-arrow\">\n                        </span>                      \n                    </a>\n                </collection-cell>\n\n                <collection-cell\n                    justifyContent=\"center\"\n                    textAlign=\"center\">\n                    <a\n                        @click.prevent.stop=\"toggleMenu(index)\"\n                        href=\"#\">\n                        {{ countMenuItems(item.items) }}\n                    </a>\n                </collection-cell>\n\n                <div\n                    v-if=\"menuIsOpened(index)\"\n                    class=\"item-content\">\n                    <p-button\n                        icon=\"add-site-mono\"\n                        type=\"secondary icon\"\n                        @click.native=\"addMenuItem(index)\">\n                        {{ $t('menu.addMenuItem') }}\n                    </p-button>\n                    <p-button\n                     icon=\"edit\"\n                        type=\"clean icon\"\n                        class=\"menu-edit-btn\"\n                        @click.native=\"editMenuName(item.name, index)\">\n                        {{ $t('menu.editMenuName') }}\n                    </p-button>\n\n                    <div class=\"menu-content\">\n                        <em\n                            v-if=\"!item.items.length\"\n                            class=\"menu-empty-list\">\n                            {{ $t('menu.noMenusMessage') }}\n                        </em>\n\n                        <draggable\n                            v-if=\"item.items.length\"\n                            tag=\"ol\"\n                            group=\"menu-items\"\n                            chosenClass=\"is-chosen\"\n                            ghostClass=\"is-ghost\"\n                            class=\"menu-item-list\"\n                            v-model=\"$store.state.currentSite.menuStructure[index].items\"\n                            v-bind=\"{\n                                animation: 0,\n                                forceFallback: true,\n                                disabled: !!selectedItem\n                            }\"\n                            :key=\"'draggable-menu-items-levle0-' + index\"\n                            @update=\"listItemUpdated($event, index)\"\n                            @add=\"listItemAdded($event, index)\">\n                            <menu-item\n                                v-for=\"(subitem, subindex) in item.items\"\n                                :key=\"'menu-' + subindex + '-' + getUID()\"\n                                :itemData=\"subitem\"\n                                :itemMenuID=\"index\"\n                                :itemOrder=\"subindex\"\n                                :editedID=\"parseInt(editedID, 10)\"\n                                :selectedItem=\"selectedItemMenuID === index ? selectedItem : null\" />\n                        </draggable>\n                    </div>\n                </div>\n            </collection-row>\n        </collection>\n\n        <empty-state\n            v-if=\"showEmptyState\"\n            imageName=\"menus.svg\"\n            imageWidth=\"344\"\n            imageHeight=\"286\"\n            :title=\"$t('menu.noMenusAvailable')\"\n            :description=\"$t('menu.noMenusCreateNewOne')\">\n            <p-button\n                slot=\"button\"\n                icon=\"add-site-mono\"\n                type=\"icon\"\n                :onClick=\"showAddMenuForm\">\n                {{ $t('menu.addNewMenu') }}\n            </p-button>\n        </empty-state>\n\n        <transition>\n            <menu-item-editor v-if=\"editorVisible\" />\n        </transition>\n\n        <menu-position-popup \n            v-if=\"menuPositionPopupVisible\"\n            :editedItem=\"selectedMenuItemToEditPosition\"\n            :editedItemIndex=\"selectedMenuItemToEditIndex\"\n            :menus=\"items\" />\n    </section>\n</template>\n\n<script>\nimport Draggable from 'vuedraggable';\nimport Vue from 'vue';\nimport MenuItem from './MenuItem.vue';\nimport MenuItemEditor from './MenuItemEditor.vue';\nimport MenuPositionPopup from './MenuPositionPopup.vue';\nimport CollectionCheckboxes from './mixins/CollectionCheckboxes.js';\n\nexport default {\n    name: 'menus',\n    mixins: [\n        CollectionCheckboxes\n    ],\n    components: {\n        'draggable': Draggable,\n        'menu-item': MenuItem,\n        'menu-item-editor': MenuItemEditor,\n        'menu-position-popup': MenuPositionPopup\n    },\n    data () {\n        return {\n            editedID: false,\n            editorVisible: false,\n            filterValue: '',\n            selectedItems: [],\n            openedItems: [],\n            openedEditForms: [],\n            selectedItem: null,\n            selectedItemMenuID: null,\n            selectedMenuItemToEditPosition: false,\n            selectedMenuItemToEditIndex: false,\n            menuPositionPopupVisible: false\n        };\n    },\n    computed: {\n        items () {\n            return this.$store.state.currentSite.menuStructure;\n        },\n        showEmptyState: function() {\n            return !this.items.length;\n        }\n    },\n    mounted () {\n        this.$bus.$on('hide-menu-item-editor', () => {\n            this.editedID = false;\n            this.editorVisible = false;\n        });\n\n        this.$bus.$on('show-menu-item-editor-from-submenu', itemID => {\n            this.editedID = itemID;\n            this.editorVisible = true;\n        });\n\n        this.$bus.$on('save-new-menu-structure', () => {\n            this.saveNewMenuStructure();\n        });\n\n        this.$bus.$on('menus-manager-selected-item', itemID => {\n            this.selectMenuItem(itemID);\n        });\n\n        this.$bus.$on('menus-manager-unselect-item', () => {\n            this.unselectMenuItem();\n        });\n\n        this.$bus.$on('menus-manager-move-item', config => {\n            this.moveMenuItem(config);\n        });\n\n        this.$bus.$on('hide-menu-position-popup', () => {\n            this.menuPositionPopupVisible = false;\n        });\n\n        this.$bus.$on('menus-manager-save-menu-positions', config => {\n            this.changeMenu(config.index, config.position, config.maxLevels);\n        });\n    },\n    methods: {\n        toggleMenu (index) {\n            if (this.openedItems.indexOf(index) === -1) {\n                this.openedItems.push(index);\n            } else {\n                let position = this.openedItems.indexOf(index);\n                this.openedItems.splice(position, 1);\n            }\n        },\n        showAddMenuForm () {\n            this.$bus.$emit('confirm-display', {\n                hasInput: true,\n                message: this.$t('menu.provideNameForNewMenu'),\n                okClick: (result) => this.addNewMenu(result),\n                okLabel: this.$t('menu.createNewMenu')\n            });\n        },\n        addNewMenu (newMenuName) {\n            if(newMenuName !== '') {\n                if(this.menuNameIsUnique(newMenuName)) {\n                    this.$store.commit('addNewMenu', newMenuName);\n                    this.saveNewMenuStructure();\n\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('menu.newMenuCreated'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('menu.menuNameExistsErrorMessage'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n                }\n            } else {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('menu.menuNameCannotBeEmptyCreateNewMenuErrorMessage'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n            }\n        },\n        menuNameIsUnique(name, oldName = false) {\n            for(let i = 0; i < this.$store.state.currentSite.menuStructure.length; i++) {\n                if(oldName) {\n                    if(name === this.$store.state.currentSite.menuStructure[i].name && name !== oldName) {\n                        return false;\n                    }\n                } else if(name === this.$store.state.currentSite.menuStructure[i].name) {\n                    return false;\n                }\n            }\n\n            return true;\n        },\n        changeMenu (itemIndex, itemPosition, itemMaxLevels) {\n            this.$store.commit('setMenuPosition', {\n                index: itemIndex,\n                position: itemPosition,\n                maxLevels: itemMaxLevels\n            });\n\n            this.saveNewMenuStructure();\n        },\n        menuIsOpened (index) {\n            return this.openedItems.indexOf(index) > -1;\n        },\n        addMenuItem (index) {\n            this.editorVisible = true;\n\n            setTimeout(() => {\n                this.$bus.$emit('show-menu-item-editor', {\n                    menuID: index\n                });\n            }, 0);\n        },\n        editMenuName (oldName, index) {\n            this.$bus.$emit('confirm-display', {\n                hasInput: true,\n                message: this.$t('menu.provideNewNameForMenu'),\n                okClick: (result) => this.changeMenuName(result, index),\n                okLabel: this.$t('menu.editMenuName'),\n                defaultText: oldName\n            });\n        },\n        changeMenuName (newMenuName, index) {\n            if(newMenuName !== '') {\n                if(this.menuNameIsUnique(newMenuName)) {\n                    this.$store.commit('editMenuName', {\n                        newName: newMenuName,\n                        index: index\n                    });\n                    this.saveNewMenuStructure();\n\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('menu.menuNameHasBeenEdited'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('menu.menuNameInUseErrorMessage'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n                }\n            } else {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('menu.menuNameCannotBeEmptyErrorMessage'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n            }\n        },\n        bulkDelete () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('menu.menusRemoveMessage'),\n                isDanger: true,\n                okClick: this.deleteSelected\n            });\n        },\n        deleteSelected () {\n            let itemsToRemove = this.getSelectedItems(false);\n\n            this.$store.commit('deleteMenuByIDs', itemsToRemove);\n            this.saveNewMenuStructure();\n            this.selectedItems = [];\n\n            this.$bus.$emit('message-display', {\n                message: this.$t('menu.menusRemoveSuccessMessage'),\n                type: 'success',\n                lifeTime: 3\n            });\n        },\n        saveNewMenuStructure () {\n            mainProcessAPI.send('app-menu-update', {\n                siteName: this.$store.state.currentSite.config.name,\n                menuStructure: this.$store.state.currentSite.menuStructure\n            });\n        },\n        countMenuItems(items) {\n            let result = items.length;\n\n            for(let i = 0; i < items.length; i++) {\n                if(items[i].items.length) {\n                    result += this.countMenuItems(items[i].items);\n                }\n            }\n\n            return result;\n        },\n        getUID () {\n            return Math.random().toString(36).substr(2, 9);\n        },\n        listItemUpdated (e, menuID) {\n            this.saveNewMenuStructure();\n        },\n        listItemAdded (e) {\n            this.saveNewMenuStructure();\n        },\n        selectMenuItem (data) {\n            this.selectedItem = data.id;\n            this.selectedItemMenuID = data.menuID;\n        },\n        unselectMenuItem () {\n            this.selectedItem = null;\n            this.selectedItemMenuID = null;\n        },\n        moveMenuItem (config) {\n            if (!this.selectedItem) {\n                return;\n            }\n\n            this.$store.commit('moveMenuItem', {\n                position: config.position,\n                menuID: config.menuID,\n                targetID: this.selectedItem,\n                destinationID: config.destinationID\n            });\n\n            this.saveNewMenuStructure();\n            this.unselectMenuItem();\n        },\n        menuPositions (positions) {\n            let menus = JSON.parse(JSON.stringify(this.$store.state.currentSite.themeSettings.menus));\n            let output = [];\n            positions = positions.split(';');\n\n            if (positions[0] === '') {\n                return this.$t('menu.unassigned');\n            }\n\n            for (let i = 0; i < positions.length; i++) {\n                let position = positions[i];\n\n                if (menus[position]) {\n                    if (typeof menus[position] === 'string') {\n                        output.push(menus[position]);\n                    } else if (menus[position].name) {\n                        output.push(menus[position].name);\n                    }\n                }\n            }\n\n            if (!output.length) {\n                return this.$t('menu.unassigned');\n            }\n\n            return output.join(', ');\n        },\n        openMenuPositionPopup (item, index) {\n            this.selectedMenuItemToEditPosition = item;\n            this.selectedMenuItemToEditIndex = index;\n\n            Vue.nextTick(() => {\n                this.menuPositionPopupVisible = true;\n            });\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('hide-menu-item-editor');\n        this.$bus.$off('show-menu-item-editor-from-submenu');\n        this.$bus.$off('save-new-menu-structure');\n        this.$bus.$off('menus-manager-selected-item');\n        this.$bus.$off('menus-manager-move-item');\n        this.$bus.$off('hide-menu-position-popup');\n        this.$bus.$off('menus-manager-save-menu-positions');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.menu {\n    overflow: auto;\n    overflow-x: hidden!important;\n\n    &.no-scroll {\n        overflow: hidden;\n    }\n\n    .item-content {\n        border-bottom: 1px solid var(--border-light-color);\n        grid-column-start: 1;\n        grid-column-end: 5;\n        overflow: hidden;\n        padding: 3rem 0 3rem 3rem;\n        user-select: none;\n        width: 100%;\n    }\n\n    .col.assignment {\n        display: flex;\n        padding-top: 0;\n        padding-bottom: 0;\n    }\n\n    &-assignment-link {\n        padding-right: 4rem;\n        position: relative;\n        width: 100%;\n\n        &-icon {\n            border-color: var(--button-gray-bg) transparent transparent;\n            border-style: solid;\n            border-width: 5px;\n            opacity: 1;\n            cursor: pointer;\n            height: 5px;\n            left: auto;\n            line-height: 1.1;\n            padding: 0;\n            position: absolute;\n            right: 0;\n            width: 5px;\n            text-align: center;\n            transition: var(--transition);\n            top: calc(50% - 2px);\n        }\n    }\n\n    &-content {\n        margin: 2.5rem 0 0 0;\n    }\n\n    &-item {\n\n        &-list {\n            list-style-type: none;\n            margin: .25 * $spacing 0;\n            padding: 0;\n        }\n    }\n\n    &-edit-btn {\n         color: var(--link-primary-color) !important;\n\n         &:active,\n         &:focus,\n         &:hover {\n            color: var(--link-primary-color-hover) !important;\n         }\n    }\n\n    &-empty-list {\n        display: block;\n        text-align: center;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Message.vue",
    "content": "<template>\n    <div class=\"messages\">\n        <transition>\n            <div v-if=\"message\" class=\"message\">\n                <div class=\"message-content\" @click=\"onClick\" :data-type=\"type\">\n                    <div class=\"icon-wrapper\">\n                        <icon size=\"m\" :name=\"type\" />\n                    </div>\n                    <p>{{ message }}</p>\n                </div>\n            </div>\n        </transition>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'message',\n    data() {\n        return {\n            clickCallback: false,\n            lifeTime: 5,\n            message: '',\n            timer: false,\n            type: 'info'\n        };\n    },\n    mounted() {\n        this.$bus.$on('message-display', this.showMessage);\n    },\n    methods: {\n        hideMessage() {\n            clearTimeout(this.timer);\n            this.message = '';\n        },\n        showMessage(messageConfig) {\n            this.message = messageConfig.message;\n            this.type = messageConfig.type;\n\n            if (messageConfig.clickCallback) {\n                this.clickCallback = messageConfig.clickCallback;\n            }\n\n            if (typeof messageConfig.lifeTime !== 'undefined') {\n                this.lifeTime = messageConfig.lifeTime;\n            }\n\n            if (this.lifeTime > 0) {\n                this.timer = setTimeout(() => {\n                    this.hideMessage();\n                }, this.lifeTime * 1100);\n            }\n        },\n        onClick() {\n            this.hideMessage();\n            if (this.clickCallback) {\n                this.clickCallback();\n            }\n        }\n    },\n    beforeDestroy() {\n        this.$bus.$off('message-display');\n    }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.messages {\n    background: transparent;\n    bottom: 0;\n    left: 50%;\n    max-width: 48rem;\n    pointer-events: none;\n    position: fixed;\n    top: 4.2rem;\n    transform: translate(-50%, 0);\n    user-select: none;\n    width: 100%;\n    z-index: 100003;\n\n    .message {\n        animation: messages-animation .24s cubic-bezier(.17, .67, .6, 1.34) forwards;\n        display: flex;\n        justify-content: center;\n        margin: 0;\n        opacity: 1;\n        padding: 0;\n        position: relative;\n        width: 100%;\n\n        @at-root {\n            @keyframes messages-animation {\n                from {\n                    opacity: 0;\n                    transform: scale(0.6);\n                }\n\n                to {\n                    opacity: 1;\n                    transform: scale(1);\n                }\n            }\n        }\n\n        &-content {\n            align-items: center;\n            background: var(--popup-bg);\n            border-radius: 12px;\n            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);\n            color: var(--text-primary-color);\n            display: flex;\n            font-size: 1.4rem;\n            gap: 2rem;\n            justify-content: center;\n            line-height: 1.4;\n            margin: 0;\n            max-width: 48rem;\n            padding: 2rem;\n            pointer-events: all;\n            position: absolute;\n\n            &[data-type=\"info\"] .icon-wrapper {\n                background: var(--color-primary);\n            }\n\n            &[data-type=\"success\"] .icon-wrapper {\n                background: var(--success);\n            }\n\n            &[data-type=\"warning\"] .icon-wrapper {\n                background: var(--warning);\n            }\n\n            .icon-wrapper {\n                align-items: center;\n                border-radius: 10px;\n                display: flex;\n                flex-shrink: 0;\n                justify-content: center;\n                margin: -.5rem;\n                padding: 1.2rem;\n\n                svg {\n                    fill: var(--white);\n                }\n            }\n\n            p {\n                margin: 0;\n            }\n\n            a {\n                color: var(--white);\n                cursor: pointer;\n                text-decoration: underline;\n\n                &:active,\n                &:focus,\n                &:hover {\n                    opacity: .75;\n                }\n            }\n        }\n\n        &-icon {\n            display: block;\n            margin: 0;\n        }\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .messages {\n        top: 4.4rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/NotificationsCenter.vue",
    "content": "<template>\n    <section :class=\"{ \n        'content': true, \n        'notifications': true,\n        'notifications-list-view': true \n    }\">\n        <div class=\"notifications-wrapper\">\n            <p-header :title=\"$t('notifications.notifications')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    type=\"clean back\"\n                    slot=\"buttons\">\n                    {{ $t('ui.goBack') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"notificationsStatus === 'accepted'\"\n                    :onClick=\"checkUpdates\"\n                    slot=\"buttons\"\n                    type=\"primary icon\"\n                    icon=\"refresh\"\n                    :disabled=\"receivingNotificationsInProgress\">\n                    {{ $t('notifications.checkUpdates') }}\n                </p-button>\n            </p-header>\n\n            <template v-if=\"notificationsStatus === 'accepted'\">\n                <fields-group :title=\"$t('notifications.news')\"\n                    v-if=\"newsToDisplay.length > 0\" \n                    class=\"notification\">\n                    <ul class=\"notification-list\">\n                        <li \n                            v-for=\"(news, index) in newsToDisplay\" \n                            :key=\"index\" \n                            class=\"notification-item is-news\"\n                            :data-type=\"news.type\">\n                            <div class=\"notification-item-content\">\n                                <div class=\"notification-icon-wrapper\">\n                                    <icon\n                                        customWidth=\"44\"\n                                        customHeight=\"44\"\n                                        :name=\"icons[news.type]\"\n                                        class=\"notification-item-icon\" />\n                                </div>\n\n                                <div class=\"notification-item-details\">\n                                    <span class=\"notification-title\">\n                                        {{ news.title }}\n                                    </span>\n                                    <span class=\"notification-description\">\n                                        {{ news.text }}\n                                    </span>\n                                </div>\n                            </div>\n\n                            <div class=\"notification-item-actions\">\n                                <p-button \n                                    class=\"button-secondary\"\n                                    v-if=\"news.link\"\n                                    :onClick=\"() => openLink(news.link)\">\n                                    {{ $t('notifications.readMore') }}\n                                </p-button>\n                            </div>\n                        </li>\n                    </ul>\n\n                    <a\n                        href=\"#\"\n                        class=\"notification-action\"\n                        @click.prevent=\"markAsRead('news')\">\n                        {{ $t('notifications.markAsRead') }}\n                    </a>\n                </fields-group>\n\n                <fields-group :title=\"$t('notifications.publiiUpdateAvailable')\"\n                    v-if=\"hasPubliiUpdate\"\n                    class=\"notification\">\n                    <div :class=\"{ \n                        'notification-item':true,\n                        'is-publii-notification': true,\n                        'is-read': isRead('PUBLII-' + notifications.publii.version + '-' + notifications.publii.build)\n                    }\">\n                        <div class=\"notification-item-content\">\n                            <img \n                                src=\"./../assets/svg/publii-app-icon-512.svg\" \n                                alt=\"Publii Logo\" \n                                class=\"notification-item-icon\"\n                                height=\"52\"\n                                width=\"52\" />  \n                                \n                            <div class=\"notification-item-details\">\n                               <div class=\"notification-item-name\" :data-new-badge=\"$t('notifications.badgeNew')\">\n                                  <span>Publii</span>\n                                </div>\n                                <div class=\"notification-item-versions\">\n                                    <span class=\"notification-item-version\">\n                                        {{ $t('notifications.latestVersion') }}: v.{{ notifications.publii.version }} (build: {{ notifications.publii.build }})\n                                    </span>\n\n                                    <span class=\"notification-item-current-version\">\n                                        {{ $t('notifications.currentVersion') }}: v.{{ $store.state.app.versionInfo.version }} (build: {{ $store.state.app.versionInfo.build }})\n                                    </span>\n\n                                    <p-button\n                                        v-if=\"notifications.publii.links.releaseNotes\"\n                                        class=\"button-clean notification-item-version-details\"\n                                        :onClick=\"() => openLink(notifications.publii.links.releaseNotes)\">\n                                        {{ $t('notifications.viewDetails') }}\n                                    </p-button>\n                                </div>\n                                <div \n                                    v-if=\"notifications.publii.description\"\n                                    class=\"notification-item-desc\">\n                                    <p v-html=\"notifications.publii.description\"></p>\n                                </div>\n                            </div>\n                        </div>\n\n                        <div class=\"notification-item-actions\">\n                            <p-button \n                                class=\"button-secondary\"\n                                :onClick=\"() => openLink(notifications.publii.links.download)\" \n                                type=\"icon\"\n                                icon=\"download\">\n                                {{ $t('notifications.downloadUpdate') }}\n                            </p-button>\n                        </div>\n                    </div>\n\n                    <a \n                        href=\"#\"\n                         class=\"notification-action\"\n                         :class=\"{ 'is-disabled': !unreadPubliiNotification }\"\n                        @click.prevent=\"markAsRead('publii')\">\n                        {{ $t('notifications.markAsRead') }}\n                    </a>\n                </fields-group>\n\n                <fields-group :title=\"$t('notifications.themeUpdatesAvailable')\"\n                    v-if=\"themeUpdates.length > 0\"\n                    class=\"notification\">\n                    <ul class=\"notification-list\">\n                        <li \n                            v-for=\"(theme, index) in themeUpdates\" \n                            :key=\"index\" \n                            :class=\"{\n                                'notification-item': true,\n                                'is-theme-update': true,\n                                'is-read': isRead('THEME-' + theme.directory + '-' + theme.version)\n                            }\">\n                            <div class=\"notification-item-content\">\n                                <img \n                                    :src=\"$store.state.themesPath + '/' + theme.directory + '/thumbnail.png'\" \n                                    alt=\"\"\n                                    class=\"notification-item-icon\"\n                                    height=\"52\"\n                                    width=\"52\" />\n\n                                <div class=\"notification-item-details\">\n                                    <div class=\"notification-item-name\" :data-new-badge=\"$t('notifications.badgeNew')\">\n                                       <span>{{ theme.name }}</span>\n                                    </div>\n                                    <div class=\"notification-item-versions\">\n                                        <span class=\"notification-item-version\">\n                                            {{ $t('notifications.latestVersion') }}: v.{{ theme.version }}\n                                        </span>\n\n                                        <span class=\"notification-item-current-version\">\n                                            {{ $t('notifications.currentVersion') }}: v.{{ theme.currentVersion }}\n                                        </span>\n\n                                        <p-button\n                                            v-if=\"theme.links.releaseNotes\"\n                                            class=\"button-clean notification-item-version-details\"\n                                            :onClick=\"() => openLink(theme.links.releaseNotes)\">\n                                            {{ $t('notifications.viewDetails') }}\n                                        </p-button>\n                                    </div>\n                                    <div \n                                        v-if=\"theme.description\"\n                                        class=\"notification-item-desc\">\n                                        <p v-html=\"theme.description\"></p>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"notification-item-actions\">\n                                <p-button\n                                    class=\"button-secondary\"\n                                    :onClick=\"() => openLink(theme.links.download)\"\n                                    type=\"icon\"\n                                    icon=\"download\">\n                                    {{ $t('notifications.downloadUpdate') }}\n                                </p-button>\n                            </div>\n                        </li>\n                    </ul>\n\n                    <a \n                        href=\"#\"\n                        class=\"notification-action\" \n                        :class=\"{ 'is-disabled': !unreadThemeUpdates }\"\n                        @click.prevent=\"markAsRead('themes')\">\n                        {{ $t('notifications.markAsRead') }}\n                    </a>\n                </fields-group>\n\n                <fields-group \n                    :title=\"$t('notifications.pluginUpdatesAvailable')\"\n                    v-if=\"pluginUpdates.length > 0\" \n                    class=\"notification\">\n                    <ul class=\"notification-list\">\n                        <li \n                            v-for=\"(plugin, index) in pluginUpdates\" \n                            :key=\"index\" \n                            :class=\"{\n                                'notification-item': true,\n                                'is-plugin-update': true,\n                                'is-read': isRead('PLUGIN-' + plugin.directory + '-' + plugin.version)\n                            }\">\n                            <div class=\"notification-item-content\">\n                                <img \n                                    :src=\"$store.state.pluginsPath + '/' + plugin.directory + '/thumbnail.svg'\" \n                                    alt=\"\"\n                                    class=\"notification-item-icon\"\n                                    height=\"52\"\n                                    width=\"52\" />\n\n                                <div class=\"notification-item-details\">\n                                    <div class=\"notification-item-name\" :data-new-badge=\"$t('notifications.badgeNew')\">\n                                       <span>{{ plugin.name }}</span>\n                                    </div>\n                                    <div class=\"notification-item-versions\">\n                                        <span class=\"notification-item-version\">\n                                            {{ $t('notifications.latestVersion') }}: v.{{ plugin.version }}\n                                        </span>\n\n                                        <span class=\"notification-item-current-version\">\n                                            {{ $t('notifications.currentVersion') }}: v.{{ plugin.currentVersion }}\n                                        </span>\n\n                                        <p-button\n                                            v-if=\"plugin.links.releaseNotes\"\n                                            class=\"button-clean notification-item-version-details\"\n                                            :onClick=\"() => openLink(plugin.links.releaseNotes)\">\n                                            {{ $t('notifications.viewDetails') }}\n                                        </p-button>\n                                    </div>\n                                    <div \n                                        v-if=\"plugin.description\"\n                                        class=\"notification-item-desc\">\n                                        <p v-html=\"plugin.description\"></p>\n                                    </div>\n                                </div>\n                            </div>\n\n                            <div class=\"notification-item-actions\">\n                                <p-button\n                                    class=\"button-secondary\"\n                                    :onClick=\"() =>openLink(plugin.links.download)\"\n                                    type=\"icon\"\n                                    icon=\"download\">\n                                    {{ $t('notifications.downloadUpdate') }}\n                                </p-button>\n                            </div>\n                        </li>\n                    </ul>\n\n                    <a \n                        href=\"#\"\n                        class=\"notification-action\" \n                        :class=\"{ 'is-disabled': !unreadPluginUpdates }\"\n                        @click.prevent=\"markAsRead('plugins')\">\n                        {{ $t('notifications.markAsRead') }}\n                    </a>\n                </fields-group>\n            </template>\n\n            <empty-state\n                v-if=\"notificationsStatus !== 'accepted'\"\n                imageName=\"notifications-center.svg\"\n                imageWidth=\"344\"\n                imageHeight=\"286\"\n                :title=\"$t('notifications.consentStateTitle')\"\n                :description=\"$t('notifications.consentStateDescription')\">\n                <p-button\n                    slot=\"button\"\n                    :onClick=\"giveConsent\">\n                    {{ $t('notifications.giveConsent') }}\n                </p-button>\n\n                <p-button\n                    slot=\"button\"\n                    type=\"outline\"\n                    :onClick=\"rejectConsent\">\n                    {{ $t('notifications.rejectConsent') }}\n                </p-button>\n            </empty-state>\n\n            <empty-state\n                v-if=\"notificationsStatus === 'accepted' && newsToDisplay.length === 0 && pluginUpdates.length === 0 && themeUpdates.length === 0 && !hasPubliiUpdate\"\n                imageName=\"notifications-center.svg\"\n                imageWidth=\"344\"\n                imageHeight=\"286\"\n                :title=\"$t('notifications.noUpdatesTitle')\"\n                :description=\"$t('notifications.noUpdatesDescription')\">\n            </empty-state>\n\n            <div \n                v-if=\"notificationsStatus === 'accepted'\"\n                class=\"notifications-consent\">\n                <span>\n                    {{ $t('notifications.consentInfo') }}\n                    <a \n                        href=\"#\"\n                        @click.prevent=\"rejectConsentConfirm\">\n                        {{ $t('notifications.consentReject') }}\n                    </a>\n                </span>\n            </div>\n        </div>\n    </section>\n</template>\n\n<script>\nimport VersionComparator from '../helpers/version-comparator';\nimport { mapGetters } from 'vuex';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\n\nexport default {\n    name: 'notifications-center',\n    mixins: [\n        GoToLastOpenedWebsite\n    ],\n    data () {\n        return {\n            icons: {\n                ok: 'success',\n                danger: 'warning',\n                warning: 'warning',\n                info: 'info'\n            },\n            receivingNotificationsInProgress: false\n        };\n    },\n    computed: {\n        ...mapGetters([\n            'notificationsCount',\n            'notifications',\n            'notificationsStatus'\n        ]),\n        hasPubliiUpdate () {\n            let currentBuild = this.$store.state.app.versionInfo.build;\n            \n            if (this.notifications.publii && parseInt(this.notifications.publii.build, 10) > parseInt(currentBuild, 10)) {\n                return true;\n            }\n\n            return false;\n        },\n        newsToDisplay () {\n            let newsToDisplay = [];\n            let currentDate = new Date().getTime();\n\n            for (let notification of this.notifications.news || []) {\n                if (\n                    notification.id && \n                    this.readedNotifications.indexOf(notification.id) === -1 &&\n                    currentDate >= new Date(notification.validFrom).getTime() &&\n                    currentDate <= new Date(notification.validTo).getTime()\n                ) {\n                    newsToDisplay.push(notification);\n                }\n            }\n\n            return newsToDisplay;\n        },\n        themeUpdates () {\n            let themeUpdates = [];\n            let installedThemes = this.$store.state.themes;\n            let availableThemes = this.notifications.themes || {};\n\n            for (let theme of installedThemes) {\n                if (availableThemes[theme.directory]) {\n                    let result = VersionComparator(availableThemes[theme.directory].version, theme.version);\n                    \n                    if (result === 1) {\n                        let themeData = {\n                            ...availableThemes[theme.directory],\n                            currentVersion: theme.version,\n                            directory: theme.directory\n                        };\n\n                        themeUpdates.push(themeData);\n                    }\n                }\n            }\n\n            themeUpdates.sort((themeA, themeB) => {\n                let isUnread = (theme) => this.readedNotifications.indexOf('THEME-' + theme.directory + '-' + theme.version) === -1;\n                let unreadA = isUnread(themeA);\n                let unreadB = isUnread(themeB);\n\n                if (unreadA !== unreadB) {\n                    return unreadA ? -1 : 1;\n                }\n\n                return themeA.directory.localeCompare(themeB.directory);\n            });\n\n            return themeUpdates\n        },\n        pluginUpdates () {\n            let pluginUpdates = [];\n            let installedPlugins = this.$store.state.plugins;\n            let availablePlugins = this.notifications.plugins || {};\n\n            for (let plugin of installedPlugins) {    \n                if (availablePlugins[plugin.directory]) {\n                    let result = VersionComparator(availablePlugins[plugin.directory].version, plugin.version);\n                    \n                    if (result === 1) {\n                        let pluginData = {\n                            ...availablePlugins[plugin.directory],\n                            currentVersion: plugin.version,\n                            directory: plugin.directory\n                        };\n\n                        pluginUpdates.push(pluginData);\n                    }\n                }\n            }\n\n            pluginUpdates.sort((pluginA, pluginB) => {\n                let isUnread = (plugin) => this.readedNotifications.indexOf('PLUGIN-' + plugin.directory + '-' + plugin.version) === -1;\n                let unreadA = isUnread(pluginA);\n                let unreadB = isUnread(pluginB);\n\n                if (unreadA !== unreadB) {\n                    return unreadA ? -1 : 1;\n                }\n\n                return pluginA.directory.localeCompare(pluginB.directory);\n            });\n\n            return pluginUpdates;\n        },\n        unreadPluginUpdates () {\n            for (let update of this.pluginUpdates) {\n                if (this.readedNotifications.indexOf('PLUGIN-' + update.directory + '-' + update.version) === -1) {\n                    return true;\n                }\n            }\n\n            return false;\n        },\n        unreadThemeUpdates () {\n            for (let update of this.themeUpdates) {\n                if (this.readedNotifications.indexOf('THEME-' + update.directory + '-' + update.version) === -1) {\n                    return true;\n                }\n            }\n\n            return false;\n        },\n        unreadPubliiNotification () {\n            return this.readedNotifications.indexOf('PUBLII-' + this.notifications.publii.version + '-' + this.notifications.publii.build) === -1;\n        },\n        readedNotifications () {\n            return this.$store.state.app.notificationsReadStatus.split(';');\n        }\n    },\n    mounted () {\n        this.$bus.$on('app-receiving-notifications', this.receivingNotifications);\n        this.$bus.$on('app-received-notifications', this.receivedNotifications);\n    },\n    methods: {\n        checkUpdates () {\n            this.$bus.$emit('app-get-forced-notifications');\n        },\n        async giveConsent () {\n            this.$store.commit('setAppNotificationsStatus', 'accepted');\n            await mainProcessAPI.send('app-set-notifications-center-state', 'accepted');\n            this.$bus.$emit('app-get-forced-notifications');\n        },\n        rejectConsentConfirm () {\n            this.$bus.$emit('confirm-display', {\n                hasInput: false,\n                message: this.$t('notifications.rejectConsentConfirm'),\n                okClick: this.rejectConsent,\n                okLabel: this.$t('ui.iUnderstand'),\n                cancelLabel: this.$t('ui.cancel')\n            });\n        },\n        async rejectConsent () {\n            this.$store.commit('setAppNotificationsStatus', 'rejected');\n            await mainProcessAPI.send('app-set-notifications-center-state', 'rejected');\n            this.goBack();\n        },\n        receivingNotifications () {\n            this.receivingNotificationsInProgress = true;\n        },\n        async receivedNotifications () {\n            this.receivingNotificationsInProgress = false;\n        },\n        openLink (url) {\n            mainProcessAPI.shellOpenExternal(url);\n        },\n        markAsRead (typeToMark) {\n            let notificationsReadStatus = this.$store.state.app.notificationsReadStatus;\n            notificationsReadStatus = notificationsReadStatus.split(';');\n\n            if (typeToMark === 'publii') {\n                let id = 'PUBLII-' + this.notifications.publii.version + '-' + this.notifications.publii.build;\n\n                if (notificationsReadStatus.indexOf(id) === -1) {\n                    notificationsReadStatus.push(id);\n                }\n            } else if (typeToMark === 'news') {\n                for (let news of this.newsToDisplay) {\n                    if (notificationsReadStatus.indexOf(news.id) === -1) {\n                        notificationsReadStatus.push(news.id);\n                    }\n                }\n            } else if (typeToMark === 'plugins') {\n                for (let plugin of this.pluginUpdates) {\n                    let id = 'PLUGIN-' + plugin.directory + '-' + plugin.version;\n\n                    if (notificationsReadStatus.indexOf(id) === -1) {\n                        notificationsReadStatus.push(id);\n                    }\n                }\n            } else if (typeToMark === 'themes') {\n                for (let theme of this.themeUpdates) {\n                    let id = 'THEME-' + theme.directory + '-' + theme.version;\n                    \n                    if (notificationsReadStatus.indexOf(id) === -1) {\n                        notificationsReadStatus.push(id);\n                    }\n                }\n            }\n\n            notificationsReadStatus = notificationsReadStatus.join(';').replace(/[^a-z0-9\\-_;\\.]/gmi, '');\n            this.$store.commit('setNotificationsReadStatus', notificationsReadStatus);\n            localStorage.setItem('publii-notifications-readed', notificationsReadStatus);\n            this.$bus.$emit('app-update-notifications-counters');\n        },\n        isRead (notificationID) {\n            return this.readedNotifications.indexOf(notificationID) > -1;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('app-receiving-notifications', this.receivingNotifications);\n        this.$bus.$off('app-received-notifications', this.receivedNotifications);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n\n.notifications {\n    padding: 3rem 0 4rem;\n    width: 100%;\n\n    &-wrapper {\n        margin: 0 auto;\n        max-width: $wrapper;\n        min-height: calc(100vh - 8rem - var(--topbar-height));\n        position: relative;\n    }\n\n    &-version {\n        margin: -2.5rem 0 4rem;\n    }\n\n    .notification {\n        .notification-title {\n            font-size: 1.5rem;\n            font-weight: bold;\n            margin-bottom: 0.25rem;\n            width: 100%;\n        }\n\n        .notification-action {\n            color: var(--text-light-color);\n            font-size: 1.1rem;\n            position: absolute;\n            right: 3rem;\n            top: 3.2rem;\n            text-transform: uppercase;\n\n            &:hover {\n                color: var(--link-primary-color-hover);\n            }\n\n            &.is-disabled {\n                pointer-events: none;\n                opacity: 0.5;\n            }\n        }\n\n        .notification-item {\n            display: flex;\n            gap: 2rem;\n\n            & + .notification-item {\n                border-top: 1px solid var(--border-light-color);\n                margin-top: 1.6rem;\n                padding-top: 1.6rem;\n            }\n\n            &:not(.is-read) {\n                .notification-item-name {\n                    &::after {\n                        content: attr(data-new-badge); \n                        display: inline-flex;\n                        margin-left: .75rem;\n                        padding: 1px 5px;\n                        border-radius: 4px;\n                        background: var(--success);\n                        color: var(--white);\n                        font-size: 1rem;\n                        font-weight: var(--font-weight-bold);\n                        position: relative;\n                        top: -8px;\n                        text-transform: uppercase;\n                    }\n                }\n            }\n        }\n\n        .notification-list {\n            list-style-type: none;\n            padding: 0;\n        }\n\n        .notification-item-content {\n            align-items: center;\n            display: flex;\n            flex: 1;\n            gap: 2rem;\n            width: calc(100% - 220px);\n        }\n\n        .notification-item-details {\n            display: flex;\n            flex-direction: column;\n            width: 100%;\n        }\n\n        .notification-item-name {\n            font-size: 1.5rem;\n            font-weight: var(--font-weight-bold);\n            margin-bottom: 0.25rem;\n            width: 100%;\n        }\n        .notification-item-versions {\n            display: flex;\n            align-items: center;\n            gap: 1rem;\n\n            & > * + *::before {\n                color: var(--input-border-color);\n                content: \"|\";\n                display: inline-block;\n                margin-right: 0.5rem;\n            }\n        }\n\n        .notification-item-current-version {\n            color: var(--text-light-color);\n        }\n\n        .notification-item-version-details {\n             height: auto;\n            line-height: inherit;\n            padding: 0;\n        }\n\n        .notification-description {\n            margin: 0;\n        }\n\n        .notification-item-desc {\n\n            p {\n                color: var(--text-lightest-color);\n                margin: 0;\n            }\n        }\n\n        .notification-item-icon {\n            border-radius: 10px;\n        }\n\n        .notification-icon-wrapper {\n            align-items: center;\n            border-radius: 10px;\n            display: flex;\n            flex-shrink: 0;\n            height: 52px;\n            justify-content: center;\n            padding: 1.2rem;\n            width: 52px;\n\n            svg {\n                fill: var(--white);\n            }\n        }\n\n        [data-type=\"info\"] .notification-icon-wrapper {\n            background: var(--color-primary);\n        }\n\n        [data-type=\"warning\"] .notification-icon-wrapper {\n            background: var(--warning);\n        }\n\n        .notification-item-actions {\n            display: flex;\n            flex-direction: column;\n            justify-content: center;\n            max-width: 200px;\n\n            .button {\n                text-align: center;\n\n                & + .button {\n                    margin-top: 0.75rem;\n                    margin-left: 0;\n                }\n            }\n        }\n    }\n\n    &-consent {\n        color: var(--text-lightest-color);\n        font-size: 13px;\n        margin: 4rem auto;\n        max-width: 50%;\n        position: sticky;\n        text-align: center;\n        top: 100%;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Pages.vue",
    "content": "<template>\n    <section :class=\"{ \n        'content': true, \n        'hierarchy-mode-enabled': hierarchyMode \n    }\">\n        <p-header\n            v-if=\"hasPages\"\n            :title=\"$t('ui.pages')\">\n            <header-search\n                slot=\"search\"\n                ref=\"search\"\n                :placeholder=\"$t('page.filterOrSearchPages')\"\n                onChangeEventName=\"pages-filter-value-changed\" />\n\n            <btn-dropdown\n                slot=\"buttons\"\n                buttonColor=\"green\"\n                localStorageKey=\"publii-current-page-editor\"\n                :previewIcon=\"true\"\n                :items=\"dropdownItems\"\n                defaultValue=\"tinymce\" />\n        </p-header>\n\n        <ul\n            v-if=\"dataLoaded && hasPages\"\n            class=\"filters\">\n            <li\n                :class=\"filterCssClasses('all')\"\n                @click=\"setFilter('')\">\n                {{ $t('page.all') }} <span class=\"filter-count\">({{ counters.all }})</span>\n            </li>\n\n            <li\n                :class=\"filterCssClasses('published')\"\n                @click=\"setFilter('is:published')\">\n                {{ $t('page.published') }} <span class=\"filter-count\">({{ counters.published }})</span>\n            </li>\n\n            <li\n                v-if=\"counters.drafts\"\n                :class=\"filterCssClasses('draft')\"\n                @click=\"setFilter('is:draft')\">\n                {{ $t('page.drafts') }} <span class=\"filter-count\">({{ counters.drafts }})</span>\n            </li>\n\n            <li\n                v-if=\"counters.trashed\"\n                :class=\"filterCssClasses('trashed')\"\n                @click=\"setFilter('is:trashed')\">\n                {{ $t('page.trashed') }} <span class=\"filter-count\">({{counters.trashed }})</span>\n            </li>\n\n            <li\n                :class=\"{\n                   'filter-value': true,\n                   'is-hierarchy': true,\n                   'is-hierarchy-active': !!hierarchyMode\n                }\">\n                <a\n                    href=\"#\"\n                    class=\"edit-page-hierarchy\"\n                    @click=\"toggleHierarchyMode\">\n                    <icon\n                        name=\"settings\"\n                        size=\"xs\" />\n                    {{ hierarchyMode ? $t('page.closeHierarchy') : $t('page.editHierarchy') }}\n                </a>\n            </li>\n        </ul>\n\n        <collection\n            v-if=\"dataLoaded && !emptySearchResults && hasPages\"\n            :itemsCount=\"showModificationDate && showModificationDateAsColumn ? 6 : 5\">\n            <collection-header slot=\"header\">\n                <collection-cell>\n                    <checkbox\n                        value=\"all\"\n                        :checked=\"anyCheckboxIsSelected\"\n                        :onClick=\"toggleAllCheckboxes.bind(this, false)\"\n                        @click.native=\"$bus.$emit('document-body-clicked')\" />\n                </collection-cell>\n\n                <collection-cell>\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('title')\">\n                        <template v-if=\"orderBy === 'title'\">\n                            <strong>{{ $t('page.title') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('page.title') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'title' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'title' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell>\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('created')\">\n                        <template v-if=\"orderBy === 'created'\">\n                            <strong>{{ $t('page.publicationDate') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('page.publicationDate') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'created' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'created' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell\n                    v-if=\"showModificationDate && showModificationDateAsColumn\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('modified')\">\n                        <template v-if=\"orderBy === 'modified'\">\n                            <strong>{{ $t('page.modificationDate') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('page.modificationDate') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'modified' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'modified' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell min-width=\"110px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('author')\">\n                        <template v-if=\"orderBy === 'author'\">\n                            <strong>{{ $t('author.author') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('author.author') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'author' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'author' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell min-width=\"35px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('id')\">\n                        <template v-if=\"orderBy === 'id'\">\n                            <strong>{{ $t('ui.id') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.id') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'id' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'id' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <div\n                    v-if=\"anyCheckboxIsSelected\"\n                    class=\"tools\">\n                    <p-button\n                        v-if=\"trashVisible\"\n                        icon=\"delete\"\n                        type=\"small light icon delete\"\n                        :onClick=\"bulkDelete\">\n                        {{ $t('ui.delete') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"trashVisible\"\n                        icon=\"restore\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkRestore\">\n                        {{ $t('file.restore') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"!trashVisible\"\n                        icon=\"trash\"\n                        type=\"small light icon delete\"\n                        :onClick=\"bulkTrash\">\n                        {{ $t('page.moveToTrash') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"!trashVisible\"\n                        icon=\"duplicate\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkDuplicate\">\n                        {{ $t('page.duplicate') }}\n                    </p-button>\n\n                    <div\n                        v-if=\"!trashVisible\"\n                        class=\"dropdown-wrapper\">\n                        <p-button\n                            icon=\"more\"\n                            :type=\"bulkDropdownVisible ? 'small light icon active' : 'small light icon'\"\n                            @click.native.stop=\"toggleBulkDropdown\">\n                            {{ $t('ui.more') }}\n                        </p-button>\n\n                        <ul\n                            v-if=\"bulkDropdownVisible\"\n                            class=\"dropdown\">\n                            <li\n                                v-if=\"selectedPagesNeedsStatus('published')\"\n                                @click=\"bulkPublish\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"publish-post\" />\n                                {{ $t('page.publish') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPagesNeedsStatus('draft')\"\n                                @click=\"bulkUnpublish\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"draft-post\" />\n                                {{ $t('page.markAsDraft') }}\n                            </li>\n                            <li\n                                @click=\"bulkConvertToPost\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"convert-to-page\" />\n                                {{ $t('page.convertToPost') }}\n                            </li>\n                        </ul>\n                    </div>\n                </div>\n            </collection-header>\n\n            <collection-row\n                v-for=\"(item, index) in items\"\n                slot=\"content\"\n                :data-is-draft=\"item.isDraft\"\n                :key=\"'collection-row-' + index\">\n                <collection-cell>\n                    <checkbox\n                        :value=\"item.id\"\n                        :checked=\"isChecked(item.id)\"\n                        :onClick=\"toggleSelection\"\n                        :key=\"'collection-row-checkbox-' + index\" />\n                </collection-cell>\n\n                <collection-cell\n                    type=\"titles\"\n                    :style=\"'--item-depth: ' + (item.depth || 0)\"\n                    :data-item-depth=\"item.depth || 0\">\n\n                    <template v-if=\"item.depth > 0\">\n                        <span class=\"collection-nested-arrow\"></span>\n                    </template>\n\n                    <h2 :class=\"{\n                        'title': true,\n                        'is-homepage': homepageID === item.id\n                    }\"> \n                        <a\n                            href=\"#\"\n                            @click.prevent.stop=\"editPage(item.id, item.editor)\">\n\n                            {{ item.title }}\n\n                            <strong v-if=\"homepageID === item.id\">\n                                &ndash; {{ $t('page.isHomepage') }}\n                            </strong>\n\n                            <icon\n                                v-if=\"item.isDraft\"\n                                size=\"xs\"\n                                name=\"draft-post\"\n                                primaryColor=\"color-7\"\n                                :title=\"$t('page.thisPageIsADraft')\" />\n                        </a>\n                    </h2>\n\n                    <div\n                        v-if=\"showPageSlugs && !hierarchyMode && !item.isTrashed && !item.isDraft\"\n                        class=\"page-slug\">\n                        {{ $t('page.url') }}: {{ item.fullSlug }}\n                    </div>\n\n                    <div v-if=\"hierarchyMode\">\n                        <a\n                            v-if=\"!subpageSelected\"\n                            href=\"#\"\n                            class=\"page-item-select\"\n                            :title=\"$t('page.moveItem')\"\n                            @click.prevent=\"selectItem(item.id)\">\n                            {{ $t('page.moveItem') }}\n                        </a>\n                        \n                        <a\n                            v-if=\"subpageSelected && item.id === subpageSelected\"\n                            href=\"#\"\n                            class=\"page-item-unselect\"\n                            :title=\"$t('page.unselectItem')\"\n                            @click.prevent=\"unselectItem()\">\n                            <icon\n                                class=\"page-item-move-icon\"\n                                customWidth=\"22\"\n                                customHeight=\"22\"\n                                name=\"sidebar-close\"/> \n                            {{ $t('page.unselectItem') }}\n                        </a>\n\n                        <span \n                            v-if=\"subpageSelected && subpageSelected !== item.id && subpageSelectedChildren.indexOf(item.id) === -1\"\n                            class=\"page-item-insert-actions\">\n                            {{ $t('page.insertActions') }}:\n                        </span>\n\n                        <a\n                            v-if=\"subpageSelected && subpageSelected !== item.id && subpageSelectedChildren.indexOf(item.id) === -1\"\n                            href=\"#\"\n                            class=\"page-item-insert-before\"\n                            :title=\"$t('page.insertBefore')\"\n                            @click.prevent=\"moveSelectedItem('before', item.id)\">\n                            <icon\n                                class=\"page-item-move-icon\"\n                                size=\"xs\"\n                                name=\"move-up\"/> \n                            {{ $t('page.insertBefore') }}\n                        </a>\n\n                        <a\n                            v-if=\"subpageSelected && subpageSelected !== item.id && subpageSelectedChildren.indexOf(item.id) === -1\"\n                            href=\"#\"\n                            class=\"page-item-insert-after\"\n                            :title=\"$t('page.insertAfter')\"\n                            @click.prevent=\"moveSelectedItem('after', item.id)\">\n                            <icon\n                                class=\"page-item-move-icon\"\n                                size=\"xs\"\n                                name=\"move-down\"/> \n                            {{ $t('page.insertAfter') }}\n                        </a>\n\n                        <a\n                            v-if=\"subpageSelected && subpageSelected !== item.id && subpageSelectedChildren.indexOf(item.id) === -1\"\n                            href=\"#\"\n                            class=\"page-item-insert-as-child\"\n                            :title=\"$t('page.insertAsChild')\"\n                            @click.prevent=\"moveSelectedItem('child', item.id)\">\n                            {{ $t('page.insertAsChild') }} \n                        </a>\n                    </div>\n                </collection-cell>\n\n                <collection-cell\n                    type=\"publish-dates\">\n                    <span class=\"publish-date\">{{ getCreationDate(item.created) }}</span>\n                    <span\n                        v-if=\"!showModificationDateAsColumn && showModificationDate\"\n                        class=\"modify-date\">\n                        {{ $t('ui.lastModified') }}: {{ getModificationDate(item.modified) }}\n                    </span>\n                </collection-cell>\n\n                <collection-cell\n                    v-if=\"showModificationDate && showModificationDateAsColumn\"\n                    type=\"modification-dates\">\n                    <span class=\"modify-date\">\n                        {{ getModificationDate(item.modified) }}\n                    </span>\n                </collection-cell>\n\n                <collection-cell\n                    type=\"authors\">\n                    <a\n                        href=\"#\"\n                        @click.prevent.stop=\"setFilter('author:' + item.author)\">\n                        {{ item.author }}\n                    </a>\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.id }}\n                </collection-cell>\n            </collection-row>\n        </collection>\n\n        <empty-state\n            v-if=\"emptySearchResults\"\n            :description=\"$t('page.noPagesMatchingYourCriteria')\">\n        </empty-state>\n\n        <div\n            v-if=\"dataLoaded && !hasPages\"\n            class=\"empty-state page\">\n\n           <div>\n                <img :src=\"'../src/assets/svg/' + appTheme + '/wysiwyg-editor.svg'\" height=\"286\" width=\"331\" />\n                <h3>{{ $t('page.editorWYSIWYG') }}</h3>\n                <p>{{ $t('page.editorWYSIWYGInfo') }}</p>\n                <p-button\n                    slot=\"button\"\n                    icon=\"add-site-mono\"\n                    type=\"icon\"\n                    :onClick=\"addNewPage.bind(this, 'tinymce')\">\n                    {{ $t('page.addNewPage') }}\n                </p-button>\n           </div>\n\n           <div>\n                <img :src=\"'../src/assets/svg/' + appTheme + '/block-editor.svg'\" height=\"286\" width=\"331\" />\n                <h3>{{ $t('page.editorBlock') }}</h3>\n                <p>{{ $t('page.editorBlockInfo') }}</p>\n                <p-button\n                    slot=\"button\"\n                    icon=\"add-site-mono\"\n                    type=\"icon\"\n                    :onClick=\"addNewPage.bind(this, 'blockeditor')\">\n                    {{ $t('page.addNewPage') }}\n                </p-button>\n           </div>\n\n           <div>\n                <img :src=\"'../src/assets/svg/' + appTheme + '/markdown-editor.svg'\" height=\"286\" width=\"331\" />\n                <h3>{{ $t('page.editorMarkdown') }}</h3>\n                <p>{{ $t('page.editorMarkdownInfo') }}</p>\n                <p-button\n                    slot=\"button\"\n                    icon=\"add-site-mono\"\n                    type=\"icon\"\n                    :onClick=\"addNewPage.bind(this, 'markdown')\">\n                    {{ $t('page.addNewPage') }}\n                </p-button>\n           </div>\n        </div>\n    </section>\n</template>\n\n<script>\nimport Vue from 'vue';\nimport CollectionCheckboxes from './mixins/CollectionCheckboxes.js';\n\nexport default {\n    name: 'pages',\n    mixins: [\n        CollectionCheckboxes\n    ],\n    data () {\n        return {\n            appTheme: '',\n            bulkDropdownVisible: false,\n            dataLoaded: false,\n            filterValue: '',\n            selectedItems: [],\n            pagesHierarchy: null,\n            hierarchyMode: false,\n            subpageSelected: false,\n            order: 'DESC',\n            orderBy: '',\n            subpageSelectedChildren: []\n        };\n    },\n    computed: {\n        items () {\n            let items = this.$store.getters.sitePages(this.filterValue, this.orderBy, this.order).filter(item => item.title !== null);\n\n            if (this.filterValue !== '' || this.orderBy !== '') {\n                return items;\n            }\n\n            let itemsMap = new Map(items.map(item => [item.id, item]));\n            let flatItems = this.hierarchyFlatten();\n            let usedItems = [];\n\n            flatItems.forEach(flatItem => {\n                let item = itemsMap.get(flatItem.id);\n\n                if (!item) {\n                    return;\n                }\n\n                usedItems.push(flatItem.id);\n\n                if (item) {\n                    item.depth = flatItem.depth;\n                    item.parentIds = flatItem.parentIds;\n                    item.fullSlug = this.generateSlug(item, itemsMap);\n                }\n            });\n\n            let results = flatItems.map(flatItem => itemsMap.get(flatItem.id));\n\n            // restore items that are not in hierarchy\n            items.forEach(item => {\n                if (item && usedItems.indexOf(item.id) === -1) {\n                    item.depth = 0;\n                    item.parentIds = [];\n\n                    if (this.$store.state.currentSite.config.advanced.urls.cleanUrls) {\n                        item.fullSlug = `/` + item.slug;\n                    } else {\n                        item.fullSlug = `/${item.slug}.html`;\n                    }\n\n                    results.push(item);\n                }\n            });\n\n            return results.filter(item => !!item);\n        },\n        hasPages () {\n            return this.$store.state.currentSite.pages && !!this.$store.state.currentSite.pages.length;\n        },\n        emptySearchResults () {\n            return this.filterValue !== '' && !this.items.length;\n        },\n        trashVisible () {\n            return this.filterValue.indexOf('is:trashed') > -1;\n        },\n        counters () {\n            if(!this.$store.state.currentSite || !this.$store.state.currentSite.pages) {\n                return {\n                    all: 0,\n                    published: 0,\n                    drafts: 0,\n                    trashed: 0\n                };\n            }\n            return {\n                all: this.$store.state.currentSite.pages.filter((page) => page.status.indexOf('trashed') === -1).length,\n                published: this.$store.state.currentSite.pages.filter((page) => page.status.indexOf('trashed') === -1 && page.status.indexOf('draft') === -1).length,\n                drafts: this.$store.state.currentSite.pages.filter((page) => page.status.indexOf('trashed') === -1 && page.status.indexOf('draft') > -1).length,\n                trashed: this.$store.state.currentSite.pages.filter((page) => page.status.indexOf('trashed') > -1).length\n            }\n        },\n        showModificationDate () {\n            return this.$store.state.app.config.showModificationDate;\n        },\n        showModificationDateAsColumn () {\n            return this.$store.state.app.config.showModificationDateAsColumn;\n        },\n        dropdownItems () {\n            return [\n                {\n                    label: this.$t('page.editorWYSIWYGUse'),\n                    activeLabel: this.$t('page.addNewPage'),\n                    value: 'tinymce',\n                    icon: 'wysiwyg',\n                    isVisible: () => true,\n                    onClick: this.addNewPage.bind(this, 'tinymce')\n                },\n                {\n                    label: this.$t('page.editorBlockUse'),\n                    activeLabel: this.$t('page.addNewPage'),\n                    value: 'blockeditor',\n                    icon: 'block',\n                    isVisible: () => true,\n                    onClick: this.addNewPage.bind(this, 'blockeditor')\n                },\n                {\n                    label: this.$t('page.editorMarkdownUse'),\n                    activeLabel: this.$t('page.addNewPage'),\n                    value: 'markdown',\n                    icon: 'markdown',\n                    isVisible: () => true,\n                    onClick: this.addNewPage.bind(this, 'markdown')\n                }\n            ]\n        },\n        showPageSlugs () {\n            return this.$store.state.app.config.showPostSlugs;\n        },\n        homepageID () {\n            if (this.$store.state.currentSite.config.advanced.usePageAsFrontpage) {\n                return this.$store.state.currentSite.config.advanced.pageAsFrontpage;\n            }\n            \n            return false;\n        }\n    },\n    async mounted () {\n        this.appTheme = await this.$root.getCurrentAppTheme();\n        this.orderBy = this.$store.state.ordering.pages.orderBy;\n        this.order = this.$store.state.ordering.pages.order;\n        this.$bus.$on('site-loaded', this.whenSiteLoaded);\n\n        this.$bus.$on('pages-filter-value-changed', (newValue) => {\n            this.filterValue = newValue.trim().toLowerCase();\n        });\n\n        this.$bus.$on('document-body-clicked', this.closeBulkDropdown);\n\n        // It is available when user comes from Tags/Authors views\n        let newFilterValue = localStorage.getItem('publii-pages-search-value');\n\n        if(newFilterValue) {\n            localStorage.removeItem('publii-pages-search-value');\n            setTimeout (() => {\n                this.setFilter(newFilterValue);\n            }, 0);\n        }\n\n        this.$bus.$on('site-switched', () => {\n            setTimeout(() => {\n                this.saveOrdering(this.$store.state.ordering.pages.orderBy, this.$store.state.ordering.pages.order);\n            }, 500);\n        });\n\n        this.$bus.$on('app-settings-saved', newSettings => {\n            if (this.orderBy + ' ' + this.order !== newSettings.pagesOrdering) {\n                let order = newSettings.pagesOrdering.split(' ');\n                this.saveOrdering(order[0], order[1]);\n            }\n        });\n\n        if (this.$store.state.currentSite.pages) {\n            this.dataLoaded = true;\n        }\n\n        if (this.$route.params.filter === 'trashed') {\n            this.setFilter('is:trashed');\n        }\n\n        mainProcessAPI.send('app-pages-hierarchy-load', this.$store.state.currentSite.config.name);\n\n        mainProcessAPI.receiveOnce('app-pages-hierarchy-loaded', (data) => {\n            if (!data) {\n                this.pagesHierarchy = this.$store.getters.sitePages(this.filterValue, this.orderBy, this.order)\n                                                            .filter(item => item.title !== null)\n                                                            .map(item => ({id: item.id, subpages: []}));\n                this.hierarchySave();\n                return;\n            }\n\n            this.pagesHierarchy = JSON.parse(JSON.stringify(data));\n            this.checkHierarchyIntegrity();\n        });\n\n        this.checkPagesSupport();\n    },\n    methods: {\n        addNewPage (editorType) {\n            if (\n                editorType === 'blockeditor' &&\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                !this.$store.state.currentSite.themeSettings.supportedFeatures.blockEditor\n            ) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('page.editorBlockNotSupportedNewPageInfo'),\n                    okLabel: this.$t('page.openEditorAnyway'),\n                    isDanger: true,\n                    okClick: () => {\n                        this.openEditor(false, editorType);\n                    }\n                });\n                return;\n            }\n\n            this.openEditor(false, editorType);\n        },\n        editPage (id, editorType) {\n            if (\n                editorType === 'blockeditor' &&\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                !this.$store.state.currentSite.themeSettings.supportedFeatures.blockEditor\n            ) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('page.editorBlockNotSupportedEditPageInfo'),\n                    okLabel: this.$t('page.editPageAnyway'),\n                    isDanger: true,\n                    okClick: () => {\n                        this.openEditor(id, editorType);\n                    }\n                });\n                return;\n            }\n\n            this.openEditor(id, editorType);\n        },\n        openEditor (id, editorType) {\n            let siteName = this.$route.params.name;\n\n            if(this.filterValue.trim() !== '' && this.$store.state.app.config.alwaysSaveSearchState) {\n                localStorage.setItem('publii-pages-search-value', this.filterValue);\n            }\n\n            this.$store.commit('setEditorOpenState', true);\n            this.$router.push('/site/' + siteName + '/pages/editor/' + editorType + '/' + (id !== false ? id : ''));\n            return false;\n        },\n        setFilter (newValue) {\n            if (this.$refs.search) {\n                this.$refs.search.isOpen = newValue !== '';\n                this.$refs.search.value = newValue;\n                this.$refs.search.updateValue();\n            }\n        },\n        filterCssClasses (type) {\n            if(type !== 'all') {\n                return {\n                    'filter-value': true,\n                    'filter-active': this.filterValue.indexOf('is:' + type) === 0,\n                    'filter-inactive': !!this.hierarchyMode\n                };\n            }\n\n            return {\n                'filter-all': true,\n                'filter-value': true,\n                'filter-active': this.filterValue.indexOf('is:') === -1,\n                'filter-inactive': !!this.hierarchyMode\n            };\n        },\n        getModificationDate (timestamp) {\n            return this.$moment(timestamp).fromNow();\n        },\n        getCreationDate (timestamp) {\n            if(this.$store.state.app.config.timeFormat == 12) {\n                return this.$moment(timestamp).format('MMM DD, YYYY  hh:mm a');\n            } else {\n                return this.$moment(timestamp).format('MMM DD, YYYY  HH:mm');\n            }\n        },\n        bulkDelete () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('page.removePageMessage'),\n                isDanger: true,\n                okClick: this.deleteSelected\n            });\n        },\n        bulkConvertToPost () {\n            let itemsToChange = this.getSelectedItems();\n\n            this.$store.commit('changePagesToPosts', {\n                pageIDs: itemsToChange\n            });\n\n            mainProcessAPI.send('app-page-status-change', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToChange,\n                \"status\": 'is-page',\n                \"inverse\": true\n            });\n\n            this.updateHierarchyForConvertedPages(itemsToChange);\n            \n            mainProcessAPI.receiveOnce('app-page-status-changed', () => {\n                this.selectedItems = [];\n            });\n\n            this.$bus.$emit('message-display', {\n                message: this.$t('page.pageStatusChangeSuccessMessage'),\n                type: 'success',\n                lifeTime: 3\n            });\n        },\n        deleteSelected () {\n            let itemsToRemove = this.getSelectedItems();\n\n            mainProcessAPI.send('app-page-delete', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToRemove\n            });\n\n            mainProcessAPI.receiveOnce('app-page-deleted', () => {\n                this.$store.commit('removePages', itemsToRemove);\n                this.selectedItems = [];\n\n                this.$bus.$emit('message-display', {\n                    message: this.$t('page.removePageSuccessMessage'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n\n                if (this.counters.trashed === 0) {\n                    this.filterValue = '';\n                }\n            });\n        },\n        bulkTrash () {\n            this.changeStateForSelected('trashed');\n        },\n        bulkPublish () {\n            this.changeStateForSelected('published');\n            this.changeStateForSelected('draft', true);\n        },\n        bulkUnpublish () {\n            this.changeStateForSelected('published', true);\n            this.changeStateForSelected('draft');\n        },\n        bulkDuplicate () {\n            let itemsToDuplicate = this.getSelectedItems();\n\n            mainProcessAPI.send('app-page-duplicate', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToDuplicate\n            });\n\n            mainProcessAPI.receiveOnce('app-page-duplicated', (data) => {\n                if(!data) {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('page.duplicatePageErrorMessage'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n\n                    return;\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('page.duplicatePageSuccessMessage'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                }\n\n                this.selectedItems = [];\n\n                mainProcessAPI.send('app-site-reload', {\n                    siteName: this.$store.state.currentSite.config.name\n                });\n\n                mainProcessAPI.receiveOnce('app-site-reloaded', (result) => {\n                    this.$store.commit('setSiteConfig', result);\n                    this.$store.commit('switchSite', result.data);\n                    this.updateHierarchyForDuplicatedPages();\n                });\n            });\n        },\n        bulkRestore () {\n            this.changeStateForSelected('trashed', true);\n        },\n        changeStateForSelected (status, inverse = false) {\n            let itemsToChange = this.getSelectedItems();\n\n            this.$store.commit('changePagesStatus', {\n                pageIDs: itemsToChange,\n                status: status,\n                inverse: inverse\n            });\n\n            if (status === 'trashed' && !inverse) {\n                this.updateHierarchyForTrashedPages(itemsToChange);\n            } else if (status === 'trashed' && inverse) {\n                this.updateHierarchyForRestoredPages(itemsToChange);\n\n                // Skip to the \"All\" filter when trash is emptied\n                Vue.nextTick(() => {\n                    if (this.counters.trashed === 0) {\n                        this.filterValue = '';\n                        this.$refs['search'].close();\n                    }\n                });\n            }\n\n            mainProcessAPI.send('app-page-status-change', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToChange,\n                \"status\": status,\n                \"inverse\": inverse\n            });\n\n            mainProcessAPI.receiveOnce('app-page-status-changed', () => {\n                this.selectedItems = [];\n            });\n\n            this.$bus.$emit('message-display', {\n                message: this.$t('page.pageStatusChangeSuccessMessage'),\n                type: 'success',\n                lifeTime: 3\n            });\n        },\n        whenSiteLoaded () {\n            this.dataLoaded = true;\n\n            setTimeout(() => {\n                this.setFilter('');\n            }, 0);\n        },\n        ordering (field) {\n            if (field !== this.orderBy) {\n                this.orderBy = field;\n                this.order = 'DESC';\n            } else {\n                if (this.order === 'DESC') {\n                    this.order = 'ASC';\n                } else {\n                    this.order = 'DESC';\n                }\n            }\n            this.saveOrdering(this.orderBy, this.order);\n        },\n        saveOrdering (orderBy, order) {\n            this.orderBy = orderBy;\n            this.order = order;\n            this.$store.commit('setOrdering', {\n                type: 'pages',\n                orderBy: this.orderBy,\n                order: this.order\n            });\n        },\n        toggleBulkDropdown () {\n            this.bulkDropdownVisible = !this.bulkDropdownVisible;\n        },\n        closeBulkDropdown () {\n            this.bulkDropdownVisible = false;\n        },\n        selectedPagesNeedsStatus (status) {\n            let selectedPages = this.items.filter(item => this.selectedItems.indexOf(item.id) > -1);\n\n            if (!selectedPages.length) {\n                return false;\n            }\n\n            let pagesWithoutGivenStatus = selectedPages.filter(item => item.status.indexOf(status) === -1);\n\n            return !!pagesWithoutGivenStatus.length;\n        },\n        selectedPagesHaveStatus (status) {\n            let selectedPages = this.items.filter(item => this.selectedItems.indexOf(item.id) > -1);\n\n            if (!selectedPages.length) {\n                return false;\n            }\n\n            let pagesWithGivenStatus = selectedPages.filter(item => item.status.indexOf(status) > -1);\n\n            return !!pagesWithGivenStatus.length;\n        },\n        toggleHierarchyMode () {\n            this.subpageSelected = false;\n            this.subpageSelectedChildren = [];\n            this.hierarchyMode = !this.hierarchyMode;  \n\n            if (this.hierarchyMode && !this.$store.state.currentSite.config.advanced.urls.cleanUrls) {\n                this.$bus.$emit('confirm-display', {\n                    hasInput: false,\n                    message: this.$t('page.cleanUrlsDisabled'),\n                    okClick: this.goToSettings,\n                    okLabel: this.$t('ui.enablePrettyURLs')\n                });\n            }\n\n            if (this.hierarchyMode) {\n                this.filterValue = '';\n                this.selectedItems = [];\n                this.orderBy = '';\n                this.order = 'DESC';\n                this.setFilter('');\n                this.saveOrdering(this.orderBy, this.order);\n            }\n        },\n        goToSettings () {\n            let siteName = this.$route.params.name;\n            this.$router.push('/site/' + siteName + '/settings/');\n        },\n        selectItem (id) {\n            this.subpageSelected = id;\n            this.subpageSelectedChildren = this.items.filter(item => item.parentIds.indexOf(id) > -1).map(item => item.id);\n        },\n        unselectItem () {\n            this.subpageSelected = false;\n            this.subpageSelectedChildren = [];\n        },\n        findAndRemoveItem(pages, selectedItem) {\n            for (let i = 0; i < pages.length; i++) {\n                if (pages[i].id === selectedItem) {\n                    return pages.splice(i, 1)[0];\n                } else if (pages[i].subpages.length > 0) {\n                    let result = this.findAndRemoveItem(pages[i].subpages, selectedItem);\n                    \n                    if (result) {\n                        return result;\n                    }\n                }\n            }\n\n            return null;\n        },\n        findItemAndParent(pages, id) {\n            for (let i = 0; i < pages.length; i++) {\n                if (pages[i].id === id) {\n                    return { \n                        item: pages[i], \n                        parent: pages \n                    };\n                } else if (pages[i].subpages.length > 0) {\n                    let result = this.findItemAndParent(pages[i].subpages, id);\n                    \n                    if (result) {\n                        return result;\n                    }\n                }\n            }\n\n            return null;\n        },\n        moveSelectedItem (position, id) {\n            let selectedItem = this.findAndRemoveItem(this.pagesHierarchy, this.subpageSelected);\n            let target = this.findItemAndParent(this.pagesHierarchy, id);\n            let { item: targetItem, parent: targetParent } = target;\n\n            switch (position) {\n                case 'before': targetParent.splice(targetParent.indexOf(targetItem), 0, selectedItem); break;\n                case 'after': targetParent.splice(targetParent.indexOf(targetItem) + 1, 0, selectedItem); break;\n                case 'child': targetItem.subpages.push(selectedItem); break;\n                default: break;\n            }\n\n            this.hierarchySave();\n            this.unselectItem();\n        },\n        hierarchySave () {\n            mainProcessAPI.send('app-pages-hierarchy-save', {\n                siteName: this.$store.state.currentSite.config.name,\n                hierarchy: this.pagesHierarchy\n            });\n        },\n        hierarchyFlatten () {\n            let flatStructure = [];\n            let pagesHierarchy = JSON.parse(JSON.stringify(this.pagesHierarchy));\n            this.hierarchyRecursiveFlatten(flatStructure, pagesHierarchy);\n            return flatStructure;\n        },\n        hierarchyRecursiveFlatten (flatStructure, subpages, depth = 0, parentIds = []) {\n            if (!Array.isArray(subpages) || subpages.length === 0) {\n                return;\n            }\n\n            subpages.filter(subpage => !!subpage).forEach((page) => {\n                let currentSubpage = {\n                    id: page.id,\n                    depth: depth,\n                    parentIds: [...parentIds]\n                };\n                flatStructure.push(currentSubpage);\n\n                if (Array.isArray(page.subpages) && page.subpages.length > 0) {\n                    this.hierarchyRecursiveFlatten (flatStructure, page.subpages, depth + 1, [...parentIds, page.id]);\n                }\n            });\n        },\n        generateSlug (item, items) {\n            if (!item.parentIds || item.parentIds.length === 0 || !this.$store.state.currentSite.config.advanced.urls.cleanUrls) {\n                if (!this.$store.state.currentSite.config.advanced.urls.cleanUrls) {\n                    return `/${item.slug}.html`;\n                }\n\n                return `/${item.slug}`;\n            }\n\n            let slugs = [];\n            item.parentIds.forEach(parentId => {\n                let parentItem = items.get(parentId);\n\n                if (parentItem) {\n                    slugs.push(parentItem.slug);\n                }\n            });\n\n            return `/${slugs.join('/')}/${item.slug}`;\n        },\n        findAllChildrenIdsInItems (parentID) {\n            let children = this.hierarchyFlatten().filter(item => item.parentIds.indexOf(parentID) > -1);\n            let childrenIDs = children.map(child => child.id);\n            return childrenIDs;\n        },\n        updateHierarchyForRestoredPages (restoredPages) {\n            restoredPages.forEach(restoredPage => {\n                this.pagesHierarchy.push({ \n                    id: restoredPage, \n                    subpages: []\n                });\n                this.hierarchySave();\n            });\n        },\n        updateHierarchyForTrashedPages (trashedPages) {\n            let childrenIDs = trashedPages.map(trashedPage => this.findAllChildrenIdsInItems(trashedPage));\n            let childrenIDsFlat = childrenIDs.flat();\n            let childrenIDsUnique = [...new Set(childrenIDsFlat)];\n            let childrenToAdd = childrenIDsUnique.filter(childID => !trashedPages.includes(childID));\n\n            trashedPages.forEach(trashedPage => {\n                this.findAndRemoveItem(this.pagesHierarchy, trashedPage);\n            });\n\n            childrenToAdd.forEach(childID => {\n                this.pagesHierarchy.push({ \n                    id: childID, \n                    subpages: []\n                });\n            });\n        \n            this.hierarchySave();\n        },\n        updateHierarchyForPublishPages () {\n\n        },\n        updateHierarchyForDraftPages () {\n\n        },\n        updateHierarchyForDuplicatedPages () {\n            let rootHierarchyItems = this.pagesHierarchy.map(item => item.id);\n            let rootItems = this.items.filter(item => item.depth === 0).map(item => item.id);\n            let itemsToAdd = rootItems.filter(item => !rootHierarchyItems.includes(item));\n\n            itemsToAdd.forEach(item => {\n                this.pagesHierarchy.push({ \n                    id: item, \n                    subpages: []\n                });\n            });\n\n            this.hierarchySave();\n        },\n        checkPagesSupport () {\n            let sessionStorageName = this.$store.state.currentSite.config.name.replace(/[^a-z\\-0-9]/gmi, '');\n\n            if (window.sessionStorage.getItem('pages-info-displayed-for-site-' + sessionStorageName)) {\n                return;\n            }\n\n            if (\n                !this.$store.state.currentSite.themeSettings.supportedFeatures ||\n                !this.$store.state.currentSite.themeSettings.supportedFeatures.pages\n            ) {\n                window.sessionStorage.setItem('pages-info-displayed-for-site-' + sessionStorageName, true);\n                this.$bus.$emit('confirm-display', {\n                    hasInput: false,\n                    message: this.$t('ui.pagesSupportNotEnabledMessage'),\n                    isDanger: true,\n                    okClick: () => false,\n                    cancelClick: this.checkWhatsChanged,\n                    okLabel: this.$t('ui.ok'),\n                    cancelLabel: this.$t('ui.learnMore'),\n                    cancelNotClosePopup: true\n                });\n            }\n        },\n        checkWhatsChanged () {\n            mainProcessAPI.shellOpenExternal('https://getpublii.com/dev/how-to-add-pages-support-to-your-theme/');\n        },\n        checkHierarchyIntegrity () {\n            let flatStructure = this.hierarchyFlatten();\n            let itemsMap = new Map(this.items.map(item => [item.id, item]));\n            let usedItems = [];\n\n            flatStructure.forEach(flatItem => {\n                let item = itemsMap.get(flatItem.id);\n\n                if (!item) {\n                    return;\n                }\n\n                usedItems.push(flatItem.id);\n            });\n\n            // find items that are not in hierarchy\n            this.items.forEach(item => {\n                if (usedItems.indexOf(item.id) === -1) {\n                    this.pagesHierarchy.push({ \n                        id: item.id, \n                        subpages: []\n                    });\n\n                    console.log('Add missing item to hierarchy', item.id);\n                }\n            });\n\n            this.hierarchySave();\n        },\n        updateHierarchyForConvertedPages (convertedPages) {\n            let childrenIDs = convertedPages.map(convertedPage => this.findAllChildrenIdsInItems(convertedPage));\n            let childrenIDsFlat = childrenIDs.flat();\n            let childrenIDsUnique = [...new Set(childrenIDsFlat)];\n            let childrenToAdd = childrenIDsUnique.filter(childID => !convertedPages.includes(childID));\n\n            convertedPages.forEach(convertedPage => {\n                let item = this.findAndRemoveItem(this.pagesHierarchy, convertedPage);\n\n                if (item) {\n                    this.hierarchySave();\n                }\n            });\n\n            childrenToAdd.forEach(childID => {\n                this.pagesHierarchy.push({ \n                    id: childID, \n                    subpages: []\n                });\n            });\n        \n            this.hierarchySave();\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('site-loaded', this.whenSiteLoaded);\n        this.$bus.$off('pages-filter-value-changed');\n        this.$bus.$off('document-body-clicked', this.closeBulkDropdown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/empty-states.scss';\n\n.header {\n    .col {\n        align-items: center;\n        display: flex;\n\n        .col-sortable-title {\n            cursor: pointer;\n        }\n    }\n}\n\n.order-ascending,\n.order-descending {\n    margin-left: 3px;\n    position: relative;\n    &:after {\n        border-top: solid 5px var(--icon-secondary-color);\n        border-left: solid 5px transparent;\n        border-right: solid 5px transparent;\n        content: \"\";\n        cursor: pointer;\n        display: inline-block;\n        height: 4px;\n        left: 0;\n        line-height: 1.1;\n        opacity: 1;\n        padding: 0;\n        position: relative;\n        text-align: center;\n        top: 50%;\n        transform: translateY(-50%);\n        width: 8px;\n    }\n}\n\n.order-descending {\n    &:after {\n        border-top-color: transparent;\n        border-bottom: solid 5px var(--icon-secondary-color);\n    }\n}\n\n.content.hierarchy-mode-enabled {\n    .heading {\n        .search,\n        ::v-deep .buttons {\n            filter: grayscale(1);\n            opacity: .5;\n            pointer-events: none;\n        }\n    }\n\n    .collection-wrapper {\n        input[type=\"checkbox\"] {\n            pointer-events: none;\n        }\n    }\n\n    .col.titles .title {\n        pointer-events: none;\n\n        a {\n            color: var(--text-light-color);\n        }\n    }\n}\n\n.header {\n    overflow-y: visible!important;\n}\n\n.item {\n    .page-slug {\n        color: var(--gray-4);\n        font-size: 11px;\n        margin-top: .2rem;\n    }\n\n    .col.titles {\n        padding-left: calc(1.8rem + (2.4rem * var(--item-depth)));\n        position: relative;\n    }\n\n    .page-item-select,\n    .page-item-submenu,\n    .page-item-insert-before,\n    .page-item-insert-after,\n    .page-item-insert-as-child,\n    .page-item-unselect {\n        color: var(--link-primary-color);\n        display: inline-block;\n        font-size: 1.4rem;\n        padding: 0 .5rem;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--link-primary-color-hover);\n        }\n        \n    }\n\n    .page-item-select,\n    .page-item-unselect,\n    .page-item-insert-actions{\n        &:first-child {\n           padding-left: 0;\n        }\n    }\n\n    .page-item-insert-actions {\n        color: var(--text-light-color);\n        font-size: 1.4rem;\n        padding: 0 .5rem;\n    }\n\n    .page-item-unselect {\n        color: var(--warning);\n        left: -9px;\n        position: relative;\n\n        & > svg {\n            fill: var(--warning);\n            position: relative;\n            left: -3px;\n            top: 3px;\n        }\n    }\n\n    .page-item-submenu,\n    .page-item-insert-before,\n    .page-item-insert-after {\n        padding-right: 1rem;\n        position: relative;\n\n        &::after {\n            background: var(--input-border-color);\n            content: \"\";\n            display: block;\n            height: 14px;\n            position: absolute;\n            right: 0;\n            top: 50%;\n            transform: translate(0, -50%);\n            width: 1px;\n        }\n    }\n\n    .page-item-move-icon {\n        vertical-align: text-bottom;\n    }\n}\n\n.filters {\n    display: flex;\n    font-size: 1.35rem;\n    list-style-type: none;\n    margin: -2.2rem 0 0 0;\n    padding: 0;\n    position: relative;\n    user-select: none;\n    z-index: 1;\n\n    .label {\n        color: var(--text-light-color);\n        float: left;\n        margin-right: 1rem;\n    }\n\n    .filter-value {\n        color: var(--text-light-color);\n        cursor: pointer;\n        display: inline-block;\n        margin-right: 1rem;\n        transition: var(--transition);\n\n        &.filter-active {\n            color: var(--link-primary-color);\n        }\n\n        &:hover {\n            color: var(--link-primary-color);\n        }\n\n        &:last-child {\n            border-right: none;\n        }\n\n        &.is-hierarchy {\n            margin-left: auto;\n            margin-right: 0;\n\n            a {\n                align-items: center;\n                display: inline-flex;\n                gap: 6px;\n                margin-top: 8px;\n            }\n        }\n    }\n\n    .filter-inactive {\n        filter: grayscale(1);\n        opacity: 0.5;\n        pointer-events: none;\n    }\n\n    .filter-all::first-letter {\n        text-transform: capitalize;\n    }\n}\n\n.tools {\n    .dropdown-wrapper {\n        position: relative;\n\n        .dropdown {\n            background: var(--popup-bg);\n            border-radius: var(--border-radius);\n            box-shadow: var(--box-shadow-medium);\n            left: 0;\n            list-style-type: none;\n            margin: 0;\n            padding: 1rem 0;\n            position: absolute;\n            top: 4rem;\n            width: auto;\n            z-index: 1;\n\n            li {\n                color: var(--text-light-color);\n                cursor: pointer;\n                display: block;\n                font-size: 1.4rem;\n                font-weight: var(--font-weight-semibold);\n                padding: .8rem 2.4rem;\n                white-space: nowrap;\n\n                &:hover {\n                    background: var(--gray-1);\n                    color: var(--text-primary-color);\n                }\n\n                & > svg {\n                    margin-right: 4px;\n                    vertical-align: text-bottom;\n                }\n            }\n        }\n    }\n}\n.collection-nested-arrow {\n    display: block;\n    border-radius: 0 0 0 2px;\n    width: 11px; \n    height: 8px;\n    border-left: 1px solid var(--gray-4);\n    border-bottom: 1px solid var(--gray-4);\n    position: absolute;\n    left: calc(-.6rem + (2.4rem * var(--item-depth)));\n    top: 50%;\n\ttransform: translate(0, -50%);\n}\n\n.edit-page-hierarchy-warning {\n    color: var(--warning);\n    padding-right: 1rem;\n    position: relative;\n    top: -4px;\n}\n\n/*\n * Responsive improvements\n */\n\n@media (max-width: 1300px) {\n    .item .page-item-insert-actions {\n        display: block;\n        padding-bottom: 0!important;\n    }\n    .item .page-item-insert-before {\n        margin-left: -4px;\n        padding-left: 0;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/PluginsList.vue",
    "content": "<template>\n    <div\n        @drop.stop.prevent=\"uploadPlugin\"\n        @dragleave.stop.prevent=\"hideOverlay\"\n        @dragenter.stop.prevent=\"showOverlay\"\n        @dragover.stop.prevent=\"showOverlay\"\n        @drag.stop.prevent=\"showOverlay\"\n        @dragstart.stop.prevent\n        @dragend.stop.prevent\n        :class=\"{ 'plugins': true, 'plugin-is-over': pluginIsOver }\">\n        <div\n            class=\"add-more-plugins\">\n                <a href=\"https://marketplace.getpublii.com/plugins/\" target=\"_blank\" rel=\"noopener noreferrer\">\n                    <icon\n                        customWidth=\"50\"\n                        customHeight=\"46\"\n                        properties=\"not-clickable\"\n                        name=\"add\" />\n                    <h3>{{ $t('plugins.getMorePlugins') }}</h3>\n                </a>\n        </div>\n\n        <plugin-item\n            v-for=\"(plugin, index) in plugins\"\n            :pluginData=\"plugin\"\n            :key=\"'plugin-item-' + index\" />\n\n        <overlay\n            v-if=\"pluginIsOver\"\n            :hasBorder=\"true\"\n            :isBlue=\"true\">\n            <div>{{ $t('file.dropYourFileHere') }}</div>\n        </overlay>\n    </div>\n</template>\n\n<script>\nimport PluginsListItem from './PluginsListItem';\n\nexport default {\n    name: 'plugins-list',\n    data () {\n        return {\n            pluginIsOver: false\n        };\n    },\n    components: {\n        'plugin-item': PluginsListItem\n    },\n    computed: {\n        plugins () {\n            let pluginsList = JSON.parse(JSON.stringify(this.$store.getters.plugins));\n            pluginsList = pluginsList.filter(plugin => !!plugin);\n            return pluginsList;\n        }\n    },\n    methods: {\n        showOverlay (e) {\n            this.pluginIsOver = true;\n        },\n        hideOverlay (e) {\n            if (e.target.classList.contains('plugins')) {\n                this.pluginIsOver = false;\n            }\n        },\n        async uploadPlugin (e) {\n            this.pluginIsOver = false;\n\n            mainProcessAPI.send('app-plugin-upload', {\n                sourcePath: await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.dataTransfer.files[0]))\n            });\n\n            mainProcessAPI.receiveOnce('app-plugin-uploaded', this.$parent.uploadedPlugin);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.plugins {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 3rem;\n    position: relative;\n    user-select: none;\n\n    &.plugin-is-over {\n        & > * {\n            pointer-events: none;\n        }\n    }\n}\n\n.add-more-plugins {\n    background-color: var(--bg-secondary);\n    border: 1px solid transparent;\n    border-radius: var(--border-radius);\n    box-shadow: var(--box-shadow-small);      \n    height: 100%;\n    transition: var(--transition);\n    text-align: center;\n\n    &:hover {\n         background: var(--bg-primary);\n         border-color: var(--color-primary);\n         box-shadow: 0 0 26px rgba(black, .07);\n\n         svg {\n             fill: var(--color-primary);\n         }\n\n         h3 {\n             color: var(--color-primary);\n         }\n    }\n\n    & > a {\n         align-items: center;\n         display: flex;\n         flex-direction: column;\n         height: 100%;\n         justify-content: center;\n         min-height: 29rem;\n         width: 100%;\n    }\n\n    h3 {\n         color: var(--text-primary-color);\n         font-size: $app-font-base;\n         font-weight: var(--font-weight-semibold);\n         margin-bottom: 0;\n         transition: inherit;\n    }\n\n    svg {\n         fill: var(--icon-primary-color);\n         transition: inherit;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/PluginsListItem.vue",
    "content": "<template>\n    <figure\n        :class=\"{\n                'plugin': true,\n                'is-incompatible': isIncompatible\n            }\">\n        <span class=\"plugin-thumbnail-wrapper\">\n            <img\n                :src=\"thumbnail\"\n                class=\"plugin-thumbnail\"\n                alt=\"\">\n        </span>\n\n        <figcaption class=\"plugin-name\">\n            <h3>\n                <span>{{ name }}</span>\n                <span class=\"plugin-version\">\n                    {{ version }}\n                </span>\n                <span \n                    v-if=\"isIncompatible\"\n                    class=\"plugin-is-incompatible\"\n                    :title=\"$t('plugins.isIncompatibleTitle', { supportedVersion: pluginData.minimumPubliiVersion, currentVersion: this.$store.state.app.versionInfo.version })\">\n                    {{ $t('plugins.isIncompatible') }}\n                </span>\n             </h3>\n            <a\n                href=\"#\"\n                class=\"plugin-delete\"\n                :title=\"$t('plugins.deletePlugin')\"\n                @click.stop.prevent=\"deletePlugin(name, directory)\">\n                    <icon\n                        size=\"xs\"\n                        properties=\"not-clickable\"\n                        name=\"trash\" />\n            </a>\n\n            <span \n                v-if=\"hasUpdateAvailable\"\n                class=\"plugin-new-version-available\">\n                {{ $t('plugins.newVersionAvailable') }}: <strong>{{ updateVersion }}</strong>   \n            </span>\n        </figcaption>\n    </figure>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex';\nimport VersionComparator from '../helpers/version-comparator';\nimport compare from 'node-version-compare';\n\nexport default {\n    name: 'plugins-list-item',\n    props: [\n        'pluginData'\n    ],\n    computed: {\n        ...mapGetters([\n            'notifications'\n        ]),\n        isIncompatible () {\n            if (compare(this.pluginData.minimumPubliiVersion, this.$store.state.app.versionInfo.version) === 1) {\n                return true;\n            }\n\n            return false;\n        },\n        thumbnail () {\n            return this.pluginData.thumbnail;\n        },\n        name () {\n            return this.pluginData.name;\n        },\n        directory () {\n            return this.pluginData.directory;\n        },\n        version () {\n            return this.pluginData.version;\n        },\n        updateVersion () {\n            let availablePlugin = this.notifications.plugins[this.directory];\n\n            if (!availablePlugin) {\n                return '';\n            }\n\n            return availablePlugin.version;\n        },\n        hasUpdateAvailable () {\n            if (!this.notifications || !this.notifications.plugins) {\n                return false;\n            }\n\n            let availablePlugin = this.notifications.plugins[this.directory];\n\n            if (!availablePlugin) {\n                return false;\n            }\n\n            return VersionComparator(availablePlugin.version, this.version) === 1;\n        }\n    },\n    methods: {\n        deletePlugin (pluginName, pluginDirectory) {\n            let confirmConfig = {\n                message: this.$t('plugins.removePluginMessage', { pluginName }),\n                isDanger: true,\n                okClick: function() {\n                    mainProcessAPI.send('app-plugin-delete', {\n                        name: pluginName,\n                        directory: pluginDirectory\n                    });\n\n                    mainProcessAPI.receiveOnce('app-plugin-deleted', (data) => {\n                        this.$bus.$emit('message-display', {\n                            message: this.$t('plugins.removePluginSuccessMessage'),\n                            type: 'success',\n                            lifeTime: 3\n                        });\n\n                        this.$store.commit('replaceAppPlugins', data.plugins);\n                    });\n                }\n            };\n\n            this.$bus.$emit('confirm-display', confirmConfig);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.plugin {\n    background-color: var(--bg-secondary);\n    border: 1px solid transparent;\n    border-radius: var(--border-radius);\n    box-shadow: var(--box-shadow-small);      \n    height: 100%;\n    margin: 0;\n    overflow: hidden;\n    padding: 1rem;\n    position: relative;\n    transition: var(--transition);\n    text-align: center;\n\n    &-thumbnail {\n        display: block;\n        max-height: 50%;\n        left: 50%;\n        position: absolute;\n        top: 50%;\n        transform: translateX(-50%) translateY(-50%);\n        max-width: 90%;\n\n        &-wrapper {\n            display: block;\n            padding-bottom: 75%;\n            position: relative;\n            transition: var(--transition);\n            width: 100%;\n        }\n    }\n\n    &-delete {\n        align-items: center;\n        background: var(--bg-primary);\n        border-radius: 50%;\n        height: 3rem;\n        justify-content: center;\n        display: inline-flex;\n        position: absolute;\n        right: 1.4rem;\n        text-align: center;\n        width: 3rem;\n\n        & > svg {\n             fill: var(--icon-secondary-color);\n             transform: scale(.9);\n             transition: var(--transition);\n        }\n\n        &:hover {\n             & > svg {\n                fill: var(--warning);\n                transform: scale(1);\n             }\n        }\n    }\n\n    &-name {\n        align-items: center;\n        background: var(--gray-1);\n        border-radius: 0 0 4px 4px;\n        display: flex;\n        justify-content: space-between;\n        padding: 0 2rem;\n        text-align: left;\n\n        & > h3 {\n             font-size: 1.4rem;\n             font-weight: var(--font-weight-semibold);\n             line-height: 1.4;\n             margin: 1.2rem 0;\n\n             span:first-of-type {\n                 display: block;\n             }\n        }\n    }\n\n    &-version {\n        color: var(--text-light-color);\n        font-size: 1.2rem;\n        font-weight: var(--font-weight-normal);\n    }\n\n    &-version,\n    &-is-incompatible {\n        color: var(--text-light-color);\n        font-size: 1.2rem;\n        font-weight: var(--font-weight-normnal);\n    }\n\n    &-is-incompatible {\n        color: var(--warning);\n        margin: 0 4rem 0 .5rem;\n        text-transform: uppercase;\n    }\n\n    &.is-incompatible {\n       .plugin-version {\n           text-decoration-color: var(--warning);\n           text-decoration-line: line-through;\n       }\n    }\n\n    &-new-version-available {\n        background: var(--highlighted);\n        left: 1rem;\n        padding: 2rem;        \n        position: absolute;\n        right: 0;\n        top: 1rem;\n        width: calc(100% - 2rem);\n\n        strong {\n            color: var(--headings-color);\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/PostEditorBlockEditor.vue",
    "content": "<template>\n    <div class=\"post-editor post-block-editor\" ref=\"post-editor\">\n        <topbar-appbar />\n        <post-editor-top-bar \n            :itemType=\"itemType\" />\n\n        <div class=\"post-editor-wrapper\">\n            <div :class=\"{\n                'post-editor-form': true,\n                'sidebar-open': sidebarVisible\n            }\">\n                <div>\n                    <publii-block-editor \n                        :itemType=\"itemType\" />\n                </div>\n            </div>\n\n            <p-button\n                id=\"post-help-button\"\n                type=\"clean-invert icon small\"\n                icon=\"help\"\n                :title=\"$t('ui.help')\"\n                @click.native=\"toggleHelp\">\n                <template v-if=\"!helpPanelOpen\">{{ $t('editor.viewHelp') }}</template>\n                <template v-else>{{ $t('editor.hideHelp') }}</template>\n            </p-button>\n\n            <sidebar \n                :isVisible=\"sidebarVisible\"\n                :itemType=\"itemType\" />\n            <author-popup \n                :itemType=\"itemType\" />\n            <date-popup \n                :itemType=\"itemType\" />\n            <help-panel-block-editor \n                :isOpen=\"helpPanelOpen\" />\n        </div>\n    </div>\n</template>\n\n<script>\nimport PostEditorSidebar from './post-editor/Sidebar';\nimport AuthorPopup from './post-editor/AuthorPopup';\nimport DatePopup from './post-editor/DatePopup';\nimport HelpPanelBlockEditor from './post-editor/HelpPanelBlockEditor';\nimport TopBarAppBar from './TopBarAppBar';\nimport PostEditorTopBar from './post-editor/TopBar';\nimport ItemHelper from './post-editor/ItemHelper';\nimport Utils from './../helpers/utils';\nimport PostEditorsCommon from './mixins/PostEditorsCommon';\nimport PubliiBlockEditor from './block-editor/PubliiBlockEditor';\n\nexport default {\n    name: 'post-editor-block-editor',\n    editorType: 'blockeditor',\n    mixins: [\n        PostEditorsCommon\n    ],\n    components: {\n        'author-popup': AuthorPopup,\n        'date-popup': DatePopup,\n        'sidebar': PostEditorSidebar,\n        'topbar-appbar': TopBarAppBar,\n        'post-editor-top-bar': PostEditorTopBar,\n        'help-panel-block-editor': HelpPanelBlockEditor,\n        'publii-block-editor': PubliiBlockEditor\n    },\n    data () {\n        return {\n            postID: this.$route.params.post_id || 0,\n            newPost: true,\n            helpPanelOpen: false,\n            postSlugEdited: false,\n            possibleDataLoss: false,\n            unwatchDataLoss: null,\n            sidebarVisible: false,\n            filesList: null,\n            postData: {\n                editor: this.$options.editorType,\n                title: '',\n                text: '',\n                slug: '',\n                author: 1,\n                tags: [],\n                mainTag: '',\n                template: '',\n                creationDate: {\n                    text: '',\n                    timestamp: 0\n                },\n                modificationDate: {\n                    text: '',\n                    timestamp: 0\n                },\n                isFeatured: false,\n                isHidden: false,\n                isExcludedOnHomepage: false,\n                status: '',\n                metaTitle: '',\n                metaDescription: '',\n                metaRobots: 'index, follow',\n                canonicalUrl: '',\n                featuredImage: {\n                    path: '',\n                    alt: '',\n                    caption: '',\n                    credits: ''\n                },\n                viewOptions: {}\n            },\n            saveAction: {\n                status: '',\n                preview: false,\n                closeEditor: false\n            }\n        };\n    },\n    computed: {\n        itemType () {\n            return this.$route.path.indexOf('/pages/editor/blockeditor/') > -1 ? 'page' : 'post';\n        },\n        isEdit () {\n            return !!this.postID;\n        }\n    },\n    mounted () {\n        if (this.itemType === 'page') {\n            delete this.postData.tags;\n            delete this.postData.mainTag;\n            delete this.postData.isExcludedOnHomepage;\n            delete this.postData.isHidden;\n            delete this.postData.isFeatured;\n        }\n\n        window.onbeforeunload = e => {\n            if (this.possibleDataLoss) {\n                e.returnValue = false;\n\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('core.sureYouWantQuit'),\n                    isDanger: true,\n                    okClick: () => {\n                        window.onbeforeunload = null;\n                        mainProcessAPI.send('app-close', true);\n                    }\n                });\n            }\n        };\n\n        this.$bus.$on('date-changed', this.setCreationDate);\n        this.$bus.$on('post-editor-possible-data-loss', this.setPossibleDataLoss);\n        this.$bus.$on('block-editor-title-updated', this.updateTitle);\n        this.$bus.$on('block-editor-content-updated', this.setPossibleDataLoss);\n        this.$bus.$on('block-editor-post-saved', this.editorPostSaved);\n\n        if (this.isEdit) {\n            this.newPost = false;\n            this.loadPostData();\n        } else {\n            this.setDataLossWatcher();\n        }\n\n        setTimeout(async () => {\n            this.$bus.$emit('block-editor-set-post-id', this.postID);\n            \n            mainProcessAPI.send('app-file-manager-list', {\n                siteName: this.$store.state.currentSite.config.name,\n                dirPath: 'root-files'\n            });\n\n            mainProcessAPI.receiveOnce('app-file-manager-listed', (data) => {\n                this.filesList = data.map(file => file.name);\n\n                mainProcessAPI.send('app-file-manager-list', {\n                    siteName: this.$store.state.currentSite.config.name,\n                    dirPath: 'media/files'\n                }); \n\n                mainProcessAPI.receiveOnce('app-file-manager-listed', (data) => {\n                    this.filesList = this.filesList.concat(data.map(file => 'media/files/' + file.name));\n\n                    this.$bus.$emit('block-editor-set-current-site-data', {\n                        tags: this.$store.state.currentSite.tags,\n                        pages: this.$store.state.currentSite.pages,\n                        posts: this.$store.state.currentSite.posts,\n                        authors: this.$store.state.currentSite.authors,\n                        files: this.filesList\n                    });\n                });\n            });\n        }, 0);\n    },\n    methods: {\n        setCreationDate (timestamp) {\n            let format = 'MMM DD, YYYY  HH:mm';\n\n            if (this.$store.state.app.config.timeFormat == 12) {\n                format = 'MMM DD, YYYY  hh:mm a';\n            }\n\n            this.postData.creationDate.timestamp = timestamp;\n            this.postData.creationDate.text = this.$moment(this.postData.creationDate.timestamp).format(format);\n        },\n        setPossibleDataLoss () {\n            this.possibleDataLoss = true;\n        },\n        async editorPostSaved () {\n            let postData = await ItemHelper.prepareItemData(this.saveAction.status, this.postID, this.$store, this.postData, this.itemType);\n\n            if (!this.saveAction.preview) {\n                this.savingPost(this.saveAction.status, postData, this.saveAction.closeEditor);\n            } else {\n                this.$bus.$emit('rendering-popup-display', {\n                    itemID: this.postID,\n                    postData: postData,\n                    postOnly: true,\n                    itemType: this.itemType\n                });\n            }\n        },\n        updateTitle (newTitle) {\n            this.postData.title = newTitle;\n            this.$bus.$emit('update-post-slug', false);\n        },\n        loadPostData () {\n            // Send request for a post to the back-end\n            let requestType = 'app-post-load';\n            let responseType = 'app-post-loaded';\n\n            if (this.itemType === 'page') {\n                requestType = 'app-page-load';\n                responseType = 'app-page-loaded';\n            }\n\n            mainProcessAPI.send(requestType, {\n                'site': this.$store.state.currentSite.config.name,\n                'id': this.postID\n            });\n\n            // Load post data\n            mainProcessAPI.receiveOnce(responseType, (data) => {\n                if (data !== false && this.postID !== 0) {\n                    let loadedPostData = ItemHelper.loadItemData(data, this.$store, this.$moment, this.itemType);\n                    this.postData = Utils.deepMerge(this.postData, loadedPostData);\n                    this.$bus.$emit('block-editor-set-post-text', this.postData.text);\n                    this.$bus.$emit('block-editor-set-post-title', this.postData.title);\n                }\n\n                setTimeout(() => {\n                    this.possibleDataLoss = false;\n                    this.setDataLossWatcher();\n                }, 100);\n            });\n        },\n        setDataLossWatcher () {\n            this.unwatchDataLoss = this.$watch('postData', (newValue, oldValue) => {\n                this.possibleDataLoss = true;\n                this.unwatchDataLoss();\n            }, { deep: true });\n        },\n        savePost (newPostStatus, preview = false, closeEditor = false) {\n            if (this.postData.title.trim() === '') {\n                this.$bus.$emit('alert-display', {\n                    message: this.itemType === 'page' ? this.$t('editor.cantSavePageWithEmptyTitle') : this.$t('editor.cantSavePostWithEmptyTitle')\n                });\n\n                return;\n            }\n\n            this.saveAction.status = newPostStatus;\n            this.saveAction.preview = preview;\n            this.saveAction.closeEditor = closeEditor;\n            this.$bus.$emit('block-editor-post-save');\n        },\n        savingPost (newStatus, postData, closeEditor = false) {\n            // Send form data to the back-end\n            let requestType = 'app-post-save';\n            let responseType = 'app-post-saved';\n\n            if (this.itemType === 'page') {\n                requestType = 'app-page-save';\n                responseType = 'app-page-saved';\n            }\n\n            mainProcessAPI.send(requestType, postData);\n\n            // Post save\n            mainProcessAPI.receiveOnce(responseType, (data) => {\n                if (this.postID === 0) {\n                    this.postID = data.postID;\n                }\n\n                if (data.posts || data.pages) {\n                    this.savedPost(newStatus, data, closeEditor);\n                } else {\n                    alert(this.$t('editor.errorOccurred'));\n                }\n            });\n        },\n        savedPost (newStatus, updatedData, closeEditor = false) {\n            if (this.itemType === 'post') {\n                this.$store.commit('refreshAfterPostUpdate', updatedData);\n            } else {\n                this.postID = updatedData.pageID;\n                this.$bus.$emit('page-data-updated', updatedData.pageID);\n                this.$store.commit('refreshAfterPageUpdate', updatedData);\n            }\n\n            if (closeEditor) {\n                this.closeEditor();\n                return;\n            } else {\n                this.$bus.$emit('block-editor-set-post-id', this.postID);\n            }\n\n            this.$router.push('/site/' + this.$route.params.name + '/' + this.itemType + 's/editor/blockeditor/' + this.postID);\n            let message = this.$t('editor.changesSaved');\n\n            if (this.newPost) {\n                this.newPost = false;\n\n                if (newStatus === 'draft') {\n                    message = this.$t('editor.newDraftCreated');\n                } else {\n                    if (this.itemType === 'page') {\n                        message = this.$t('editor.newPageCreated');\n                    } else {\n                        message = this.$t('editor.newPostCreated');\n                    }\n                }\n            }\n\n            this.$bus.$emit('message-display', {\n                message: message,\n                type: 'success',\n                lifeTime: 3\n            });\n\n            this.loadPostData();\n            this.possibleDataLoss = false;\n        },\n        toggleHelp () {\n            this.helpPanelOpen = !this.helpPanelOpen;\n        }\n    },\n    beforeDestroy () {\n        if (this.unwatchDataLoss) {\n            this.unwatchDataLoss();\n        }\n\n        this.$bus.$off('date-changed', this.setCreationDate);\n        this.$bus.$off('post-editor-possible-data-loss', this.setPossibleDataLoss);\n        this.$bus.$off('block-editor-title-updated', this.updateTitle);\n        this.$bus.$off('block-editor-content-updated', this.setPossibleDataLoss);\n        this.$bus.$off('block-editor-post-saved', this.editorPostSaved);\n        window.onbeforeunload = null;\n    }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n@import '../scss/editor/post-editors-common.scss';\n   \n#publii-block-editor {\n    font-family: -apple-system, system-ui, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n}\n\n#post-editor-fake-image-uploader,\n#post-editor-fake-multiple-images-uploader {\n    display: none;\n}\n\n.post-editor {\n    overflow-x: hidden;\n    position: relative;\n    width: 100%;\n    z-index: 2;\n\n    &-wrapper {\n        overflow: auto;\n        padding-top: var(--topbar-height);\n    }\n\n    &-form {\n        height: calc(100vh - var(--topbar-height));\n        overflow: scroll;\n\n        & > div {\n            padding: 9rem 4rem 3rem 4rem;\n        }\n    }\n\n    #post-editor {\n        display: none;\n    }\n}\n\n#post-help-button {\n    align-items: center;\n    bottom: .4rem;\n    display: flex;\n    height: 4.4rem;\n    line-height: 4.3rem;\n    position: absolute;\n    right: 1.8rem;\n    z-index: 99991;\n}\n\nbody[data-os=\"win\"] {\n    .post-editor-wrapper {\n        height: 100vh;\n        padding-top: 2.2rem;\n    }\n\n    .post-editor-form {\n        height: calc(100vh - 2.2rem);\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .post-editor-wrapper {\n        height: 100vh;\n        padding-top: 0;\n    }\n\n    .post-editor-form {\n        height: 100vh;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/PostEditorMarkdown.vue",
    "content": "<template>\n    <div\n        class=\"post-editor post-editor-markdown\"\n        ref=\"post-editor\"\n        :data-post-id=\"postID\">\n        <topbar-appbar />\n        <post-editor-top-bar\n            :itemType=\"itemType\" />\n\n        <div class=\"post-editor-wrapper\">\n            <div :class=\"{\n                'post-editor-form': true,\n                'sidebar-open': sidebarVisible\n            }\">\n                <div>\n                    <div\n                        id=\"post-title\"\n                        ref=\"post-title\"\n                        class=\"post-editor-form-title\"\n                        contenteditable=\"true\"\n                        :data-translation=\"itemType === 'post' ? $t('post.addPostTitle') : $t('page.addPageTitle')\"\n                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                        @paste.prevent=\"pasteTitle\"\n                        @keydown=\"detectEnterInTitle\"\n                        @input=\"updateTitle\" />\n\n                    <vue-easymde\n                        ref=\"markdownEditor\"\n                        v-model=\"postData.text\"\n                        name=\"markdown-editor\"\n                        :configs=\"editorConfig\" />\n\n                    <input\n                        name=\"image\"\n                        id=\"post-editor-fake-image-uploader\"\n                        class=\"is-hidden\"\n                        type=\"file\" />\n                </div>\n            </div>\n\n            <p-button\n                id=\"post-help-button\"\n                type=\"clean-invert icon small\"\n                icon=\"help\"\n                :title=\"$t('ui.help')\"\n                @click.native=\"toggleHelp\">\n                <template v-if=\"!helpPanelOpen\">{{ $t('editor.viewHelp') }}</template>\n                <template v-else>{{ $t('editor.hideHelp') }}</template>\n            </p-button>\n\n            <sidebar \n                :isVisible=\"sidebarVisible\"\n                :itemType=\"itemType\" />\n            <author-popup \n                :itemType=\"itemType\" />\n            <date-popup\n                :itemType=\"itemType\" />\n            <link-popup\n                ref=\"linkPopup\"\n                :markdown=\"true\" />\n            <help-panel-markdown \n                :isOpen=\"helpPanelOpen\" />\n        </div>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\nimport VueEasyMde from './post-editor/EasyMde';\nimport PostEditorSidebar from './post-editor/Sidebar';\nimport AuthorPopup from './post-editor/AuthorPopup';\nimport DatePopup from './post-editor/DatePopup';\nimport HelpPanelMarkdown from './post-editor/HelpPanelMarkdown';\nimport LinkPopup from './post-editor/LinkPopup';\nimport TopBarAppBar from './TopBarAppBar';\nimport PostEditorTopBar from './post-editor/TopBar';\nimport ItemHelper from './post-editor/ItemHelper';\nimport Utils from './../helpers/utils';\nimport InlineAttachment from './post-editor/CodeMirror/inline-attachment.js';\nimport InlineAttachmentC4 from './post-editor/CodeMirror/codemirror-4.inline-attachment.js';\nimport PostEditorsCommon from './mixins/PostEditorsCommon';\n\nexport default {\n    name: 'post-editor-markdown',\n    editorType: 'markdown',\n    mixins: [\n        PostEditorsCommon\n    ],\n    components: {\n        'author-popup': AuthorPopup,\n        'date-popup': DatePopup,\n        'link-popup': LinkPopup,\n        'sidebar': PostEditorSidebar,\n        'topbar-appbar': TopBarAppBar,\n        'post-editor-top-bar': PostEditorTopBar,\n        'help-panel-markdown': HelpPanelMarkdown,\n        'vue-easymde': VueEasyMde\n    },\n    data () {\n        return {\n            postID: this.$route.params.post_id || 0,\n            newPost: true,\n            helpPanelOpen: false,\n            postSlugEdited: false,\n            possibleDataLoss: false,\n            unwatchDataLoss: null,\n            sidebarVisible: false,\n            editorIsInitialized: false,\n            sidebarVisible: false,\n            editorConfig: {\n                status: false,\n                toolbar: false,\n                placeholder: this.$t('editor.startWriting'),\n                spellChecker: false,\n                promptURLs: true,\n                shortcuts: {\n                    toggleHeadingSmaller: this.isMac ? null : 'Ctrl-H'\n                }\n            },\n            postData: {\n                editor: this.$options.editorType,\n                title: '',\n                text: '',\n                slug: '',\n                author: 1,\n                tags: [],\n                mainTag: '',\n                template: '',\n                creationDate: {\n                    text: '',\n                    timestamp: 0\n                },\n                modificationDate: {\n                    text: '',\n                    timestamp: 0\n                },\n                isFeatured: false,\n                isHidden: false,\n                isExcludedOnHomepage: false,\n                status: '',\n                metaTitle: '',\n                metaDescription: '',\n                metaRobots: 'index, follow',\n                canonicalUrl: '',\n                featuredImage: {\n                    path: '',\n                    alt: '',\n                    caption: '',\n                    credits: ''\n                },\n                viewOptions: {}\n            }\n        };\n    },\n    computed: {\n        itemType () {\n            return this.$route.path.indexOf('/pages/editor/markdown/') > -1 ? 'page' : 'post';\n        },\n        isEdit () {\n            return !!this.postID;\n        },\n        isMac () {\n            return document.body.getAttribute('data-os') === 'osx';\n        },\n        easymde () {\n            return this.$refs.markdownEditor.easymde;\n        }\n    },\n    mounted () {\n        if (this.itemType === 'page') {\n            delete this.postData.tags;\n            delete this.postData.mainTag;\n            delete this.postData.isExcludedOnHomepage;\n            delete this.postData.isHidden;\n            delete this.postData.isFeatured;\n        }\n\n        window.onbeforeunload = e => {\n            if (this.possibleDataLoss) {\n                e.returnValue = false;\n\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('core.sureYouWantQuit'),\n                    isDanger: true,\n                    okClick: () => {\n                        window.onbeforeunload = null;\n                        mainProcessAPI.send('app-close', true);\n                    }\n                });\n            }\n        };\n\n        if (this.isEdit) {\n            this.newPost = false;\n            this.loadPostData();\n        } else {\n            this.setDataLossWatcher();\n        }\n\n        this.$bus.$on('date-changed', (timestamp) => {\n            let format = 'MMM DD, YYYY  HH:mm';\n\n            if(this.$store.state.app.config.timeFormat == 12) {\n                format = 'MMM DD, YYYY  hh:mm a';\n            }\n\n            this.postData.creationDate.timestamp = timestamp;\n            this.postData.creationDate.text = this.$moment(this.postData.creationDate.timestamp).format(format);\n        });\n\n        this.$bus.$on('post-editor-possible-data-loss', () => {\n            this.possibleDataLoss = true;\n        });\n\n        Vue.nextTick(() => {\n            this.easymde.codemirror.on('change', this.detectDataLoss);\n            window.prompt = this.linkPopupHandler;\n            this.$refs.linkPopup.setEasyMdeInstance(this.easymde);\n            inlineAttachment.editors.codemirror4.attach(this.easymde.codemirror, {});\n            this.$refs['post-title'].focus();\n        });\n    },\n    methods: {\n        detectEnterInTitle (event) {\n            if (event.code === 'Enter' && !event.isComposing) {\n                event.preventDefault();\n                this.easymde.codemirror.focus();\n            }\n        },\n        updateTitle () {\n            this.postData.title = this.$refs['post-title'].innerText.replace(/\\n/gmi, ' ');\n            this.$bus.$emit('update-post-slug', false);\n        },\n        loadPostData () {\n            // Send request for a post to the back-end\n            let requestType = 'app-post-load';\n            let responseType = 'app-post-loaded';\n\n            if (this.itemType === 'page') {\n                requestType = 'app-page-load';\n                responseType = 'app-page-loaded';\n            }\n\n            mainProcessAPI.send(requestType, {\n                'site': this.$store.state.currentSite.config.name,\n                'id': this.postID\n            });\n\n            // Load post data\n            mainProcessAPI.receiveOnce(responseType, (data) => {\n                if(data !== false && this.postID !== 0) {\n                    let loadedPostData = ItemHelper.loadItemData(data, this.$store, this.$moment, this.itemType);\n                    this.postData = Utils.deepMerge(this.postData, loadedPostData);\n                    this.$refs['post-title'].innerText = this.postData.title;\n                }\n\n                setTimeout(() => {\n                    this.possibleDataLoss = false;\n                    this.setDataLossWatcher();\n                }, 100);\n            });\n        },\n        setDataLossWatcher () {\n            this.unwatchDataLoss = this.$watch('postData', (newValue, oldValue) => {\n                this.possibleDataLoss = true;\n                this.unwatchDataLoss();\n            }, { deep: true });\n        },\n        async savePost (newPostStatus, preview = false, closeEditor = false) {\n            if (this.postData.title.trim() === '') {\n                this.$bus.$emit('alert-display', {\n                    message: this.itemType === 'page' ? this.$t('editor.cantSavePageWithEmptyTitle') : this.$t('editor.cantSavePostWithEmptyTitle')\n                });\n\n                return;\n            }\n\n            let postData = await ItemHelper.prepareItemData(newPostStatus, this.postID, this.$store, this.postData, this.itemType);\n\n            if(!preview) {\n                this.savingPost(newPostStatus, postData, closeEditor);\n            } else {\n                this.$bus.$emit('rendering-popup-display', {\n                    itemID: this.postID,\n                    postData: postData,\n                    postOnly: true,\n                    itemType: this.itemType\n                });\n            }\n        },\n        savingPost (newStatus, postData, closeEditor = false) {\n            // Send form data to the back-end\n            let requestType = 'app-post-save';\n            let responseType = 'app-post-saved';\n\n            if (this.itemType === 'page') {\n                requestType = 'app-page-save';\n                responseType = 'app-page-saved';\n            }\n\n            mainProcessAPI.send(requestType, postData);\n\n            // Post save\n            mainProcessAPI.receiveOnce(responseType, (data) => {\n                if (this.postID === 0) {\n                    this.postID = data.postID;\n                }\n\n                if (data.posts || data.pages) {\n                    this.savedPost(newStatus, data, closeEditor);\n                } else {\n                    alert(this.$t('editor.errorOccurred'));\n                }\n            });\n        },\n        savedPost (newStatus, updatedData, closeEditor = false) {\n            if (this.itemType === 'post') {\n                this.$store.commit('refreshAfterPostUpdate', updatedData);\n            } else {\n                this.postID = updatedData.pageID;\n                this.$bus.$emit('page-data-updated', updatedData.pageID);\n                this.$store.commit('refreshAfterPageUpdate', updatedData);\n            }\n\n            if (closeEditor) {\n                this.closeEditor();\n                return;\n            }\n\n            this.$router.push('/site/' + this.$route.params.name + '/' + this.itemType + 's/editor/markdown/' + this.postID);\n            let message = this.$t('editor.changesSaved');\n\n            if (this.newPost) {\n                this.newPost = false;\n\n                if (newStatus === 'draft') {\n                    message = this.$t('editor.newDraftCreated');\n                } else {\n                    if (this.itemType === 'page') {\n                        message = this.$t('editor.newPageCreated');\n                    } else {\n                        message = this.$t('editor.newPostCreated');\n                    }\n                }\n            }\n\n            this.$bus.$emit('message-display', {\n                message: message,\n                type: 'success',\n                lifeTime: 3\n            });\n\n            this.loadPostData();\n            this.possibleDataLoss = false;\n        },\n        toggleHelp () {\n            this.helpPanelOpen = !this.helpPanelOpen;\n        },\n        detectDataLoss () {\n            if (!this.editorIsInitialized) {\n                this.editorIsInitialized = true;\n                return;\n            }\n\n            this.$bus.$emit('post-editor-possible-data-loss');\n            this.easymde.codemirror.off('change', this.detectDataLoss);\n        },\n        linkPopupHandler (e) {\n            let selectedText = this.easymde.codemirror.getSelections();\n\n            if (selectedText && selectedText.length) {\n                selectedText = selectedText[0];\n            } else {\n                selectedText = '';\n            }\n\n            this.$bus.$emit('init-link-popup', {\n                postID: this.postID,\n                selection: selectedText\n            });\n        },\n        pasteTitle (e) {\n            let text = (e.originalEvent || e).clipboardData.getData('text/plain').replace(/\\n/gmi, '');\n            document.execCommand('insertText', false, text);\n        }\n    },\n    beforeDestroy () {\n        if (this.unwatchDataLoss) {\n            this.unwatchDataLoss();\n        }\n\n        this.$bus.$off('date-changed');\n        this.$bus.$off('post-editor-possible-data-loss');\n        window.prompt = () => false;\n        window.onbeforeunload = null;\n    }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n@import '../scss/editor/post-editors-common.scss';\n@import '../scss/editor/editor-markdown.scss';\n\n\n.post-editor-markdown {\n    overflow-x: hidden;\n    position: relative;\n    width: 100%;\n    z-index: 2;\n\n   .post-editor {\n        &-wrapper {\n            overflow: auto;\n            padding-top: var(--topbar-height);\n        }\n\n        &-form {\n            height: calc(100vh - var(--topbar-height));\n            overflow: scroll;\n\n            & > div {\n                padding: 9rem 4rem 3rem 4rem;\n            }\n\n            #post-title {\n                border: none;\n                box-shadow: none;\n                columns: var(--headings-color);\n                display: block;\n                font-family: -apple-system, BlinkMacSystemFont, Arial, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n                font-size: 3.6rem;\n                font-weight: var(--font-weight-bold);\n                letter-spacing: var(--letter-spacing);\n                line-height: 1.2;\n                margin: 0 10% 2.6rem;\n                padding: 0;\n                text-align: center;\n                width: 80%;\n\n                &:empty {\n                    color: var(--gray-4);\n\n                    &:before {\n                        content: attr(data-translation);\n                    }\n\n                    &:focus:before {\n                        content: \"\";\n                    }\n                }\n            }\n\n            .vue-easymde {\n                position: relative;\n                z-index: 1000;\n\n                .CodeMirror {\n                    border: none;\n                    height: auto!important;\n                    margin: 0 auto;\n                    max-width: var(--editor-width);\n                    padding: 0;\n\n                    .CodeMirror-selected {\n                        background: var(--text-selection-color)!important;\n                    }\n                }\n\n                .CodeMirror-advanced-dialog + .CodeMirror {\n                    padding-bottom: 80px;\n                }\n            }\n       }\n    }\n}\n\n@media (min-width: 1800px) {\n    .post-editor-markdown .post-editor-form #post-title {\n        margin: 0 auto 2.6rem;\n        max-width: calc(100vw - 880px);\n        width: 100%;\n    }\n}\n\n#post-help-button {\n    align-items: center;\n    bottom: .4rem;\n    display: flex;\n    height: 4.4;\n    line-height: 4.3rem;\n    position: absolute;\n    right: 1.8rem;\n    z-index: 99991;\n}\n\nbody[data-os=\"win\"] {\n    .post-editor-wrapper {\n        height: 100vh;\n        padding-top: 2.2rem;\n    }\n\n    .post-editor-form {\n        height: calc(100vh - 2.2rem);\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .post-editor-wrapper {\n        height: 100vh;\n        padding-top: 0;\n    }\n\n    .post-editor-form {\n        height: 100vh;\n    }\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/PostEditorTinyMCE.vue",
    "content": "<template>\n    <div class=\"post-editor post-editor-wysiwyg\" ref=\"post-editor\">\n        <topbar-appbar />\n        <post-editor-top-bar \n            :itemType=\"itemType\" />\n\n        <div class=\"post-editor-wrapper\">\n            <div :class=\"{\n                'post-editor-form': true,\n                'writers-panel-open': writersPanelOpen,\n                'sidebar-open': sidebarVisible\n            }\">\n                <div>\n                    <div\n                        id=\"post-title\"\n                        ref=\"post-title\"\n                        class=\"post-editor-form-title\"\n                        contenteditable=\"true\"\n                        :data-translation=\"itemType === 'post' ? $t('post.addPostTitle') : $t('page.addPageTitle')\"\n                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                        @paste.prevent=\"pasteTitle\"\n                        @keydown=\"detectEnterInTitle\"\n                        @input=\"updateTitle\" />\n\n                    <editor \n                        ref=\"tinymceEditor\" />\n\n                    <input\n                        name=\"image\"\n                        id=\"post-editor-fake-image-uploader\"\n                        class=\"is-hidden\"\n                        type=\"file\" />\n                </div>\n            </div>\n\n            <writers-panel \n                :isVisible=\"writersPanelOpen\" />\n\n            <p-button\n                id=\"post-stats-button\"\n                type=\"clean-invert icon small\"\n                icon=\"stats\"\n                :title=\"$t('editor.togglePostStatsPanel')\"\n                @click.native=\"togglePostStats\">\n                <template v-if=\"!writersPanelOpen\">{{ $t('editor.viewStats') }}</template>\n                <template v-else>{{ $t('editor.hideStats') }}</template>\n            </p-button>\n\n            <sidebar \n                :isVisible=\"sidebarVisible\"\n                :itemType=\"itemType\" />\n            <author-popup \n                :itemType=\"itemType\" />\n            <date-popup \n                :itemType=\"itemType\" />\n            <source-code-editor \n                ref=\"source-code-editor\" />\n            <link-popup />\n            <link-toolbar />\n            <gallery-popup />\n            <inline-editor \n                ref=\"inline-editor\" />\n        </div>\n    </div>\n</template>\n\n<script>\nimport PostEditorSidebar from './post-editor/Sidebar';\nimport InlineEditor from './post-editor/InlineEditor';\nimport PostEditorSourceCode from './post-editor/SourceCodeEditor';\nimport PostEditorWritersPanel from './post-editor/WritersPanel';\nimport AuthorPopup from './post-editor/AuthorPopup';\nimport DatePopup from './post-editor/DatePopup';\nimport LinkPopup from './post-editor/LinkPopup';\nimport LinkToolbar from './post-editor/LinkToolbar';\nimport GalleryPopup from './post-editor/GalleryPopup';\nimport Editor from './post-editor/Editor';\nimport TopBarAppBar from './TopBarAppBar';\nimport PostEditorTopBar from './post-editor/TopBar';\nimport ItemHelper from './post-editor/ItemHelper';\nimport Utils from './../helpers/utils';\nimport PostEditorsCommon from './mixins/PostEditorsCommon';\n\nexport default {\n    name: 'post-editor-tinymce',\n    editorType: 'tinymce',\n    mixins: [\n        PostEditorsCommon\n    ],\n    components: {\n        'author-popup': AuthorPopup,\n        'date-popup': DatePopup,\n        'link-popup': LinkPopup,\n        'gallery-popup': GalleryPopup,\n        'link-toolbar': LinkToolbar,\n        'editor': Editor,\n        'inline-editor': InlineEditor,\n        'sidebar': PostEditorSidebar,\n        'source-code-editor': PostEditorSourceCode,\n        'topbar-appbar': TopBarAppBar,\n        'writers-panel': PostEditorWritersPanel,\n        'post-editor-top-bar': PostEditorTopBar\n    },\n    data () {\n        return {\n            postID: this.$route.params.post_id || 0,\n            newPost: true,\n            writersPanelOpen: localStorage.getItem('publii-writers-panel') === 'opened',\n            postSlugEdited: false,\n            possibleDataLoss: false,\n            unwatchDataLoss: null,\n            sidebarVisible: false,\n            postData: {\n                editor: this.$options.editorType,\n                title: '',\n                text: '',\n                slug: '',\n                author: 1,\n                tags: [],\n                mainTag: '',\n                template: '',\n                creationDate: {\n                    text: '',\n                    timestamp: 0\n                },\n                modificationDate: {\n                    text: '',\n                    timestamp: 0\n                },\n                isFeatured: false,\n                isHidden: false,\n                isExcludedOnHomepage: false,\n                status: '',\n                metaTitle: '',\n                metaDescription: '',\n                metaRobots: 'index, follow',\n                canonicalUrl: '',\n                featuredImage: {\n                    path: '',\n                    alt: '',\n                    caption: '',\n                    credits: ''\n                },\n                viewOptions: {}\n            }\n        };\n    },\n    computed: {\n        itemType () {\n            return this.$route.path.indexOf('/pages/editor/tinymce/') > -1 ? 'page' : 'post';\n        },\n        isEdit () {\n            return !!this.postID;\n        },\n        extensionsPath () {\n            return [\n                'file:///',\n                this.$store.state.currentSite.siteDir,\n                '/input/themes/',\n                this.$store.state.currentSite.config.theme,\n                '/'\n            ].join('');\n        },\n        vendorPath () {\n            return [\n                'file:///',\n                this.$store.state.vendorPath\n            ].join('');\n        }\n    },\n    mounted () {\n        if (this.itemType === 'page') {\n            delete this.postData.tags;\n            delete this.postData.mainTag;\n            delete this.postData.isExcludedOnHomepage;\n            delete this.postData.isHidden;\n            delete this.postData.isFeatured;\n        }\n\n        window.onbeforeunload = e => {\n            if (this.possibleDataLoss) {\n                e.returnValue = false;\n\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('core.sureYouWantQuit'),\n                    isDanger: true,\n                    okClick: () => {\n                        window.onbeforeunload = null;\n                        mainProcessAPI.send('app-close', true);\n                    }\n                });\n            }\n        };\n\n        if (this.isEdit) {\n            this.newPost = false;\n            this.loadPostData();\n        } else {\n            this.$refs['tinymceEditor'].init();\n            this.setDataLossWatcher();\n        }\n\n        this.loadAdditionalScripts();\n\n        this.$bus.$on('date-changed', (timestamp) => {\n            let format = 'MMM DD, YYYY  HH:mm';\n\n            if(this.$store.state.app.config.timeFormat == 12) {\n                format = 'MMM DD, YYYY  hh:mm a';\n            }\n\n            this.postData.creationDate.timestamp = timestamp;\n            this.postData.creationDate.text = this.$moment(this.postData.creationDate.timestamp).format(format);\n        });\n\n        this.$bus.$on('post-editor-possible-data-loss', () => {\n            this.possibleDataLoss = true;\n        });\n\n        this.$refs['post-title'].focus();\n    },\n    methods: {\n        updateTitle () {\n            this.postData.title = this.$refs['post-title'].innerText.replace(/\\n/gmi, ' ');\n            this.$bus.$emit('update-post-slug', false);\n        },\n        detectEnterInTitle (event) {\n            if (event.code === 'Enter' && !event.isComposing) {\n                event.preventDefault();\n                this.$refs['tinymceEditor'].focus();\n            }\n        },\n        loadPostData () {\n            // Send request for a post to the back-end\n            let requestType = 'app-post-load';\n            let responseType = 'app-post-loaded';\n\n            if (this.itemType === 'page') {\n                requestType = 'app-page-load';\n                responseType = 'app-page-loaded';\n            }\n\n            mainProcessAPI.send(requestType, {\n                'site': this.$store.state.currentSite.config.name,\n                'id': this.postID\n            });\n\n            // Load post data\n            mainProcessAPI.receiveOnce(responseType, (data) => {\n                if (data !== false && this.postID !== 0) {\n                    let loadedPostData = ItemHelper.loadItemData(data, this.$store, this.$moment, this.itemType);\n                    this.postData = Utils.deepMerge(this.postData, loadedPostData);\n                    this.$refs['post-title'].innerText = this.postData.title;\n                    $('#post-editor').val(this.postData.text);\n                    this.$refs['tinymceEditor'].init();\n                }\n                \n                setTimeout(() => {\n                    this.possibleDataLoss = false;\n                    this.setDataLossWatcher();\n                }, 100);\n            });\n        },\n        setDataLossWatcher () {\n            this.unwatchDataLoss = this.$watch('postData', (newValue, oldValue) => {\n                this.possibleDataLoss = true;\n                this.unwatchDataLoss();\n            }, { deep: true });\n        },\n        async savePost (newPostStatus, preview = false, closeEditor = false) {\n            if (this.postData.title.trim() === '') {\n                this.$bus.$emit('alert-display', {\n                    message: this.itemType === 'page' ? this.$t('editor.cantSavePageWithEmptyTitle') : this.$t('editor.cantSavePostWithEmptyTitle')\n                });\n\n                return;\n            }\n\n            tinymce.triggerSave();\n            let postData = await ItemHelper.prepareItemData(newPostStatus, this.postID, this.$store, this.postData, this.itemType);\n\n            if(!preview) {\n                this.savingPost(newPostStatus, postData, closeEditor);\n            } else {\n                this.$bus.$emit('rendering-popup-display', {\n                    itemID: this.postID,\n                    postData: postData,\n                    postOnly: true,\n                    itemType: this.itemType\n                });\n            }\n        },\n        savingPost (newStatus, postData, closeEditor = false) {\n            // Send form data to the back-end\n            let requestType = 'app-post-save';\n            let responseType = 'app-post-saved';\n\n            if (this.itemType === 'page') {\n                requestType = 'app-page-save';\n                responseType = 'app-page-saved';\n            }\n\n            mainProcessAPI.send(requestType, postData);\n\n            // Post save\n            mainProcessAPI.receiveOnce(responseType, (data) => {\n                if (this.postID === 0) {\n                    this.postID = data.postID;\n                }\n\n                if (data.posts || data.pages) {\n                    this.savedPost(newStatus, data, closeEditor);\n                } else {\n                    alert(this.$t('editor.errorOccurred'));\n                }\n            });\n        },\n        savedPost (newStatus, updatedData, closeEditor = false) {\n            if (this.itemType === 'post') {\n                this.$store.commit('refreshAfterPostUpdate', updatedData);\n            } else {\n                this.postID = updatedData.pageID;\n                this.$bus.$emit('page-data-updated', updatedData.pageID);\n                this.$store.commit('refreshAfterPageUpdate', updatedData);\n            }\n\n            if (closeEditor) {\n                this.closeEditor();\n                return;\n            } else {\n                this.$refs['tinymceEditor'].editorInstance.updateItemID(this.postID);\n            }\n\n            this.$router.push('/site/' + this.$route.params.name + '/' + this.itemType + 's/editor/tinymce/' + this.postID);\n            let message = this.$t('editor.changesSaved');\n\n            if (this.newPost) {\n                this.newPost = false;\n\n                if (newStatus === 'draft') {\n                    message = this.$t('editor.newDraftCreated');\n                } else {\n                    if (this.itemType === 'page') {\n                        message = this.$t('editor.newPageCreated');\n                    } else {\n                        message = this.$t('editor.newPostCreated');\n                    }\n                }\n            }\n\n            this.$bus.$emit('message-display', {\n                message: message,\n                type: 'success',\n                lifeTime: 3\n            });\n\n            this.loadPostData();\n            this.possibleDataLoss = false;\n        },\n        togglePostStats () {\n            if (this.writersPanelOpen) {\n                this.writersPanelOpen = false;\n                localStorage.setItem('publii-writers-panel', 'closed');\n            } else {\n                this.writersPanelOpen = true;\n                localStorage.setItem('publii-writers-panel', 'opened');\n            }\n        },\n        pasteTitle (e) {\n            let text = (e.originalEvent || e).clipboardData.getData('text/plain').replace(/\\n/gmi, '');\n            document.execCommand('insertText', false, text);\n        },\n        loadAdditionalScripts () {\n            // Add custom editor script\n            if (\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.extensions &&\n                this.$store.state.currentSite.themeSettings.extensions.postEditorCustomScript\n            ) {\n                let customEditorScriptPath = this.extensionsPath + 'tinymce.script.js';\n\n                if (!document.querySelector('#custom-post-editor-script')) {\n                    $(document.body).append(\n                        // It seems that Webpack goes crazy when it sees 'script' tag :)\n                        $('<' + 'script' + ' id=\"custom-post-editor-script\" src=\"' + customEditorScriptPath + '\"></' + 'script' + '>')\n                    );\n                }\n            }\n\n            // Add prism.js script\n            if (!document.querySelector('#custom-prismjs-script')) {\n                $(document.body).append(\n                    // It seems that Webpack goes crazy when it sees 'script' tag :)\n                    $('<' + 'script' + ' id=\"custom-prismjs-script\" src=\"' + this.vendorPath + '/prism.js\"></' + 'script' + '>')\n                );\n            }\n        }\n    }, \n    beforeDestroy () {\n        if (this.unwatchDataLoss) {\n            this.unwatchDataLoss();\n        }\n\n        $('#custom-post-editor-script').remove();\n        this.$bus.$off('date-changed');\n        this.$bus.$off('post-editor-possible-data-loss');\n        window.onbeforeunload = null;\n    }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n@import '../scss/editor/post-editors-common.scss';\n@import '../scss/editor/editor-overrides.scss';\n\n.post-editor {\n    overflow-x: hidden;\n    position: relative;\n    width: 100%;\n    z-index: 2;\n\n    &-form {\n        #post-title {\n            border: none;\n            box-shadow: none;\n            color: var(--headings-color);\n            display: block;\n            font-family: -apple-system, BlinkMacSystemFont, Arial, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n            font-size: 3.6rem;\n            font-weight: var(--font-weight-bold);\n            letter-spacing: var(--letter-spacing);\n            line-height: 1.2;\n            margin: 0 10% 2.6rem;\n            padding: 0;\n            text-align: center;\n            width: 80%;\n\n            &:empty {\n                color: var(--gray-4);\n\n                &:before {\n                    content: attr(data-translation);\n                }\n\n                &:focus:before {\n                    content: \"\";\n                }\n            }\n        }\n\n        #post-editor_ifr {\n            height: calc( 100vh - 30rem )!important;\n        }\n    }\n}\n\n@media (min-width: 1800px) {\n    .post-editor-form #post-title {\n        margin: 0 auto 2.6rem;\n        max-width: calc(100vw - 880px);\n        width: 100%;\n    }\n}\n\n/*\n * Special styles for win & linux\n */\n\nbody[data-os=\"win\"] {\n    .post-editor-form {\n        #post-editor_ifr {\n            height: calc( 100vh - 31rem )!important;\n        }\n    }\n\n    .post-editor {\n        #inline-toolbar {\n            padding-top: 0;\n            padding-bottom: 0;\n\n            select {\n                padding-top: 0;\n                padding-bottom: 0;\n                top: 0;\n            }\n        }\n\n        #link-toolbar {\n            padding-top: .75rem;\n        }\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .post-editor-form {\n        #post-editor_ifr {\n            height: calc( 100vh - 26.8rem )!important;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Posts.vue",
    "content": "<template>\n    <section class=\"content\">\n        <p-header\n            v-if=\"hasPosts\"\n            :title=\"$t('ui.posts')\">\n            <header-search\n                slot=\"search\"\n                ref=\"search\"\n                :placeholder=\"$t('post.filterOrSearchPosts')\"\n                onChangeEventName=\"posts-filter-value-changed\" />\n\n            <btn-dropdown\n                slot=\"buttons\"\n                buttonColor=\"green\"\n                localStorageKey=\"publii-current-editor\"\n                :previewIcon=\"true\"\n                :items=\"dropdownItems\"\n                defaultValue=\"tinymce\" />\n        </p-header>\n\n        <ul\n            v-if=\"dataLoaded && hasPosts\"\n            class=\"filters\">\n            <li\n                :class=\"filterCssClasses('all')\"\n                @click=\"setFilter('')\">\n                {{ $t('post.all') }} <span class=\"filter-count\">({{ counters.all }})</span>\n            </li>\n\n            <li\n                :class=\"filterCssClasses('published')\"\n                @click=\"setFilter('is:published')\">\n                {{ $t('post.published') }} <span class=\"filter-count\">({{ counters.published }})</span>\n            </li>\n\n            <li\n                v-if=\"counters.featured\"\n                :class=\"filterCssClasses('featured')\"\n                @click=\"setFilter('is:featured')\">\n                {{ $t('post.featured') }} <span class=\"filter-count\">({{ counters.featured }})</span>\n            </li>\n\n            <li\n                v-if=\"counters.hidden\"\n                :class=\"filterCssClasses('hidden')\"\n                @click=\"setFilter('is:hidden')\">\n                {{ $t('post.hidden') }} <span class=\"filter-count\">({{ counters.hidden }})</span>\n            </li>\n\n            <li\n                v-if=\"counters.excluded\"\n                :class=\"filterCssClasses('excluded')\"\n                @click=\"setFilter('is:excluded')\">\n                {{ $t('post.excluded') }} <span class=\"filter-count\">({{ counters.excluded }})</span>\n            </li>\n\n            <li\n                v-if=\"counters.drafts\"\n                :class=\"filterCssClasses('draft')\"\n                @click=\"setFilter('is:draft')\">\n                {{ $t('post.drafts') }} <span class=\"filter-count\">({{ counters.drafts }})</span>\n            </li>\n\n            <li\n                v-if=\"counters.trashed\"\n                :class=\"filterCssClasses('trashed')\"\n                @click=\"setFilter('is:trashed')\">\n                {{ $t('post.trashed') }} <span class=\"filter-count\">({{counters.trashed }})</span>\n            </li>\n        </ul>\n\n\n        <collection\n            v-if=\"dataLoaded && !emptySearchResults && hasPosts\"\n            :itemsCount=\"showModificationDate && showModificationDateAsColumn ? 6 : 5\">\n            <collection-header slot=\"header\">\n                <collection-cell>\n                    <checkbox\n                        value=\"all\"\n                        :checked=\"anyCheckboxIsSelected\"\n                        :onClick=\"toggleAllCheckboxes.bind(this, false)\"\n                        @click.native=\"$bus.$emit('document-body-clicked')\" />\n                </collection-cell>\n\n                <collection-cell>\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('title')\">\n                        <template v-if=\"orderBy === 'title'\">\n                            <strong>{{ $t('post.title') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('post.title') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'title' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'title' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell>\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('created')\">\n                        <template v-if=\"orderBy === 'created'\">\n                            <strong>{{ $t('post.publicationDate') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('post.publicationDate') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'created' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'created' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell\n                    v-if=\"showModificationDate && showModificationDateAsColumn\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('modified')\">\n                        <template v-if=\"orderBy === 'modified'\">\n                            <strong>{{ $t('post.modificationDate') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('post.modificationDate') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'modified' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'modified' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell min-width=\"110px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('author')\">\n                        <template v-if=\"orderBy === 'author'\">\n                            <strong>{{ $t('author.author') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('author.author') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'author' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'author' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell min-width=\"35px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('id')\">\n                        <template v-if=\"orderBy === 'id'\">\n                            <strong>{{ $t('ui.id') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.id') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'id' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'id' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <div\n                    v-if=\"anyCheckboxIsSelected\"\n                    class=\"tools\">\n                    <p-button\n                        v-if=\"trashVisible\"\n                        icon=\"delete\"\n                        type=\"small light icon delete\"\n                        :onClick=\"bulkDelete\">\n                        {{ $t('ui.delete') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"trashVisible\"\n                        icon=\"restore\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkRestore\">\n                        {{ $t('file.restore') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"!trashVisible\"\n                        icon=\"trash\"\n                        type=\"small light icon delete\"\n                        :onClick=\"bulkTrash\">\n                        {{ $t('post.moveToTrash') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"!trashVisible\"\n                        icon=\"duplicate\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkDuplicate\">\n                        {{ $t('post.duplicate') }}\n                    </p-button>\n\n                    <div\n                        v-if=\"!trashVisible\"\n                        class=\"dropdown-wrapper\">\n                        <p-button\n                            icon=\"more\"\n                            :type=\"bulkDropdownVisible ? 'small light icon active' : 'small light icon'\"\n                            @click.native.stop=\"toggleBulkDropdown\">\n                            {{ $t('ui.more') }}\n                        </p-button>\n\n                        <ul\n                            v-if=\"bulkDropdownVisible\"\n                            class=\"dropdown\">\n                            <li\n                                v-if=\"selectedPostsNeedsStatus('published')\"\n                                @click=\"bulkPublish\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"publish-post\" />\n                                {{ $t('post.publish') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPostsNeedsStatus('draft')\"\n                                @click=\"bulkUnpublish\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"draft-post\" />\n                                {{ $t('post.markAsDraft') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPostsNeedsStatus('featured')\"\n                                @click=\"bulkFeatured\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"featured-post\"\n                                    strokeColor=\"color-helper-6\" />\n                                {{ $t('post.markAsFeatured') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPostsHaveStatus('featured')\"\n                                @click=\"bulkUnfeatured\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"unfeatured-post\" \n                                    strokeColor=\"color-helper-6\" />\n                                {{ $t('post.markAsUnfeatured') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPostsNeedsStatus('excluded_homepage')\"\n                                @click=\"bulkExclude\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"excluded-post\"\n                                    strokeColor=\"color-3\" />\n                                {{ $t('post.excludeFromHomepage') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPostsHaveStatus('excluded_homepage')\"\n                                @click=\"bulkInclude\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"included-post\" \n                                    strokeColor=\"color-3\" />\n                                {{ $t('post.includeInHomepage') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPostsNeedsStatus('hidden')\"\n                                @click=\"bulkHide\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"hidden-post\" />\n                                {{ $t('ui.hide') }}\n                            </li>\n                            <li\n                                v-if=\"selectedPostsHaveStatus('hidden')\"\n                                @click=\"bulkUnhide\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"unhidden-post\" />\n                                {{ $t('ui.unhide') }}\n                            </li>\n                            <li\n                                @click=\"bulkConvertToPage\">\n                                <icon\n                                    size=\"xs\"\n                                    name=\"convert-to-page\"/>\n                                {{ $t('post.convertToPage') }}\n                            </li>\n                        </ul>\n                    </div>\n                </div>\n            </collection-header>\n\n            <collection-row\n                v-for=\"(item, index) in items\"\n                slot=\"content\"\n                :data-is-draft=\"item.isDraft\"\n                :key=\"'collection-row-' + index\">\n                <collection-cell>\n                    <checkbox\n                        :value=\"item.id\"\n                        :checked=\"isChecked(item.id)\"\n                        :onClick=\"toggleSelection\"\n                        :key=\"'collection-row-checkbox-' + index\" />\n                </collection-cell>\n\n                <collection-cell\n                    type=\"titles\">\n                    <h2 class=\"title\">\n                        <a\n                            href=\"#\"\n                            @click.prevent.stop=\"editPost(item.id, item.editor)\">\n\n                            {{ item.title }}\n\n                            <icon\n                                v-if=\"item.isFeatured\"\n                                size=\"xs\"\n                                name=\"featured-post\"\n                                strokeColor=\"color-helper-6\"\n                                :title=\"$t('post.thisPostIsFeatured')\" />\n                            <icon\n                                v-if=\"item.isHidden\"\n                                size=\"xs\"\n                                name=\"hidden-post\"\n                                strokeColor=\"color-7\"\n                                :title=\"$t('post.thisPostIsHidden')\" />\n                            <icon\n                                v-if=\"item.isExcludedOnHomepage\"\n                                name=\"excluded-post\"\n                                size=\"xs\"\n                                strokeColor=\"color-3\"\n                                :title=\"$t('post.thisPostIsExcludedFromHomepage')\" />\n                            <icon\n                                v-if=\"item.isDraft\"\n                                size=\"xs\"\n                                name=\"draft-post\"\n                                strokeColor=\"color-7\"\n                                :title=\"$t('post.thisPostIsADraft')\" />\n                        </a>\n                    </h2>\n\n                    <div\n                        v-if=\"showPostSlugs\"\n                        class=\"post-slug\">\n                        {{ $t('post.url') }}: /{{ item.slug }}<template v-if=\"!$store.state.currentSite.config.advanced.urls.cleanUrls\">.html</template>\n                    </div>\n\n                    <div\n                        v-if=\"showPostTags && item.tags\"\n                        class=\"post-tags\"\n                        style=\"width: 100%;\">\n                        <a\n                            v-for=\"tag in item.tags\"\n                            href=\"#\"\n                            :class=\"{ 'tag': true, 'is-main-tag': tag.id === item.mainTag }\"\n                            :key=\"'tag-' + tag.id\"\n                            @click.stop.prevent=\"setFilter('tag:' + tag.name)\">\n                            #{{ tag.name }}\n                        </a>\n                    </div>\n                </collection-cell>\n\n                <collection-cell\n                    type=\"publish-dates\">\n                    <span class=\"publish-date\">{{ getCreationDate(item.created) }}</span>\n                    <span\n                        v-if=\"!showModificationDateAsColumn && showModificationDate\"\n                        class=\"modify-date\">\n                        {{ $t('ui.lastModified') }}: {{ getModificationDate(item.modified) }}\n                    </span>\n                </collection-cell>\n\n                <collection-cell\n                    v-if=\"showModificationDate && showModificationDateAsColumn\"\n                    type=\"modification-dates\">\n                    <span class=\"modify-date\">\n                        {{ getModificationDate(item.modified) }}\n                    </span>\n                </collection-cell>\n\n                <collection-cell\n                    type=\"authors\">\n                    <a\n                        href=\"#\"\n                        @click.prevent.stop=\"setFilter('author:' + item.author)\">\n                        {{ item.author }}\n                    </a>\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.id }}\n                </collection-cell>\n            </collection-row>\n        </collection>\n\n        <empty-state\n            v-if=\"emptySearchResults\"\n            :description=\"$t('post.noPostsMatchingYourCriteria')\"></empty-state>\n\n        <div\n            v-if=\"dataLoaded && !hasPosts\"\n            class=\"empty-state post\">\n\n           <div>\n                <img :src=\"'../src/assets/svg/' + appTheme + '/wysiwyg-editor.svg'\" height=\"286\" width=\"331\" />\n                <h3>{{ $t('post.editorWYSIWYG') }}</h3>\n                <p>{{ $t('post.editorWYSIWYGInfo') }}</p>\n                <p-button\n                    slot=\"button\"\n                    icon=\"add-site-mono\"\n                    type=\"icon\"\n                    :onClick=\"addNewPost.bind(this, 'tinymce')\">\n                    {{ $t('post.addNewPost') }}\n                </p-button>\n           </div>\n\n           <div>\n                <img :src=\"'../src/assets/svg/' + appTheme + '/block-editor.svg'\" height=\"286\" width=\"331\" />\n                <h3>{{ $t('post.editorBlock') }}</h3>\n                <p>{{ $t('post.editorBlockInfo') }}</p>\n                <p-button\n                    slot=\"button\"\n                    icon=\"add-site-mono\"\n                    type=\"icon\"\n                    :onClick=\"addNewPost.bind(this, 'blockeditor')\">\n                    {{ $t('post.addNewPost') }}\n                </p-button>\n           </div>\n\n           <div>\n                <img :src=\"'../src/assets/svg/' + appTheme + '/markdown-editor.svg'\" height=\"286\" width=\"331\" />\n                <h3>{{ $t('post.editorMarkdown') }}</h3>\n                <p>{{ $t('post.editorMarkdownInfo') }}</p>\n                <p-button\n                    slot=\"button\"\n                    icon=\"add-site-mono\"\n                    type=\"icon\"\n                    :onClick=\"addNewPost.bind(this, 'markdown')\">\n                    {{ $t('post.addNewPost') }}\n                </p-button>\n           </div>\n        </div>\n    </section>\n</template>\n\n<script>\nimport CollectionCheckboxes from './mixins/CollectionCheckboxes.js';\n\nexport default {\n    name: 'posts',\n    mixins: [\n        CollectionCheckboxes\n    ],\n    data () {\n        return {\n            appTheme: '',\n            bulkDropdownVisible: false,\n            dataLoaded: false,\n            filterValue: '',\n            selectedItems: [],\n            orderBy: 'id',\n            order: 'DESC'\n        };\n    },\n    computed: {\n        items () {\n            let items = this.$store.getters.sitePosts(this.filterValue, this.orderBy, this.order);\n            items = items.filter(item => item.title !== null);\n\n            items.forEach((item, i) => {\n                if (item.tags.length) {\n                    item.tags.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name));\n                }\n            });\n\n            return items;\n        },\n        hasPosts () {\n            return this.$store.state.currentSite.posts && !!this.$store.state.currentSite.posts.length;\n        },\n        emptySearchResults () {\n            return this.filterValue !== '' && !this.items.length;\n        },\n        trashVisible () {\n            return this.filterValue.indexOf('is:trashed') > -1;\n        },\n        counters () {\n            if(!this.$store.state.currentSite || !this.$store.state.currentSite.posts) {\n                return {\n                    all: 0,\n                    published: 0,\n                    featured: 0,\n                    hidden: 0,\n                    excluded: 0,\n                    drafts: 0,\n                    trashed: 0\n                };\n            }\n\n            return {\n                all: this.$store.state.currentSite.posts.filter((post) => post.status.indexOf('trashed') === -1).length,\n                published: this.$store.state.currentSite.posts.filter((post) => post.status.indexOf('trashed') === -1 && post.status.indexOf('draft') === -1).length,\n                featured: this.$store.state.currentSite.posts.filter((post) => post.status.indexOf('trashed') === -1 && post.status.indexOf('featured') > -1).length,\n                hidden: this.$store.state.currentSite.posts.filter((post) => post.status.indexOf('trashed') === -1 && post.status.indexOf('hidden') > -1).length,\n                excluded: this.$store.state.currentSite.posts.filter((post) => post.status.indexOf('trashed') === -1 && post.status.indexOf('excluded_homepage') > -1).length,\n                drafts: this.$store.state.currentSite.posts.filter((post) => post.status.indexOf('trashed') === -1 && post.status.indexOf('draft') > -1).length,\n                trashed: this.$store.state.currentSite.posts.filter((post) => post.status.indexOf('trashed') > -1).length\n            }\n        },\n        showModificationDate () {\n            return this.$store.state.app.config.showModificationDate;\n        },\n        showModificationDateAsColumn () {\n            return this.$store.state.app.config.showModificationDateAsColumn;\n        },\n        showPostTags () {\n            return this.$store.state.app.config.showPostTags;\n        },\n        dropdownItems () {\n            return [\n                {\n                    label: this.$t('post.editorWYSIWYGUse'),\n                    activeLabel: this.$t('post.addNewPost'),\n                    value: 'tinymce',\n                    icon: 'wysiwyg',\n                    isVisible: () => true,\n                    onClick: this.addNewPost.bind(this, 'tinymce')\n                },\n                {\n                    label: this.$t('post.editorBlockUse'),\n                    activeLabel: this.$t('post.addNewPost'),\n                    value: 'blockeditor',\n                    icon: 'block',\n                    isVisible: () => true,\n                    onClick: this.addNewPost.bind(this, 'blockeditor')\n                },\n                {\n                    label: this.$t('post.editorMarkdownUse'),\n                    activeLabel: this.$t('post.addNewPost'),\n                    value: 'markdown',\n                    icon: 'markdown',\n                    isVisible: () => true,\n                    onClick: this.addNewPost.bind(this, 'markdown')\n                }\n            ]\n        },\n        showPostSlugs () {\n            return this.$store.state.app.config.showPostSlugs;\n        }\n    },\n    async mounted () {\n        this.appTheme = await this.$root.getCurrentAppTheme();\n        this.orderBy = this.$store.state.ordering.posts.orderBy;\n        this.order = this.$store.state.ordering.posts.order;\n        this.$bus.$on('site-loaded', this.whenSiteLoaded);\n\n        this.$bus.$on('posts-filter-value-changed', (newValue) => {\n            this.filterValue = newValue.trim().toLowerCase();\n        });\n\n        this.$bus.$on('document-body-clicked', this.closeBulkDropdown);\n\n        // It is available when user comes from Tags/Authors views\n        let newFilterValue = localStorage.getItem('publii-posts-search-value');\n\n        if(newFilterValue) {\n            localStorage.removeItem('publii-posts-search-value');\n            setTimeout (() => {\n                this.setFilter(newFilterValue);\n            }, 0);\n        }\n\n        this.$bus.$on('site-switched', () => {\n            setTimeout(() => {\n                this.saveOrdering(this.$store.state.ordering.posts.orderBy, this.$store.state.ordering.posts.order);\n            }, 500);\n        });\n\n        this.$bus.$on('app-settings-saved', newSettings => {\n            if (this.orderBy + ' ' + this.order !== newSettings.postsOrdering) {\n                let order = newSettings.postsOrdering.split(' ');\n                this.saveOrdering(order[0], order[1]);\n            }\n        });\n\n        if (this.$store.state.currentSite.posts) {\n            this.dataLoaded = true;\n        }\n\n        if (this.$route.params.filter === 'trashed') {\n            this.setFilter('is:trashed');\n        }\n    },\n    methods: {\n        addNewPost (editorType) {\n            if (\n                editorType === 'blockeditor' &&\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                !this.$store.state.currentSite.themeSettings.supportedFeatures.blockEditor\n            ) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('post.editorBlockNotSupportedNewPostInfo'),\n                    okLabel: this.$t('post.openEditorAnyway'),\n                    isDanger: true,\n                    okClick: () => {\n                        this.openEditor(false, editorType);\n                    }\n                });\n                return;\n            }\n\n            this.openEditor(false, editorType);\n        },\n        editPost (id, editorType) {\n            if (\n                editorType === 'blockeditor' &&\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                !this.$store.state.currentSite.themeSettings.supportedFeatures.blockEditor\n            ) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('post.editorBlockNotSupportedEditPostInfo'),\n                    okLabel: this.$t('post.editPostAnyway'),\n                    isDanger: true,\n                    okClick: () => {\n                        this.openEditor(id, editorType);\n                    }\n                });\n                return;\n            }\n\n            this.openEditor(id, editorType);\n        },\n        openEditor (id, editorType) {\n            let siteName = this.$route.params.name;\n\n            if(this.filterValue.trim() !== '' && this.$store.state.app.config.alwaysSaveSearchState) {\n                localStorage.setItem('publii-posts-search-value', this.filterValue);\n            }\n\n            this.$store.commit('setEditorOpenState', true);\n            this.$router.push('/site/' + siteName + '/posts/editor/' + editorType + '/' + (id !== false ? id : ''));\n            return false;\n        },\n        setFilter (newValue) {\n            if (this.$refs.search) {\n                this.$refs.search.isOpen = newValue !== '';\n                this.$refs.search.value = newValue;\n                this.$refs.search.updateValue();\n            }\n        },\n        filterCssClasses (type) {\n            if(type !== 'all') {\n                return {\n                    'filter-value': true,\n                    'filter-active': this.filterValue.indexOf('is:' + type) === 0\n                };\n            }\n\n            return {\n                'filter-value': true,\n                'filter-active': this.filterValue.indexOf('is:') === -1\n            };\n        },\n        getModificationDate (timestamp) {\n            return this.$moment(timestamp).fromNow();\n        },\n        getCreationDate (timestamp) {\n            if(this.$store.state.app.config.timeFormat == 12) {\n                return this.$moment(timestamp).format('MMM DD, YYYY  hh:mm a');\n            } else {\n                return this.$moment(timestamp).format('MMM DD, YYYY  HH:mm');\n            }\n        },\n        bulkDelete () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('post.removePostMessage'),\n                isDanger: true,\n                okClick: this.deleteSelected\n            });\n        },\n        deleteSelected () {\n            let itemsToRemove = this.getSelectedItems();\n\n            mainProcessAPI.send('app-post-delete', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToRemove\n            });\n\n            mainProcessAPI.receiveOnce('app-post-deleted', () => {\n                this.$store.commit('removePosts', itemsToRemove);\n                this.selectedItems = [];\n\n                this.$bus.$emit('message-display', {\n                    message: this.$t('post.removePostSuccessMessage'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n\n                if (this.counters.trashed === 0) {\n                    this.filterValue = '';\n                }\n            });\n        },\n        bulkTrash () {\n            this.changeStateForSelected('trashed');\n        },\n        bulkPublish () {\n            this.changeStateForSelected('published');\n            this.changeStateForSelected('draft', true);\n        },\n        bulkUnpublish () {\n            this.changeStateForSelected('published', true);\n            this.changeStateForSelected('draft');\n        },\n        bulkFeatured () {\n            this.changeStateForSelected('featured');\n        },\n        bulkUnfeatured () {\n            this.changeStateForSelected('featured', true);\n        },\n        bulkExclude () {\n            this.changeStateForSelected('excluded_homepage');\n        },\n        bulkInclude () {\n            this.changeStateForSelected('excluded_homepage', true);\n        },\n        bulkHide () {\n            this.changeStateForSelected('hidden');\n        },\n        bulkUnhide () {\n            this.changeStateForSelected('hidden', true);\n        },\n        bulkDuplicate () {\n            let itemsToDuplicate = this.getSelectedItems();\n\n            mainProcessAPI.send('app-post-duplicate', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToDuplicate\n            });\n\n            mainProcessAPI.receiveOnce('app-post-duplicated', (data) => {\n                if(!data) {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('post.duplicatePostErrorMessage'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n\n                    return;\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('post.duplicatePostSuccessMessage'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                }\n\n                this.selectedItems = [];\n\n                mainProcessAPI.send('app-site-reload', {\n                    siteName: this.$store.state.currentSite.config.name\n                });\n\n                mainProcessAPI.receiveOnce('app-site-reloaded', (result) => {\n                    this.$store.commit('setSiteConfig', result);\n                    this.$store.commit('switchSite', result.data);\n                });\n            });\n        },\n        bulkRestore () {\n            this.changeStateForSelected('trashed', true);\n        },\n        bulkConvertToPage () {\n            let itemsToChange = this.getSelectedItems();\n\n            this.$store.commit('changePostsToPages', {\n                postIDs: itemsToChange\n            });\n\n            mainProcessAPI.send('app-post-status-change', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToChange,\n                \"status\": 'is-page',\n                \"inverse\": false\n            });\n\n            mainProcessAPI.send('app-pages-hierarchy-update', {\n                postIDs: itemsToChange,\n                siteName: this.$store.state.currentSite.config.name\n            });\n\n            mainProcessAPI.receiveOnce('app-post-status-changed', () => {\n                this.selectedItems = [];\n            });\n\n            this.$bus.$emit('message-display', {\n                message: this.$t('post.postStatusChangeSuccessMessage'),\n                type: 'success',\n                lifeTime: 3\n            });\n        },\n        changeStateForSelected (status, inverse = false) {\n            let itemsToChange = this.getSelectedItems();\n\n            this.$store.commit('changePostsStatus', {\n                postIDs: itemsToChange,\n                status: status,\n                inverse: inverse\n            });\n\n            mainProcessAPI.send('app-post-status-change', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToChange,\n                \"status\": status,\n                \"inverse\": inverse\n            });\n\n            mainProcessAPI.receiveOnce('app-post-status-changed', () => {\n                this.selectedItems = [];\n            });\n\n            this.$bus.$emit('message-display', {\n                message: this.$t('post.postStatusChangeSuccessMessage'),\n                type: 'success',\n                lifeTime: 3\n            });\n        },\n        whenSiteLoaded () {\n            this.dataLoaded = true;\n\n            setTimeout(() => {\n                this.setFilter('');\n            }, 0);\n        },\n        ordering (field) {\n            if (field !== this.orderBy) {\n                this.orderBy = field;\n                this.order = 'DESC';\n            } else {\n                if (this.order === 'DESC') {\n                    this.order = 'ASC';\n                } else {\n                    this.order = 'DESC';\n                }\n            }\n\n            this.saveOrdering(this.orderBy, this.order);\n        },\n        saveOrdering (orderBy, order) {\n            this.orderBy = orderBy;\n            this.order = order;\n\n            this.$store.commit('setOrdering', {\n                type: 'posts',\n                orderBy: this.orderBy,\n                order: this.order\n            });\n        },\n        toggleBulkDropdown () {\n            this.bulkDropdownVisible = !this.bulkDropdownVisible;\n        },\n        closeBulkDropdown () {\n            this.bulkDropdownVisible = false;\n        },\n        selectedPostsNeedsStatus (status) {\n            let selectedPosts = this.items.filter(item => this.selectedItems.indexOf(item.id) > -1);\n\n            if (!selectedPosts.length) {\n                return false;\n            }\n\n            let postsWithoutGivenStatus = selectedPosts.filter(item => item.status.indexOf(status) === -1);\n\n            return !!postsWithoutGivenStatus.length;\n        },\n        selectedPostsHaveStatus (status) {\n            let selectedPosts = this.items.filter(item => this.selectedItems.indexOf(item.id) > -1);\n\n            if (!selectedPosts.length) {\n                return false;\n            }\n\n            let postsWithGivenStatus = selectedPosts.filter(item => item.status.indexOf(status) > -1);\n\n            return !!postsWithGivenStatus.length;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('site-loaded', this.whenSiteLoaded);\n        this.$bus.$off('posts-filter-value-changed');\n        this.$bus.$off('document-body-clicked', this.closeBulkDropdown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/empty-states.scss';\n\n.header {\n    .col {\n        align-items: center;\n        display: flex;\n\n        .col-sortable-title {\n            cursor: pointer;\n        }\n    }\n}\n\n.order-ascending,\n.order-descending {\n    margin-left: 3px;\n    position: relative;\n    &:after {\n        border-top: solid 5px var(--icon-secondary-color);\n        border-left: solid 5px transparent;\n        border-right: solid 5px transparent;\n        content: \"\";\n        cursor: pointer;\n        display: inline-block;\n        height: 4px;\n        left: 0;\n        line-height: 1.1;\n        opacity: 1;\n        padding: 0;\n        position: relative;\n        text-align: center;\n        top: 50%;\n        transform: translateY(-50%);\n        width: 8px;\n    }\n}\n\n.order-descending {\n    &:after {\n        border-top-color: transparent;\n        border-bottom: solid 5px var(--icon-secondary-color);\n    }\n}\n\n.header {\n    overflow-y: visible!important;\n}\n\n.item {\n    .post-tags {\n        display: flex;\n        flex-wrap: wrap;\n\n        a {\n            order: 2;\n            margin: .2rem .5rem 0 0;\n\n            &.is-main-tag {\n                order: 1;\n            }\n        }\n    }\n\n    .post-slug {\n        color: var(--gray-4);\n        font-size: 11px;\n        margin-top: .2rem;\n    }\n}\n\n.filters {\n    font-size: 1.35rem;\n    list-style-type: none;\n    margin: -2.2rem 0 0 0;\n    padding: 0;\n    position: relative;\n    user-select: none;\n    z-index: 1;\n\n    .label {\n        color: var(--text-light-color);\n        float: left;\n        margin-right: 1rem;\n    }\n\n    .filter-value {\n        color: var(--text-light-color);\n        cursor: pointer;\n        display: inline-block;\n        margin-right: 1rem;\n        transition: var(--transition);\n\n        &.filter-active {\n            color: var(--link-primary-color);\n        }\n\n        &:hover {\n            color: var(--link-primary-color);\n        }\n\n        &:last-child {\n            border-right: none;\n        }\n    }\n}\n\n.tools {\n    \n    .dropdown-wrapper {\n        position: relative;\n\n        .dropdown {\n            background: var(--popup-bg);\n            border-radius: var(--border-radius);\n            box-shadow: var(--box-shadow-medium);\n            left: 0;\n            list-style-type: none;\n            margin: 0;\n            padding: 1rem 0;\n            position: absolute;\n            top: 4rem;\n            width: auto;\n            z-index: 1;\n\n            li {\n                color: var(--text-light-color);\n                cursor: pointer;\n                display: block;\n                font-size: 1.4rem;\n                font-weight: var(--font-weight-semibold);\n                padding: .8rem 2.4rem;\n                white-space: nowrap;\n\n                &:hover {\n                    background: var(--gray-1);\n                    color: var(--text-primary-color);\n                }\n\n                & > svg {\n                    margin-right: 4px;\n                    vertical-align: text-bottom;\n                }\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/RegenerateThumbnails.vue",
    "content": "<template>\n    <section class=\"content\">\n        <div class=\"regenerate-thumbnails\">\n            <p-header :title=\"$t('tools.thumbnails.regenerateThumbnails')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    slot=\"buttons\"\n                    type=\"clean back\">\n                    {{ $t('ui.backToTools') }}\n                </p-button>\n            </p-header>\n\n            <fields-group v-if=\"currentSiteHasTheme\">\n                <p>\n                    {{ $t('tools.thumbnails.regenerateThumbnailsInfo') }}\n                </p>\n\n                <div class=\"result-wrapper\">\n                    <p-button\n                        v-if=\"regeneratingInProgress\"\n                        :onClick=\"abortRegenerate\"\n                        type=\"danger\">\n                        {{ $t('ui.cancel') }}\n                    </p-button>\n\n                    <p-button\n                        class=\"button-secondary\"\n                        :onClick=\"regenerate\"\n                        :type=\"buttonStatus\">\n                        {{ $t('tools.thumbnails.regenerateThumbnails') }}\n                    </p-button>\n\n                    <span\n                        v-if=\"resultLabel\"\n                        :class=\"resultCssClass\">\n                        {{ resultLabel }}\n                    </span>\n                </div>\n           \n\n            <div\n                v-if=\"regeneratingStarted\"\n                class=\"regenerate-thumbnails-list-container\">\n                <h4>{{ $t('tools.thumbnails.listRegeneratedFiles') }}</h4>\n\n                <ul\n                    class=\"regenerate-thumbnails-list\">\n                    <li\n                        v-for=\"(file, index) in files\"\n                        :key=\"file + '-' + index\"\n                        class=\"item\"\n                        :title=\"getFilePhrase(file)\">\n                        {{ removeSiteDir(file) }}\n                    </li>\n                </ul>\n            </div>\n\n            <p v-if=\"!currentSiteHasTheme\">\n                {{ $t('tools.thumbnails.regenerateThumbnailsNotNecessaryInfo') }}\n            </p>\n\n             </fields-group>\n        </div>\n    </section>\n</template>\n\n<script>\nimport BackToTools from './mixins/BackToTools.js';\n\nexport default {\n    name: 'regenerate-thumbnails',\n    mixins: [\n        BackToTools\n    ],\n    data: function() {\n        return {\n            regeneratingInProgress: false,\n            regeneratingStarted: false,\n            resultLabel: '',\n            resultCssClass: {\n                'error': false,\n                'success': false\n            },\n            buttonStatus: '',\n            files: []\n        };\n    },\n    computed: {\n        currentSiteHasTheme: function() {\n            return !!this.$store.state.currentSite.config.theme;\n        }\n    },\n    methods: {\n        getFilePhrase (filePath) {\n            if (filePath.translation) {\n                return this.$t(filePath.translation);\n            }\n\n            return filePath;\n        },\n        removeSiteDir (filePath) {\n            filePath = this.getFilePhrase(filePath);\n\n            return filePath.replace(this.$store.state.currentSite.siteDir, '');\n        },\n        regenerate () {\n            if(this.regeneratingInProgress) {\n                return;\n            }\n\n            this.buttonStatus = 'disabled preloader';\n            this.regeneratingInProgress = true;\n            this.regeneratingStarted = true;\n            this.files = [];\n            this.resultLabel = this.$t('tools.thumbnails.regeneratingThumbnails');\n            this.resultCssClass = {\n                'result': true,\n                'error': false,\n                'success': false\n            };\n\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-error');\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-progress');\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-success');\n\n            setTimeout(() => {\n                mainProcessAPI.send('app-site-regenerate-thumbnails', {\n                    name: this.$store.state.currentSite.config.name\n                });\n\n                mainProcessAPI.receiveOnce('app-site-regenerate-thumbnails-error', (data) => {\n                    this.resultCssClass = {\n                        'result': true,\n                        'error': true,\n                        'success': false\n                    };\n                    this.resultLabel = data.message.translation ? this.$t(data.message.translation) : data.message;\n                    this.buttonStatus = '';\n                });\n\n                mainProcessAPI.receive('app-site-regenerate-thumbnails-progress', (data) => {\n                    this.resultLabel = this.$t('tools.thumbnails.progress') + data.value + '%';\n\n                    for(let file of data.files) {\n                        if (file) {\n                            this.files.unshift(file);\n                        }\n                    }\n                });\n\n                mainProcessAPI.receiveOnce('app-site-regenerate-thumbnails-success', (data) => {\n                    this.resultCssClass = {\n                        'result': true,\n                        'error': false,\n                        'success': true\n                    };\n                    this.resultLabel = this.$t('tools.thumbnails.thumbnailsCreated');\n                    this.buttonStatus = '';\n                    this.regeneratingInProgress = false;\n                });\n            }, 350);\n        },\n        abortRegenerate () {\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-progress');\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-error');\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-success');\n            mainProcessAPI.send('app-site-abort-regenerate-thumbnails', true);\n\n            this.resultCssClass = {\n                'result': true,\n                'error': false,\n                'success': false\n            };\n            this.resultLabel = this.$t('tools.thumbnails.thumbnailsRegenerationCancelled');\n            this.buttonStatus = '';\n            this.regeneratingInProgress = false;\n        }\n    },\n    beforeDestroy: function() {\n        mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-error');\n        mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-progress');\n        mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-success');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.regenerate-thumbnails {\n    margin: 0 auto;\n    max-width: $wrapper;\n    user-select: none;\n\n    &-list {  \n        list-style-type: decimal;\n        list-style-position: inside;\n        margin: 0;\n        padding: 0;\n        user-select: text;\n\n        &-container {\n           border-top: 1px solid var(--border-light-color);\n           margin-top: 4rem;\n           padding: 3rem 0 0;\n        }\n\n        .item {\n            font-size: 1.4rem;\n            padding: .5rem 0 .5rem .5rem;\n\n            &:first-child {\n                border-top: none;\n            }\n        }\n    }\n\n    .result {\n        padding-left: 2rem;\n\n        &-wrapper {\n            align-items: center;\n            display: flex;\n        }\n\n        &.error {\n            color: var(--warning);\n        }\n\n        &.success {\n            color: var(--success);\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/RegenerateThumbnailsPopup.vue",
    "content": "<template>\n    <div\n        class=\"overlay\"\n        v-if=\"isVisible\">\n        <div class=\"popup\">\n            <icon\n                name=\"blank-image\"\n                customWidth=\"75\"\n                customHeight=\"62\" />\n\n            <h1>\n                {{ $t('tools.thumbnails.themeOrThumbnailsSettingsChanged') }}\n            </h1>\n\n            <p class=\"popup-info\">\n                {{ $t('tools.thumbnails.processingRegenerateThumbnailsInfo') }}\n            </p>\n\n            <progress-bar\n                :color=\"progressColor\"\n                :progress=\"progress\"\n                :stopped=\"progressIsStopped\"\n                :message=\"message\" />\n\n            <div class=\"buttons\">\n                <p-button\n                    v-if=\"!regenerateIsDone\"\n                    @click.native=\"regenerate\"\n                    :disabled=\"regeneratingThumbnails\"\n                    type=\"medium no-border-radius half-width\">\n                    {{ $t('tools.thumbnails.regenerateThumbnails') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"!regenerateIsDone && !regeneratingThumbnails\"\n                    @click.native=\"skip\"\n                    :disabled=\"regeneratingThumbnails\"\n                    type=\"medium no-border-radius half-width cancel-popup\">\n                    {{ $t('tools.thumbnails.skipRegeneration') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"regeneratingThumbnails\"\n                    @click.native=\"abortRegenerate\"\n                    type=\"medium no-border-radius half-width cancel-popup\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"regenerateIsDone\"\n                    @click.native=\"skip\"\n                    :disabled=\"regeneratingThumbnails\"\n                    type=\"medium no-border-radius full-width\">\n                    {{ $t('ui.ok') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'regenerate-thumbnails-popup',\n    data () {\n        return {\n            isVisible: false,\n            message: '',\n            progress: 0,\n            progressColor: 'blue',\n            progressIsStopped: false,\n            regeneratingThumbnails: false,\n            regenerateIsDone: false,\n            savedSettingsCallback: false\n        };\n    },\n    mounted () {\n        this.$bus.$on('regenerate-thumbnails-display', (config) => {\n            this.isVisible = true;\n            this.message = '';\n            this.progress = 0;\n            this.progressColor = 'blue';\n            this.progressIsStopped = false;\n            this.regeneratingThumbnails = false;\n            this.regenerateIsDone = false;\n            this.savedSettingsCallback = config.savedSettingsCallback || false;\n        });\n\n        document.body.addEventListener('keydown', this.onDocumentKeyDown);\n    },\n    methods: {\n        skip () {\n            this.isVisible = false;\n            this.message = '';\n            this.progress = 0;\n            this.progressColor = 'blue';\n            this.progressIsStopped = false;\n            this.regeneratingThumbnails = false;\n            this.regenerateIsDone = false;\n\n            if (this.savedSettingsCallback) {\n                this.$bus.$emit('regenerate-thumbnails-close', this.savedSettingsCallback);\n            }\n        },\n        regenerate () {\n            if (this.regeneratingThumbnails) {\n                return;\n            }\n\n            this.regeneratingThumbnails = true;\n            this.progressIsStopped = false;\n            this.message = this.$t('tools.thumbnails.regeneratingThumbnails');\n\n            setTimeout(() => {\n                mainProcessAPI.send('app-site-regenerate-thumbnails', {\n                    name: this.$store.state.currentSite.config.name\n                });\n\n                mainProcessAPI.receiveOnce('app-site-regenerate-thumbnails-error', (data) => {\n                    this.progressColor = 'red';\n                    this.progressIsStopped = true;\n                    this.message = data.message.translation ? this.$t(data.message.translation) : data.message;\n                    this.regeneratingThumbnails = false;\n                    this.regenerateIsDone = true;\n                });\n\n                mainProcessAPI.receive('app-site-regenerate-thumbnails-progress', (data) => {\n                    this.progress = data.value;\n                    this.message = this.$t('tools.thumbnails.progress') + data.value + '%';\n                });\n\n                mainProcessAPI.receiveOnce('app-site-regenerate-thumbnails-success', (data) => {\n                    this.progress = 100;\n                    this.progressColor = 'green';\n                    this.progressIsStopped = true;\n                    this.message = this.$t('tools.thumbnails.thumbnailsCreated');\n                    this.regeneratingThumbnails = false;\n                    this.regenerateIsDone = true;\n\n                    if (this.savedSettingsCallback) {\n                        this.skip();\n                    }\n                });\n            }, 350);\n        },\n        onDocumentKeyDown (e) {\n            if (e.code === 'Enter' && !event.isComposing && this.isVisible && !this.regeneratingThumbnails) {\n                this.onEnterKey();\n            }\n        },\n        onEnterKey () {\n            if (this.regenerateIsDone) {\n                this.skip();\n            } else {\n                this.regenerate();\n            }\n        },\n        abortRegenerate () {\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-progress');\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-error');\n            mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-success');\n            mainProcessAPI.send('app-site-abort-regenerate-thumbnails', true);\n            this.skip();\n        }\n    },\n    beforeDestroy: function() {\n        this.$bus.$off('regenerate-thumbnails-display');\n        mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-error');\n        mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-progress');\n        mainProcessAPI.stopReceiveAll('app-site-regenerate-thumbnails-success');\n        document.body.removeEventListener('keydown', this.onDocumentKeyDown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100006;\n}\n\n.popup {\n    padding: 4rem 4rem 6rem 4rem;\n    width: 60rem;\n\n    h1 {\n        margin-top: 2rem;\n    }\n\n    svg {\n        fill: var(--icon-quaternary-color);\n\n    }\n\n    &-info {\n        font-size: 1.4rem;\n        color: var(--text-light-color);\n        margin: -1.5rem 0 4rem;\n    }\n}\n\n.message {\n    color: var(--text-primary-color);\n    font-weight: 400;\n    margin: 0;\n    padding: 4rem;\n    position: relative;\n    text-align: left;\n\n    &.text-centered {\n        text-align: center;\n    }\n}\n.buttons {\n    display: flex;\n    margin: 0 -4rem -6rem -4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/RenderingPopup.vue",
    "content": "<template>\n    <div class=\"overlay\" v-if=\"isVisible\">\n        <div class=\"popup\">\n            <h1>{{ $t('rendering.rendering') }}</h1>\n            <p class=\"popup-info\">\n                {{ $t('rendering.renderingPleaseWait') }}\n            </p>\n\n            <progress-bar\n                v-if=\"!isPostPreview && !isHomepagePreview\"\n                :color=\"progressColor\"\n                :progress=\"progress\"\n                :stopped=\"progressIsStopped\"\n                :message=\"messageFromRenderer\" />\n        </div>\n    </div>\n</template>\n\n<script>\nimport Utils from './../helpers/utils.js';\n\nexport default {\n    name: 'rendering-popup',\n    data: function() {\n        return {\n            isVisible: false,\n            isPostPreview: false,\n            isHomepagePreview: false,\n            isTagPreview: false,\n            isAuthorPreview: false,\n            messageFromRenderer: '',\n            progress: 0,\n            progressColor: 'blue',\n            progressIsStopped: false\n        };\n    },\n    mounted: function() {\n        this.$bus.$on('rendering-popup-display', (config) => {\n            this.isVisible = true;\n            this.messageFromRenderer = '';\n            this.progress = 0;\n            this.progressColor = 'blue';\n            this.progressIsStopped = false;\n            this.isPostPreview = false;\n            this.isHomepagePreview = false;\n            this.isTagPreview = false;\n            this.isAuthorPreview = false;\n            this.showPreview = true;\n\n            if (config && typeof config.showPreview !== 'undefined') {\n                this.showPreview = config.showPreview;\n            }\n\n            if (config && config.homepageOnly) {\n                this.isHomepagePreview = true;\n                this.runRenderingPreview(false, 'home');\n            } else if (config && config.tagOnly) {\n                this.isTagPreview = true;\n                this.runRenderingPreview(config, 'tag');\n            } else if (config && config.authorOnly) {\n                this.isAuthorPreview = true;\n                this.runRenderingPreview(config, 'author');\n            } else if (config && config.postOnly) {\n                this.isPostPreview = true;\n                this.runRenderingPreview(config, config.itemType);\n            } else {\n                this.runRenderingPreview();\n            }\n        });\n\n        mainProcessAPI.receive('app-rendering-progress', this.renderingProgress);\n    },\n    methods: {\n        runRenderingPreview (itemConfig = false, mode = false) {\n            if(!this.themeIsSelected) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('rendering.selectThemeBeforeCreatingPreviewMsg'),\n                    okLabel: this.$t('sync.goToSettings'),\n                    okClick: () => {\n                        let siteName = this.$route.params.name;\n                        this.$route.push('/site/' + siteName + '/settings/');\n                    }\n                });\n\n                return;\n            }\n\n            let renderConfig = {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"theme\": this.$store.state.currentSite.config.theme,\n                \"showPreview\": this.showPreview\n            };\n\n            if (mode === 'post' && itemConfig) {\n                renderConfig.mode = 'post';\n                renderConfig.itemID = itemConfig.itemID;\n                renderConfig.postData = itemConfig.postData;\n                renderConfig.source = 'post-editor';\n            } else if (mode === 'page' && itemConfig) {\n                renderConfig.mode = 'page';\n                renderConfig.itemID = itemConfig.itemID;\n                renderConfig.postData = itemConfig.postData;\n                renderConfig.source = 'post-editor';\n            } else if (mode === 'home') {\n                renderConfig.mode = 'home';\n            } else if (mode === 'tag') {\n                renderConfig.mode = 'tag';\n                renderConfig.itemID = itemConfig.itemID;\n            } else if (mode === 'author') {\n                renderConfig.mode = 'author';\n                renderConfig.itemID = itemConfig.itemID;\n            }\n\n            mainProcessAPI.send('app-preview-render', renderConfig);\n\n            console.log('SEND');\n\n            mainProcessAPI.receiveOnce('app-preview-rendered', (data) => {\n                console.log('RECEIVE', data);\n                if (data.status === true) {\n                    if (mode === 'post' || mode === 'page' || mode === 'home' || mode === 'tag' || mode === 'author') {\n                        setTimeout(() => {\n                            this.isVisible = false;\n                        }, 500);\n                    }\n                } else {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('rendering.errorDuringPreviewCreatingMsg')\n                    });\n                }\n            });\n\n            console.log('STOP RECEIVEING');\n            mainProcessAPI.stopReceiveAll('app-preview-render-error');\n            mainProcessAPI.receiveOnce('app-preview-render-error', this.renderError);\n        },\n        renderingProgress: function(data) {\n            this.messageFromRenderer = data.message + ' - ' + data.progress + '%';\n            this.progress = data.progress;\n\n            if(this.progress === 100) {\n                this.progressColor = 'green';\n                this.progressIsStopped = true;\n                this.messageFromRenderer = '';\n\n                setTimeout(() => {\n                    this.isVisible = false;\n                }, 500);\n            }\n        },\n        renderError(data) {\n            if (data.message[0].message.translation) {\n                data.message[0].message = this.$t(data.message[0].message.translation);\n            }\n\n            if (data.message[0].desc.translation) {\n                data.message[0].desc = this.$t(data.message[0].desc.translation);\n            }\n\n            let errorsHTML = Utils.generateErrorLog(data.message);\n            let errorsText = Utils.generateErrorLog(data.message, true);\n\n            this.$bus.$emit('error-popup-display', {\n                errors: errorsHTML,\n                text: errorsText\n            });\n\n            this.isVisible = false;\n        },\n        themeIsSelected() {\n            return !(!this.$store.state.currentSite.config.theme || this.$store.state.currentSite.config.theme === '');\n        }\n    },\n    beforeDestroy: function() {\n        this.$bus.$off('rendering-popup-display');\n        mainProcessAPI.stopReceiveAll('app-preview-render-error');\n        mainProcessAPI.stopReceiveAll('app-rendering-progress');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/popup-common.scss';\n\n.popup {\n    padding: 4rem 4rem 1rem 4rem;\n    width: 60rem;\n\n    &-info {\n        margin: -1.5rem 0 4rem;\n    }\n}\n\n.message {\n    color: var(--text-primary-color);\n    font-weight: 400;\n    margin: 0;\n    padding: 4rem;\n    position: relative;\n    text-align: left;\n\n    &.text-centered {\n        text-align: center;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/ServerSettings.vue",
    "content": "<template>\n    <section\n        class=\"content\"\n        ref=\"content\">\n        <div\n            v-if=\"isLoaded\"\n            class=\"server-settings\">\n            <p-header\n                v-if=\"deploymentMethodSelected !== ''\"\n                :title=\"getDeploymentMethodName(deploymentMethodSelected)\">\n                <p-button\n                    @click.native=\"deploymentMethodSelected = ''\"\n                    slot=\"buttons\"\n                    :title=\"$t('sync.clickToChangeDeploymentMethod')\"\n                    type=\"clean back\">\n                    {{ $t('sync.changeServerType') }}\n                </p-button>\n\n                <p-button\n                    @click.native=\"visitWebsite\"\n                    slot=\"buttons\"\n                    :type=\"siteIsOnline ? 'outline' : 'outline disabled-with-events'\"\n                    :title=\"visitTitle\">\n                    {{ $t('sync.visitWebsite') }}\n                </p-button>\n\n                <p-button\n                    :onClick=\"save\"\n                    slot=\"buttons\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n            </p-header>\n\n            <p-header\n                v-if=\"deploymentMethodSelected === ''\"\n                :title=\"$t('sync.selectServerType')\">\n            </p-header>\n\n            <div\n                v-if=\"deploymentMethodSelected === ''\"\n                class=\"msg msg-icon msg-info msg-onbg\">\n                <icon name=\"info\" customWidth=\"28\" customHeight=\"28\" />\n                <p v-pure-html=\"$t('settings.readAboutOurRecommendedServerSettings')\"></p>\n            </div>\n\n            <div\n                v-if=\"deploymentMethodSelected === ''\"\n                class=\"server-settings-grid\">\n\n                 <div\n                    @click=\"deploymentMethodSelected = 'ftp'\"\n                    :title=\"$t('sync.ftp')\"\n                    class=\"server-settings-grid-item\">\n                   <icon\n                      customWidth=\"48\"\n                      customHeight=\"48\"\n                      name=\"ftp\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.ftp') }}</span>\n                </div>\n\n                <div\n                    @click=\"deploymentMethodSelected = 'sftp'\"\n                    :title=\"$t('sync.sftp')\"\n                    class=\"server-settings-grid-item\">\n                   <icon\n                      customWidth=\"48\"\n                      customHeight=\"48\"\n                      name=\"sftp\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.sftp') }}</span>\n                </div>\n\n                <div\n                    @click=\"deploymentMethodSelected = 's3'\"\n                    :title=\"$t('sync.s3CompatibleStorage')\"\n                    class=\"server-settings-grid-item\">\n                   <icon\n                      customWidth=\"48\"\n                      customHeight=\"48\"\n                      name=\"s3storage\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.s3CompatibleStorage') }}</span>\n                </div>\n\n                <div\n                    @click=\"deploymentMethodSelected = 'git'\"\n                    :title=\"$t('sync.git')\"\n                    class=\"server-settings-grid-item\">\n                    <icon\n                      customWidth=\"84\"\n                      customHeight=\"48\"\n                      name=\"git\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.git') }}</span>\n                </div>\n                \n                <div\n                    @click=\"deploymentMethodSelected = 'github-pages'\"\n                    :title=\"$t('sync.github')\"\n                    class=\"server-settings-grid-item\">\n                    <icon\n                      customWidth=\"48\"\n                      customHeight=\"48\"\n                      name=\"githubpages\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.githubPages') }}</span>\n                </div>\n\n                <div\n                    @click=\"deploymentMethodSelected = 'gitlab-pages'\"\n                    :title=\"$t('sync.gitlabPages')\"\n                    class=\"server-settings-grid-item\">\n                    <icon\n                      customWidth=\"48\"\n                      customHeight=\"48\"\n                      name=\"gitlab\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.gitlabPages') }}</span>\n                </div>\n\n                <div\n                    @click=\"deploymentMethodSelected = 'netlify'\"\n                    :title=\"$t('sync.netlify')\"\n                    class=\"server-settings-grid-item\">\n                   <icon\n                      customWidth=\"54\"\n                      customHeight=\"48\"\n                      name=\"netlify\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.netlify') }}</span>\n                </div>\n\n                <div\n                    @click=\"deploymentMethodSelected = 'google-cloud'\"\n                    :title=\"$t('sync.googleCloud')\"\n                    class=\"server-settings-grid-item\">\n                    <icon\n                      customWidth=\"48\"\n                      customHeight=\"48\"\n                      name=\"googlecloud\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.googleCloud') }}</span>\n                </div>\n\n                <div\n                    @click=\"deploymentMethodSelected = 'manual'\"\n                    :title=\"$t('sync.manualDeployment')\"\n                    class=\"server-settings-grid-item\">\n                   <icon\n                      customWidth=\"48\"\n                      customHeight=\"48\"\n                      name=\"zip\"\n                      iconset=\"svg-map-server\"/>\n                      <span>{{ $t('sync.manualDeployment') }}</span>\n                </div>\n\n                <a\n                    href=\"https://getpublii.com/docs/deployment/\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    class=\"server-settings-grid-item deployment-others\">\n\n                    <icon\n                        customWidth=\"50\"\n                        customHeight=\"46\"\n                        properties=\"not-clickable\"\n                        name=\"add\" />\n\n                    <h3>{{ $t('ui.more') }}...</h3>\n                </a>\n\n            </div>\n\n            <fields-group \n                v-if=\"deploymentMethodSelected !== ''\" \n                :title=\"$t('sync.settings')\">\n                <div class=\"msg msg-icon msg-info\">\n                    <icon name=\"info\" customWidth=\"28\" customHeight=\"28\" />\n                    <p>\n                        <span v-if=\"deploymentMethodSelected !== 'git' && deploymentMethodSelected !== 'netlify' && deploymentMethodSelected !== 'github-pages'\" v-pure-html=\"$t('sync.deploymentMethodFilesPubliiMsg')\"></span>\n                        \n                        <br v-if=\"deploymentMethodSelected !== 'git' && deploymentMethodSelected !== 'netlify' && deploymentMethodSelected !== 'github-pages'\">\n\n                        <span\n                            v-if=\"deploymentMethodSelected === 'netlify'\"\n                            v-pure-html=\"$t('sync.deploymentMethodNetlifyMsg')\">\n                        </span>\n\n                        <span\n                            v-if=\"deploymentMethodSelected === 'git'\"\n                            v-pure-html=\"$t('sync.deploymentMethodGitMsg')\">\n                        </span>\n                        \n                        <span\n                            v-if=\"deploymentMethodSelected === 'github-pages'\"\n                            v-pure-html=\"$t('sync.deploymentMethodGithubPagesMsg')\">\n                        </span>\n\n                        <span\n                            v-if=\"deploymentMethodSelected === 'gitlab-pages'\"\n                            v-pure-html=\"$t('sync.deploymentMethodGitlabPagesMsg')\">\n                        </span>\n\n                        <span\n                            v-if=\"deploymentMethodSelected === 's3'\"\n                            v-pure-html=\"$t('sync.deploymentMethodS3Msg')\">\n                        </span>\n\n                        <span\n                            v-if=\"deploymentMethodSelected === 'google-cloud'\"\n                            v-pure-html=\"$t('sync.deploymentMethodGoogleCloudMsg')\">\n                        </span>\n\n                        <span v-pure-html=\"$t('settings.readAboutOurRecommendedServerSettings')\"></span>\n                    </p>\n                </div>\n\n                <field\n                    id=\"domain\"\n                    :label=\"$t('sync.websiteURL')\">\n                    <dropdown\n                        v-if=\"!deploymentSettings.relativeUrls\"\n                        slot=\"field\"\n                        id=\"http-protocol\"\n                        key=\"httpProtocol\"\n                        :items=\"httpProtocols\"\n                        v-model=\"httpProtocolSelected\"></dropdown>\n\n                    <text-input\n                        slot=\"field\"\n                        id=\"domain\"\n                        key=\"domain\"\n                        :disabled=\"deploymentSettings.relativeUrls\"\n                        :spellcheck=\"false\"\n                        v-model=\"domain\" />\n                    <small\n                        v-if=\"deploymentMethodSelected === 'github-pages'\"\n                        class=\"note\"\n                        slot=\"note\"\n                        v-pure-html=\"$t('sync.deploymentMethodGithubPagesNote')\">\n                    </small>\n                    <small\n                        v-if=\"deploymentMethodSelected === 'git'\"\n                        class=\"note\"\n                        slot=\"note\"\n                        v-pure-html=\"$t('sync.deploymentMethodGitNote')\">\n                    </small>\n\n                    <small\n                        v-if=\"!deploymentSettings.relativeUrls && httpProtocolSelected === 'file'\"\n                        class=\"note\"\n                        slot=\"note\">\n                        {{ $t('sync.deploymentSettingFileProtocolNote') }}\n                    </small>\n                    <small\n                        v-if=\"!deploymentSettings.relativeUrls && (['dat', 'hyper', 'ipfs', 'dweb'].indexOf(httpProtocolSelected) > -1)\"\n                        class=\"note\"\n                        slot=\"note\"\n                        v-pure-html=\"$t('sync.deploymentSettingDatHyperIpfsProtocolNote')\">\n                    </small>\n                    <small\n                        v-if=\"!deploymentSettings.relativeUrls && httpProtocolSelected === '//'\"\n                        class=\"note\"\n                        slot=\"note\">\n                        {{ $t('sync.deploymentSettingDoubleSlashProtocolNote') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"relative-urls\"\n                    label=\" \">\n                    <switcher\n                        slot=\"field\"\n                        id=\"relative-urls\"\n                        key=\"relative-urls\"\n                        v-model=\"deploymentSettings.relativeUrls\"\n                        @click.native=\"toggleDomainName\" />\n                    <template slot=\"second-label\">\n                        {{ $t('sync.useRelativeURLs') }}\n                    </template>\n                    <small\n                        class=\"note\"\n                        slot=\"note\">\n                        {{ $t('sync.deploymentSettingRelativeUrlsNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"['ftp', 'ftp+tls', 'sftp', 'sftp+key'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"port\"\n                    :label=\"$t('sync.port')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"port\"\n                        type=\"number\"\n                        key=\"port\"\n                        min=\"1\"\n                        max=\"65535\"\n                        step=\"1\"\n                        :class=\"{ 'is-invalid': errors.indexOf('port') > -1 }\"\n                        @keyup.native=\"cleanError('port')\"\n                        v-model=\"deploymentSettings.port\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('port') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.portFormatNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"['ftp', 'ftp+tls'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"secure-connection\"\n                    :label=\"$t('sync.ftps')\"\n                    :labelSeparated=\"true\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"secure-connection\"\n                        key=\"secure-connection\"\n                        :value=\"deploymentMethodSelected === 'ftp+tls'\"\n                        @click.native=\"toggleFtpDeploymentMethod\" />\n                    <template slot=\"second-label\">\n                        {{ $t('sync.useFtps') }}\n                    </template>\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.deploymentMethodFtpMsg') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"['ftp', 'ftp+tls', 'sftp', 'sftp+key'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"server\"\n                    :label=\"$t('ui.server')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"server\"\n                        key=\"server\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('server') > -1 }\"\n                        @keyup.native=\"cleanError('server')\"\n                        v-model=\"deploymentSettings.server\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('server') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.serverFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"['ftp', 'ftp+tls', 'sftp', 'sftp+key'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"username\"\n                    :label=\"$t('sync.username')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"username\"\n                        key=\"username\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('username') > -1 }\"\n                        @keyup.native=\"cleanError('username')\"\n                        v-model=\"deploymentSettings.username\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('username') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.usernameFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"['sftp', 'sftp+key'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"sftp-auth-method\"\n                    :label=\"$t('sync.authenticationMethod')\">\n                    <radio-buttons\n                        slot=\"field\"\n                        id=\"sftp-auth-method\"\n                        name=\"sftp-auth-method\"\n                        key=\"sftp-auth-method\"\n                        :items=\"sftpAuthMethodItems\"\n                        v-model=\"deploymentMethodSelected\" />\n                </field>\n\n                <field\n                    v-if=\"['ftp', 'ftp+tls', 'sftp'].indexOf(deploymentMethodSelected) > -1 && !deploymentSettings.askforpassword\"\n                    id=\"password\"\n                    :label=\"$t('settings.password.password')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"password\"\n                        type=\"password\"\n                        key=\"password\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('password') > -1 }\"\n                        @keyup.native=\"cleanError('password')\"\n                        v-model=\"deploymentSettings.password\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('username') > -1\"\n                        class=\"note\">\n                        {{ $t('settings.password.passwordFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"['ftp', 'ftp+tls', 'sftp'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"askforpassword\"\n                    :label=\"$t('settings.password.alwaysAskForPassword')\"\n                    :labelSeparated=\"true\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"askforpassword\"\n                        key=\"askforpassword\"\n                        v-model=\"deploymentSettings.askforpassword\" />\n                    <template slot=\"second-label\">\n                        {{ $t('sync.requireFTPPassAlwaysOnSync') }}\n                    </template>\n                </field>\n\n                <field\n                    v-if=\"['ftp+tls', 'gitlab-pages'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"rejectunauthorized\"\n                    :label=\"$t('sync.certificates')\"\n                    :labelSeparated=\"true\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"rejectunauthorized\"\n                        key=\"rejectunauthorized\"\n                        v-model=\"deploymentSettings.rejectUnauthorized\" />\n                    <template slot=\"second-label\">\n                        {{ $t('sync.requireCertificateForConnection') }}\n                    </template>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'sftp+key'\"\n                    id=\"sftpkey\"\n                    :label=\"$t('sync.yourPrivateKey')\">\n                    <file-select\n                        id=\"sftpkey\"\n                        :class=\"{ 'is-invalid': errors.indexOf('key') > -1 }\"\n                        @click.native=\"cleanError('key')\"\n                        v-model=\"deploymentSettings.sftpkey\"\n                        key=\"sftpkey\"\n                        slot=\"field\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('key') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.sftpKeyNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'sftp+key'\"\n                    id=\"passphrase\"\n                    :label=\"$t('sync.passphraseForKey')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"passphrase\"\n                        type=\"password\"\n                        :spellcheck=\"false\"\n                        key=\"passphrase\"\n                        v-model=\"deploymentSettings.passphrase\" />\n\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.passphraseForKeyNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"['ftp', 'ftp+tls', 'sftp', 'sftp+key'].indexOf(deploymentMethodSelected) > -1\"\n                    id=\"path\"\n                    :label=\"$t('sync.remotePath')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"path\"\n                        key=\"path\"\n                        :spellcheck=\"false\"\n                        v-model=\"deploymentSettings.path\" />\n\n                    <small\n                        v-if=\"['ftp', 'ftp+tls'].indexOf(deploymentMethodSelected) > -1\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.remotePathMatchServerRootPathNote') }}\n                    </small>\n\n                    <small\n                        v-if=\"['sftp', 'sftp+key'].indexOf(deploymentMethodSelected) > -1\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.remotePathMatchServerOrRootPathNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'github-pages'\"\n                    id=\"gh-server\"\n                    :label=\"$t('sync.apiServer')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gh-server\"\n                        key=\"gh-server\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('github-server') > -1 }\"\n                        @keyup.native=\"cleanError('github-server')\"\n                        v-model=\"deploymentSettings.github.server\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.apiServerNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'github-pages'\"\n                    id=\"gh-user\"\n                    :label=\"$t('sync.usernameOrganization')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gh-user\"\n                        key=\"gh-user\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('github-user') > -1 }\"\n                        @keyup.native=\"cleanError('github-user')\"\n                        v-model=\"deploymentSettings.github.user\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('github-user') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.usernameFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'github-pages'\"\n                    id=\"gh-repo\"\n                    :label=\"$t('sync.repository')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gh-repo\"\n                        key=\"gh-repo\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('github-repo') > -1 }\"\n                        @keyup.native=\"cleanError('github-repo')\"\n                        v-model=\"deploymentSettings.github.repo\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('github-repo') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.repositoryFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'github-pages'\"\n                    id=\"gh-branch\"\n                    :label=\"$t('sync.branch')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gh-branch\"\n                        key=\"gh-branch\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('github-branch') > -1 }\"\n                        @keyup.native=\"cleanError('github-branch')\"\n                        v-model=\"deploymentSettings.github.branch\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('github-branch') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.branchFieldCantBeEmpty') }}\n                    </small>\n                    <small\n                        slot=\"note\"\n                        class=\"note\"\n                        v-pure-html=\"$t('sync.branchExampleGitHubNote')\">\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'github-pages'\"\n                    id=\"gh-token\"\n                    :label=\"$t('sync.token')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gh-token\"\n                        type=\"password\"\n                        key=\"gh-token\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('github-token') > -1 }\"\n                        @keyup.native=\"cleanError('github-token')\"\n                        v-model=\"deploymentSettings.github.token\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('github-token') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.tokenFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'git'\"\n                    id=\"git-url\"\n                    :label=\"$t('sync.repositoryUrl')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"git-url\"\n                        key=\"git-url\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('git-url') > -1 }\"\n                        @keyup.native=\"cleanError('git-url')\"\n                        v-model=\"deploymentSettings.git.url\" />\n                    <small\n                        v-if=\"errors.indexOf('git-url') > -1\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.repositoryUrlFieldCantBeEmpty') }}\n                    </small>\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.repositoryUrlNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'git'\"\n                    id=\"git-branch\"\n                    :label=\"$t('sync.branch')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"git-branch\"\n                        key=\"git-branch\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('git-branch') > -1 }\"\n                        @keyup.native=\"cleanError('git-branch')\"\n                        v-model=\"deploymentSettings.git.branch\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('git-branch') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.branchFieldCantBeEmpty') }}\n                    </small>\n                    <small\n                        slot=\"note\"\n                        class=\"note\"\n                        v-pure-html=\"$t('sync.branchExampleGitNote')\">\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'git'\"\n                    id=\"git-user\"\n                    :label=\"$t('sync.username')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"git-user\"\n                        key=\"git-user\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('git-user') > -1 }\"\n                        @keyup.native=\"cleanError('git-user')\"\n                        v-model=\"deploymentSettings.git.user\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('github-user') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.usernameFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'git'\"\n                    id=\"git-password\"\n                    :label=\"$t('sync.gitPassword')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"git-password\"\n                        type=\"password\"\n                        key=\"git-password\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('git-password') > -1 }\"\n                        @keyup.native=\"cleanError('git-password')\"\n                        v-model=\"deploymentSettings.git.password\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('git-password') > -1\"\n                        class=\"note\">\n                        {{ $t('settings.password.passwordFieldCantBeEmpty') }}\n                    </small>\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.gitPasswordNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'git'\"\n                    id=\"git-commit-author\"\n                    :label=\"$t('sync.commitAuthor')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"git-commit-author\"\n                        key=\"git-commit-author\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('git-commitAuthor') > -1 }\"\n                        @keyup.native=\"cleanError('git-commitAuthor')\"\n                        v-model=\"deploymentSettings.git.commitAuthor\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('git-commitAuthor') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.commitAuthorFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'git'\"\n                    id=\"git-commit-email\"\n                    :label=\"$t('sync.commitEmail')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"git-commit-email\"\n                        key=\"git-commit-email\"\n                        :spellcheck=\"false\"\n                        v-model=\"deploymentSettings.git.commitEmail\" />\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'git'\"\n                    id=\"git-commit-message\"\n                    :label=\"$t('sync.commitMessage')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"git-commit-message\"\n                        key=\"git-commit-message\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('git-commitMessage') > -1 }\"\n                        @keyup.native=\"cleanError('git-commitMessage')\"\n                        v-model=\"deploymentSettings.git.commitMessage\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('git-commitMessage') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.commitMessageFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'github-pages'\"\n                    id=\"gh-parallel-operations\"\n                    :label=\"$t('sync.parallelUploads')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"gh-parallel-operations\"\n                        :items=\"Array.from(Array(25).keys()).map(n => ({value: n + 1, label: n + 1}))\"\n                        key=\"gh-parallel-operations\"\n                        v-model=\"deploymentSettings.github.parallelOperations\"></dropdown>\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.parallelUploadsNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'github-pages'\"\n                    id=\"gh-api-rate-limiting\"\n                    :label=\"$t('sync.apiRateLimiting')\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"gh-api-rate-limiting\"\n                        key=\"gh-api-rate-limiting\"\n                        v-model=\"deploymentSettings.github.apiRateLimiting\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.apiRateLimitingNote') }}\n                    </small>\n                </field>\n\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'gitlab-pages'\"\n                    id=\"gl-server\"\n                    :label=\"$t('ui.server')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gl-server\"\n                        key=\"gl-server\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('gitlab-server') > -1 }\"\n                        @keyup.native=\"cleanError('gitlab-server')\"\n                        v-model=\"deploymentSettings.gitlab.server\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.serverGitLabNote') }}\n                    </small>\n                </field>\n                \n                <field\n                    v-if=\"deploymentMethodSelected === 'gitlab-pages'\"\n                    id=\"gl-repo\"\n                    :label=\"$t('sync.repository')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gl-repo\"\n                        key=\"gl-repo\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('gitlab-repo') > -1 }\"\n                        @keyup.native=\"cleanError('gitlab-repo')\"\n                        v-model=\"deploymentSettings.gitlab.repo\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('gitlab-repo') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.repositoryFieldCantBeEmpty') }}\n                    </small>\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.fieldIsCaseSensitive') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'gitlab-pages'\"\n                    id=\"gl-branch\"\n                    :label=\"$t('sync.branch')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gl-branch\"\n                        key=\"gl-branch\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('gitlab-branch') > -1 }\"\n                        @keyup.native=\"cleanError('gitlab-branch')\"\n                        v-model=\"deploymentSettings.gitlab.branch\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('gitlab-branch') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.branchFieldCantBeEmpty') }}\n                    </small>\n                    <small\n                        slot=\"note\"\n                        class=\"note\"\n                        v-pure-html=\"$t('sync.branchExampleGitLabeNote')\">\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'gitlab-pages'\"\n                    id=\"gl-token\"\n                    :label=\"$t('sync.token')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"gl-token\"\n                        type=\"password\"\n                        key=\"gl-token\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('gitlab-token') > -1 }\"\n                        @keyup.native=\"cleanError('gitlab-token')\"\n                        v-model=\"deploymentSettings.gitlab.token\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('gitlab-token') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.tokenFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'netlify'\"\n                    id=\"netlify-id\"\n                    :label=\"$t('sync.siteID')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"netlify-id\"\n                        key=\"netlify-id\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('netlify-id') > -1 }\"\n                        @keyup.native=\"cleanError('netlify-id')\"\n                        v-model=\"deploymentSettings.netlify.id\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('netlify-id') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.siteFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'netlify'\"\n                    id=\"netlify-token\"\n                    :label=\"$t('sync.netlifyToken')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"netlify-token\"\n                        type=\"password\"\n                        key=\"netlify-token\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('netlify-token') > -1 }\"\n                        @keyup.native=\"cleanError('netlify-token')\"\n                        v-model=\"deploymentSettings.netlify.token\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('netlify-token') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.tokenFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-provider\"\n                    label=\" \">\n                    <switcher\n                        slot=\"field\"\n                        id=\"s3-provider\"\n                        key=\"s3-provider\"\n                        v-model=\"deploymentSettings.s3.customProvider\"\n                        @click.native=\"toggleS3Provider\" />\n                    <template slot=\"second-label\">\n                        {{ $t('sync.useCustomS3Provider') }}\n                    </template>\n                    <small\n                        class=\"note\"\n                        slot=\"note\">\n                        {{ $t('sync.useCustomS3ProviderNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3' && deploymentSettings.s3.customProvider\"\n                    id=\"s3-endpoint\"\n                    :label=\"$t('sync.s3ProviderEndpoint')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"s3-endpoint\"\n                        key=\"s3-endpoint\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('s3-endpoint') > -1 }\"\n                        @keyup.native=\"cleanError('s3-endpoint')\"\n                        v-model=\"deploymentSettings.s3.endpoint\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('s3-endpoint') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.s3ProviderEndpointNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-id\"\n                    :label=\"$t('sync.accessID')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"s3-id\"\n                        type=\"password\"\n                        key=\"s3-id\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('s3-id') > -1 }\"\n                        @keyup.native=\"cleanError('s3-id')\"\n                        v-model=\"deploymentSettings.s3.id\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('s3-id') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.accessIDFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-key\"\n                    :label=\"$t('sync.secretKey')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"s3-key\"\n                        type=\"password\"\n                        key=\"s3-key\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('s3-key') > -1 }\"\n                        @keyup.native=\"cleanError('s3-key')\"\n                        v-model=\"deploymentSettings.s3.key\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('s3-key') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.secretKeyFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-bucket\"\n                    :label=\"$t('sync.bucket')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"s3-bucket\"\n                        key=\"s3-bucket\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('s3-bucket') > -1 }\"\n                        @keyup.native=\"cleanError('s3-bucket')\"\n                        v-model=\"deploymentSettings.s3.bucket\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('s3-bucket') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.bucketFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-region\"\n                    :label=\"$t('sync.region')\">\n                    <dropdown\n                        v-if=\"!deploymentSettings.s3.customProvider\"\n                        slot=\"field\"\n                        id=\"s3-region\"\n                        :items=\"s3Regions\"\n                        key=\"s3-region\"\n                        :class=\"{ 'is-invalid': errors.indexOf('s3-region') > -1 }\"\n                        @click.native=\"cleanError('s3-region')\"\n                        v-model=\"deploymentSettings.s3.region\"></dropdown>\n                    <text-input\n                        v-if=\"deploymentSettings.s3.customProvider\"\n                        slot=\"field\"\n                        id=\"s3-customRegion\"\n                        key=\"s3-customRegion\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('s3-customRegion') > -1 }\"\n                        @keyup.native=\"cleanError('s3-customRegion')\"\n                        v-model=\"deploymentSettings.s3.customRegion\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('s3-region') > -1 || errors.indexOf('s3-customRegion') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.regionFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-prefix\"\n                    :label=\"$t('sync.prefix')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"s3-prefix\"\n                        key=\"s3-prefix\"\n                        :spellcheck=\"false\"\n                        v-model=\"deploymentSettings.s3.prefix\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\"\n                        v-pure-html=\"$t('sync.s3PrefixNote')\">\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-html-cache-control\"\n                    :label=\"$t('sync.htmlCacheControl')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"s3-html-cache-control\"\n                        key=\"s3-html-cache-control\"\n                        :spellcheck=\"false\"\n                        v-model=\"deploymentSettings.s3.htmlCacheControl\" />\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-other-cache-control\"\n                    :label=\"$t('sync.otherCacheControl')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"s3-other-cache-control\"\n                        key=\"s3-other-cache-control\"\n                        :spellcheck=\"false\"\n                        v-model=\"deploymentSettings.s3.otherCacheControl\" />\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 's3'\"\n                    id=\"s3-acl\"\n                    :label=\"$t('sync.acl')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"s3-acl\"\n                        :items=\"s3acls\"\n                        key=\"s3-acl\"\n                        v-model=\"deploymentSettings.s3.acl\"></dropdown>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'google-cloud'\"\n                    id=\"google-key\"\n                    :label=\"$t('sync.yourJSONKey')\">\n                    <file-select\n                        id=\"google-key\"\n                        v-model=\"deploymentSettings.google.key\"\n                        key=\"google-key\"\n                        :class=\"{ 'is-invalid': errors.indexOf('google-key') > -1 }\"\n                        @click.native=\"cleanError('google-key')\"\n                        slot=\"field\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('google-key') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.yourJSONKeyFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'google-cloud'\"\n                    id=\"google-bucket\"\n                    :label=\"$t('sync.bucket')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"google-bucket\"\n                        key=\"google-bucket\"\n                        :spellcheck=\"false\"\n                        :class=\"{ 'is-invalid': errors.indexOf('google-bucket') > -1 }\"\n                        @keyup.native=\"cleanError('google-bucket')\"\n                        v-model=\"deploymentSettings.google.bucket\" />\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('google-bucket') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.bucketFieldCantBeEmpty') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'google-cloud'\"\n                    id=\"google-prefix\"\n                    :label=\"$t('sync.prefix')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"google-prefix\"\n                        key=\"google-prefix\"\n                        :spellcheck=\"false\"\n                        v-model=\"deploymentSettings.google.prefix\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\"\n                        v-pure-html=\"$t('sync.googleCloudPrefixNote')\">\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'manual'\"\n                    id=\"manual-output\"\n                    :label=\"$t('sync.outputType')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"manual-output\"\n                        key=\"manual-output\"\n                        :items=\"{ 'catalog': $t('sync.nonCompressedCatalog'), 'zip-archive': $t('sync.zipArchive'), 'tar-archive': $t('sync.tarArchive') }\"\n                        :class=\"{ 'is-invalid': errors.indexOf('manual-output') > -1 }\"\n                        @click.native=\"cleanError('manual-output')\"\n                        v-model=\"deploymentSettings.manual.output\"></dropdown>\n\n                    <small\n                        slot=\"note\"\n                        v-if=\"errors.indexOf('manual-output') > -1\"\n                        class=\"note\">\n                        {{ $t('sync.manualOutputFieldCantBeEmpty') }}\n                    </small>\n\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.outputTypeNote') }}\n                    </small>\n                </field>\n\n                <field\n                    v-if=\"deploymentMethodSelected === 'manual'\"\n                    id=\"manual-output-dir\"\n                    :label=\"$t('sync.outputDirectory')\">\n                    <dir-select\n                        id=\"manual-output-dir\"\n                        v-model=\"deploymentSettings.manual.outputDirectory\"\n                        :placeholder=\"$t('sync.leaveBlankToUseDefaultOutputDirectory')\"\n                        slot=\"field\"\n                        key=\"manual-output-dir\" />\n\n                    <small\n                        v-if=\"deploymentSettings.manual.outputDirectory\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.outputDirectoryNote', { siteName: this.$store.state.currentSite.config.name }) }}\n                    </small>\n                </field>\n            </fields-group>\n\n            <p-footer v-if=\"deploymentMethodSelected !== ''\">\n                <p-button\n                    :onClick=\"save\"\n                    slot=\"buttons\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"deploymentMethodSelected !== 'manual'\"\n                    :onClick=\"testConnection\"\n                    :disabled=\"testInProgress\"\n                    slot=\"buttons\"\n                    type=\"secondary\">\n                    <template v-if=\"!testInProgress\">{{ $t('sync.testConnection') }}</template>\n                    <template v-if=\"testInProgress\">{{ $t('sync.checkingConnection') }}</template>\n                </p-button>\n            </p-footer>\n        </div>\n    </section>\n</template>\n\n<script>\nimport Vue from 'vue';\nimport Utils from './../helpers/utils.js';\nimport defaultDeploymentSettings from './configs/defaultDeploymentSettings.js';\nimport s3RegionsList from './configs/s3Regions.js';\nimport s3ACLs from './configs/s3ACLs.js';\n\nexport default {\n    name: 'server-settings',\n    data () {\n        return {\n            isLoaded: false,\n            domain: '',\n            httpProtocols: {\n                'https': 'https://',\n                'http': 'http://',\n                'file': 'file://',\n                'dat': 'dat://',\n                'hyper': 'hyper://',\n                'ipfs': 'ipfs://',\n                'dweb': 'dweb://',\n                '//': '//'\n            },\n            httpProtocolSelected: '',\n            deploymentMethods: {\n                'git': this.$t('sync.git'),\n                'github-pages': this.$t('sync.githubPages'),\n                'gitlab-pages': this.$t('sync.gitlabPages'),\n                'netlify': this.$t('sync.netlify'),\n                's3': this.$t('sync.s3CompatibleStorage'),\n                'google-cloud': this.$t('sync.googleCloud'),\n                'ftp': this.$t('sync.ftp'),\n                'sftp': this.$t('sync.sftpWithPassword'),\n                'sftp+key': this.$t('sync.sftpWithKey'),\n                'ftp+tls': this.$t('sync.ftpWithSSLTLS'),\n                'manual': this.$t('sync.ManualUpload')\n            },\n            deploymentMethodSelected: '',\n            deploymentSettings: defaultDeploymentSettings,\n            s3Regions: s3RegionsList,\n            s3acls: s3ACLs,\n            testInProgress: false,\n            errors: []\n        };\n    },\n    computed: {\n        currentDomain () {\n            if (this.$store.state.currentSite.config.domain === '.') {\n                return './';\n            }\n\n            return this.$store.state.currentSite.config.domain.replace('http://', '').replace('https://', '').replace('file:///', '').replace('file://', '');\n        },\n        currentHttpProtocol () {\n            if (this.$store.state.currentSite.config.domain.indexOf('file') === 0) {\n                return 'file';\n            } else if (this.$store.state.currentSite.config.domain.indexOf('http:') === 0) {\n                return 'http';\n            } else {\n                return 'https';\n            }\n        },\n        siteIsOnline () {\n            if(\n                !this.$store.state.currentSite.config.domain ||\n                this.$store.state.currentSite.config.deployment.protocol === 'manual'\n            ) {\n                return false;\n            }\n\n            return !!this.$store.state.currentSite.config.syncDate;\n        },\n        visitTitle () {\n            if(this.siteIsOnline) {\n                return this.$t('sync.visitYourWebsite');\n            } else {\n                return this.$t('sync.afterInitialSyncSiteWillBeAvailableOnline');\n            }\n        },\n        sftpAuthMethodItems () {\n            return [\n                { value: \"sftp\", label: this.$t('settings.password.password') },\n                { value: \"sftp+key\", label: this.$t('sync.keyFile') }\n            ];\n        }\n    },\n    watch: {\n        deploymentMethodSelected: function (newValue) {\n            setTimeout(() => {\n                this.setPortValue();\n            }, 0);\n        }\n    },\n    async mounted () {\n        this.isLoaded = false;\n        this.domain = this.currentDomain;\n        this.httpProtocolSelected = this.currentHttpProtocol;\n        this.deploymentMethodSelected = this.$store.state.currentSite.config.deployment.protocol || '';\n        let storedDeploymentSettings = JSON.parse(JSON.stringify(this.$store.state.currentSite.config.deployment));\n        storedDeploymentSettings = await this.loadPasswords(storedDeploymentSettings);\n        Vue.set(this, 'deploymentSettings', Utils.deepMerge(this.deploymentSettings, storedDeploymentSettings));\n\n        if (this.deploymentSettings.manual.output === '') {\n            this.deploymentSettings.manual.output = 'catalog';\n        }\n\n        setTimeout(() => {\n            this.setPortValue();\n            this.isLoaded = true;\n        }, 0);\n    },\n    methods: {\n        setPortValue () {\n            if (['', '21', '22', '990'].indexOf(this.deploymentSettings.port) === -1) {\n                return;\n            }\n\n            switch(this.deploymentMethodSelected) {\n                case 'sftp':\n                case 'sftp+key':\n                    this.deploymentSettings.port = '22'; break;\n                case 's3':\n                case 'git':\n                case 'github-pages':\n                case 'gitlab-pages':\n                case 'netlify':\n                case 'google-cloud':\n                case 'manual':\n                    this.deploymentSettings.port = ''; break;\n                case 'ftp':\n                case 'ftp+tls':\n                default:\n                    this.deploymentSettings.port = '21';\n            }\n        },\n        prepareDomain () {\n            return this.domain.replace('http://', '').replace('https://', '').replace('file://', '').replace(/\\/$/, '');\n        },\n        fullDomainName () {\n            let domain = this.prepareDomain();\n\n            if ((domain === '' || domain === '.') && this.deploymentSettings.relativeUrls) {\n                domain = '/';\n            }\n\n            if (this.deploymentSettings.relativeUrls) {\n                return domain;\n            }\n\n            if(this.deploymentMethodSelected === 'github-pages') {\n                if(domain.indexOf('github.io') > -1) {\n                    this.httpProtocolSelected = 'https';\n                }\n            }\n\n            if(this.deploymentMethodSelected === 'gitlab-pages') {\n                if(domain.indexOf('gitlab.io') > -1) {\n                    this.httpProtocolSelected = 'https';\n                }\n            }\n\n            if (this.httpProtocolSelected === 'file' && this.deploymentMethodSelected !== 'manual') {\n                this.httpProtocolSelected = 'https';\n            }\n\n            if (this.httpProtocolSelected === '//') {\n                return '//' + domain;\n            }\n\n            return this.httpProtocolSelected + '://' + domain;\n        },\n        save () {\n            if(!this.validate()) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('ui.fillAllRequiredFields'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                return;\n            }\n\n            let newSettings = this.getDeploymentSettings();\n\n            mainProcessAPI.send('app-site-config-save', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"settings\": newSettings,\n                \"source\": \"server\"\n            });\n\n            mainProcessAPI.receiveOnce('app-site-config-saved', (data) => {\n                if(data.status === true) {\n                    this.saved(newSettings);\n                }\n\n                if(data.message === 'success-save') {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('sync.serverSettingsSaveSuccessMsg'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                }\n\n                if(data.message === 'no-keyring') {\n                    if (document.body.getAttribute('data-os') === 'linux') {\n                        this.$bus.$emit('alert-display', {\n                            message: this.$t('sync.serverSettingsSaveLinuxErrorMsg'),\n                            okLabel: this.$t('ui.iUnderstand'),\n                        });\n                    } else {\n                        this.$bus.$emit('alert-display', {\n                            message: this.$t('sync.serverSettingsSaveErrorMsg'),\n                            okLabel: this.$t('ui.iUnderstand'),\n                        });\n                    }\n                }\n            });\n        },\n        saved (newSettings) {\n            newSettings.deployment = this.setHiddenPasswords(JSON.parse(JSON.stringify(newSettings.deployment)));\n\n            this.$store.commit('setSiteConfig', {\n                name: this.$store.state.currentSite.config.name,\n                config: newSettings\n            });\n        },\n        getDeploymentSettings () {\n            let newSettings = {\n                domain: this.fullDomainName().trim(),\n                deployment: Object.assign({}, defaultDeploymentSettings)\n            };\n            newSettings.deployment = Object.assign({}, newSettings.deployment, this.deploymentSettings);\n            newSettings.deployment.protocol = this.deploymentMethodSelected;\n            let currentSiteConfigCopy = JSON.parse(JSON.stringify(this.$store.state.currentSite.config));\n            return Utils.deepMerge(currentSiteConfigCopy, newSettings);\n        },\n        testConnection () {\n            if(['ftp', 'sftp', 'sftp+key', 'ftp+tls'].indexOf(this.deploymentMethodSelected) > -1 && this.deploymentSettings.askforpassword) {\n                this.$bus.$emit('confirm-display', {\n                    hasInput: true,\n                    inputIsPassword: true,\n                    message: this.$t('sync.provideFTPPasswordFor') + this.$refs['server'].getValue(),\n                    okClick: (password) => {\n                        if(!password) {\n                            this.$bus.$emit('alert-display', {\n                                message: this.$t('sync.testConnectionNoPasswordMsg')\n                            });\n                        } else {\n                            this.test(password);\n                        }\n                    }\n                });\n            } else {\n                this.test();\n            }\n        },\n        test (password = false) {\n            if(!this.validate()) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('ui.fillAllRequiredFields'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                return;\n            }\n\n            let deploymentSettings = this.getDeploymentSettings().deployment;\n\n            if(password) {\n                deploymentSettings.password = password;\n            }\n\n            this.testInProgress = true;\n\n            mainProcessAPI.send('app-deploy-test', {\n                siteName: this.$store.state.currentSite.config.name,\n                deploymentConfig: deploymentSettings,\n                uuid: this.$store.state.currentSite.config.uuid\n            });\n\n            mainProcessAPI.receiveOnce('app-deploy-test-success', (data) => {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('sync.connectToServerSuccessMsg'),\n                    buttonStyle: 'success'\n                });\n\n                this.testInProgress = false;\n            });\n\n            mainProcessAPI.receiveOnce('app-deploy-test-write-error', (data) => {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('sync.connectToServerCantStoreFilesErrorMsg'),\n                    buttonStyle: 'danger'\n                });\n\n                this.testInProgress = false;\n            });\n\n            mainProcessAPI.receiveOnce('app-deploy-test-error', (data) => {\n                console.log(data);\n                if(data && data.message) {\n                    if (data.message.translation) {\n                        data.message = this.$t(data.message.translation);\n                    }\n                    this.$bus.$emit('alert-display', {\n                        message: data.noAdditionalMessage ? data.message : this.$t('sync.connectToServerCantStoreFilesErrorMsg') + ': ' + data.message,\n                        buttonStyle: 'danger'\n                    });\n                } else {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('sync.connectToServerCantStoreFilesErrorMsg') + '.',\n                        buttonStyle: 'danger'\n                    });\n                }\n\n                this.testInProgress = false;\n            });\n        },\n        validate () {\n            this.errors = [];\n\n            switch (this.deploymentMethodSelected) {\n                case 'ftp':\n                case 'sftp':\n                case 'sftp+key':\n                case 'ftp+tls':\n                    this.validateFtp();\n                    break;\n                case 's3':\n                    this.validateS3();\n                    break;\n                case 'git':\n                    this.validateGit();\n                    break;\n                case 'github-pages':\n                    this.validateGithubPages();\n                    break;\n                case 'gitlab-pages':\n                    this.validateGitlabPages();\n                    break;\n                case 'netlify':\n                    this.validateNetlify();\n                    break;\n                case 'google-cloud':\n                    this.validateGoogleCloud();\n                    break;\n                case 'manual':\n                    this.validateManual();\n                    break;\n            }\n\n            return !this.errors.length;\n        },\n        validateFtp () {\n            let portValue = parseInt(this.deploymentSettings.port.trim(), 10)\n\n            if (this.deploymentSettings.port.trim() === '' || isNaN(portValue) || portValue < 1 || portValue > 65535) {\n                this.errors.push('port');\n            }\n\n            if (this.deploymentSettings.server.trim() === '') {\n                this.errors.push('server');\n            }\n\n            if (this.deploymentSettings.username.trim() === '') {\n                this.errors.push('username');\n            }\n\n            if (\n                this.deploymentMethodSelected !== 'sftp+key' &&\n                !this.deploymentSettings.askforpassword &&\n                this.deploymentSettings.password.trim() === ''\n            ) {\n                this.errors.push('password');\n            }\n\n            if (\n                this.deploymentMethodSelected === 'sftp+key' &&\n                this.deploymentSettings.sftpkey.trim() === ''\n            ) {\n                this.errors.push('key');\n            }\n        },\n        validateFields (fields) {\n            for (let i = 0; i < fields.length; i++) {\n                let field = fields[i].split('_');\n\n                if (!this.deploymentSettings[field[0]][field[1]]) {\n                    this.errors.push(fields[i].replace('_', '-'));\n                } else if (this.deploymentSettings[field[0]][field[1]] && this.deploymentSettings[field[0]][field[1]].trim() === '') {\n                    this.errors.push(fields[i].replace('_', '-'));\n                }\n            }\n        },\n        validateS3 () {\n            let fields = ['s3_id', 's3_key', 's3_bucket', 's3_region'];\n\n            if (this.deploymentSettings.s3.customProvider) {\n                fields = ['s3_endpoint', 's3_id', 's3_key', 's3_bucket', 's3_customRegion'];\n            }\n\n            return this.validateFields(fields);\n        },\n        validateGit () {\n            let fields = ['git_url', 'git_user', 'git_password', 'git_branch', 'git_commitAuthor', 'git_commitMessage'];\n            return this.validateFields(fields);\n        },\n        validateGithubPages () {\n            let fields = ['github_server', 'github_user', 'github_repo', 'github_branch', 'github_token'];\n            return this.validateFields(fields);\n        },\n        validateGitlabPages () {\n            let fields = ['gitlab_server', 'gitlab_repo', 'gitlab_branch', 'gitlab_token'];\n            return this.validateFields(fields);\n        },\n        validateNetlify () {\n            let fields = ['netlify_id', 'netlify_token'];\n            return this.validateFields(fields);\n        },\n        validateGoogleCloud () {\n            let fields = ['google_key', 'google_bucket'];\n            return this.validateFields(fields);\n        },\n        validateManual () {\n            let fields = ['manual_output'];\n            return this.validateFields(fields);\n        },\n        visitWebsite () {\n            if(!this.siteIsOnline) {\n                return false;\n            }\n\n            let urlToOpen = Utils.getValidUrl(this.$store.state.currentSite.config.domain);\n\n            if (urlToOpen) {\n                mainProcessAPI.shellOpenExternal(urlToOpen);\n            } else {\n                alert(this.$t('sync.websiteLinkInvalidMsg'));\n            }\n        },\n        cleanError (field) {\n            let pos = this.errors.indexOf(field);\n\n            if (pos !== -1) {\n                this.errors.splice(pos, 1);\n            }\n        },\n        toggleDomainName () {\n            if (this.deploymentSettings.relativeUrls) {\n                this.domain = '/';\n            } else {\n                this.domain = '';\n            }\n        },\n        getDeploymentMethodName (method) {\n            switch (method) {\n                case 'github-pages': return this.$t('sync.githubPages');\n                case 'gitlab-pages': return this.$t('sync.gitlabPages');\n                case 'git': return this.$t('sync.git');\n                case 'netlify': return this.$t('sync.netlify');\n                case 's3': return this.$t('sync.s3CompatibleStorage');\n                case 'google-cloud': return this.$t('sync.googleCloud');\n                case 'ftp': return this.$t('sync.ftp');\n                case 'sftp': return this.$t('sync.sftp');\n                case 'sftp+key': return  this.$t('sync.sftpWithKey');\n                case 'ftp+tls': return this.$t('sync.ftpWithSSLTLS');\n                case 'manual': return this.$t('sync.ManualUpload');\n            }\n\n            return '';\n        },\n        async loadPasswords (deploymentSettings) {\n            deploymentSettings.password = await mainProcessAPI.invoke('app-main-process-load-password', 'publii', deploymentSettings.password);\n\n            if (deploymentSettings.passphrase) {\n                deploymentSettings.passphrase = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-passphrase', deploymentSettings.passphrase);\n            }\n\n            if (deploymentSettings.s3) {\n                deploymentSettings.s3.id = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-s3-id', deploymentSettings.s3.id);\n                deploymentSettings.s3.key = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-s3-key', deploymentSettings.s3.key);\n            }\n\n            if (deploymentSettings.netlify) {\n                deploymentSettings.netlify.id = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-netlify-id', deploymentSettings.netlify.id);\n                deploymentSettings.netlify.token = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-netlify-token', deploymentSettings.netlify.token);\n            }\n\n            if (deploymentSettings.github) {\n                deploymentSettings.github.token = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-gh-token', deploymentSettings.github.token);\n            }\n\n            if (deploymentSettings.git) {\n                deploymentSettings.git.password = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-git-password', deploymentSettings.git.password);\n            }\n\n            if (deploymentSettings.gitlab) {\n                deploymentSettings.gitlab.token = await mainProcessAPI.invoke('app-main-process-load-password', 'publii-gl-token', deploymentSettings.gitlab.token);\n            }\n\n            return deploymentSettings;\n        },\n        setHiddenPasswords (deploymentSettings) {\n            let passwordKey = this.$store.state.currentSite.config.name;\n\n            if (this.$store.state.currentSite.config.uuid) {\n                passwordKey = this.$store.state.currentSite.config.uuid;\n            }\n\n            deploymentSettings.password = 'publii ' + passwordKey;\n\n            if (deploymentSettings.passphrase) {\n                deploymentSettings.passphrase = 'publii-passphrase ' + passwordKey;\n            }\n\n            if (deploymentSettings.s3) {\n                deploymentSettings.s3.id = 'publii-s3-id ' + passwordKey;\n                deploymentSettings.s3.key = 'publii-s3-key ' + passwordKey;\n            }\n\n            if (deploymentSettings.netlify) {\n                deploymentSettings.netlify.id = 'publii-netlify-id ' + passwordKey;\n                deploymentSettings.netlify.token = 'publii-netlify-token ' + passwordKey;\n            }\n\n            if (deploymentSettings.git) {\n                deploymentSettings.git.password = 'publii-git-password ' + passwordKey;\n            }\n\n            if (deploymentSettings.github) {\n                deploymentSettings.github.token = 'publii-gh-token ' + passwordKey;\n            }\n\n            if (deploymentSettings.gitlab) {\n                deploymentSettings.gitlab.token = 'publii-gl-token ' + passwordKey;\n            }\n\n            return deploymentSettings;\n        },\n        toggleFtpDeploymentMethod () {\n            if (this.deploymentMethodSelected === 'ftp+tls') {\n                this.deploymentMethodSelected = 'ftp';\n            } else {\n                this.deploymentMethodSelected = 'ftp+tls';\n            }\n        },\n        toggleS3Provider () {\n            if (this.deploymentSettings.s3.customProvider) {\n                this.deploymentSettings.s3.provider = 'custom';\n            } else {\n                this.deploymentSettings.s3.provider = 'aws';\n            }\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/notifications.scss';\n\n.server-settings {\n    margin: 0 auto;\n    max-width: $wrapper;\n    user-select: none;\n\n    #http-protocol {\n        float: left;\n        width: 110px;\n\n        & + .input-wrapper {\n            float: right;\n            width: calc(100% - 120px);\n        }\n    }\n\n    .is-invalid + .note {\n        color: var(--warning);\n    }\n\n    &-grid {\n        display: grid;\n        grid-template-columns: repeat(3, 1fr);\n        gap: 2rem;\n\n        &-item {\n            align-items: center;\n            background-color: var(--bg-secondary);\n            border: 1px solid transparent;\n            border-radius: var(--border-radius);\n            box-shadow: var(--box-shadow-small);\n            color: var(--text-primary-color);\n            display: flex;\n            flex-direction: column;\n            fill: var(--icon-primary-color);\n            font-weight: var(--font-weight-semibold);\n            justify-content: center;\n            min-height: calc(8rem + 8vh);\n            position: relative;\n            transition: var(--transition);\n\n            &:hover {\n                background: var(--bg-primary);\n                border-color: var(--color-primary);\n                box-shadow: var(--box-shadow-medium);\n                color: var(--color-primary);\n                cursor: pointer;\n            }\n\n            & > svg {\n                margin: 0 auto 1rem;\n                transition: inherit;\n            }\n\n            &.deployment-others {\n                h3 {\n                    color: var(--text-primary-color);\n                    font-size: $app-font-base;\n                    font-weight: var(--font-weight-semibold);\n                    margin-bottom: 0;\n                    transition: inherit;\n                }\n\n                svg {\n                    fill: var(--icon-primary-color);\n                    transition: inherit;\n                }\n\n                &:hover {\n                    svg {\n                        fill: var(--color-primary);\n                    }\n\n                    h3 {\n                        color: var(--color-primary);\n                    }\n                }\n            }\n        }\n    }\n\n    #relative-urls {\n        margin-top: 0;\n    }\n\n    .msg {\n        margin-bottom: 3rem;\n    }\n}\n\n/*\n * Responsive improvements\n */\n\n @media (max-height: 900px) {\n    .server-settings-grid-item > svg {\n        transform: scale(0.9);\n    }\n}\n\n@media (max-width: 1400px) {\n    .server-settings-grid-item > svg {\n        transform: scale(0.9);\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Settings.vue",
    "content": "<template>\n    <section\n        class=\"content\"\n        ref=\"content\">\n        <div class=\"site-settings\">\n            <p-header :title=\"$t('settings.siteSettings')\">\n                <p-button\n                    @click.prevent.native=\"checkBeforeSave(false)\"\n                    slot=\"buttons\"\n                    type=\"secondary\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n\n                <btn-dropdown\n                    slot=\"buttons\"\n                    buttonColor=\"green\"\n                    :items=\"dropdownItems\"\n                    :disabled=\"!siteHasTheme || buttonsLocked\"\n                    localStorageKey=\"publii-preview-mode\"\n                    :previewIcon=\"true\"\n                    defaultValue=\"full-site-preview\" />\n            </p-header>\n\n            <fields-group :title=\"$t('settings.basicSettings')\">\n                <logo-creator\n                    ref=\"logo-creator\"\n                    slot=\"\" />\n\n                <field\n                    id=\"name\"\n                    :label=\"$t('site.siteName')\">\n                    <text-input\n                        slot=\"field\"\n                        ref=\"name\"\n                        id=\"name\"\n                        key=\"name\"\n                        :readonly=\"syncInProgress\"\n                        :spellcheck=\"false\"\n                        v-model=\"name\" />\n                    <small\n                        v-if=\"syncInProgress\"\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('sync.syncInProgressMessage') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"description\"\n                    :label=\"$t('site.siteDescription')\">\n                    <text-area\n                        slot=\"field\"\n                        ref=\"description\"\n                        id=\"description\"\n                        key=\"description\"\n                        :spellcheck=\"false\"\n                        v-model=\"description\" />\n                </field>\n\n                <field\n                    id=\"language\"\n                    :label=\"$t('langs.language')\">\n                    <dropdown\n                        slot=\"field\"\n                        id=\"language\"\n                        ref=\"language\"\n                        key=\"language\"\n                        v-model=\"language\"\n                        :items=\"availableLanguages\"></dropdown>\n                </field>\n\n                <field\n                    v-if=\"language === 'custom'\"\n                    id=\"customLanguage\"\n                    :label=\"$t('settings.customLanguageCode')\">\n                    <text-input\n                        slot=\"field\"\n                        id=\"customLanguage\"\n                        ref=\"customLanguage\"\n                        key=\"customLanguage\"\n                        :spellcheck=\"false\"\n                        v-model=\"customLanguage\" />\n                </field>\n\n                <field\n                    id=\"spellchecking\"\n                    :label=\"$t('settings.enableSpellchecker')\">\n                    <switcher\n                        slot=\"field\"\n                        id=\"spellchecking\"\n                        v-model=\"spellchecking\" />\n                    <small\n                        v-if=\"hasNonAutomaticSpellchecker && spellcheckIsNotSupported\"\n                        slot=\"note\"\n                        class=\"note is-invalid\">\n                        {{ $t('settings.spellcheckerDoesNotSupportLanguage') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"theme\"\n                    :label=\"$t('settings.currentTheme')\">\n                    <strong\n                        v-if=\"currentTheme\"\n                        slot=\"field\">\n                        {{ currentTheme }} (v.{{currentThemeVersion}})\n                    </strong>\n\n                    <strong\n                        v-if=\"!currentTheme\"\n                        slot=\"field\">\n                        {{ $t('settings.notSelected') }}\n                    </strong>\n\n                    <themes-dropdown\n                        slot=\"field\"\n                        id=\"theme\"\n                        ref=\"theme\"\n                        key=\"theme\"\n                        v-model=\"theme\"></themes-dropdown>\n                </field>\n\n                <div\n                    v-if=\"!currentThemeHasSupportedFeaturesList\"\n                    class=\"msg msg-icon msg-alert\">\n                    <icon\n                        name=\"warning\"\n                        customWidth=\"28\"\n                        customHeight=\"28\" />\n                    <div v-pure-html=\"$t('settings.themeDoesNotHaveSupportedFeaturesList')\"></div>\n                </div>\n            </fields-group>\n            \n            <fields-group :title=\"$t('settings.advancedOptions')\">\n                <div\n                    v-if=\"this.$store.state.currentSite.config.advanced && this.$store.state.currentSite.config.advanced.gdpr && this.$store.state.currentSite.config.advanced.gdpr.enabled && !this.$store.state.currentSite.config.advanced.gdpr.settingsVersion\"\n                    class=\"msg msg-icon msg-alert msg-bm\">\n                    <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                    <p v-pure-html=\"$t('settings.youMustReviewGdprSettings')\"></p>\n                </div>\n\n                <tabs\n                    ref=\"advanced-tabs\"\n                    id=\"advanced-basic-settings-tabs\"\n                    :items=\"advancedTabs\">\n                    <div slot=\"tab-0\">\n                        <field\n                            id=\"no-index-this-page\"\n                            :label=\"$t('settings.noIndexWebsite')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.noIndexThisPage\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.dontIndexThisPage') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage\"\n                            id=\"no-index-for-chat-gpt-bot\"\n                            :label=\"$t('settings.noIndexForChatGPTBot')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.noIndexForChatGPTBot\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.noIndexForChatGPTBotInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage\"\n                            id=\"no-index-for-chat-gpt-user\"\n                            :label=\"$t('settings.noIndexForChatGPTUser')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.noIndexForChatGPTUser\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.noIndexForChatGPTUserInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage\"\n                            id=\"no-index-for-common-crawl-bots\"\n                            :label=\"$t('settings.noIndexForCommonCrawlBots')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.noIndexForCommonCrawlBots\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.noIndexForCommonCrawlBotsInfo') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            type=\"medium\"\n                            :label=\"$t('settings.frontpage')\" />\n\n                        <field\n                            id=\"homepage-as-page\"\n                            :label=\"$t('settings.usePageAsFrontpage')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"homepage-as-page\"\n                                v-model=\"advanced.usePageAsFrontpage\"\n                                :disabled=\"!currentThemeSupportsPages\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.usePageAsFrontpageNotice')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.usePageAsFrontpage\"\n                            id=\"page-as-frontpage\"\n                            :label=\"$t('settings.pageAsFrontpage')\">\n                            <pages-dropdown\n                                v-model=\"advanced.pageAsFrontpage\"\n                                :multiple=\"false\"\n                                slot=\"field\"></pages-dropdown>\n                            <small\n                                v-if=\"errors.indexOf('page-as-frontpage') > -1\"\n                                class=\"note is-warning\"\n                                slot=\"note\">\n                                {{ $t('settings.youMustSelectFrontpage') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            v-if=\"!advanced.noIndexThisPage && !advanced.usePageAsFrontpage && advanced.urls.postsPrefix\"\n                            type=\"medium\"\n                            :label=\"$t('settings.homepage')\" />\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && advanced.urls.postsPrefix && !advanced.usePageAsFrontpage\"\n                            id=\"meta-title\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"meta-title\"\n                                v-model=\"advanced.homepageMetaTitle\"\n                                slot=\"field\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && advanced.urls.postsPrefix && !advanced.usePageAsFrontpage\"\n                            id=\"meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"meta-description\"\n                                v-model=\"advanced.homepageMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && advanced.urls.postsPrefix && !advanced.usePageAsFrontpage\"\n                            id=\"meta-robots-index\"\n                            :label=\"$t('settings.metaRobots')\">\n                            <dropdown\n                                id=\"meta-robots-index\"\n                                slot=\"field\"\n                                :items=\"seoOptions\"\n                                v-model=\"advanced.homepageMetaRobotsIndex\">\n                            </dropdown>\n                        </field>\n\n                        <separator\n                            v-if=\"!advanced.noIndexThisPage && advanced.urls.postsPrefix\"\n                            type=\"medium\"\n                            :label=\"$t('settings.postsIndex')\" />\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && (advanced.urls.postsPrefix || (!advanced.urls.postsPrefix && !advanced.usePageAsFrontpage))\"\n                            id=\"meta-title\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"meta-title\"\n                                v-model=\"advanced.metaTitle\"\n                                slot=\"field\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && (advanced.urls.postsPrefix || (!advanced.urls.postsPrefix && !advanced.usePageAsFrontpage))\"\n                            id=\"meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"meta-description\"\n                                v-model=\"advanced.metaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && (advanced.urls.postsPrefix || (!advanced.urls.postsPrefix && !advanced.usePageAsFrontpage))\"\n                            id=\"meta-robots-index\"\n                            :label=\"$t('settings.metaRobots')\">\n                            <dropdown\n                                id=\"meta-robots-index\"\n                                slot=\"field\"\n                                :items=\"seoOptions\"\n                                v-model=\"advanced.metaRobotsIndex\">\n                            </dropdown>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && (advanced.urls.postsPrefix || (!advanced.urls.postsPrefix && !advanced.usePageAsFrontpage))\"\n                            id=\"homepage-no-index-pagination\"\n                            :label=\"$t('settings.disableHomepagePaginationIndexing')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"homepage-no-index-pagination\"\n                                v-model=\"advanced.homepageNoIndexPagination\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.homepageNoIndexPagination')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.urls.postsPrefix || (!advanced.urls.postsPrefix && !advanced.usePageAsFrontpage)\"\n                            id=\"homepage-no-pagination\"\n                            :label=\"$t('settings.disableHomepagePagination')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"homepage-no-pagination\"\n                                v-model=\"advanced.homepageNoPagination\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.disableHomepagePaginationInfo')\">\n                            </small>\n                        </field>\n\n                        <separator\n                            type=\"medium\"\n                            :label=\"$t('post.postPage')\" />\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage\"\n                            id=\"post-meta-title\"\n                            :withCharCounter=\"true\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"post-meta-title\"\n                                slot=\"field\"\n                                v-model=\"advanced.postMetaTitle\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"70\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.postPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage\"\n                            id=\"post-meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"post-meta-description\"\n                                v-model=\"advanced.postMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.postPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"post-use-text-without-custom-excerpt\"\n                            :label=\"$t('settings.hideCustomExcerptsOnPostPages')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"post-use-text-without-custom-excerpt\"\n                                v-model=\"advanced.postUseTextWithoutCustomExcerpt\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.hideCustomExcerptsOnPostPagesInfo') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            type=\"medium\"\n                            :label=\"$t('page.pages')\" />\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage\"\n                            id=\"page-meta-title\"\n                            :withCharCounter=\"true\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"page-meta-title\"\n                                slot=\"field\"\n                                v-model=\"advanced.pageMetaTitle\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"70\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.pageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage\"\n                            id=\"page-meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"page-meta-description\"\n                                v-model=\"advanced.pageMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.pageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"page-use-text-without-custom-excerpt\"\n                            :label=\"$t('settings.hideCustomExcerptsOnPagePages')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"page-use-text-without-custom-excerpt\"\n                                v-model=\"advanced.pageUseTextWithoutCustomExcerpt\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.hideCustomExcerptsOnPagePagesInfo') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.urls.tagsPrefix !== '' && !advanced.noIndexThisPage\"\n                            type=\"medium\"\n                            :label=\"$t('settings.tagsListPage')\" />\n\n                        <div\n                            v-if=\"advanced.urls.tagsPrefix !== '' && !currentThemeSupportsTagsList\"\n                            class=\"msg msg-icon msg-alert\">\n                            <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                            <p>{{ $t('settings.themeDoesNotSupportTagsListPage') }} </p>\n                        </div>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && advanced.urls.tagsPrefix !== '' && currentThemeSupportsTagsList\"\n                            id=\"tags-list-meta-title\"\n                            :withCharCounter=\"true\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"tags-list-meta-title\"\n                                v-model=\"advanced.tagsMetaTitle\"\n                                slot=\"field\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.tagsListPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && advanced.urls.tagsPrefix !== '' && currentThemeSupportsTagsList\"\n                            id=\"tags-list-meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"tags-list-meta-description\"\n                                v-model=\"advanced.tagsMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.tagsListPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && advanced.urls.tagsPrefix !== '' && currentThemeSupportsTagsList\"\n                            id=\"meta-robots-tags-list\"\n                            :label=\"$t('settings.metaRobots')\">\n                            <dropdown\n                                id=\"meta-robots-tags-list\"\n                                slot=\"field\"\n                                :items=\"seoOptions\"\n                                v-model=\"advanced.metaRobotsTagsList\">\n                            </dropdown>\n                        </field>\n\n                        <separator\n                            type=\"medium\"\n                            :label=\"$t('tag.tagPage')\" />\n\n                        <div\n                            v-if=\"!currentThemeSupportsTagPages\"\n                            class=\"msg msg-icon msg-alert\">\n                            <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                            <p>{{ $t('settings.themeDoesNotSupportTagPages') }}</p>\n                        </div>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsTagPages\"\n                            id=\"tag-meta-title\"\n                            :withCharCounter=\"true\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"tag-meta-title\"\n                                v-model=\"advanced.tagMetaTitle\"\n                                slot=\"field\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.tagPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsTagPages\"\n                            id=\"tag-meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"tag-meta-description\"\n                                v-model=\"advanced.tagMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.tagPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsTagPages\"\n                            id=\"meta-robots-tags\"\n                            :label=\"$t('settings.metaRobots')\">\n                            <dropdown\n                                id=\"meta-robots-tags\"\n                                slot=\"field\"\n                                :items=\"seoOptions\"\n                                v-model=\"advanced.metaRobotsTags\">\n                            </dropdown>\n                        </field>\n\n                        <field\n                            v-if=\"currentThemeSupportsTagPages\"\n                            id=\"display-empty-tags\"\n                            :label=\"$t('settings.displayEmptyTags')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"display-empty-tags\"\n                                v-model=\"advanced.displayEmptyTags\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.displayEmptyTagsInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsTagPages\"\n                            id=\"tag-no-index-pagination\"\n                            :label=\"$t('settings.disableTagsPaginationIndexing')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"tag-no-index-pagination\"\n                                v-model=\"advanced.tagNoIndexPagination\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.disableTagsPaginationIndexingInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"currentThemeSupportsTagPages\"\n                            id=\"tag-no-pagination\"\n                            :label=\"$t('settings.disableTagsPagination')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"tag-no-pagination\"\n                                v-model=\"advanced.tagNoPagination\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.disableTagsPaginationInfo') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            type=\"medium\"\n                            :label=\"$t('settings.authorPage')\" />\n\n                        <div\n                            v-if=\"!currentThemeSupportsAuthorPages\"\n                            class=\"msg msg-icon msg-alert\">\n                            <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                            <p>{{ $t('settings.themeDoesNotSupportAuthorPages') }}</p>\n                        </div>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsAuthorPages\"\n                            id=\"author-meta-title\"\n                            :withCharCounter=\"true\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"author-meta-title\"\n                                v-model=\"advanced.authorMetaTitle\"\n                                slot=\"field\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.authorPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsAuthorPages\"\n                            id=\"author-meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"author-meta-description\"\n                                v-model=\"advanced.authorMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.authorPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsAuthorPages\"\n                            id=\"meta-robots-authors\"\n                            :label=\"$t('settings.metaRobots')\">\n                            <dropdown\n                                id=\"meta-robots-tags\"\n                                slot=\"field\"\n                                :items=\"seoOptions\"\n                                v-model=\"advanced.metaRobotsAuthors\">\n                            </dropdown>\n                        </field>\n\n                        <field\n                            v-if=\"currentThemeSupportsAuthorPages\"\n                            id=\"display-empty-authors\"\n                            :label=\"$t('settings.displayEmptyAuthors')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.displayEmptyAuthors\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.displayEmptyAuthorsInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsAuthorPages\"\n                            id=\"author-no-index-pagination\"\n                            :label=\"$t('settings.disableAuthorsPaginationIndexing')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"author-no-index-pagination\"\n                                v-model=\"advanced.authorNoIndexPagination\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.disableAuthorsPaginationIndexingInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"currentThemeSupportsAuthorPages\"\n                            id=\"author-no-pagination\"\n                            :label=\"$t('settings.disableAuthorsPagination')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"author-no-pagination\"\n                                v-model=\"advanced.authorNoPagination\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.disableAuthorsPaginationInfo') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            v-if=\"!advanced.noIndexThisPage\"\n                            type=\"medium\"\n                            :label=\"$t('settings.errorPage')\" />\n\n                        <div\n                            v-if=\"!advanced.noIndexThisPage && !currentThemeSupportsErrorPage\"\n                            class=\"msg msg-icon msg-alert\">\n                            <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                            <p>{{ $t('settings.themeDoesNotSupport404ErrorPage') }}</p>\n                        </div>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsErrorPage\"\n                            id=\"error-meta-title\"\n                            :withCharCounter=\"true\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"author-meta-title\"\n                                v-model=\"advanced.errorMetaTitle\"\n                                slot=\"field\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.errorPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsErrorPage\"\n                            id=\"error-meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"error-meta-description\"\n                                v-model=\"advanced.errorMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.errorPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsErrorPage\"\n                            id=\"meta-robots-error\"\n                            :label=\"$t('settings.metaRobots')\">\n                            <dropdown\n                                id=\"meta-robots-error\"\n                                slot=\"field\"\n                                :items=\"seoOptions\"\n                                v-model=\"advanced.metaRobotsError\">\n                            </dropdown>\n                        </field>\n\n                        <separator\n                            v-if=\"!advanced.noIndexThisPage\"\n                            type=\"medium\"\n                            :label=\"$t('settings.searchPage')\" />\n\n                        <div\n                            v-if=\"!advanced.noIndexThisPage && !currentThemeSupportsSearchPage\"\n                            class=\"msg msg-icon msg-alert\">\n                            <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                            <p>{{ $t('settings.themeDoesNotSupportSearchPages') }}</p>\n                        </div>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsSearchPage\"\n                            id=\"error-meta-title\"\n                            :withCharCounter=\"true\"\n                            :label=\"$t('settings.pageTitle')\">\n                            <text-input\n                                id=\"search-meta-title\"\n                                v-model=\"advanced.searchMetaTitle\"\n                                slot=\"field\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.searchPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsSearchPage\"\n                            id=\"search-meta-description\"\n                            :label=\"$t('settings.metaDescription')\">\n                            <text-area\n                                id=\"search-meta-description\"\n                                v-model=\"advanced.searchMetaDescription\"\n                                slot=\"field\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.searchPageTitleVariables') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && currentThemeSupportsSearchPage\"\n                            id=\"meta-robots-search\"\n                            :label=\"$t('settings.metaRobots')\">\n                            <dropdown\n                                id=\"meta-robots-search\"\n                                slot=\"field\"\n                                :items=\"seoOptions\"\n                                v-model=\"advanced.metaRobotsSearch\">\n                            </dropdown>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-1\">\n                        <field\n                            id=\"clean-urls\"\n                            :label=\"$t('settings.usePrettyURLs')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.urls.cleanUrls\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.usePrettyURLsInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"add-index\"\n                            :label=\"$t('settings.alwaysAddIndexHTMLInURLs')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.urls.addIndex\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.cannotAddIndexHTMLInURLsInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.urls.cleanUrls\"\n                            id=\"url-posts-prefix\"\n                            :label=\"$t('settings.postsPrefix')\">\n                            <text-input\n                                id=\"url-posts-prefix\"\n                                :class=\"{ 'is-invalid': errors.indexOf('posts-prefix') > -1 }\"\n                                @click.native=\"clearErrors('posts-prefix')\"\n                                v-model=\"advanced.urls.postsPrefix\"\n                                :spellcheck=\"false\"\n                                slot=\"field\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.postPrefixInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"url-tags-prefix\"\n                            :label=\"$t('settings.tagPrefix')\">\n                            <text-input\n                                id=\"url-tags-prefix\"\n                                :class=\"{ 'is-invalid': errors.indexOf('tags-prefix') > -1 }\"\n                                @click.native=\"clearErrors('tags-prefix')\"\n                                v-model=\"advanced.urls.tagsPrefix\"\n                                :spellcheck=\"false\"\n                                :disabled=\"!currentThemeSupportsTagPages\"\n                                slot=\"field\" />\n                            <small\n                                v-if=\"!currentThemeSupportsTagPages\"\n                                class=\"note is-warning\"\n                                slot=\"note\">\n                                {{ $t('settings.themeDoesNotSupportTagPages') }}\n                            </small>\n                            <small\n                                v-else\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"advanced.urls.postsPrefix && advanced.urls.tagsPrefixAfterPostsPrefix ? $t('settings.tagPrefixInfoExtended') : $t('settings.tagPrefixInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.urls.postsPrefix\"\n                            id=\"tags-prefix-after-posts-prefix\"\n                            :label=\"$t('settings.tagsPrefixAfterPostsPrefix')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.urls.tagsPrefixAfterPostsPrefix\" />\n                        </field>\n\n                        <field\n                            id=\"url-authors-prefix\"\n                            :label=\"$t('settings.authorPrefix')\">\n                            <text-input\n                                id=\"url-authors-prefix\"\n                                :class=\"{ 'is-invalid': errors.indexOf('authors-prefix') > -1 }\"\n                                @click.native=\"clearErrors('authors-prefix')\"\n                                v-model=\"advanced.urls.authorsPrefix\"\n                                :spellcheck=\"false\"\n                                :disabled=\"!currentThemeSupportsAuthorPages\"\n                                slot=\"field\" />\n                            <small\n                                v-if=\"!currentThemeSupportsAuthorPages\"\n                                class=\"note is-warning\"\n                                slot=\"note\">\n                                {{ $t('settings.themeDoesNotSupportAuthorPages') }}\n                            </small>\n                            <small\n                                v-else\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"advanced.urls.postsPrefix && advanced.urls.authorsPrefixAfterPostsPrefix ? $t('settings.authorPrefixInfoExtended') : $t('settings.authorPrefixInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.urls.postsPrefix\"\n                            id=\"authors-prefix-after-posts-prefix\"\n                            :label=\"$t('settings.authorsPrefixAfterPostsPrefix')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.urls.authorsPrefixAfterPostsPrefix\" />\n                        </field>\n\n                        <field\n                            id=\"url-pagination-phrase\"\n                            :label=\"$t('settings.paginationPhrase')\">\n                            <text-input\n                                id=\"url-pagination-phrase\"\n                                :class=\"{ 'is-invalid': errors.indexOf('pagination-phrase') > -1 }\"\n                                @click.native=\"clearErrors('pagination-phrase')\"\n                                v-model=\"advanced.urls.pageName\"\n                                :spellcheck=\"false\"\n                                slot=\"field\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.paginationPhraseInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"error-page-file\"\n                            :label=\"$t('settings.errorPage')\">\n                            <text-input\n                                id=\"error-page-file\"\n                                :class=\"{ 'is-invalid': errors.indexOf('error-page') > -1 }\"\n                                @click.native=\"clearErrors('error-page')\"\n                                :readonly=\"!currentThemeSupportsErrorPage\"\n                                v-model=\"advanced.urls.errorPage\"\n                                :spellcheck=\"false\"\n                                :disabled=\"!currentThemeSupportsErrorPage\"\n                                slot=\"field\" />\n                            <small\n                                v-if=\"!currentThemeSupportsErrorPage\"\n                                class=\"note is-warning\"\n                                slot=\"note\">\n                                {{ $t('settings.themeDoesNotSupportErrorPages') }}\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"search-page-file\"\n                            :label=\"$t('settings.searchPage')\">\n                            <text-input\n                                id=\"search-page-file\"\n                                :class=\"{ 'is-invalid': errors.indexOf('search-page') > -1 }\"\n                                @click.native=\"clearErrors('search-page')\"\n                                :readonly=\"!currentThemeSupportsSearchPage\"\n                                v-model=\"advanced.urls.searchPage\"\n                                :spellcheck=\"false\"\n                                :disabled=\"!currentThemeSupportsSearchPage\"\n                                slot=\"field\" />\n                            <small\n                                v-if=\"!currentThemeSupportsSearchPage\"\n                                class=\"note is-warning\"\n                                slot=\"note\">\n                                {{ $t('settings.themeDoesNotSupportSearchPages') }}\n                            </small>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-2\">\n                        <field\n                            v-if=\"advanced.noIndexThisPage\"\n                            :label=\"$t('settings.toViewSitemapEnableIndexingInfo')\"\n                            :labelFullWidth=\"true\" />\n\n                        <field\n                            v-if=\"siteUsesRelativeUrls\"\n                            :label=\"$t('settings.sitemapRelativeUrlsInfo')\"\n                            :labelFullWidth=\"true\" />\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && !siteUsesRelativeUrls\"\n                            id=\"sitemap-enabled\"\n                            :label=\"$t('settings.createXMLSitemap')\">\n                            <switcher\n                                slot=\"field\"\n                                v-model=\"advanced.sitemapEnabled\" />\n                            <small\n                                v-if=\"sitemapLink\"\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.sitemapLinkInfo', { sitemapLink: sitemapLink })\">\n                            </small>\n                        </field>\n\n                        <field\n                            class=\"multiple-checkboxes field-with-switcher\"\n                            v-if=\"!advanced.noIndexThisPage && !siteUsesRelativeUrls && advanced.sitemapEnabled\"\n                            :label=\"$t('settings.content')\">\n                            <label\n                                v-if=\"advanced.sitemapEnabled\"\n                                slot=\"field\">\n                                <switcher \n                                    v-model=\"advanced.sitemapAddTags\"\n                                    :disabled=\"!currentThemeSupportsTagPages\" />\n                                {{ $t('settings.tagPages') }}\n                            </label>\n\n                            <label\n                                v-if=\"advanced.sitemapEnabled && !siteUsesRelativeUrls\"\n                                slot=\"field\">\n                                <switcher \n                                    v-model=\"advanced.sitemapAddAuthors\"\n                                    :disabled=\"!currentThemeSupportsAuthorPages\" />\n                                {{ $t('settings.authorPages') }}\n                            </label>\n\n                            <label\n                                v-if=\"advanced.sitemapEnabled && !siteUsesRelativeUrls\"\n                                slot=\"field\">\n                                <switcher v-model=\"advanced.sitemapAddHomepage\" />\n                                {{ $t('settings.homepagePagination') }}\n                            </label>\n\n                            <label\n                                v-if=\"advanced.sitemapEnabled && !siteUsesRelativeUrls\"\n                                slot=\"field\">\n                                <switcher v-model=\"advanced.sitemapAddExternalImages\" />\n                                {{ $t('settings.externalImages') }}\n                            </label>\n                        </field>\n\n                        <field\n                            v-if=\"!advanced.noIndexThisPage && !siteUsesRelativeUrls && advanced.sitemapEnabled\"\n                            :label=\"$t('settings.excludedFiles')\">\n                            <text-area\n                                slot=\"field\"\n                                :spellcheck=\"false\"\n                                v-model=\"advanced.sitemapExcludedFiles\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.excludeFilesFromSitemapInfo')\">\n                            </small>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-3\">\n                        <field\n                            id=\"open-graph-enabled\"\n                            :label=\"$t('settings.generateOpenGraphTags')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"open-graph-enabled\"\n                                v-model=\"advanced.openGraphEnabled\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.openGraphEnabled\"\n                            :label=\"$t('settings.fallbackImage')\">\n                            <image-upload\n                                slot=\"field\"\n                                v-model=\"advanced.openGraphImage\"\n                                imageType=\"optionImages\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.openGraphEnabled\"\n                            id=\"use-page-title-instead-item-name\"\n                            :label=\"$t('settings.useAsTitlePageTitle')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"use-page-title-instead-item-name\"\n                                v-model=\"advanced.usePageTitleInsteadItemName\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.useAsTitlePageTitleInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.openGraphEnabled\"\n                            :label=\"$t('settings.facebookAppID')\">\n                            <input\n                                slot=\"field\"\n                                type=\"text\"\n                                spellcheck=\"false\"\n                                v-model=\"advanced.openGraphAppId\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.facebookAppIDInfo')\">\n                            </small>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-4\">\n                        <field\n                            id=\"twitter-cards-enabled\"\n                            :label=\"$t('settings.generateTwitterCards')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"twitter-cards-enabled\"\n                                v-model=\"advanced.twitterCardsEnabled\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.twitterCardsEnabled\"\n                            id=\"twitter-username\"\n                            :label=\"$t('settings.twitterUsername')\">\n                            <text-input\n                                id=\"twitter-username\"\n                                v-model=\"advanced.twitterUsername\"\n                                :spellcheck=\"false\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.twitterCardsEnabled\"\n                            id=\"twitter-cards-type\"\n                            :label=\"$t('settings.cardTypes')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"twitter-cards-type\"\n                                v-model=\"advanced.twitterCardsType\"\n                                :items=\"twitterCardsTypes\">\n                            </dropdown>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-5\">\n                        <separator\n                            type=\"small\"\n                            :is-line=\"true\"\n                            :label=\"$t('settings.embedVideos')\" />\n\n                        <field\n                            id=\"yt-nocookie\"\n                            :label=\"$t('settings.ytNoCookies')\">\n                            <switcher\n                                id=\"yt-nocookies\"\n                                slot=\"field\"\n                                v-model=\"advanced.gdpr.ytNoCookies\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.howYtNoCookiesWorks')\">\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"vimeo-no-track\"\n                            :label=\"$t('settings.vimeoNoTrack')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"vimeo-no-track\"\n                                v-model=\"advanced.gdpr.vimeoNoTrack\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.howVimeoNoTrackWorks')\">\n                            </small>\n                        </field>\n\n                        <separator\n                            type=\"big\"\n                            :is-line=\"true\"\n                            :label=\"$t('settings.cookieBanner')\" />\n\n                        <field\n                            id=\"gdpr-enabled\"\n                            :label=\"$t('settings.addGDPRCookieBanner')\">\n                            <switcher\n                                id=\"html-compression\"\n                                slot=\"field\"\n                                v-model=\"advanced.gdpr.enabled\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.howToPrepareYourThemeForGDPRInfo')\">\n                            </small>\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled\"\n                            type=\"medium thin\"\n                            :is-line=\"true\"\n                            :note=\"$t('settings.cookieBasicDescription')\"\n                            :label=\"$t('settings.cookieBasic')\" />                     \n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-popup-title-primary\"\n                            :label=\"$t('settings.gdprBannerTitle')\">\n                            <text-input\n                                id=\"gdpr-popup-title-primary\"\n                                v-model=\"advanced.gdpr.popupTitlePrimary\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-popup-desc\"\n                            :label=\"$t('settings.gdprBannerMessage')\">\n                            <text-area\n                                id=\"gdpr-popup-desc\"\n                                v-model=\"advanced.gdpr.popupDesc\"\n                                :rows=\"6\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled\"\n                            type=\"big thin\"\n                            :is-line=\"true\"/>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"show-privacy-policy-link\"\n                            :label=\"$t('settings.showPrivacyPolicyLink')\">\n                            <switcher\n                                id=\"show-privacy-policy-link\"\n                                slot=\"field\"\n                                v-model=\"advanced.gdpr.showPrivacyPolicyLink\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.showPrivacyPolicyLink\"\n                            id=\"gdpr-readmore-link-label\"\n                            :label=\"$t('settings.privacyPolicyLinkLabel')\">\n                            <text-input\n                                id=\"gdpr-readmore-link-label\"\n                                v-model=\"advanced.gdpr.privacyPolicyLinkLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.showPrivacyPolicyLink\"\n                            id=\"gdpr-page-type\"\n                            :label=\"$t('settings.privacyPolicyURL')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"gdpr-page-type\"\n                                key=\"gdpr-page-type\"\n                                v-model=\"advanced.gdpr.privacyPolicyLinkType\"\n                                :items=\"{ \n                                    'internal': $t('settings.internalPage'), \n                                    'external': $t('settings.externalPage'),  \n                                }\">\n                            </dropdown>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.showPrivacyPolicyLink && advanced.gdpr.privacyPolicyLinkType === 'internal'\"\n                            id=\"gdpr-page\"\n                            :label=\"$t('settings.privacyPolicyPage')\">\n                            <v-select\n                                ref=\"gdprPagesSelect\"\n                                slot=\"field\"\n                                :options=\"postPages\"\n                                v-model=\"advanced.gdpr.privacyPolicyPostId\"\n                                :custom-label=\"customPostLabels\"\n                                :close-on-select=\"true\"\n                                :show-labels=\"false\"\n                                @select=\"closeDropdown('gdprPagesSelect')\"\n                                :placeholder=\"$t('settings.selectPage')\"></v-select>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.showPrivacyPolicyLink && advanced.gdpr.privacyPolicyLinkType === 'external'\"\n                            id=\"gdpr-page-url\"\n                            :label=\"$t('settings.privacyPolicyPageURL')\">\n                            <text-input\n                                id=\"gdpr-page-url\"\n                                v-model=\"advanced.gdpr.privacyPolicyExternalUrl\"\n                                :spellcheck=\"false\"\n                                slot=\"field\" />\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled\"\n                            type=\"small thin\"\n                            :is-line=\"true\"/>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-save-button-label\"\n                            :label=\"$t('settings.saveButtonLabel')\">\n                            <text-input\n                                id=\"gdpr-save-button-label\"\n                                v-model=\"advanced.gdpr.saveButtonLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-show-reject-button\"\n                            :label=\"$t('settings.gdprShowRejectButton')\">\n                            <switcher\n                                id=\"gdpr-show-reject-button\"\n                                slot=\"field\"\n                                v-model=\"advanced.gdpr.popupShowRejectButton\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration && advanced.gdpr.popupShowRejectButton\"\n                            id=\"gdpr-reject-button-label\"\n                            :label=\"$t('settings.gdprRejectButtonLabel')\">\n                            <text-input\n                                id=\"gdpr-reject-button-label\"\n                                slot=\"field\"\n                                v-model=\"advanced.gdpr.popupRejectButtonLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\" />\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled\"\n                            type=\"small thin\"\n                            :is-line=\"true\"/>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-behaviour\"\n                            :label=\"$t('settings.openPopupWindowBy')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"posts-listing-order-by\"\n                                key=\"posts-listing-order-by\"\n                                v-model=\"advanced.gdpr.behaviour\"\n                                :items=\"{ \n                                    'badge': $t('settings.badge'), \n                                    'link': $t('ui.customLink'), \n                                    'badge-link': $t('settings.badgeAndCustomLink') \n                                }\">\n                            </dropdown>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.behaviour !== 'link'\"\n                            id=\"gdpr-popup-label\"\n                            :label=\"$t('settings.badgeLabel')\">\n                            <text-input\n                                id=\"gdpr-popup-label\"\n                                v-model=\"advanced.gdpr.badgeLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.behaviour !== 'badge'\"\n                            id=\"gdpr-behaviour-link\"\n                            :label=\"$t('settings.anchorLink')\">\n                            <text-input\n                                id=\"gdpr-behaviour-link\"\n                                v-model=\"advanced.gdpr.behaviourLink\"\n                                :spellcheck=\"false\"\n                                slot=\"field\" />\n                            <small\n                                class=\"note\"\n                                slot=\"note\">\n                                {{ $t('settings.gdprBehaviourInfo') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled\"\n                            type=\"small thin\"\n                            :is-line=\"true\"/>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-banner-position\"\n                            :label=\"$t('settings.bannerPosition')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"gdpr-banner-position\"\n                                key=\"gdpr-banner-position\"\n                                v-model=\"advanced.gdpr.popupPosition\"\n                                :items=\"{ \n                                    'centered': $t('settings.gdprBannerPosition.centered'), \n                                    'left': $t('settings.gdprBannerPosition.left'), \n                                    'right': $t('settings.gdprBannerPosition.right'),\n                                    'bar': $t('settings.gdprBannerPosition.bar') \n                                }\">\n                            </dropdown>\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled\"\n                            type=\"ultra thin\"\n                            :is-line=\"true\" \n                            :note=\"$t('settings.cookieAdvancedDescription')\"\n                            :label=\"$t('settings.cookieAdvanced')\" />\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-allow-advanced-configuration\"\n                            :label=\"$t('settings.gdprAllowAdvancedConfiguration')\">\n                            <switcher\n                                id=\"gdpr-allow-advanced-configuration\"\n                                slot=\"field\"\n                                v-model=\"advanced.gdpr.allowAdvancedConfiguration\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-advanced-configuration-link-label\"\n                            :label=\"$t('settings.gdprAdvancedConfigurationLinkLabel')\">\n                            <text-input\n                                id=\"gdpr-advanced-configuration-link-label\"\n                                v-model=\"advanced.gdpr.advancedConfigurationLinkLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\" \n                                :placeholder=\"$t('settings.gdprAdvancedConfigurationLinkPlaceholder')\"\n                                slot=\"field\" />\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            type=\"small thin\"\n                            :is-line=\"true\"/>\n\n                         <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-advanced-configuration-title\"\n                            :label=\"$t('settings.gdprAdvancedConfigurationTitle')\">\n                            <text-input\n                                id=\"gdpr-advanced-configuration-title\"\n                                v-model=\"advanced.gdpr.advancedConfigurationTitle\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-advanced-configuration-description\"\n                            :label=\"$t('settings.gdprAdvancedConfigurationDescription')\">\n                            <text-area\n                                id=\"gdpr-advanced-configuration-description\"\n                                v-model=\"advanced.gdpr.advancedConfigurationDescription\"\n                                :rows=\"4\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n                        \n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-advanced-configuration-accept-button-label\"\n                            :label=\"$t('settings.gdprAdvancedConfigurationAcceptButtonLabel')\">\n                            <text-input\n                                id=\"gdpr-advanced-configuration-accept-button-label\"\n                                v-model=\"advanced.gdpr.advancedConfigurationAcceptButtonLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-advanced-configuration-reject-button-label\"\n                            :label=\"$t('settings.gdprAdvancedConfigurationRejectButtonLabel')\">\n                            <text-input\n                                id=\"gdpr-advanced-configuration-reject-button-label\"\n                                v-model=\"advanced.gdpr.advancedConfigurationRejectButtonLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-advanced-configuration-save-button-label\"\n                            :label=\"$t('settings.gdprAdvancedConfigurationSaveButtonLabel')\">\n                            <text-input\n                                id=\"gdpr-advanced-configuration-reject-button-label\"\n                                v-model=\"advanced.gdpr.advancedConfigurationSaveButtonLabel\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-advanced-configuration-show-description-link\"\n                            :label=\"$t('settings.gdprAdvancedConfigurationPrivacyLink')\">\n                            <switcher\n                                id=\"gdpr-advanced-configuration-show-description-link\"\n                                v-model=\"advanced.gdpr.advancedConfigurationShowDescriptionLink\"\n                                slot=\"field\" />\n                             <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.gdprAdvancedConfigurationPrivacyLinkDescription')\">\n                            </small>\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled\"\n                            type=\"small thin\"\n                            :is-line=\"true\"/>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-debug-mode\"\n                            :label=\"$t('settings.gdprDebugMode')\">\n                            <switcher\n                                id=\"gdpr-debug-mode\"\n                                v-model=\"advanced.gdpr.debugMode\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-cookie-settings-revision\"\n                            :label=\"$t('settings.gdprCookieSettingsRevision')\">\n                            <text-input\n                                id=\"gdpr-cookie-settings-revision\"\n                                v-model=\"advanced.gdpr.cookieSettingsRevision\"\n                                :spellcheck=\"false\"\n                                type=\"number\"\n                                min=\"1\"\n                                step=\"1\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled\"\n                            id=\"gdpr-cookie-settings-ttl\"\n                            :label=\"$t('settings.gdprCookieSettingsTTL')\">\n                            <text-input\n                                id=\"gdpr-cookie-settings-ttl\"\n                                v-model=\"advanced.gdpr.cookieSettingsTTL\"\n                                :spellcheck=\"false\"\n                                type=\"number\"\n                                min=\"0\"\n                                step=\"1\"\n                                slot=\"field\" />\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            type=\"ultra thin\"\n                            :is-line=\"true\"\n                            :label=\"$t('settings.cookieGroups')\" />\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"gdpr-popup-groups\"\n                            label=\"\"\n                            :labelFullWidth=\"true\">\n                            <gdpr-groups\n                                id=\"gdpr-popup-groups\"\n                                v-model=\"advanced.gdpr.groups\"\n                                slot=\"field\" />\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            type=\"ultra thin\"\n                            :is-line=\"true\"\n                            :label=\"$t('settings.gConsentMode.title')\"\n                            :note=\"$t('settings.gConsentMode.description')\" />\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"g-consent-mode-enabled\"\n                            :label=\"$t('settings.gConsentModeEnabled')\">\n                            <switcher\n                                id=\"g-consent-mode-enabled\"\n                                v-model=\"advanced.gdpr.gConsentModeEnabled\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration && advanced.gdpr.gConsentModeEnabled\"\n                            id=\"g-consent-mode-default-state\"\n                            :label=\"$t('settings.gConsentModeDefaultState')\">\n\n                            <switcher \n                                key=\"g-consent-mode-default-state-switcher-1\"\n                                v-model=\"advanced.gdpr.gConsentModeDefaultState.ad_storage\"\n                                label=\"ad_storage\"\n                                slot=\"field\" />\n                            \n                            <switcher \n                            key=\"g-consent-mode-default-state-switcher-2\"\n                                v-model=\"advanced.gdpr.gConsentModeDefaultState.ad_personalization\"\n                                label=\"ad_personalization\"\n                                slot=\"field\" />\n\n                            <switcher \n                                key=\"g-consent-mode-default-state-switcher-3\"\n                                v-model=\"advanced.gdpr.gConsentModeDefaultState.ad_user_data\"\n                                label=\"ad_user_data\"\n                                slot=\"field\" />\n\n                            <switcher \n                                key=\"g-consent-mode-default-state-switcher-4\"\n                                v-model=\"advanced.gdpr.gConsentModeDefaultState.analytics_storage\"\n                                label=\"analytics_storage\"\n                                slot=\"field\" />\n\n                            <switcher \n                                key=\"g-consent-mode-default-state-switcher-5\"\n                                v-model=\"advanced.gdpr.gConsentModeDefaultState.personalization_storage\"\n                                label=\"personalization_storage\"\n                                slot=\"field\" />\n\n                            <switcher \n                                key=\"g-consent-mode-default-state-switcher-6\"\n                                v-model=\"advanced.gdpr.gConsentModeDefaultState.functionality_storage\"\n                                label=\"functionality_storage\"\n                                slot=\"field\" />\n\n                            <switcher \n                                key=\"g-consent-mode-default-state-switcher-7\"\n                                v-model=\"advanced.gdpr.gConsentModeDefaultState.security_storage\"\n                                label=\"security_storage\"\n                                slot=\"field\" />\n                        </field>\n\n                        <g-consent-mode-groups\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration && advanced.gdpr.gConsentModeEnabled\"\n                            id=\"g-consent-mode-groups\"\n                            v-model=\"advanced.gdpr.gConsentModeGroups\"\n                            :cookieGroups=\"advanced.gdpr.groups\"\n                            slot=\"field\" />\n\n                        <separator\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            type=\"ultra thin\"\n                            :is-line=\"true\"\n                            :label=\"$t('settings.embedConsents')\"\n                            :note=\"$t('settings.embedConsentsDescription')\" />\n\n                        <div\n                            v-if=\"!currentThemeSupportsEmbedConsents && advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            class=\"msg msg-icon msg-alert\">\n                            <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                            <p>{{ $t('settings.themeDoesNotSupportEmbedConsents') }}</p>\n                        </div>\n\n                        <field\n                            v-if=\"advanced.gdpr.enabled && advanced.gdpr.allowAdvancedConfiguration\"\n                            id=\"embed-consents-groups\"\n                            label=\"\"\n                            :labelFullWidth=\"true\">\n                            <embed-consents-groups\n                                id=\"embed-consents-groups\"\n                                v-model=\"advanced.gdpr.embedConsents\"\n                                :cookieGroups=\"advanced.gdpr.groups\"\n                                slot=\"field\" />\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-6\">\n                        <field\n                            id=\"html-compression\"\n                            :label=\"$t('settings.enableHTMLCompression')\">\n                            <switcher\n                                id=\"html-compression\"\n                                v-model=\"advanced.htmlCompression\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            id=\"css-compression\"\n                            :label=\"$t('settings.enableCSSCompression')\">\n                            <switcher\n                                id=\"css-compression\"\n                                v-model=\"advanced.cssCompression\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            id=\"html-compression-remove-comments\"\n                            :label=\"$t('settings.removeHTMLComments')\">\n                            <switcher\n                                id=\"html-compression-remove-comments\"\n                                v-model=\"advanced.htmlCompressionRemoveComments\" \n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            id=\"media-lazyload\"\n                            :label=\"$t('settings.enableMediaLazyLoad')\">\n                            <switcher\n                                id=\"media-lazyload\"\n                                v-model=\"advanced.mediaLazyLoad\"\n                                slot=\"field\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.enableMediaLazyLoadInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            id=\"version-suffix\"\n                            :label=\"$t('settings.versionParameter')\">\n                            <switcher\n                                id=\"version-suffix\"\n                                v-model=\"advanced.versionSuffix\"\n                                slot=\"field\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.versionParameterInfo') }}\n                            </small>\n                        </field>\n\n                        <separator\n                            type=\"small thin\"\n                            :is-line=\"true\"/>\n\n                        <field\n                            id=\"responsive-images\"\n                            :label=\"$t('settings.enableResponsiveImages')\">\n                            <switcher\n                                id=\"responsive-images\"\n                                v-model=\"advanced.responsiveImages\"\n                                slot=\"field\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.enableResponsiveImagesInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.responsiveImages\"\n                            id=\"images-quality\"\n                            :label=\"$t('settings.responsiveImagesQuality')\">\n                            <label slot=\"field\">\n                                <text-input\n                                    id=\"images-quality\"\n                                    type=\"number\"\n                                    min=\"1\"\n                                    max=\"100\"\n                                    step=\"1\"\n                                    :disabled=\"advanced.forceWebp && advanced.webpLossless\"\n                                    v-model=\"advanced.imagesQuality\" />\n                                %\n                            </label>\n                        </field>\n\n                        <separator\n                            v-if=\"advanced.responsiveImages\"\n                            type=\"small thin\"\n                            :is-line=\"true\"/>\n                        \n                        <field\n                            v-if=\"advanced.responsiveImages\"\n                            id=\"convert-to-webp\"\n                            :label=\"$t('settings.convertToWebp')\">\n                            <switcher\n                                id=\"version-suffix\"\n                                v-model=\"advanced.forceWebp\"\n                                slot=\"field\" />\n\n                            <p\n                                v-if=\"advanced.forceWebp && $store.state.app.config.resizeEngine === 'jimp'\"\n                                slot=\"note\" \n                                class=\"msg msg-icon msg-alert\">\n                                <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n                                <span>{{ $t('settings.convertToWebpJimpWarning') }}</span>\n                            </p>\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.convertToWebpInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"advanced.responsiveImages && advanced.forceWebp\"\n                            id=\"webp-lossless\"\n                            :label=\"$t('settings.webpLossless')\">\n                            <switcher\n                                id=\"webp-lossless\"\n                                v-model=\"advanced.webpLossless\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"advanced.responsiveImages && advanced.forceWebp\"\n                            id=\"images-alpha-quality\"\n                            :label=\"$t('settings.responsiveImagesAlphaQuality')\">\n                            <label slot=\"field\">\n                                <text-input\n                                    id=\"images-alpha-quality\"\n                                    type=\"number\"\n                                    min=\"1\"\n                                    max=\"100\"\n                                    step=\"1\"\n                                    :disabled=\"advanced.forceWebp && advanced.webpLossless\"\n                                    v-model=\"advanced.alphaQuality\" />\n                                %\n                            </label>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-7\">\n                        <field\n                            v-if=\"siteUsesRelativeUrls\"\n                            :label=\"$t('settings.feedsRelativeUrlsInfo')\"\n                            :labelFullWidth=\"true\" />\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls\"\n                            id=\"feed-enable-rss\"\n                            :label=\"$t('settings.enableRSSFeed')\">\n                            <switcher\n                                id=\"feed-enable-rss\"\n                                v-model=\"advanced.feed.enableRss\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls\"\n                            id=\"feed-enable-json\"\n                            :label=\"$t('settings.enableJSONFeed')\">\n                            <switcher\n                                id=\"feed-enable-json\"\n                                v-model=\"advanced.feed.enableJson\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls && (advanced.feed.enableRss || advanced.feed.enableJson)\"\n                            id=\"feed-title\"\n                            :label=\"$t('settings.feedTitle')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"feed-title\"\n                                key=\"feed-title\"\n                                v-model=\"advanced.feed.title\"\n                                :items=\"{ \n                                    'displayName': $t('settings.websiteName'), \n                                    'customTitle': $t('settings.customFeedTitle') \n                                }\"></dropdown>\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls && (advanced.feed.enableRss || advanced.feed.enableJson) && advanced.feed.title === 'customTitle'\"\n                            id=\"feed-title-value\"\n                            :label=\"$t('settings.customFeedTitle')\">\n                            <text-input\n                                id=\"feed-title-value\"\n                                type=\"text\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                v-model=\"advanced.feed.titleValue\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls && (advanced.feed.enableRss || advanced.feed.enableJson)\"\n                            id=\"feed-show-full-text\"\n                            :label=\"$t('settings.showFullText')\">\n                            <switcher\n                                id=\"feed-show-full-text\"\n                                v-model=\"advanced.feed.showFullText\"\n                                slot=\"field\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.showFullTextInFeedInfo') }}\n                            </small>\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls && advanced.feed.enableRss\"\n                            id=\"feed-updated-date-type\"\n                            :label=\"$t('settings.feedUpdatedDateType')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"feed-updated-date-type\"\n                                key=\"feed-updated-date-type\"\n                                v-model=\"advanced.feed.updatedDateType\"\n                                :items=\"{ \n                                    'createdAt': $t('settings.postCreationDate'), \n                                    'modifiedAt': $t('settings.postModificationDate') \n                                }\">\n                            </dropdown>\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls\"\n                            id=\"feed-show-only-featured\"\n                            :label=\"$t('settings.showOnlyFeaturedPosts')\">\n                            <switcher\n                                id=\"feed-show-only-featured\"\n                                v-model=\"advanced.feed.showOnlyFeatured\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls && !advanced.feed.showOnlyFeatured\"\n                            id=\"feed-exclude-featured\"\n                            :label=\"$t('settings.excludeFeaturedPosts')\">\n                            <switcher\n                                id=\"feed-exclude-featured\"\n                                v-model=\"advanced.feed.excludeFeatured\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls && (advanced.feed.enableRss || advanced.feed.enableJson)\"\n                            id=\"feed-number-of-posts\"\n                            :label=\"$t('settings.numberOfPostsInFeed')\">\n                            <text-input\n                                id=\"feed-number-of-posts\"\n                                type=\"number\"\n                                min=\"0\"\n                                max=\"10000000\"\n                                step=\"1\"\n                                v-model=\"advanced.feed.numberOfPosts\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            v-if=\"!siteUsesRelativeUrls && (advanced.feed.enableRss || advanced.feed.enableJson)\"\n                            id=\"feed-show-featured-image\"\n                            :label=\"$t('settings.showFeaturedImage')\">\n                            <switcher\n                                id=\"feed-show-featured-image\"\n                                v-model=\"advanced.feed.showFeaturedImage\"\n                                slot=\"field\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.showFeaturedImageInfo') }}\n                            </small>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-8\">\n                        <field\n                            id=\"posts-listing-order-by\"\n                            :label=\"$t('settings.postsOrderBy')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"posts-listing-order-by\"\n                                key=\"posts-listing-order-by\"\n                                v-model=\"advanced.postsListingOrderBy\"\n                                :items=\"orderByOptions\"></dropdown>\n                        </field>\n\n                        <field\n                            id=\"posts-listing-order\"\n                            :label=\"$t('settings.postsOrdering')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"posts-listing-order\"\n                                key=\"posts-listing-order\"\n                                v-model=\"advanced.postsListingOrder\"\n                                :items=\"orderOptions\"></dropdown>\n                        </field>\n\n                        <field\n                            id=\"featured-posts-listing-order-by\"\n                            :label=\"$t('settings.featuredPostsOrderBy')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"featured-posts-listing-order-by\"\n                                key=\"featured-posts-listing-order-by\"\n                                v-model=\"advanced.featuredPostsListingOrderBy\"\n                                :items=\"orderByOptions\"></dropdown>\n                        </field>\n\n                        <field\n                            id=\"featured-posts-listing-order\"\n                            :label=\"$t('settings.featuredPostsOrdering')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"featured-posts-listing-order\"\n                                key=\"featured-posts-listing-order\"\n                                v-model=\"advanced.featuredPostsListingOrder\"\n                                :items=\"orderOptions\"></dropdown>\n                        </field>\n\n                        <field\n                            id=\"hidden-posts-listing-order-by\"\n                            :label=\"$t('settings.hiddenPostsOrderBy')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"hidden-posts-listing-order-by\"\n                                key=\"hidden-posts-listing-order-by\"\n                                v-model=\"advanced.hiddenPostsListingOrderBy\"\n                                :items=\"orderByOptions\"></dropdown>\n                        </field>\n\n                        <field\n                            id=\"hidden-posts-listing-order\"\n                            :label=\"$t('settings.hiddenPostsOrdering')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"hidden-posts-listing-order\"\n                                key=\"hidden-posts-listing-order\"\n                                v-model=\"advanced.hiddenPostsListingOrder\"\n                                :items=\"orderOptions\"></dropdown>\n                        </field>\n\n                        <separator\n                            type=\"small\"\n                            :is-line=\"true\"\n                            label=\"\" />\n\n                        <field\n                            id=\"related-posts-criteria\"\n                            :label=\"$t('settings.relatedPostsSelectionMechanism')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"related-posts-order-by\"\n                                key=\"related-posts-order-by\"\n                                v-model=\"advanced.relatedPostsCriteria\"\n                                :items=\"relatedPostsCriteriaOptions\"></dropdown>\n                        </field>\n\n                        <field\n                            id=\"related-posts-order-by\"\n                            :label=\"$t('settings.relatedPostsOrdering')\">\n                            <dropdown\n                                slot=\"field\"\n                                id=\"related-posts-order-by\"\n                                key=\"related-posts-order-by\"\n                                v-model=\"advanced.relatedPostsOrder\"\n                                :items=\"relatedPostsOrderingOptions\"></dropdown>\n                        </field>\n\n                         <field\n                            id=\"related-posts-use-all-posts\"\n                            :label=\"$t('settings.relatedPostsOptions')\">\n                            <switcher\n                                id=\"related-posts-use-all-posts\"\n                                v-model=\"advanced.relatedPostsIncludeAllPosts\"\n                                slot=\"field\" />\n\n                            <small\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ $t('settings.relatedPostsOptionsInfo') }}\n                            </small>\n                        </field>\n                    </div>\n\n                    <div slot=\"tab-9\">\n                        <separator\n                            type=\"medium\"\n                            :label=\"$t('post.editorWYSIWYG')\" />\n\n                        <field\n                            :label=\"$t('settings.additionalValidElementsInWYSIWYGEditor')\">\n                            <text-area\n                                slot=\"field\"\n                                :spellcheck=\"false\"\n                                :rows=\"8\"\n                                v-model=\"advanced.editors.wysiwygAdditionalValidElements\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.additionalValidElementsInWYSIWYGEditorInfo')\">\n                            </small>\n                        </field>\n\n                        <field\n                            :label=\"$t('settings.customElementsInWYSIWYGEditor')\">\n                            <text-area\n                                slot=\"field\"\n                                :spellcheck=\"false\"\n                                :rows=\"8\"\n                                v-model=\"advanced.editors.wysiwygCustomElements\" />\n                            <small\n                                slot=\"note\"\n                                class=\"note\"\n                                v-pure-html=\"$t('settings.customElementsInWYSIWYGEditorInfo')\">\n                            </small>\n                        </field>\n\n                        <separator\n                            type=\"medium\"\n                            :label=\"$t('settings.codeEditor')\" />\n\n                        <field\n                            id=\"codemirror-indent-size\"\n                            :label=\"$t('settings.indentSize')\">\n                            <text-input\n                                id=\"codemirror-indent-size\"\n                                type=\"number\"\n                                min=\"1\"\n                                max=\"100\"\n                                step=\"1\"\n                                v-model=\"advanced.editors.codemirrorTabSize\"\n                                slot=\"field\" />\n                        </field>\n\n                        <field\n                            id=\"codemirror-auto-indent\"\n                            :label=\"$t('settings.enableAutoIndent')\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"codemirror-auto-indent\"\n                                v-model=\"advanced.editors.codemirrorAutoIndent\" />\n                        </field>\n                    </div>\n                </tabs>\n            </fields-group>\n\n            <p-footer>\n                <btn-dropdown\n                    slot=\"buttons\"\n                    buttonColor=\"green\"\n                    :items=\"dropdownItems\"\n                    :disabled=\"!siteHasTheme || buttonsLocked\"\n                    localStorageKey=\"publii-preview-mode\"\n                    :previewIcon=\"true\"\n                    :isReversed=\"true\"\n                    defaultValue=\"full-site-preview\" />\n\n                <p-button\n                    @click.native=\"checkBeforeSave(false)\"\n                    slot=\"buttons\"\n                    type=\"secondary\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n            </p-footer>\n        </div>\n    </section>\n</template>\n\n<script>\nimport Utils from './../helpers/utils.js';\nimport AvailableLanguagesList from './../config/langs.js';\nimport EmbedConsentsGroups from './basic-elements/EmbedConsentsGroups';\nimport GConsentModeGroups from './basic-elements/GConsentModeGroups';\nimport GdprGroups from './basic-elements/GdprGroups';\nimport ThemesDropdown from './basic-elements/ThemesDropdown';\n\nexport default {\n    name: 'site-settings',\n    components: {\n        'embed-consents-groups': EmbedConsentsGroups,\n        'g-consent-mode-groups': GConsentModeGroups,\n        'gdpr-groups': GdprGroups,\n        'themes-dropdown': ThemesDropdown\n    },\n    data () {\n        return {\n            buttonsLocked: false,\n            logo: {\n                color: '',\n                icon: ''\n            },\n            language: '',\n            customLanguage: '',\n            spellchecking: false,\n            description: '',\n            name: '',\n            uuid: '',\n            theme: '',\n            currentTheme: '',\n            currentThemeVersion: '',\n            advanced: {},\n            errors: [],\n            spellcheckerLanguages: false\n        };\n    },\n    computed: {\n        currentThemeHasSupportedFeaturesList () {\n            return this.$store.state.currentSite.themeSettings.supportedFeatures;\n        },\n        currentThemeSupportsEmbedConsents () {\n            return this.$store.state.currentSite.themeSettings.supportedFeatures && this.$store.state.currentSite.themeSettings.supportedFeatures.embedConsents;\n        },\n        currentThemeSupportsPages () {\n            return this.$store.state.currentSite.themeSettings.supportedFeatures && this.$store.state.currentSite.themeSettings.supportedFeatures.pages;\n        },\n        currentThemeSupportsTagsList () {\n            return this.$store.state.currentSite.themeSettings.supportedFeatures && this.$store.state.currentSite.themeSettings.supportedFeatures.tagsList;\n        },\n        currentThemeSupportsTagPages () {\n            return (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                (\n                    this.$store.state.currentSite.themeSettings.supportedFeatures.tagPages ||\n                    typeof this.$store.state.currentSite.themeSettings.supportedFeatures.tagPages === 'undefined'\n                )\n            ) || (\n                !this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.createTagPages\n            ) || (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                (\n                    this.$store.state.currentSite.themeSettings.supportedFeatures.tagPages ||\n                    typeof this.$store.state.currentSite.themeSettings.supportedFeatures.tagPages === 'undefined'\n                ) &&\n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.createTagPages\n            );\n        },\n        currentThemeSupportsAuthorPages () {\n            return (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                (\n                    this.$store.state.currentSite.themeSettings.supportedFeatures.authorPages ||\n                    typeof this.$store.state.currentSite.themeSettings.supportedFeatures.authorPages === 'undefined'\n                )\n            ) || (\n                !this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.createAuthorPages\n            ) || (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                (\n                    this.$store.state.currentSite.themeSettings.supportedFeatures.authorPages ||\n                    typeof this.$store.state.currentSite.themeSettings.supportedFeatures.authorPages === 'undefined'\n                ) &&\n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.createAuthorPages\n            );\n        },\n        currentThemeSupportsSearchPage () {\n            return (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures.searchPage\n            ) || (\n                !this.$store.state.currentSite.themeSettings.supportedFeatures && \n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.createSearchPage\n            ) || (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                (\n                    this.$store.state.currentSite.themeSettings.supportedFeatures.searchPage ||\n                    typeof this.$store.state.currentSite.themeSettings.supportedFeatures.searchPage === 'undefined'\n                ) &&\n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.createSearchPage\n            );\n        },\n        currentThemeSupportsErrorPage () {\n            return (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures.errorPage\n            ) || (\n                !this.$store.state.currentSite.themeSettings.supportedFeatures && \n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.create404Page\n            ) || (\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                (\n                    this.$store.state.currentSite.themeSettings.supportedFeatures.errorPage ||\n                    typeof this.$store.state.currentSite.themeSettings.supportedFeatures.errorPage === 'undefined'\n                ) &&\n                this.$store.state.currentSite.themeSettings.renderer &&\n                this.$store.state.currentSite.themeSettings.renderer.create404Page\n            );\n        },\n        advancedTabs () {\n            return [\n                this.$t('ui.seo'),\n                this.$t('settings.urls'),\n                this.$t('settings.sitemap'),\n                this.$t('settings.openGraph'),\n                this.$t('settings.twitterCards'),\n                this.$t('settings.GDPR'),\n                this.$t('settings.websiteSpeed'),\n                this.$t('settings.RSSJSONFeed'),\n                this.$t('settings.postsListing'),\n                this.$t('settings.editors')\n            ];\n        },\n        seoOptions () {\n            return {\n                'index, follow': this.$t('ui.indexFollow'),\n                'index, nofollow': this.$t('ui.indexNofollow'),\n                'index, follow, noarchive': this.$t('ui.indexFollowNoArchive'),\n                'index, nofollow, noarchive': this.$t('ui.indexNofollowNoArchive'),\n                'noindex, follow': this.$t('ui.noindexFollow'),\n                'noindex, nofollow': this.$t('ui.noindexNofollow')\n            };\n        },\n        orderByOptions () {\n            return {\n                'created_at': this.$t('settings.postCreationDate'),\n                'title': this.$t('settings.postTile'),\n                'modified_at': this.$t('settings.postModificationDate'),\n                'id': this.$t('settings.postID')\n            };\n        },\n        orderOptions () {\n            return {\n                'DESC': this.$t('settings.descending'),\n                'ASC': this.$t('settings.ascending')\n            };\n        },\n        relatedPostsOrderingOptions () {\n            return {\n                'default': this.$t('settings.byIDDescending'),\n                'id-asc': this.$t('settings.byIDAscending'),\n                'random': this.$t('settings.random')\n            };\n        },\n        relatedPostsCriteriaOptions () {\n            return {\n                'titles': this.$t('settings.useOnlyTitles'),\n                'tags': this.$t('settings.useOnlyTags'),\n                'titles-and-tags': this.$t('settings.useTitlesAndTags')\n            };\n        },\n        siteHasTheme () {\n            if (\n                !this.$store.state.currentSite.config.theme &&\n                this.theme.indexOf('use-') !== 0 &&\n                this.theme.indexOf('install-use-') !== 0\n            ) {\n                return false;\n            }\n\n            if (\n                this.$store.state.currentSite.config.theme &&\n                this.theme === 'uninstall-' + this.$store.state.currentSite.config.theme\n            ) {\n                return false;\n            }\n\n            return true;\n        },\n        siteUsesRelativeUrls () {\n            return !!this.$store.state.currentSite.config.deployment.relativeUrls;\n        },\n        websiteName () {\n            return this.$store.state.currentSite.config.name;\n        },\n        availableLanguages () {\n            let langs = {};\n\n            for (let lang of AvailableLanguagesList) {\n                langs[lang.code] = lang.name;\n            }\n\n            return langs;\n        },\n        twitterCardsTypes () {\n            return {\n                'summary': this.$t('settings.twitterSummaryCard'),\n                'summary_large_image': this.$t('settings.twitterSummaryCardLargeImage')\n            };\n        },\n        sitemapLink () {\n            let sitemapLink = false;\n            let syncDate = this.$store.state.currentSite.config.syncDate;\n\n            if (this.$store.state.currentSite.config.domain && typeof syncDate !== 'undefined') {\n                sitemapLink = this.$store.state.currentSite.config.domain + '/sitemap.xml';\n            }\n\n            return sitemapLink;\n        },\n        postPages () {\n            let posts = this.$store.state.currentSite.posts.filter(post => post.status.indexOf('published') > -1).map(post => post.id);\n            let pages = this.$store.state.currentSite.pages.filter(page => page.status.indexOf('published') > -1).map(page => page.id)\n            return posts.concat(pages);\n        },\n        dropdownItems () {\n            return [\n                {\n                    label: this.$t('ui.previewFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'full-site-preview',\n                    isVisible: () => true,\n                    icon: 'full-preview-monitor',\n                    onClick: this.saveAndPreview.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.renderFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'full-site-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    icon: 'full-render-monitor',\n                    onClick: this.saveAndRender.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.previewFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'homepage-preview',\n                    icon: 'quick-preview',\n                    isVisible: () => true,\n                    onClick: this.saveAndPreview.bind(this, 'homepage')\n                },\n                {\n                    label: this.$t('ui.renderFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'homepage-render',\n                    icon: 'quick-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    onClick: this.saveAndRender.bind(this, 'homepage')\n                }\n            ];\n        },\n        hasNonAutomaticSpellchecker () {\n            return mainProcessAPI.getEnv().platformName !== 'darwin';\n        },\n        spellcheckIsNotSupported () {\n            if (this.spellcheckerLanguages === false) {\n                return false;\n            }\n\n            let language = this.language;\n\n            if (language === 'custom') {\n                language = this.customLanguage;\n            }\n\n            if (this.spellcheckerLanguages.indexOf(language) > -1) {\n                return false;\n            } else {\n                language = language.split('-');\n                language = language[0];\n\n                if (this.spellcheckerLanguages.indexOf(language) > -1) {\n                    return false;\n                }\n            }\n\n            return true;\n        },\n        syncInProgress () {\n            return this.$store.state.components.sidebar.syncInProgress;\n        }\n    },\n    beforeMount () {\n        this.logo.color = this.$store.state.currentSite.config.logo.color;\n        this.logo.icon = this.$store.state.currentSite.config.logo.icon;\n        this.language = this.$store.state.currentSite.config.language;\n        this.spellchecking = this.$store.state.currentSite.config.spellchecking;\n\n        if (!this.availableLanguages[this.language]) {\n            this.customLanguage = this.language;\n            this.language = 'custom';\n        }\n\n        this.name = this.$store.state.currentSite.config.displayName;\n        this.description = this.$store.state.currentSite.config.description;\n\n        if (this.$store.state.currentSite.config.uuid) {\n            this.uuid = this.$store.state.currentSite.config.uuid;\n        }\n\n        this.setCurrentTheme();\n        this.advanced = Object.assign({}, this.advanced, this.$store.state.currentSite.config.advanced);\n    },\n    watch: {\n        'advanced.urls.cleanUrls': function (newValue, oldValue) {\n            if (newValue === false && oldValue === true) {\n                this.advanced.urls.postsPrefix = '';\n                this.advanced.urls.tagsPrefixAfterPostsPrefix = false;\n                this.advanced.urls.authorsPrefixAfterPostsPrefix = false;\n            }\n        }\n    },\n    async mounted () {\n        setTimeout(() => {\n            this.$refs['logo-creator'].changeIcon(this.logo.icon);\n            this.$refs['logo-creator'].changeColor(this.logo.color);\n        }, 0);\n\n        this.$bus.$on('regenerate-thumbnails-close', this.savedFromPopup);\n        this.spellcheckerLanguages = await mainProcessAPI.invoke('app-main-get-spellchecker-languages');\n\n        if (this.spellcheckerLanguages.length) {\n            this.spellcheckerLanguages = this.spellcheckerLanguages.map(lang => lang.toLocaleLowerCase());\n        }\n    },\n    methods: {\n        checkBeforeSave (showPreview, renderingType, renderFiles) {\n            if (\n                this.$store.state.currentSite.config.theme && (\n                    this.theme === 'install-use-' + this.$store.state.currentSite.config.theme\n                ) && \n                this.$store.state.currentSite.themeHasOverrides\n            ) {\n                this.$bus.$emit('confirm-display', {\n                    hasInput: false,\n                    message: this.$t('settings.currentThemeHasOverrides'),\n                    okClick: () => {\n                        this.save(showPreview, renderingType, renderFiles);\n                    },\n                    okLabel: this.$t('ui.ok'),\n                    cancelLabel: this.$t('ui.cancel')\n                });\n            } else {\n                this.save(showPreview, renderingType, renderFiles);\n            }\n        },\n        saveAndPreview (renderingType = false) {\n            this.checkBeforeSave(true, renderingType, false);\n        },\n        saveAndRender (renderingType = false) {\n            this.checkBeforeSave(true, renderingType, true);\n        },\n        save (showPreview = false, renderingType = false, renderFiles = false) {\n            this.buttonsLocked = true;\n\n            if (!this.validate()) {\n                this.buttonsLocked = false;\n                return;\n            }\n\n            let siteName = this.$store.state.currentSite.config.name;\n            let newTheme = this.$store.state.currentSite.config.theme;\n\n            if(this.theme !== '') {\n                newTheme = this.theme;\n            }\n\n            let newSettings = {};\n            newSettings.name = this.name;\n            newSettings.description = this.description;\n\n            if (this.uuid) {\n                newSettings.uuid = this.uuid;\n            }\n\n            newSettings.displayName = this.name;\n            newSettings.spellchecking = this.spellchecking;\n            newSettings.logo = {\n                icon: this.$refs['logo-creator'].getActiveIcon(),\n                color: this.$refs['logo-creator'].getActiveColor()\n            };\n\n            if (this.language === 'custom') {\n                newSettings.language = this.customLanguage;\n            } else {\n                newSettings.language = this.language;\n            }\n\n            // Remove GDPR script groups with empty name or ID\n            this.advanced.gdpr.groups = this.advanced.gdpr.groups.filter(group => {\n                if (group.name === '') {\n                    return false;\n                }\n\n                return true;\n            });\n            newSettings.advanced = Object.assign({}, this.advanced);\n            newSettings.advanced.gdpr.settingsVersion = 'v2';\n            newSettings.theme = this.theme;\n\n            // Merge new settings with existing settings\n            let currentSiteConfigCopy = JSON.parse(JSON.stringify(this.$store.state.currentSite.config));\n            newSettings = Utils.deepMerge(currentSiteConfigCopy, newSettings);\n\n            // Send request to the back-end\n            mainProcessAPI.send('app-site-config-save', {\n                \"site\": siteName,\n                \"settings\": newSettings,\n                \"source\": \"settings\"\n            });\n\n            // Settings saved\n            mainProcessAPI.receiveOnce('app-site-config-saved', (data) => {\n                if (data.status === true) {\n                    newSettings.name = data.siteName;\n                }\n\n                if(\n                    data.status === true &&\n                    siteName !== data.siteName &&\n                    data.siteName !== '' &&\n                    Object.keys(this.$store.state.sites).indexOf(data.siteName) === -1\n                ) {\n                    this.$store.commit('replaceSite', {\n                        oldSiteName: siteName,\n                        newSiteName: newSettings.name,\n                        displayName: newSettings.displayName\n                    });\n\n                    siteName = data.siteName;\n                    this.$router.push('/site/' + siteName + '/settings/');\n                    window.localStorage.setItem('publii-last-opened-website', siteName);\n                }\n\n                if(data.status === true) {\n                    if (\n                        data.thumbnailsRegenerateRequired &&\n                        this.$store.state.currentSite.posts &&\n                        this.$store.state.currentSite.posts.length > 0\n                    ) {\n                        mainProcessAPI.send('app-site-regenerate-thumbnails-required', {\n                            name: this.$store.state.currentSite.config.name\n                        });\n\n                        mainProcessAPI.receiveOnce('app-site-regenerate-thumbnails-required-status', (data) => {\n                            if (data.message) {\n                                this.$bus.$emit('regenerate-thumbnails-display', {\n                                    qualityChanged: false,\n                                    savedSettingsCallback: {\n                                        newSettings,\n                                        siteName,\n                                        showPreview,\n                                        renderingType,\n                                        renderFiles\n                                    }\n                                });\n                            } else {\n                                this.saved(newSettings, siteName, showPreview, renderingType, renderFiles);\n                            }\n                        });\n                    } else {\n                        this.saved(newSettings, siteName, showPreview, renderingType, renderFiles);\n                    }\n\n                    if(data.newThemeConfig) {\n                        this.$store.commit('setNewThemeConfig', {\n                            themeName: data.themeName,\n                            newThemeConfig: data.newThemeConfig\n                        });\n                    }\n                }\n\n                if(data.message === 'success-save') {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('settings.siteSettingsSaveSuccessMessage'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n\n                    this.buttonsLocked = false;\n                }\n\n                if(data.message === 'empty-name') {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('settings.siteSettingsSaveEmptyNameErrorMessage'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n\n                    this.buttonsLocked = false;\n                }\n\n                if(data.message === 'duplicated-name') {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('settings.siteSettingsSaveDuplicateNameErrorMessage'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n\n                    this.buttonsLocked = false;\n                }\n\n                if(data.message === 'site-not-exists') {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('settings.siteSettingsSaveSiteNotExistsErrorMessage'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n\n                    this.buttonsLocked = false;\n                }\n\n                if(data.message === 'no-keyring') {\n                    if (document.body.getAttribute('data-os') === 'linux') {\n                        this.$bus.$emit('alert-display', {\n                            message: this.$t('settings.siteSettingsSaveNoKeyringErrorMessageLinux'),\n                            okLabel: this.$t('ui.iUnderstand'),\n                        });\n                    } else {\n                        this.$bus.$emit('alert-display', {\n                            message: this.$t('settings.siteSettingsSaveNoKeyringErrorMessage'),\n                            okLabel: this.$t('ui.iUnderstand'),\n                        });\n                    }\n\n                    this.buttonsLocked = false;\n                }\n\n                mainProcessAPI.send('app-site-reload', {\n                    siteName: siteName\n                });\n\n                mainProcessAPI.receiveOnce('app-site-reloaded', (result) => {\n                    this.$store.commit('setSiteConfig', result);\n                    this.$store.commit('switchSite', result.data);\n                });\n            });\n        },\n        savedFromPopup (callbackData) {\n            this.saved(callbackData.newSettings, callbackData.siteName, callbackData.showPreview, callbackData.renderingType, callbackData.renderFiles);\n        },\n        saved (newSettings, oldName, showPreview = false, renderingType = false, renderFiles = false) {\n            let oldTheme = this.$store.state.currentSite.config.theme;\n\n            if (newSettings.theme) {\n                newSettings.theme =    newSettings.theme.replace(/^site-/, '')\n                                                        .replace(/^app-/, '')\n                                                        .replace(/^install-use-/, '')\n                                                        .replace(/^use-/, '')\n                                                        .replace(/^uninstall-/, '');\n            }\n\n            if (newSettings.theme === '') {\n                newSettings.theme = oldTheme;\n            }\n\n            this.$store.commit('replaceSiteConfig', {\n                newSettings: newSettings,\n                oldName: oldName\n            });\n\n            setTimeout(async () => {\n                this.setCurrentTheme();\n\n                // Remove old entry if user changed the site name\n                if (oldName !== this.$store.state.currentSite.config.name) {\n                    this.$store.commit('removeWebsite', oldName);\n                }\n\n                this.buttonsLocked = false;\n\n                if (showPreview) {\n                    let previewLocationExists = await mainProcessAPI.existsSync(this.$store.state.app.config.previewLocation);\n\n                    if (this.$store.state.app.config.previewLocation !== '' && !previewLocationExists) {\n                        this.$bus.$emit('confirm-display', {\n                            message: this.$t('sync.previewCatalogDoesNotExistInfo'),\n                            okLabel: this.$t('sync.goToAppSettings'),\n                            okClick: () => {\n                                this.$router.push(`/app-settings/`);\n                            }\n                        });\n                        return;\n                    }\n\n                    if (renderingType === 'homepage') {\n                        this.$bus.$emit('rendering-popup-display', {\n                            homepageOnly: true,\n                            showPreview: !renderFiles,\n                        });\n                    } else {\n                        this.$bus.$emit('rendering-popup-display', {\n                            showPreview: !renderFiles \n                        });\n                    }\n                }\n            }, 1000);\n        },\n        validate () {\n            if (this.advanced.urls.tagsPrefix.trim() === '' && !!this.advanced.urls.cleanUrls) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.tagsPrefixCannotBeEmpty'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('tags-prefix');\n                this.$refs['advanced-tabs'].toggle('URLs');\n\n                return false;\n            }\n\n            if (this.advanced.urls.authorsPrefix.trim() === '') {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.authorsPrefixCannotBeEmpty'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('authors-prefix');\n                this.$refs['advanced-tabs'].toggle('URLs');\n\n                return false;\n            }\n\n            if (this.advanced.usePageAsFrontpage && !this.advanced.pageAsFrontpage) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.frontpagePageCannotBeEmpty'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('page-as-frontpage');\n                this.$refs['advanced-tabs'].toggle('SEO');\n\n                return false;\n            }\n\n            if (this.advanced.urls.authorsPrefix.trim() === this.advanced.urls.tagsPrefix.trim()) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.authorsPrefixCannotEqualTagsPrefix'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('tags-prefix');\n                this.errors.push('authors-prefix');\n                this.$refs['advanced-tabs'].toggle('URLs');\n\n                return false;\n            }\n\n            if (this.advanced.urls.pageName.trim() === '') {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.paginationPhraseCannotBeEmpty'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('pagination-phrase');\n                this.$refs['advanced-tabs'].toggle('URLs');\n\n                return false;\n            }\n\n            if (this.advanced.urls.errorPage.trim() === '') {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.errorPageFilenameCannotBeEmpty'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('error-page');\n                this.$refs['advanced-tabs'].toggle('URLs');\n\n                return false;\n            }\n\n            if (this.advanced.urls.searchPage.trim() === '') {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.searchPageFilenameCannotBeEmpty'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('search-page');\n                this.$refs['advanced-tabs'].toggle('URLs');\n\n                return false;\n            }\n\n            if (this.advanced.urls.errorPage.trim() === this.advanced.urls.searchPage.trim()) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('settings.errorPageFilenameCannotEqualSearchPageFilename'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n\n                this.errors.push('error-page');\n                this.errors.push('search-page');\n                this.$refs['advanced-tabs'].toggle('URLs');\n\n                return false;\n            }\n\n            return true;\n        },\n        clearErrors (errorName) {\n            let pos = this.errors.indexOf(errorName);\n            this.errors.splice(pos, 1);\n        },\n        setCurrentTheme () {\n            this.currentTheme = this.$store.state.currentSite.config.theme;\n\n            if (this.currentTheme !== '') {\n                let oldName = this.currentTheme;\n                this.currentTheme = this.$store.state.currentSite.themes.filter(theme => theme.directory === this.currentTheme);\n\n                if(this.currentTheme.length) {\n                    this.currentTheme = this.currentTheme[0].name;\n                } else {\n                    this.currentTheme = oldName;\n                }\n            }\n\n            if (this.$store.state.currentSite.themeSettings) {\n                this.currentThemeVersion = this.$store.state.currentSite.themeSettings.version;\n            }\n        },\n        customPostLabels (value) {\n            let postsAndPages = this.$store.state.currentSite.posts.concat(this.$store.state.currentSite.pages);\n            return postsAndPages.filter(item => item.id === value).map(item => item.title)[0];\n        },\n        closeDropdown (refID) {\n            this.$refs[refID].isOpen = false;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('regenerate-thumbnails-close', this.savedFromPopup);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/notifications.scss';\n\n.site-settings {\n    margin: 0 auto;\n    max-width: $wrapper;\n    user-select: none;\n\n    .multiple-checkboxes {\n        label {\n            display: block;\n            margin-bottom: 1rem;\n        }\n    }\n}\n\n.note.is-warning {\n    color: var(--warning);\n}\n.msg-bm {\n   margin-bottom:3rem;\n}\n\nlabel[for=\"g-consent-mode-default-state\"] + div > .has-label {\n    display: block;\n    margin: 0 0 10px 0;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Sidebar.vue",
    "content": "<template>\n    <aside class=\"sidebar\">\n        <topbar-sites />\n        <sidebar-menu />\n        <sidebar-sync-button />\n    </aside>\n</template>\n\n<script>\nimport SidebarMenu from './SidebarMenu';\nimport SidebarSyncButton from './SidebarSyncButton';\nimport SidebarSites from './SidebarSites';\n\nexport default {\n    name: 'sidebar',\n    components: {\n        'sidebar-menu': SidebarMenu,\n        'sidebar-sync-button': SidebarSyncButton,\n        'topbar-sites': SidebarSites\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.sidebar {\n    background: var(--sidebar-bg);\n    background: linear-gradient(to bottom, var(--sidebar-bg-top) 0%, var(--sidebar-bg-bottom) 100%);\n    height: 100%;\n    padding: 0 $app-sidebar-margin 3rem;\n    position: absolute;\n    -webkit-app-region: no-drag;\n    -webkit-user-select: none;\n    width: 100%;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/SidebarMenu.vue",
    "content": "<template>\n    <ul class=\"sidebar-menu\">\n        <li\n            v-for=\"(item, index) in items\"\n            :key=\"'sidebar-item-' + index\"\n            :class=\"{ 'sidebar-menu-item': true, 'is-active': item.icon === activeMenuItem }\"\n            @click=\"setActiveMenuItem(item.icon)\">\n            <router-link :to=\"item.url\">\n                <icon\n                    customWidth=\"18\"\n                    customHeight=\"18\"\n                    :name=\"item.icon\" />\n                {{ item.label }}\n            </router-link>\n        </li>\n    </ul>\n</template>\n\n<script>\nexport default {\n    name: 'sidebar-menu',\n    data () {\n        let activeMenuItem = this.$route.path.endsWith('/pages/') ? 'pages' : 'posts';\n\n        return {\n            activeMenuItem\n        };\n    },\n    computed: {\n        items: function() {\n            let siteName = this.$route.params.name;\n            let menuItems = [{\n                icon: 'posts',\n                label: this.$t('ui.posts'),\n                url: '/site/' + siteName + '/posts/'\n            }, {\n                icon: 'pages',\n                label: this.$t('ui.pages'),\n                url: '/site/' + siteName + '/pages/'\n            }, {\n                icon: 'tags',\n                label: this.$t('ui.tags'),\n                url: '/site/' + siteName + '/tags/'\n            }, {\n                icon: 'menus',\n                label: this.$t('ui.menus'),\n                url: '/site/' + siteName + '/menus/'\n            }, {\n                icon: 'authors',\n                label: this.$t('ui.authors'),\n                url: '/site/' + siteName + '/authors/'\n            }, {\n                icon: 'themes',\n                label: this.$t('ui.theme'),\n                url: '/site/' + siteName + '/settings/themes/'\n            }, {\n                icon: 'settings',\n                label: this.$t('settings.siteSettings'),\n                url: '/site/' + siteName + '/settings/'\n            }, {\n                icon: 'server',\n                label: this.$t('ui.server'),\n                url: '/site/' + siteName + '/settings/server/'\n            }, {\n                icon: 'tools',\n                label: this.$t('ui.tools'),\n                url: '/site/' + siteName + '/tools/'\n            }];\n\n            if (this.$store.state.app.config.experimentalFileManagerInSidebar) {\n                menuItems.splice(5, 0, {\n                    icon: 'folder',\n                    label: this.$t('file.fileManager'),\n                    url: '/site/' + siteName + '/tools/file-manager'\n                });\n            }\n\n            return menuItems;\n        }\n    },\n    watch: {\n        '$route': function(newValue, oldValue) {\n            let pathElements = newValue.path.split('/');\n\n            \n\n            if (\n                pathElements[3] && \n                pathElements[3] !== 'settings' && \n                !(pathElements[3] === 'tools' && pathElements[4] === 'file-manager' && this.$store.state.app.config.experimentalFileManagerInSidebar)\n            ) {\n                this.setActiveMenuItem(pathElements[3]);\n            } else if (pathElements[3] === 'tools' && pathElements[4] === 'file-manager' && this.$store.state.app.config.experimentalFileManagerInSidebar) {\n                this.setActiveMenuItem('folder');\n            } else if(pathElements[3] === 'settings' && pathElements[4]) {\n                this.setActiveMenuItem(pathElements[4]);\n            } else if(pathElements[3] === 'settings' && !pathElements[4]) {\n                this.setActiveMenuItem(pathElements[3]);\n            }\n        }\n    },\n    methods: {\n        setActiveMenuItem: function(newValue) {\n            this.activeMenuItem = newValue;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.sidebar-menu {\n    clear: both;\n    list-style-type: none;\n    margin: 0;\n    padding: 0;\n\n    a {\n        border-radius: var(--border-radius);\n        color: var(--sidebar-link-color);\n        display: block;\n        font-size: $app-font-base;\n        font-weight: var(--font-weight-normal);\n        line-height: 2;\n        margin: 0;\n        opacity: var(--sidebar-link-opacity);\n        position: relative;\n        padding: .825rem .6rem;\n        transition: var(--transition);\n\n        &:active,\n        &:focus,\n        &:hover {\n            background: var(--sidebar-link-bg-hover);\n            color: var(--sidebar-link-color-hover);\n            opacity: 1;\n\n            svg {\n               fill: var(--sidebar-link-icon-hover);\n            }\n        }\n    }\n\n    svg {\n        fill: var(--sidebar-link-icon);\n        left: 1rem;\n        margin-right: 2.4rem;\n        position: relative;\n        transition: var(--transition);\n        top: .5rem;\n    }\n\n    &-item {\n        margin: 0 0 .2rem;\n\n        a {\n            display: flex;\n        }\n\n        &.is-active {\n            a {\n                background: var(--sidebar-link-bg-active);\n                color: var(--sidebar-link-color-active);\n                opacity: 1;\n            }\n        }\n\n        .old-git-warning {\n            background: var(--warning);\n            border-radius: 50px;\n            fill: var(--white);\n            margin-left: auto;\n            padding: 2px;\n        }\n    }\n}\n\n/*\n * Responsive improvements\n */\n@media (max-height: 736px) {\n    .sidebar-menu {\n        a {\n            padding: 0.5rem 0.8rem;\n        }\n    }\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/SidebarSites.vue",
    "content": "<template>\n    <div\n        @click=\"toggle\"\n        class=\"site-switch\">\n        <site-logo />\n    </div>\n</template>\n\n<script>\nimport SiteLogo from './SiteLogo';\n\nexport default {\n    name: 'sites',\n    components: {\n        'site-logo': SiteLogo\n    },\n    computed: {\n        syncInProgress () {\n            return this.$store.state.components.sidebar.syncInProgress;\n        }\n    },\n    data () {\n        return {\n            submenuIsOpen: false\n        };\n    },\n    methods: {\n        toggle (e) {\n            if (this.syncInProgress) {\n                this.$bus.$emit('sync-popup-maximize');\n                return;\n            }\n\n            this.$bus.$emit('sites-popup-show');\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.site-switch {\n    -webkit-app-region: no-drag; // Make the buttons clickable again         \n    cursor: pointer;\n    display: block;\n    font-weight: var(--font-weight-semibold);  \n    margin: 1rem -#{$app-sidebar-margin} .5rem;    \n    position: relative;\n    order: 1;\n\n    & > span {\n        transition: var(--transition);\n    }\n\n    & > svg {\n        height: 2.4rem;\n        position: relative;\n        top: .6rem;\n        width: 2.4rem;\n    }\n\n    &:active,\n    &:focus,\n    &:hover {\n        & > span {\n            color: var(--color-primary);\n\n        }\n\n        & > svg {\n            fill: lighten($color-1, 10);\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/SidebarSyncButton.vue",
    "content": "<template>\n    <div\n        :class=\"{\n            'sidebar-sync': true,\n            'sidebar-sync-in-progress': syncInProgress\n        }\">\n\n        <a\n            href=\"#\"\n            class=\"sidebar-preview-link\"\n            @click=\"renderPreview\">\n            {{ $t('sync.previewChanges') }}\n        </a>\n\n        <a\n            v-if=\"$store.state.app.config.enableAdvancedPreview\"\n            href=\"#\"\n            class=\"sidebar-preview-link\"\n            @click=\"renderFiles\">\n            {{ $t('sync.generatePreviewFiles') }}\n        </a>\n\n        <a\n            href=\"#\"\n            :class=\"cssClasses\"\n            @click.prevent.stop=\"syncWebsite\"\n        >\n            <span v-pure-html=\"icon\" class=\"sidebar-sync-link-icon\"></span>\n            <span>{{ status }}</span>\n        </a>\n        <a\n            v-if=\"hasSyncDate && websiteUrl\"\n            :href=\"websiteUrl\"\n            :title=\"$t('sync.visitYourWebsite')\"\n            target=\"_blank\"\n            class=\"sidebar-sync-date\"\n            rel=\"noreferrer noopener\">\n                <template v-if=\"!hasManualDeploy\">\n                    {{ $t('sync.lastSync') }}: <span>{{ syncDate }}</span>\n                </template>\n                <template v-if=\"hasManualDeploy\">\n                    {{ $t('sync.lastRendered') }}: <span>{{ syncDate }}</span>\n                </template>\n        </a>\n    </div>\n</template>\n\n<script>\nimport SidebarIcons from './configs/sidebar-icons.js';\n\nexport default {\n    name: 'sidebar-sync-button',\n    data: function() {\n        return {\n            icon: SidebarIcons.DEFAULT,\n            redirectTo: 'sync'\n        };\n    },\n    computed: {\n        cssClasses: function() {\n            return {\n                'sidebar-sync-link': true\n            };\n        },\n        status: function() {\n            let status = this.$store.state.components.sidebar.status;\n\n            if(status !== false) {\n                if(!this.checkDeploymentConfig()) {\n                    this.redirectTo = 'site-settings';\n                    this.icon = SidebarIcons.NO_CONFIG;\n                    return this.$t('sync.provideAccessData');\n                }\n\n                switch(status) {\n                    case 'preparing':\n                        this.redirectTo = 'sync';\n                        this.icon = SidebarIcons.PREPARING;\n                        return this.$t('sync.preparingFiles');\n                    case 'prepared':\n                        this.redirectTo = 'sync';\n                        this.icon = SidebarIcons.PREPARED;\n                        return this.$t('sync.preparedToUpload');\n                    case 'not-prepared':\n                        this.redirectTo = 'sync';\n                        this.icon = SidebarIcons.NOT_PREPARED;\n                        return this.$t('sync.preparationError');\n                    case 'syncing':\n                        this.redirectTo = 'sync';\n                        this.icon = SidebarIcons.SYNCING;\n                        return this.$t('sync.syncInProgress');\n                    case 'synced':\n                    case 'not-synced':\n                        this.redirectTo = 'sync';\n                        this.icon = SidebarIcons.NOT_SYNCED;\n                        return this.$t('sync.syncYourWebsite');\n                }\n            } else if(!this.checkDeploymentConfig()) {\n                this.redirectTo = 'site-settings';\n                this.icon = SidebarIcons.PROVIDE_ACCESS;\n                return this.$t('sync.configureServer');\n            } else {\n                this.redirectTo = 'sync';\n                this.icon = SidebarIcons.SYNC;\n                return this.$t('sync.syncYourWebsite');\n            }\n\n            this.redirectTo = 'sync';\n            return this.$t('sync.siteIsInSync');\n        },\n        hasSyncDate: function() {\n            if(!this.$store.state.currentSite.config) {\n                return false;\n            }\n\n            return !!(this.$store.state.currentSite.config.syncDate);\n        },\n        syncDate: function() {\n            let syncDate = this.$store.state.currentSite.config.syncDate;\n\n            if(this.$store.state.app.config.timeFormat && this.$store.state.app.config.timeFormat == 24) {\n                return this.$moment(syncDate).format('MMM DD, YYYY HH:mm');\n            } else {\n                return this.$moment(syncDate).format('MMM DD, YYYY hh:mm A');\n            }\n        },\n        hasManualDeploy () {\n            return this.$store.state.currentSite.config.deployment.protocol === 'manual';\n        },\n        websiteUrl () {\n            return this.$store.state.currentSite.config.domain;\n        },\n        syncInProgress () {\n            return this.$store.state.components.sidebar.syncInProgress;\n        }\n    },\n    methods: {\n        renderPreview: async function() {\n            if (!this.$store.state.currentSite.config.theme) {\n                let siteName = this.$store.state.currentSite.config.name;\n\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('sync.youHaventSelectedAnyThemeInfo'),\n                    okLabel: this.$t('sync.goToSettings'),\n                    okClick: () => {\n                        this.$router.push(`/site/${siteName}/settings/`);\n                    }\n                });\n                return;\n            }\n\n            let previewLocationExists = await mainProcessAPI.existsSync(this.$store.state.app.config.previewLocation);\n\n            if (this.$store.state.app.config.previewLocation !== '' && !previewLocationExists) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('sync.previewCatalogDoesNotExistInfo'),\n                    okLabel: this.$t('sync.goToAppSettings'),\n                    okClick: () => {\n                        this.$router.push(`/app-settings/`);\n                    }\n                });\n                return;\n            }\n\n            this.$bus.$emit('rendering-popup-display');\n        },\n        renderFiles: async function() {\n            if (!this.$store.state.currentSite.config.theme) {\n                let siteName = this.$store.state.currentSite.config.name;\n\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('sync.youHaventSelectedAnyThemeInfo'),\n                    okLabel: this.$t('sync.goToSettings'),\n                    okClick: () => {\n                        this.$router.push(`/site/${siteName}/settings/`);\n                    }\n                });\n                return;\n            }\n\n            let previewLocationExists = await mainProcessAPI.existsSync(this.$store.state.app.config.previewLocation);\n\n            if (this.$store.state.app.config.previewLocation !== '' && !previewLocationExists) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('sync.previewCatalogDoesNotExistInfo'),\n                    okLabel: this.$t('sync.goToAppSettings'),\n                    okClick: () => {\n                        this.$router.push(`/app-settings/`);\n                    }\n                });\n                return;\n            }\n\n            this.$bus.$emit('rendering-popup-display', {\n                showPreview: false\n            });\n        },\n        syncWebsite: function(e) {\n            if (e.screenX === 0 && e.screenY === 0) {\n                return;\n            }\n\n            if (this.redirectTo === 'sync') {\n                if (!this.$store.state.currentSite.config.theme) {\n                    let siteName = this.$store.state.currentSite.config.name;\n\n                    this.$bus.$emit('confirm-display', {\n                        message: this.$t('sync.youHaventSelectedAnyThemeInfo'),\n                        okLabel: this.$t('sync.goToSettings'),\n                        okClick: () => {\n                            this.$router.push(`/site/${siteName}/settings/`);\n                        }\n                    })\n                    return;\n                }\n\n                this.$bus.$emit('sync-popup-display');\n            } else if (this.redirectTo === 'site-settings') {\n                let siteName = this.$store.state.currentSite.config.name;\n\n                this.$router.push('/site/' + siteName + '/settings/server');\n            }\n        },\n        checkDeploymentConfig() {\n            let config = this.$store.state.currentSite.config;\n\n            if(!config || !config.deployment) {\n                return false;\n            }\n\n            if(config.deployment.protocol === '' || config.deployment.domain === '') {\n                return false;\n            }\n\n            return true;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../scss/variables.scss';\n\n.sidebar {\n    &-sync {\n        bottom: 3rem;\n        left: $app-sidebar-margin;\n        position: absolute;\n        right: $app-sidebar-margin;\n\n        &-icon {\n            stroke: var(--white);\n        }\n\n        &-date {\n            color: var(--sidebar-link-color);\n            display: block;\n            font-size: 1.2rem;\n            height: 16px; \n            letter-spacing: -.025em;\n            margin-top: 1.2rem;\n            opacity: var(--sidebar-link-opacity);\n            text-align: center;\n\n            &:hover {\n                color: var(--sidebar-link-color-hover);\n                opacity: 1;\n            }\n\n            &:focus {\n                color: var(--sidebar-link-color);\n            }\n        }\n\n        &-link {\n            align-items: center;\n            background: var(--sidebar-sync-btn-bg);\n            border-radius: var(--border-radius);\n            color: var(--sidebar-sync-btn-color);\n            display: flex;\n            gap: .6rem;\n            font-size: $app-font-base;\n            font-weight: var(--font-weight-semibold);\n            justify-content: center;\n            padding: 1.4rem 1rem;\n            position: relative;\n\n            // sync cloud icon\n            .sidebar-sync-icon {\n                height: 2.2rem;\n                display: inherit;\n                width: 3rem;\n\n                path {\n                    stroke: var(--white);\n                    transition: var(--transition);\n                }\n\n                polygon {\n                    stroke: var(--color-primary);\n                    transition: var(--transition);\n                }\n            }\n\n            &:active .sidebar-sync-icon,\n            &:focus .sidebar-sync-icon,\n            &:hover .sidebar-sync-icon {\n                path {\n                    stroke: var(--white);\n                }\n\n                polygon {\n                    stroke: $color-helper-6;\n                }\n            }\n\n            // .sidebar-sync-icon {\n            //     &.is-animated {\n            //         polygon {\n            //             animation: pulse 1s infinite;\n            //         }\n            //     }\n            // }\n\n            // interjection mark icon\n            .sidebar-interjection-icon {\n                display: block;\n                height: 2.3rem;\n                width: 2.4rem;\n\n                path {\n                    stroke: var(--white);\n                }\n            }\n\n            &:active,\n            &:focus,\n            &:hover {\n                background: $color-helper-6;\n                color: var(--white);\n\n                .sidebar-sync-icon {\n                    stroke: $color-helper-6;\n                }\n            }\n\n            &-icon {\n                display: block;\n                height: 100%;\n                width: auto;\n            }\n        }\n\n        &-synced {}\n        &-not-synced {}\n        &-preparing,\n        &-prepared,\n        &-syncing {}\n        &-not-prepared,\n        &-noftp {}\n\n        &-preparing {\n            display: block;\n            height: 2.1rem;\n            width: 2.1rem;\n\n            & > span {\n                animation: spin .9s infinite linear;\n                border-top: 2px solid rgba(255,255,255, .2);\n                border-right: 2px solid rgba(255,255,255, .2);\n                border-bottom: 2px solid rgba(255,255,255, .2);\n                border-left: 2px solid var(--white);\n                border-radius: 50%;\n                display: inline-block;\n                height: 2.1rem;\n                width: 2.1rem;\n\n                &::after {\n                    border-radius: 50%;\n                    content: \"\";\n                    display: block;\n                }\n            }\n        }\n\n        &-in-progress {\n            .sidebar-sync-link, .sidebar-sync-date, .sidebar-preview-website {\n               opacity: 0;\n               transition: .35s cubic-bezier(.17,.67,.13,1.05) .35s all;\n               visibility: hidden;\n            }\n        }\n    }\n\n    &-preview-link {\n        border: 2px solid var(--sidebar-preview-btn-border-color);\n        border-radius: var(--border-radius);\n        color: var(--sidebar-preview-btn-color) !important;\n        display: block;\n        font-size: $app-font-base;\n        font-weight: var(--font-weight-semibold);\n        margin-bottom: 1rem;\n        padding: 1.2rem 1rem;\n        text-align: center;\n\n        & > span {\n            display: inline-block;\n            width: 3.2rem;\n        }\n\n        &:hover {\n            border-color: var(--sidebar-preview-btn-border-color-hover) !important;\n            color: var(--sidebar-preview-btn-color-hover) !important;\n        }\n\n        &.is-disabled {\n            opacity: .75;\n            pointer-events: none;\n        }\n    }\n}\n\n.minimized-sync {\n    &-in-progress {\n        .progress-message {\n            color: white;\n            position: initial;\n        }\n\n        .progress-wrapper {\n            min-height: 50px;\n            padding: 0;\n        }\n\n        .progress {\n            background-color: var(--sidebar-preview-btn-border-color);\n            height: 4px;\n\n            &-bar {\n                height: 4px;\n            }\n        }\n    }\n\n    &-error {\n         font-size: 1.3rem;\n    }\n\n\n}\n\n@keyframes pulse {\n    from {\n        transform: translateY(50%);\n        opacity: 1;\n    }\n    to {\n        transform: translateY(-200%);\n        opacity: 0.5;\n    }\n}\n\n@keyframes spin {\n    100% {\n        transform: rotate(360deg);\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Site.vue",
    "content": "<template>\n    <div :class=\"cssClasses\">\n        <add-website v-if=\"isEmpty\" />\n\n        <sidebar v-if=\"!isEmpty\" />\n\n        <transition>\n            <router-view v-if=\"!isEmpty\" />\n        </transition>\n    </div>\n</template>\n\n<script>\nimport SiteAddForm from './SiteAddForm';\nimport Sidebar from './Sidebar';\n\nexport default {\n    name: 'site',\n    components: {\n        'add-website': SiteAddForm,\n        'sidebar': Sidebar\n    },\n    data: function() {\n        return {\n            currentSite: '',\n            siteIsLoaded: false\n        };\n    },\n    computed: {\n        siteName: function() {\n            return this.$route.params.name;\n        },\n        isEmpty: function() {\n            if(this.currentSite !== this.siteName && this.siteName !== '!' && !this.$store.state.editorOpened) {\n                this.switchSite(this.siteName);\n            }\n\n            if (this.$store.state.editorOpened) {\n                this.$store.commit('setEditorOpenState', false);\n\n                setTimeout(() => {\n                    this.$bus.$emit('site-view-restored');\n                }, 0);\n            }\n\n            return this.siteName === '!';\n        },\n        cssClasses: function() {\n            return {\n                'site': true,\n                'is-empty': this.isEmpty\n            };\n        }\n    },\n    mounted () {\n        this.$bus.$on('add-website-form-displayed', () => {\n            this.currentSite = '';\n        });\n\n        this.$bus.$on('site-loaded', this.whenSiteLoaded);\n    },\n    methods: {\n        switchSite: function(siteName) {\n            let self = this;\n            this.currentSite = siteName;\n            this.siteIsLoaded = false;\n\n            mainProcessAPI.send('app-site-switch', {\n                'site': siteName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-switched', (data) => {\n                console.log('app-site-switched', data);\n                if (data.status !== false) {\n                    this.loadSite(siteName, data);\n                    this.siteIsLoaded = true;\n                    this.checkGdprConfig();\n                    this.$bus.$emit('site-loaded', true);\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('site.siteLoadingErrorMsg'),\n                        type: 'warning'\n                    });\n                }\n            });\n        },\n\n        /**\n         *\n         * Function used to:\n         * - switch a website to another one\n         * - reload a website data (i.e. after backup restore) - with config\n         *\n         * @param siteName - name of the website to reload\n         * @param data - (optional) loaded data of the website\n         * @param config - (optional) config of the website to use during switching\n         *\n         */\n        loadSite: function(siteName, data = false, config = false) {\n            // Write a config of the current Site - if necessary\n            if(siteName !== false && config === false) {\n                this.$store.commit('copySiteConfig', siteName);\n            }\n\n            // Write a config of the current Site - if necessary\n            if(siteName !== false && config !== false) {\n                this.$store.commit('setSiteConfig', {\n                    name: siteName,\n                    config: config\n                });\n            }\n\n            this.$store.commit('switchSite', data);\n        },\n        whenSiteLoaded () {\n            this.currentSite = this.$store.state.currentSite.config.name;\n        },\n        checkGdprConfig () {\n            if (\n                this.$store.state.currentSite.config.advanced &&\n                this.$store.state.currentSite.config.advanced.gdpr &&\n                this.$store.state.currentSite.config.advanced.gdpr.enabled &&\n                !this.$store.state.currentSite.config.advanced.gdpr.settingsVersion\n            ) {\n                this.$bus.$emit('confirm-display', {\n                    hasInput: false,\n                    message: this.$t('ui.gdprBreakingChangesConfirmMsg'),\n                    okClick: this.goToGdprSettings,\n                    cancelClick: this.checkWhatsNew,\n                    okLabel: this.$t('ui.goToSettings'),\n                    cancelLabel: this.$t('ui.whatsNew'),\n                    cancelNotClosePopup: true\n                });\n            }\n        },\n        goToGdprSettings () {\n            let siteName = this.$route.params.name;\n            this.$router.push('/site/' + siteName + '/settings/');\n        },\n        checkWhatsNew () {\n            mainProcessAPI.shellOpenExternal('https://getpublii.com/blog/release-040.html#cookie-banner');\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('add-website-form-displayed');\n        this.$bus.$off('site-loaded', this.whenSiteLoaded);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.site {\n    padding: 2rem;\n\n    &.is-empty {\n        height: 100%;\n        position: absolute;\n        width: 100%;\n    }\n\n    .sidebar,\n    .content {\n        position: absolute;\n        top: 0;\n    }\n\n    .sidebar {\n        bottom: 0;\n        left: 0;\n        width: $app-sidebar;\n    }\n\n    .content {\n        background: var(--bg-site);\n        bottom: 0;\n        overflow: scroll;\n        padding: 3rem 4rem;\n        right: 0;\n        width: calc(100% - $app-sidebar); \n\n        &.is-wide {\n            width: 100%;\n        }\n    }\n\n    .v-enter-to,\n    .v-leave-to {\n        opacity: 1;\n        transition: opacity .25s ease-out;\n    }\n\n    .v-enter,\n    .v-leave-to {\n        opacity: 0;\n        position: absolute;\n    }\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/SiteAddForm.vue",
    "content": "<template>\n    <div class=\"site-create-wrapper\">\n        <div class=\"site-create\">\n            <tabs\n                ref=\"site-create-tabs\"\n                :items=\"tabsItems\"\n                isHorizontal\n                :onToggle=\"tabChanged\">\n                <div slot=\"tab-0\">\n                    <div class=\"site-create-form\">\n                        <logo-creator ref=\"logo-creator\" />\n\n                        <div class=\"site-create-field\">\n                            <label for=\"site-name\">\n                                {{ $t('site.websiteName') }}:\n                                <span\n                                    v-if=\"siteNameError\"\n                                    class=\"site-create-field-error\">\n                                    {{ $t('site.websiteNameRequired') }}\n                                </span>\n                            </label>\n\n                            <text-input\n                                ref=\"site-name\"\n                                id=\"site-name\"\n                                :spellcheck=\"false\"\n                                changeEventName=\"add-website-name-changed\"\n                                :customCssClasses=\"siteNameCssClasses\" />\n                        </div>\n\n                        <div class=\"site-create-field\">\n                            <label for=\"author-name\">\n                                {{ $t('author.authorName') }}:\n                                <span\n                                    v-if=\"authorNameError\"\n                                    class=\"site-create-field-error\">\n                                    {{ $t('site.websiteAuthorRequired') }}\n                                </span>\n                            </label>\n\n                            <text-input\n                                ref=\"author-name\"\n                                id=\"author-name\"\n                                :spellcheck=\"false\"\n                                changeEventName=\"add-website-author-changed\"\n                                :customCssClasses=\"authorNameCssClasses\" />\n                        </div>\n                    </div>\n                </div>\n                <div slot=\"tab-1\">\n                    <div\n                        @drop.stop.prevent=\"uploadBackup\"\n                        @dragleave.stop.prevent=\"hideOverlay\"\n                        @dragenter.stop.prevent=\"showOverlay\"\n                        @dragover.stop.prevent=\"showOverlay\"\n                        @drag.stop.prevent=\"showOverlay\"\n                        @dragstart.stop.prevent\n                        @dragend.stop.prevent\n                        :class=\"{ \n                            'backup': true, \n                            'backup-is-over': backupIsOver,\n                            'restore-in-progress': restoreInProgress\n                        }\">\n                        <div class=\"backup-upload\">\n                            <icon\n                                customWidth=\"60\"\n                                customHeight=\"60\"\n                                properties=\"not-clickable\"\n                                name=\"backup\" />\n\n                                <span>{{ $t('file.dragAndDropBackupFile') }}</span>\n\n                                <input\n                                    ref=\"input\"\n                                    type=\"file\"\n                                    class=\"backup-upload-input\"\n                                    spellcheck=\"false\"\n                                    @change=\"valueChanged\">\n                        </div>\n\n                        <overlay\n                            v-if=\"backupIsOver\"\n                            :hasBorder=\"true\"\n                            :isBlue=\"true\">\n                            <div>{{ $t('file.dropYourFileHere') }}</div>\n                        </overlay>\n                    </div>\n                </div>\n            </tabs>\n\n            <div :data-mode=\"status\" class=\"site-create-buttons\">\n                <p-button\n                    v-if=\"tabsActiveIndex === 0\"\n                    type=\"primary bottom\"\n                    :onClick=\"addWebsite\">\n                    {{ $t('site.createWebsite') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"this.$store.getters.siteNames.length\"\n                    type=\"outline bottom\"\n                    :onClick=\"goBack\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n        </div>\n\n        <overlay v-if=\"overlayIsVisible\">\n            <div>\n                <div class=\"loader\"><span></span></div>\n                {{ $t('site.creationInProgress') }}\n            </div>\n        </overlay>\n    </div>\n</template>\n\n<script>\nimport defaultSiteConfig from './../../config/AST.currentSite.config';\nimport Utils from './../helpers/utils.js';\nimport GoToLastOpenedWebsite from './mixins/GoToLastOpenedWebsite';\n\nexport default {\n    name: 'site-add-form',\n    mixins: [\n        GoToLastOpenedWebsite\n    ],\n    data () {\n        return {\n            siteName: '',\n            authorName: '',\n            siteNameError: false,\n            authorNameError: false,\n            overlayIsVisible: false,\n            backupFile: null,\n            backupIsOver: false,\n            tabsActiveIndex: 0,\n            restoreInProgress: false\n        }\n    },\n    computed: {\n        status () {\n            if(this.$store.getters.siteNames.length) {\n                return 'new-website';\n            }\n\n            return 'first-website';\n        },\n        header () {\n            if(this.status === 'new-website') {\n                return this.$t('site.createNewWebsite');\n            }\n\n            return this.$t('site.createYourFirstWebsite');\n        },\n        siteNameCssClasses () {\n            if(this.siteNameError) {\n                return 'has-error';\n            }\n        },\n        authorNameCssClasses () {\n            if(this.authorNameError) {\n                return 'has-error';\n            }\n        },\n        defaultSiteConfig () {\n            return JSON.parse(JSON.stringify(defaultSiteConfig));\n        },\n        tabsItems () {\n            return [\n                this.header,\n                this.$t('site.installFromBackup'),\n            ];\n        }\n    },\n    mounted () {\n        this.$bus.$on('add-website-name-changed', (newValue) => {\n            this.siteName = newValue;\n            this.siteNameError = false;\n        });\n\n        this.$bus.$on('add-website-author-changed', (newValue) => {\n            this.authorName = newValue;\n            this.authorNameError = false;\n        });\n\n        this.$bus.$emit('add-website-form-displayed');\n        document.body.addEventListener('keydown', this.onDocumentKeyDown);\n    },\n    methods: {\n        checkWebsiteName () {\n            if(this.siteName.trim() === '') {\n                this.siteNameError = true;\n            } else {\n                this.siteNameError = false;\n            }\n        },\n        checkAuthorName () {\n            if(this.authorName.trim() === '' || this.authorName.trim() === '') {\n                this.authorNameError = true;\n            } else {\n                this.authorNameError = false;\n            }\n        },\n        formIsInvalid () {\n            this.checkWebsiteName();\n            this.checkAuthorName();\n\n            if (this.siteNameError || this.authorNameError) {\n                return true;\n            }\n\n            return false;\n        },\n        addWebsite (e) {\n            let self = this;\n\n            if (this.formIsInvalid()) {\n                return;\n            }\n\n            this.overlayIsVisible = true;\n\n            setTimeout(() => {\n                mainProcessAPI.send('app-site-create', this.setBaseConfig(), this.authorName.trim());\n\n                mainProcessAPI.receiveOnce('app-site-creation-error', (data) => {\n                    this.overlayIsVisible = false;\n                    if (data.name) {\n                        this.siteNameError = true;\n                    }\n\n                    if (data.author) {\n                        this.authorNameError = true;\n                    }\n\n                    mainProcessAPI.stopReceiveAll('app-site-created');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-duplicate');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-db-error');\n                });\n\n                mainProcessAPI.receiveOnce('app-site-creation-duplicate', (data) => {\n                    this.overlayIsVisible = false;\n                    this.siteNameError = true;\n\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('site.siteWithThisNameExists'),\n                        textCentered: true\n                    });\n\n                    mainProcessAPI.stopReceiveAll('app-site-created');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-error');\n                });\n\n                mainProcessAPI.receiveOnce('app-site-creation-db-error', (data) => {\n                    this.overlayIsVisible = false;\n                    this.siteNameError = true;\n\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('site.erroOcurredDuringSiteDatabaseCreationInfo'),\n                        textCentered: true\n                    });\n\n                    mainProcessAPI.stopReceiveAll('app-site-created');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-error');\n                });\n\n                mainProcessAPI.receiveOnce('app-site-created', (data) => {\n                    this.overlayIsVisible = false;\n                    data.authors = self.setAuthor(data.authorName);\n                    this.$store.commit('addNewSite', data);\n                    window.localStorage.setItem('publii-last-opened-website', data.siteConfig.name);\n                    this.$router.push(`/site/${data.siteConfig.name}`);\n\n                    mainProcessAPI.stopReceiveAll('app-site-creation-error');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-duplicate');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-db-error');\n                });\n            }, 250);\n        },\n        setBaseConfig () {\n            let baseConfig = {\n                name: this.siteName.trim(),\n                displayName: this.siteName.trim(),\n                synced: false,\n                logo: {\n                    color: this.$refs['logo-creator'].getActiveColor(),\n                    icon: this.$refs['logo-creator'].getActiveIcon()\n                }\n            };\n\n            return Utils.deepMerge(this.defaultSiteConfig, baseConfig);\n        },\n        setAuthor (authorName) {\n            return [{\n                id: 1,\n                name: this.authorName.trim(),\n                username: authorName,\n                config: \"{}\",\n                additionalData: \"{}\",\n                postCounter: 0\n            }];\n        },\n        onDocumentKeyDown (e) {\n            if (e.code === 'Enter' && !event.isComposing) {\n                this.addWebsite();\n            }\n        },\n        onEnterKey () {\n            this.onOk();\n        },\n        showOverlay (e) {\n            this.backupIsOver = true;\n        },\n        hideOverlay (e) {\n            this.backupIsOver = false;\n        },\n        async uploadBackup (e) {\n            this.backupIsOver = false;\n\n            if (typeof e === 'string') {\n                this.backupFile = e;\n            } else {\n                this.backupFile = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.dataTransfer.files[0]));\n            }\n\n            this.restoreInProgress = true;\n            console.log('T', this.backupFile);\n\n            mainProcessAPI.send('app-site-check-website-to-restore', {\n                backupPath: this.backupFile\n            });\n\n            mainProcessAPI.receiveOnce('app-site-backup-checked', (data) => {\n                if (data.status === 'error') {\n                    this.handleCreateFromBackupError(data.type);\n                } else if (data.status === 'success') {\n                    this.askForWebsiteName(data.data.displayName);\n                }\n            });\n        },\n        async valueChanged (e) {\n            if (!e.target.files.length) {\n                return;\n            }\n\n            let sourcePath = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.target.files[0]));\n            await this.uploadBackup(sourcePath);\n        },\n        removeBackupFile () {\n            this.backupFile = null;\n        },\n        tabChanged () {\n            this.tabsActiveIndex = this.$refs['site-create-tabs'].activeIndex;\n        },\n        handleCreateFromBackupError (problemType) {\n            if (problemType === 'unsupported-format') {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('site.restoreFromBackup.unsupportedFormat'),\n                    buttonStyle: 'danger'\n                });\n            }\n\n            if (problemType === 'unpack-error') {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('site.restoreFromBackup.unpackError'),\n                    buttonStyle: 'danger'\n                });\n            }\n\n            if (problemType === 'invalid-backup-content') {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('site.restoreFromBackup.invalidBackupContent'),\n                    buttonStyle: 'danger'\n                });\n            }\n\n            if (problemType === 'invalid-site-data') {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('site.restoreFromBackup.invalidSiteData'),\n                    buttonStyle: 'danger'\n                });\n            }\n\n            this.restoreInProgress = false;\n        },\n        askForWebsiteName (siteName) {\n            this.$bus.$emit('confirm-display', {\n                hasInput: true,\n                message: this.$t('site.restoreFromBackup.selectSiteName'),\n                okClick: this.checkCatalogAvailability,\n                okLabel: this.$t('site.restoreFromBackup.createWebsite'),\n                cancelLabel: this.$t('ui.cancel'),\n                cancelClick: () => {\n                    this.removeTemporaryBackupFiles();\n                    this.restoreInProgress = false;\n                },\n                defaultText: siteName\n            });\n        },\n        checkCatalogAvailability (siteName) {\n            if (siteName.trim() === '') {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('site.restoreFromBackup.siteNameCannotBeEmpty'),\n                    buttonStyle: 'danger',\n                    okClick: () => {\n                        this.askForWebsiteName (siteName);\n                    }\n                });\n                return;\n            }\n\n            mainProcessAPI.send('app-site-check-website-catalog-availability', {\n                siteName: siteName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-website-catalog-availability-checked', (data) => {\n                if (data.catalogExists === true) {\n                    this.$bus.$emit('confirm-display', {\n                        message: this.$t('site.restoreFromBackup.siteExistsWantOverride'),\n                        okClick: () => {\n                            this.restoreWebsiteFromBackup(siteName);\n                        },\n                        isDanger: true,\n                        okLabel: this.$t('site.restoreFromBackup.yesPleaseOverride'),\n                        cancelLabel: this.$t('site.restoreFromBackup.iWantChangeName'),\n                        cancelClick: () => {\n                            this.askForWebsiteName(siteName);\n                        }\n                    });\n                } else if (data.catalogExists === false) {\n                    this.restoreWebsiteFromBackup(siteName);\n                }\n            });\n        },\n        restoreWebsiteFromBackup (siteName) {\n            mainProcessAPI.send('app-site-restore-from-backup', {\n                siteName: siteName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-restored-from-backup', (data) => {\n                this.restoreInProgress = false;\n\n                if (data.status === 'error') {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('site.restoreFromBackup.restoreFailed'),\n                        buttonStyle: 'danger'\n                    });\n                } else if (data.status === 'success') {\n                    this.overlayIsVisible = false;\n                    let siteCatalogName = data.data.siteCatalogName;\n\n                    mainProcessAPI.stopReceiveAll('app-site-creation-error');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-duplicate');\n                    mainProcessAPI.stopReceiveAll('app-site-creation-db-error'); \n                    \n                    mainProcessAPI.send('app-site-reload', {\n                        siteName: siteCatalogName\n                    });\n\n                    mainProcessAPI.receiveOnce('app-site-reloaded', (result) => {\n                        this.$store.commit('setSiteConfig', result);\n                        this.$store.commit('switchSite', result.data);\n                        window.localStorage.setItem('publii-last-opened-website', siteCatalogName);\n                        this.$router.push(`/site/${siteCatalogName}`);\n                    });  \n                }\n            });\n        },\n        removeTemporaryBackupFiles () {\n            mainProcessAPI.send('app-site-remove-temporary-backup-files');\n        } \n    },\n    beforeDestroy () {\n        this.$bus.$off('add-website-name-changed');\n        this.$bus.$off('add-website-author-changed');\n        document.body.removeEventListener('keydown', this.onDocumentKeyDown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n/*\n * Site create form\n */\n.site-create {\n    background: var(--popup-bg);\n    border-radius: var(--border-radius);\n    box-shadow: 0 0 60px rgba(0, 0, 0, 0.06);\n    font-size: $app-font-base;\n    margin: 0;\n    left: 50%;\n    padding: 4.8rem 4.8rem 5.6rem 4.8rem;\n    position: absolute;\n    text-align: center;\n    top: 50%;\n    transform: translateX(-50%) translateY(-50%);\n    user-select: none;\n    width: 770px;\n\n    &-form {\n        height: 344px;  \n        overflow: hidden;\n        ::v-deep .logo-creator-preview {\n            min-width: 10rem !important;\n        }\n    }\n\n    .title {\n        color: var(--text-primary-color);\n        font-size: 1.8rem;\n        font-weight: 600;\n        margin: 0 0 4rem 0!important;\n        text-transform: none;\n    }\n\n    &-field {\n        margin: 0 0 3rem 0;\n        text-align: left;\n\n        & > label {\n            display: block;\n            font-size: $app-font-base;\n            font-weight: 400;\n            line-height: 1.4;\n            margin-bottom: 1rem;\n        }\n\n        &-error {\n            color: var(--warning);\n            font-size: 1.4rem;\n        }\n\n        &:last-child {\n            margin-bottom: 0;\n        }\n    }\n\n    &-buttons {\n        display: flex;\n        margin: 0 -4.8rem -5.6rem -4.8rem;\n        overflow: hidden;\n        padding: 5.6rem 0 0 0;\n        position: relative;\n        text-align: center;\n        top: 1px;\n\n        .button {\n            border-radius: 0 0 0 var(--border-radius);\n\n            &:last-child:first-child {\n                border-radius: 0 0 var(--border-radius) var(--border-radius);\n            }\n        }\n        .button-outline {\n            box-shadow: none!important;\n            border-top: 1px solid var(--input-border-color);\n            border-radius: 0 0 var(--border-radius) 0;\n            color: var(--popup-btn-cancel-color);\n            margin-left: 0;\n\n            &:hover {\n                background: var(--popup-btn-cancel-bg-hover);\n                color: var(--popup-btn-cancel-hover-color);\n            }\n        }\n    }\n\n    &-wrapper {\n        .loader {\n            display: block;\n            height: 2.8rem;\n            margin: -5.6rem auto 2rem;\n            width: 2.8rem;\n\n            & > span {\n                animation: spin .9s infinite linear;\n                border-top: 2px solid var(--border-light-color);\n                border-right: 2px solid var(--border-light-color);\n                border-bottom: 2px solid var(--border-light-color);\n                border-left: 2px solid var(--gray-4);\n                border-radius: 50%;\n                display: block;\n                height: 3.5rem;\n                width: 3.5rem;\n\n                &::after {\n                    border-radius: 50%;\n                    content: \"\";\n                    display: block;\n                }\n\n                @at-root {\n                    @keyframes spin {\n                       100% {\n                          transform: rotate(360deg);\n                       }\n                    }\n                }\n          }\n       }\n    }\n\n    .backup-selected {\n        text-align: left;\n\n        &-file {\n            align-items: center;\n            display: flex;\n            justify-content: space-between;\n            margin: 1rem 0;\n\n            strong {\n                margin-right: 1rem;\n            }\n        }\n    }\n\n    .backup {\n        border: 2px dashed var(--input-border-color);\n        border-radius: var(--border-radius);\n        color: var(--gray-3);\n        position: relative;\n\n        &-upload {\n            align-items: center;\n            display: flex;\n            flex-direction: column;\n            height: 340px;\n            justify-content: center;\n            padding: 2rem;\n\n            .icon {\n                fill: var(--icon-primary-color);\n                margin-bottom: 1.5rem;\n            }\n\n            &-input {\n                clear: both;\n                color: transparent; // hack to remove the phrase \"no file selected\" from the file input\n                display: block;\n                line-height: 1.6!important;\n                margin: 3rem auto 0 auto!important;\n\n                &::-webkit-file-upload-button {\n                    -webkit-appearance: none;\n                    background: var(--button-secondary-bg);\n                    border: 1px solid var(--button-secondary-bg);\n                    border-radius: var(--border-radius);\n                    color: var(--button-secondary-color);\n                    cursor: pointer;\n                    display: inline-block;\n                    font-size: 1.4rem;\n                    font-weight: var(--font-weight-semibold);\n                    left: 50%;\n                    padding: .75rem 1.5rem;\n                    position: relative;\n                    transform: translate(-50%, 0);\n                    outline: none;\n\n                    &:hover {\n                        background: var(--button-secondary-bg-hover);\n                        border-color: var(--button-secondary-bg-hover);\n                        color: var(--button-secondary-color-hover);\n                    }\n                }\n            }\n        }\n\n        .overlay.has-border {\n            pointer-events: none;\n            border-radius: 3px;\n        }\n        &.restore-in-progress {\n            position: relative;\n          \n            &::after {\n                border: 3px solid var(--color-primary);\n                background: rgba(var(--color-primary-rgb), .17);\n                content:\"\";\n                height: 100%;\n                left: 0;\n                position: absolute;\n                top: 0;\n                width: 100%;\n            }\n\n            &::before {\n                animation: spin .9s infinite linear;\n                border-top: 2px solid rgba(var(--color-primary-rgb), 0.3);\n                border-right: 2px solid rgba(var(--color-primary-rgb), 0.3);\n                border-bottom: 2px solid rgba(var(--color-primary-rgb), 0.3);\n                border-left: 2px solid var(--color-primary);\n                border-radius: 50%;\n                content:\"\";\n                display: inline-block;\n                height: 3rem;      \n                left: calc(50% - 1.5rem);\n                position: absolute;      \n                top: calc(50% - 1.5rem);      \n                vertical-align: middle;\n                width: 3rem;\n\n                @at-root {\n                    @keyframes spin {\n                        100% {\n                            transform: rotate(360deg);\n                        }\n                    }\n                }\n            }\n            .backup-upload {\n                opacity: 0;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/SiteLogo.vue",
    "content": "<template>\n    <div\n        ref=\"content\"\n        class=\"site-logo\">\n        <span\n            v-if=\"logoColor\"\n            class=\"site-logo-bg\">\n            <icon\n                :name=\"logoIcon\"\n                size=\"s\"\n                iconset=\"svg-map-site\"/>\n        </span>\n\n        <span class=\"site-logo-name\">\n            <strong\n                class=\"site-logo-link\">\n                {{ siteName }}\n            </strong>\n        </span>\n\n        <span\n            class=\"site-logo-icon-open\"\n            name=\"sidebar-arrow\">\n        </span>\n\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'site-logo',\n    data: function() {\n        return {\n            siteIsLoaded: false\n        };\n    },\n    computed: {\n        logoColor: function() {\n            if(!this.siteIsLoaded) {\n                return '';\n            }\n\n            return this.$store.state.currentSite.config.logo.color;\n        },\n        logoIcon: function() {\n            if(!this.siteIsLoaded) {\n                return '';\n            }\n\n            return this.$store.state.currentSite.config.logo.icon;\n        },\n        siteName: function() {\n            if(!this.siteIsLoaded) {\n                return this.$t('ui.selectWebsite');\n            }\n\n            return this.$store.state.currentSite.config.displayName;\n        },\n        isOnline: function() {\n            if(!this.siteIsLoaded) {\n                return false;\n            }\n\n            if(!this.$store.state.currentSite.config.domain || this.$store.state.currentSite.config.deployment.protocol === 'manual') {\n                return false;\n            }\n\n            return !!this.$store.state.currentSite.config.syncDate;\n        },\n        linkTitle: function() {\n            if(this.isOnline) {\n                return this.$t('sync.visitYourWebsite');\n            } else {\n                return this.$t('sync.afterInitialSyncSiteWillBeAvailableOnline');\n            }\n        },\n        siteLink: function() {\n            if(!this.siteIsLoaded) {\n                return '';\n            }\n\n            return this.$store.state.currentSite.config.domain;\n        },\n        settingsLink: function() {\n            return '/site/' + this.$route.params.name + '/settings/';\n        },\n        previewIconName: function() {\n            if(this.isOnline) {\n                return 'on-live-preview';\n            }\n\n            return 'off-live-preview';\n        }\n    },\n    mounted: function() {\n        this.$bus.$on('site-loaded', this.whenSiteLoaded);\n\n        this.$bus.$on('site-view-restored', () => {\n            this.siteIsLoaded = true;\n        });\n\n        this.$bus.$on('sites-list-reset', () => {\n            this.siteIsLoaded = false;\n        })\n    },\n    methods: {\n        whenSiteLoaded () {\n            this.siteIsLoaded = true;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('site-loaded', this.whenSiteLoaded);\n        this.$bus.$off('site-view-restored');\n        this.$bus.$off('sites-list-reset');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n\n.site-logo {\n    align-items: center;\n    color: var(--sidebar-preview-btn-color);\n    display: flex;\n    padding: 2.5rem 4rem;\n    width: 100%;\n    transition: var(--transition);\n\n    &:active,\n    &:focus,\n    &:hover {\n\n        .site-logo-icon-open {\n            border-top-color: var(--sidebar-link-icon-hover);\n            opacity: 1;\n        }\n    }\n\n    & > a {\n        display: block;\n        height: 4rem;\n        margin: .5rem 1rem 0 0.92rem;\n        position: relative;\n        width: 4rem;\n        z-index: 1;\n    }\n\n    &-bg {\n        align-items: center;\n        border-radius: 3px;\n        color: var(--sidebar-icon);\n        display: flex;\n        height: 20px;\n        justify-content: center;\n        width: 20px;\n    }\n\n    &-name {\n        margin: 0 0 0 1.5rem;\n        width: calc(100% - 5rem);\n    }\n\n    &-link {\n        display: block;\n        font-size: $app-font-base;\n        font-weight: var(--font-weight-semibold);\n        margin: 0;\n        overflow: hidden;\n        position: relative;\n        text-overflow: ellipsis;\n        transition: all .3s ease-out;\n        white-space: nowrap;\n\n        & > span {\n            display: inline-block;\n            overflow: hidden;\n            pointer-events: none;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n            width: 160px;\n        }\n    }\n\n    &-icon-open {\n        border-color: var(--sidebar-link-icon) transparent transparent;\n        border-style: solid;\n        border-width: 5px;\n        opacity: 1;\n        cursor: pointer;\n        height: 5px;\n        left: auto;\n        line-height: 1.1;\n        opacity: var(--sidebar-link-opacity);\n        padding: 0;\n        position: absolute;\n        right: 4rem;\n        width: 5px;\n        text-align: center;\n        transition: var(--transition);\n        top: calc(50% - 2px);\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/SitesList.vue",
    "content": "<template>\n    <div class=\"sites-list-wrapper\">\n        <ul class=\"sites-list\">\n            <sites-list-item\n                v-for=\"(siteName, key) in sites\"\n                :site=\"siteName\"\n                :key=\"key\"\n                :duplicateInProgress=\"siteDuplicateInProgress\"\n                tabindex=\"1\">\n            </sites-list-item>\n\n            <li\n                v-if=\"!sites.length\"\n                class=\"empty-state\">\n                {{ $t('site.websiteNotFound') }}&hellip;\n            </li>\n        </ul>\n    </div>\n</template>\n\n<script>\nimport SitesListItem from './SitesListItem';\n\nexport default {\n    name: 'sites-list',\n    data: function() {\n        return {\n            filterValue: '',\n            siteDuplicateInProgress: false\n        };\n    },\n    computed: {\n        sites: function() {\n            return this.$store.getters.siteNames.filter(siteName => {\n                let displayName = this.$store.state.sites[siteName].displayName;\n\n                if(\n                    this.filterValue.trim() === '' ||\n                    displayName.indexOf(this.filterValue) > -1 ||\n                    displayName.toLowerCase().indexOf(this.filterValue) > -1\n                ) {\n                    return true;\n                }\n\n                return false;\n            });\n        }\n    },\n    mounted () {\n        this.$bus.$on('sites-list-filtered', data => {\n            this.filterValue = data;\n        });\n\n        this.$bus.$on('sites-list-duplicate-in-progress', (inProgress) => {\n            this.siteDuplicateInProgress = inProgress;\n        });\n    },\n    components: {\n        'sites-list-item': SitesListItem\n    },\n    beforeDestroy () {\n        this.$bus.$off('sites-list-filtered');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.sites {\n    &-list {\n        clear: both;\n        list-style-type: none;\n        margin: 0.5rem 0 0;\n        padding: 0 2rem;\n        text-align: center;\n\n        &-wrapper {\n            margin-top: 1rem;\n            max-height: calc(100vh - 24rem);\n            overflow-y: auto;\n        }\n    }\n}\n\n.empty-state {\n    color: var(--text-light-color);\n    line-height: 47px;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/SitesListItem.vue",
    "content": "<template>\n    <li\n        :class=\"{ \n            'single-site': true, \n            'is-duplicating': isDuplicating \n        }\"\n        @click=\"showWebsite(site)\"\n        @keydown=\"showWebsiteOnEnter($event, site)\">\n\n        <span class=\"single-site-icon\">\n            <icon\n                :name=\"siteLogoIcon\"\n                iconset=\"svg-map-site\"\n                customWidth=\"22\"\n                customHeight=\"22\" />\n        </span>\n\n        <strong class=\"single-site-name\" :title=\"displayName\">\n            <span>\n                {{ displayName }}\n            </span>\n\n            <small v-if=\"description\">\n                {{ description }}\n            </small>\n        </strong>\n\n        <div \n            :class=\"{\n                'single-site-actions': true, \n                'single-site-actions-disabled': duplicateInProgress \n            }\">\n            <a\n                href=\"#\"\n                :class=\"{ 'single-site-actions-btn': true, 'is-duplicating': isDuplicating }\"\n                :title=\"$t('site.duplicateWebsite')\"\n                tabindex=\"-1\"\n                @click.stop.prevent=\"askForClone\">\n                <icon\n                    name=\"duplicate\"\n                    size=\"xs\" />\n            </a>\n\n            <a\n                href=\"#\"\n                class=\"single-site-actions-btn delete\"\n                :title=\"$t('site.deleteWebsite')\"\n                tabindex=\"-1\"\n                @click.stop.prevent=\"askForRemove\">\n                <icon\n                    name=\"trash\"\n                    size=\"xs\" />\n            </a>\n        </div>\n    </li>\n</template>\n\n<script>\nexport default {\n    name: 'sites-list-item',\n    props: [\n        'site',\n        'duplicateInProgress'\n    ],\n    computed: {\n        description: function() {\n            return this.$store.state.sites[this.site].description;\n        },\n        displayName: function() {\n            return this.$store.state.sites[this.site].displayName;\n        },\n        siteLogoIcon: function() {\n            return this.$store.state.sites[this.site].logo.icon;\n        },\n        siteLogoColor: function() {\n            return this.$store.state.sites[this.site].logo.color;\n        }\n    },\n    data () {\n        return {\n            isDuplicating: false\n        };\n    },\n    methods: {\n        showWebsite (siteToDisplay) {\n            window.localStorage.setItem('publii-last-opened-website', siteToDisplay);\n            this.$bus.$emit('site-switched');\n            this.$bus.$emit('sites-popup-hide');\n            this.$router.push(`/site/${siteToDisplay}`);\n        },\n        showWebsiteOnEnter (event, siteToDisplay) {\n            if (event.key === 'Enter' && !event.isComposing && !document.body.classList.contains('has-popup-visible')) {\n                this.showWebsite(siteToDisplay);\n            }\n        },\n        askForRemove () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('site.deleteWebsiteConfirmMsg'),\n                okClick: this.removeWebsite.bind(this, this.site),\n                okLabel: this.$t('site.removeWebsite'),\n                isDanger: true\n            });\n        },\n        askForClone () {\n            this.isDuplicating = true;\n\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('site.specifyNameForWebsiteDuplicate'),\n                okClick: this.cloneWebsite,\n                cancelClick: () => this.isDuplicating = false,\n                hasInput: true,\n                okLabel: this.$t('site.cloneWebsite')\n            });\n        },\n        cloneWebsite (newName) {\n            if (newName.replace(/\\s/gmi, '').trim() === '') {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('site.websiteNameCantBeEmpty')\n                });\n\n                this.isDuplicating = false;\n                return;\n            }\n\n            if (!this.checkIfNewNameIsFree(newName)) {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('site.websiteNameAlreadyInUseMsg')\n                });\n\n                this.isDuplicating = false;\n                return;\n            }\n\n            this.$bus.$emit('sites-list-duplicate-in-progress', true);\n\n            mainProcessAPI.send('app-site-clone', {\n                catalogName: this.site,\n                siteName: newName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-cloned', (clonedWebsiteData) => {\n                this.$bus.$emit('sites-list-duplicate-in-progress', false);\n                this.isDuplicating = false;\n\n                this.$store.commit('cloneWebsite', {\n                    clonedWebsiteCatalog: this.site,\n                    newSiteName: clonedWebsiteData.siteName,\n                    newSiteCatalog: clonedWebsiteData.siteCatalog,\n                    newSiteConfig: clonedWebsiteData.siteConfig\n                });\n\n                this.$router.push(`/site/${clonedWebsiteData.siteCatalog}`);\n                window.localStorage.setItem('publii-last-opened-website', clonedWebsiteData.siteCatalog);\n                this.$bus.$emit('message-display', {\n                    message: this.$t('site.cloneWebsiteSuccessMsg') + clonedWebsiteData.siteCatalog,\n                    type: 'success',\n                    lifeTime: 3\n                });\n\n                this.$bus.$emit('sites-popup-hide');\n            });\n        },\n        removeWebsite (name) {\n            mainProcessAPI.send('app-site-delete', {\n                site: name\n            });\n\n            mainProcessAPI.receiveOnce('app-site-deleted', () => {\n                this.$store.commit('removeWebsite', name);\n                let sites = Object.keys(this.$store.state.sites);\n\n                if(sites.length > 0) {\n                    this.$router.push(`/site/${sites[0]}`);\n                    window.localStorage.setItem('publii-last-opened-website', sites[0]);\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('site.deleteWebsiteSuccessMsg') + sites[0],\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                    return;\n                }\n\n                this.$router.push(`/site/!`);\n                this.$bus.$emit('message-display', {\n                    message: this.$t('site.deleteWebsiteCSuccessMsg'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n            });\n        },\n        checkIfNewNameIsFree (newName) {\n            let keys = Object.keys(this.$store.state.sites);\n\n            for (let i = 0; i < keys.length; i++) {\n                if (newName === this.$store.state.sites[keys[i]].displayName) {\n                    return false;\n                }\n            }\n\n            return true;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/mixins.scss';\n\n/*\n * Single site\n */\n.single-site {\n    align-items: center;\n    background: var(--collection-bg);\n    border-bottom: 1px solid var(--border-light-color);\n    color: var(--link-primary-color-hover);\n    cursor: pointer;\n    display: flex;\n    margin: 0;\n    padding: 1.2rem 2rem;\n    position: relative;\n\n    &:focus {\n        background: var(--input-bg-light);\n    }\n\n    &:last-child {\n        border: none;\n    }\n\n    &-actions {\n        display: flex;\n        margin-left: auto;\n\n        &-disabled {\n            opacity: .75;\n            pointer-events: none;\n        }\n\n        &-btn {\n            align-items: center;\n            background: var(--bg-primary);\n            position: relative;\n            border-radius: 50%;\n            display: inline-flex;\n            height: 3rem;\n            justify-content: center;\n            margin: 0 2px;\n            opacity: 0;\n            position: relative;\n            text-align: center;\n            width: 3rem;\n\n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--headings-color);\n            }\n\n            &:hover {\n\n                & > svg {\n                   fill: var(--icon-tertiary-color);\n                   transform: scale(1);\n               }\n            }\n\n            svg {\n                fill: var(--icon-secondary-color);\n                height: 1.6rem;\n                pointer-events: none;\n                transform: scale(.9);\n                transition: var(--transition);\n                width: 1.6rem;\n            }\n\n            &.delete {\n\n                &:hover {\n\n                    & > svg {\n                       fill: var(--warning);\n                   }\n                }\n            }\n\n            &.is-duplicating {\n                &::after { \n                   animation: spin .9s infinite linear;\n                   border-top: 2px solid rgba(var(--color-primary-rgb), .2);\n                   border-right: 2px solid rgba(var(--color-primary-rgb), .2);\n                   border-bottom: 2px solid rgba(var(--color-primary-rgb), .2);\n                   border-left: 2px solid var(--color-primary);\n                   border-radius: 50%;\n                   content: \"\";\n                   display: block;\n                   height: 100%;\n                   width: 100%;\n                   @include centerXY(true, true);\n\n                    @at-root {\n                        @keyframes spin {\n                            100% {\n                                transform: translate(-50%, -50%) rotate(360deg);\n                            }\n                        }\n                    }\n                }\n\n                svg {\n                    opacity: 0;\n                }\n            }\n        }\n    }\n\n    &-icon {\n        align-items: center;\n        border-radius: 3px;\n        color: currentColor;\n        display: flex;\n        height: auto;\n        justify-content: center;\n        margin-right: 1.5rem;\n        position: relative;\n        width: 3.3rem;\n    }\n\n    &:hover,\n    &.is-duplicating {\n        background: var(--collection-bg-hover);\n        box-shadow: inset 3px 0 0 var(--color-primary);\n        color: var(--link-primary-color);\n        will-change: transform;\n\n        .single-site-actions-btn {\n            opacity: 1;\n        }\n    }\n\n    &-name {\n        display: block;    \n        font-weight: var(--font-weight-semibold);\n        line-height: 3.6rem;\n        margin: 0;\n        overflow: hidden;\n        padding: 0;\n        text-align: left;\n        text-overflow: ellipsis;\n        transition: var(--transition);\n        white-space: nowrap;\n        max-width: 82%;\n\n        span {\n            display: block;\n            line-height: 1.4;\n        }\n\n        small {\n            color: var(--gray-4);\n            display: block;\n            line-height: 1.4;\n            white-space: normal;\n        }\n    }\n}\n</style>\n\n\n"
  },
  {
    "path": "app/src/components/SitesPopup.vue",
    "content": "<template>\n    <div\n        :class=\"{ 'sites-popup': true, 'is-hidden': !isVisible }\"\n        @click=\"hide\">\n        <p-header @click.native.stop>\n            <p-button\n                type=\"clean back\"\n                slot=\"buttons\"\n                :onClick=\"hide\">\n                {{ $t('ui.goBack') }}\n            </p-button>\n\n            <p-button\n                icon=\"add-site-mono\"\n                type=\"primary icon\"\n                slot=\"buttons\"\n                :onClick=\"addNewWebsite\">\n                {{ $t('site.addNewWebsite') }}\n            </p-button>\n        </p-header>\n        <div class=\"sites-popup-container\" @click.stop>\n           <sites-search ref=\"search\" />\n           <sites-list />\n        </div>\n    </div>\n</template>\n\n<script>\nimport SitesSearch from './SitesSearch';\nimport SitesList from './SitesList';\n\nexport default {\n    name: 'sites-popup',\n    components: {\n        'sites-search': SitesSearch,\n        'sites-list': SitesList\n    },\n    data () {\n        return {\n            isVisible: false\n        }\n    },\n    mounted () {\n        this.$bus.$on('sites-popup-show', this.show);\n        this.$bus.$on('sites-popup-hide', this.hide);\n    },\n    methods: {\n        show (e) {\n            this.isVisible = true;\n\n            setTimeout(() => {\n                this.$refs['search'].$refs['search-input'].$refs['input'].value = '';\n                this.$refs['search'].$refs['search-input'].$refs['input'].focus();\n                this.$bus.$emit('sites-list-filtered', '');\n            }, 100);\n        },\n        hide () {\n            this.isVisible = false;\n        },\n        addNewWebsite (e) {\n            this.$router.push('/site/!');\n\n            setTimeout(() => {\n                this.isVisible = false;\n            }, 500);\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('sites-popup-show', this.show);\n        this.$bus.$off('sites-popup-hide', this.hide);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.sites-popup {\n    background: var(--bg-site);\n    height: 100vh;\n    left: 0;\n    opacity: 1;\n    padding: 4.4rem 3.2rem;\n    pointer-events: auto;\n    position: fixed;\n    top: 0;\n    transition: all .25s ease-out;\n    width: 100vw;\n    z-index: 99999;\n\n    &.is-hidden {\n        pointer-events: none;\n        opacity: 0;\n    }\n\n    &-container {\n        margin: auto;\n        max-width: $wrapper;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/SitesSearch.vue",
    "content": "<template>\n    <div class=\"search\">\n        <text-input\n            changeEventName=\"sites-list-filtered\"\n            v-model=\"value\"\n            icon=\"magnifier-small\"\n            properties=\"is-small\"\n            ref=\"search-input\"\n            :spellcheck=\"false\"\n            :placeholder=\"$t('ui.search')\"\n            tabindex=\"0\" />\n        <span\n            v-if=\"value !== ''\"\n            @click.stop=\"clear\">\n            &times;\n        </span>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'sites-search',\n    data () {\n        return {\n            value: ''\n        };\n    },\n    methods: {\n        clear () {\n            this.value = '';\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../scss/variables.scss';\n\n.sites-popup {\n    .search {\n        position: relative;\n\n        .input-wrapper {\n            padding: 1rem 2rem;\n\n            input {\n                background: var(--input-bg-lightest);\n                border-radius: 30px;\n                box-shadow: none!important;\n                padding: 1.5rem 4.4rem 1.5rem 6rem!important;\n            }\n\n            svg {\n                fill: var(--icon-primary-color)!important;\n                height: 1.7rem;\n                left: 3.4rem!important;\n                width: 1.7rem;\n            }\n        }\n\n        & > span {\n        border-radius: 50%;\n        color: var(--icon-secondary-color);\n        cursor: pointer;\n        font-size: 2.4rem;\n        font-weight: 300;\n        height: 3rem;\n        line-height: 1;\n        padding: 0;\n        position: absolute;\n        right: 3rem;\n        text-align: center;\n        transition: all .3s ease-out;\n        top: 50%;\n        transform: translate(0, -50%);\n        width: 3rem;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--icon-tertiary-color);\n        }\n\n        &:hover {\n            background: var(--input-border-color);\n          }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Splashscreen.vue",
    "content": "<template>\n    <div class=\"screen\">\n        <div class=\"splashscreen\">\n            <h1 class=\"title\">\n                <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 511 202\" xml:space=\"preserve\"><linearGradient id=\"a\" gradientUnits=\"userSpaceOnUse\" x1=\"15.452\" y1=\"-2.861\" x2=\"352.485\" y2=\"112.589\" gradientTransform=\"matrix(1 0 0 -1 -38.95 128.877)\"><stop offset=\".387\" style=\"stop-color:#0079f2\"/><stop offset=\".471\" style=\"stop-color:#0079f2\"/><stop offset=\".634\" style=\"stop-color:#0079f2\"/><stop offset=\".86\" style=\"stop-color:#1089ff\"/><stop offset=\"1\" style=\"stop-color:#1089ff\"/></linearGradient><path fill=\"url(#a)\" d=\"M6.708 69.285C3.103 71.991 1 61.767 1 58.76c.3-9.021 9.013-13.532 17.125-18.042 21.031-12.028 34.25-17.741 53.178-23.755.601-3.608.901-6.315 1.202-7.818 1.202-10.224 4.807-8.72 9.314-6.615 2.704 1.203 6.009.301 3.305 10.525 26.739-7.818 39.358-10.224 68.501-9.622 31.246.601 68.801 23.154 63.393 48.413-15.323 70.364-119.276 85.7-146.917 82.994 0 23.455 1.502 51.721 1.502 54.427.3 3.608 3.305 9.021-6.61 5.713-10.215-3.308-7.812-18.644-7.812-19.546 0-1.804 1.202-24.357 2.103-43.903-9.614-5.112-8.713-9.622-8.713-9.923 0-2.105 1.202-4.21 3.004-2.406 1.202 1.203 2.704 2.406 6.309 3.308.601-9.923.901-19.546.901-26.763.3-16.839 5.108-47.21 8.713-67.357-26.439 9.021-54.08 24.056-60.99 33.077-2.7 3.608.905 6.014-1.8 7.818zm198.293-20.373c6.009-30.972-34.551-35.182-51.075-35.784-29.143-1.203-47.77 3.308-72.407 11.427-3.605 16.238-6.009 47.21-9.614 73.371-.901 7.818-1.502 17.14-1.803 26.763 31.848 3.909 121.983-9.923 134.901-75.777h-.002z\"/><linearGradient id=\"b\" gradientUnits=\"userSpaceOnUse\" x1=\"46.556\" y1=\"-93.792\" x2=\"383.604\" y2=\"21.663\" gradientTransform=\"matrix(1 0 0 -1 -38.95 128.877)\"><stop offset=\".387\" style=\"stop-color:#0079f2\"/><stop offset=\".471\" style=\"stop-color:#0079f2\"/><stop offset=\".634\" style=\"stop-color:#0079f2\"/><stop offset=\".86\" style=\"stop-color:#1089ff\"/><stop offset=\"1\" style=\"stop-color:#1089ff\"/></linearGradient><path fill=\"url(#b)\" d=\"M210.408 192.874c-19.829.301-23.134-22.553-23.134-22.553s-7.511 23.154-22.533 21.951c-12.318-1.203-17.426-18.343-15.623-30.672s5.708-23.755 6.61-26.763c.3-1.804 1.502-6.615 8.112-4.511 5.408 1.504 2.404 6.315.601 12.329-3.004 10.224-9.013 37.889 2.404 39.392 7.812.902 16.524-19.846 18.327-27.063 1.202-4.811 2.103-9.322 3.305-14.133 1.202-5.112 3.906-6.916 7.511-6.014 6.61 1.804 3.605 4.811 2.404 9.622-.601 3.308-.601 10.224-1.202 21.049 0 9.021 2.404 17.741 10.215 18.644 19.829 2.105 30.645-22.853 37.555-35.483 2.704-4.811 7.511 2.105.901 13.832-6.01 10.527-19.83 30.073-35.453 30.373z\"/><linearGradient id=\"c\" gradientUnits=\"userSpaceOnUse\" x1=\"41.481\" y1=\"-79.345\" x2=\"378.689\" y2=\"36.166\" gradientTransform=\"matrix(1 0 0 -1 -38.95 128.877)\"><stop offset=\".387\" style=\"stop-color:#0079f2\"/><stop offset=\".471\" style=\"stop-color:#0079f2\"/><stop offset=\".634\" style=\"stop-color:#0079f2\"/><stop offset=\".86\" style=\"stop-color:#1089ff\"/><stop offset=\"1\" style=\"stop-color:#1089ff\"/></linearGradient><path fill=\"url(#c)\" d=\"M251.267 195.581c0 .301-6.61 8.119-11.116-3.007-4.807-11.727-.3-37.588-.3-37.588s6.61-40.294 13.52-113.665c5.108-10.525 16.224 0 14.421 6.615.3 0-15.022 55.931-17.726 117.575 0-.301 10.516-43.903 34.551-40.896 18.327 2.406 13.52 33.378 12.619 39.091-4.206 27.665-24.336 37.588-31.547 37.287s-14.422-5.112-14.422-5.412zm2.704-12.93c0 .301 2.103 8.72 10.816 8.42 21.932-1.203 35.452-57.434 16.524-56.231-17.727.9-27.34 47.811-27.34 47.811z\"/><linearGradient id=\"d\" gradientUnits=\"userSpaceOnUse\" x1=\"49.951\" y1=\"-104.032\" x2=\"387.105\" y2=\"11.459\" gradientTransform=\"matrix(1 0 0 -1 -38.95 128.877)\"><stop offset=\".387\" style=\"stop-color:#0079f2\"/><stop offset=\".471\" style=\"stop-color:#0079f2\"/><stop offset=\".634\" style=\"stop-color:#0079f2\"/><stop offset=\".86\" style=\"stop-color:#1089ff\"/><stop offset=\"1\" style=\"stop-color:#1089ff\"/></linearGradient><path fill=\"url(#d)\" d=\"M390.367 148.369c3.004-4.811 6.91 2.105.601 13.832-2.103 4.21-18.027 37.287-47.17 35.784-35.452-1.804-36.354-41.196-36.053-74.273.3-29.168 17.726-88.707 40.86-87.204 14.722.902 20.13 18.944 15.623 48.413-5.708 39.993-29.143 79.385-38.757 90.511 5.408 8.42 12.919 13.532 21.632 13.532 21.931.001 36.354-27.964 43.264-40.595zM348.305 47.634c-11.417-.902-27.641 36.084-31.547 76.679-.901 12.329.901 33.077 4.807 43.903 15.623-22.553 20.43-34.28 31.246-76.98 2.103-7.818 6.91-43-4.508-43.603l.002.001z\"/><linearGradient id=\"e\" gradientUnits=\"userSpaceOnUse\" x1=\"67.053\" y1=\"-153.738\" x2=\"404.116\" y2=\"-38.277\" gradientTransform=\"matrix(1 0 0 -1 -38.95 128.877)\"><stop offset=\".387\" style=\"stop-color:#0079f2\"/><stop offset=\".471\" style=\"stop-color:#0079f2\"/><stop offset=\".634\" style=\"stop-color:#0079f2\"/><stop offset=\".86\" style=\"stop-color:#1089ff\"/><stop offset=\"1\" style=\"stop-color:#1089ff\"/></linearGradient><path fill=\"url(#e)\" d=\"M408.99 197.986c-23.435.301-25.838-27.665-25.237-33.077.601-6.615 3.004-20.147 3.004-20.147 2.103-5.413 11.417-4.811 11.417.601 0 1.504-11.417 43.903 12.018 42.7 15.923-.601 30.946-27.665 37.856-40.294 2.704-5.112 6.91 1.804.901 13.832-2.102 4.21-18.626 36.385-39.959 36.385zm-9.013-75.476c-.654 4.311-4.677 7.275-8.984 6.62a.25.25 0 0 1-.029-.005c-5.708-.902-6.91-5.713-6.61-8.72 0-2.706 3.004-10.224 9.314-9.322 6.609.901 6.91 7.216 6.309 11.427z\"/><linearGradient id=\"f\" gradientUnits=\"userSpaceOnUse\" x1=\"73.136\" y1=\"-171.516\" x2=\"410.199\" y2=\"-56.056\" gradientTransform=\"matrix(1 0 0 -1 -38.95 128.877)\"><stop offset=\".387\" style=\"stop-color:#0079f2\"/><stop offset=\".471\" style=\"stop-color:#0079f2\"/><stop offset=\".634\" style=\"stop-color:#0079f2\"/><stop offset=\".86\" style=\"stop-color:#1089ff\"/><stop offset=\"1\" style=\"stop-color:#1089ff\"/></linearGradient><path fill=\"url(#f)\" d=\"M466.973 197.986c-23.435.301-25.838-27.665-25.237-33.077.601-6.615 3.004-20.147 3.004-20.147 2.103-5.413 11.417-4.811 11.417.601 0 1.504-11.417 43.903 12.018 42.7 15.923-.601 30.946-27.665 37.856-40.294 2.704-5.112 6.91 1.804.901 13.832-2.102 4.21-18.627 36.385-39.959 36.385zm-9.014-75.476c-.654 4.311-4.677 7.275-8.984 6.62a.25.25 0 0 1-.029-.005c-5.708-.902-6.91-5.713-6.61-8.72 0-2.706 3.004-10.224 9.314-9.322 6.61.901 6.911 7.216 6.309 11.427z\"/></svg>\n            </h1>\n\n            <small class=\"version\">\n                v.{{ version }} (build <span class=\"splashscreen-build\">{{ build }}</span>)\n            </small>\n\n            <div\n                v-if=\"appConfigLoaded && !licenseAccepted\"\n                class=\"license\">\n                <p>\n                    <span v-pure-html=\"$t('publii.publiiLicenseAgreementInfo')\"></span>\n                    <a\n                        href=\"#\"\n                        @click=\"showLicense\">\n                        {{ $t('publii.publiiLicenseAgreement') }}\n                    </a>\n                </p>\n\n                <p-button\n                    :onClick=\"acceptLicense\">\n                    {{ $t('publii.accept') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'splashscreen',\n    computed: {\n        appConfigLoaded () {\n            if (!this.$store || !this.$store.state || !this.$store.state.app || !this.$store.state.app.config) {\n                return false;\n            } \n\n            let keys = Object.keys(this.$store.state.app.config);\n\n            return keys.length > 0;\n        },\n        licenseAccepted: function() {\n            return this.$store.state.app.config.licenseAccepted;\n        },\n        version: function() {\n            return this.$store.state.app.versionInfo.version;\n        },\n        build: function() {\n            return this.$store.state.app.versionInfo.build;\n        }\n    },\n    beforeDestroy: function() {\n        mainProcessAPI.stopReceiveAll('app-license-accepted');\n    },\n    methods: {\n        showLicense: function(e) {\n            e.preventDefault();\n            mainProcessAPI.shellOpenExternal('https://getpublii.com/license.html');\n        },\n        acceptLicense: function() {\n            let self = this;\n\n            mainProcessAPI.send('app-license-accept', true);\n            mainProcessAPI.receiveOnce('app-license-accepted', function(data) {\n                self.$bus.$emit('license-accepted');\n            });\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n/*\n * Splashscreen component\n */\n.splashscreen {\n    left: 50%;\n    position: absolute;\n    text-align: center;\n    top: 50%;\n    transform: translateX(-50%) translateY(-50%);\n}\n\n.title {\n    height: 81px;\n    margin: 1.5rem auto;\n    width: 206px;\n\n    img {\n        display: block;\n        height: 81px;\n        width: 206px;\n    }\n}\n\n.version {\n    color: var(--gray-4);\n    font-size: 1.4rem;\n    font-weight: 400;\n}\n\n.license {\n    -webkit-app-region: no-drag;\n    color: var(--gray-4);\n    font-weight: 400;\n    margin-top: 5rem;\n}\n\n.accept {\n    -webkit-app-region: no-drag;\n    font-weight: bold;\n    height: 3.6rem;\n    line-height: 3.4rem;\n    margin-top: 1rem;\n    padding: 0 2rem;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/SyncPopup.vue",
    "content": "<template>\n    <div\n        v-if=\"isVisible\"\n        @click=\"maximizePopup\"\n        :class=\"{\n            'overlay': true,\n            'as-page': true,\n            'is-minimized': isMinimized\n        }\">\n        <div class=\"popup sync\">\n            <div\n                v-if=\"isInSync && noIssues && !isMinimized\"\n                class=\"sync-success\">\n\n                <h1>{{ $t('sync.yourWebsiteIsInSync') }}</h1>\n\n                <p\n                    v-if=\"isManual\"\n                    class=\"description\"\n                    v-pure-html=\"$t('sync.websiteFilesPreparedInfo')\">\n                </p>\n\n                <p\n                    v-if=\"isGithubPages\"\n                    class=\"description\">\n                    <strong>{{ $t('sync.note') }}</strong> \n                    {{ $t('sync.githubSyncedPart1') }}<br>\n                    {{ $t('sync.githubSyncedPart2') }}\n                </p>\n\n                <p\n                    v-if=\"isGitlabPages\"\n                    class=\"description\">\n                    <strong>{{ $t('sync.note') }}</strong> \n                    {{ $t('sync.gitlabSyncedPart1') }}<br>\n                    {{ $t('sync.gitlabSyncedPart2') }}\n                </p>\n\n                <p \n                    v-if=\"!(isGithubPages || isGitlabPages || isManual)\"\n                    class=\"description\">\n                    {{ $t('sync.allFilesUploadedPart1') }}<br>\n                    {{ $t('sync.allFilesUploadedPart2') }}\n                </p>\n\n                <div class=\"progress-bars-wrapper\">\n                    <progress-bar\n                        :cssClasses=\"{ 'sync-progress-bar': true, 'is-synced': true }\"\n                        color=\"green\"\n                        :progress=\"100\"\n                        :stopped=\"false\"\n                        message=\"\" />\n                </div>\n\n                <div class=\"buttons\">\n                    <p-button\n                        v-if=\"isManual\"\n                        type=\"primary medium green quarter-width\"\n                        :onClick=\"showFolder\">\n                        {{ $t('sync.getWebsiteFiles') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"!isManual\"\n                        type=\"primary medium green quarter-width\"\n                        :onClick=\"openWebsite\">\n                        {{ $t('sync.visitYourWebsite') }}\n                    </p-button>\n\n                    <p-button\n                        :onClick=\"close\"\n                        type=\"outline medium quarter-width\">\n                        {{ $t('ui.ok') }}\n                    </p-button>\n                </div>\n            </div>\n\n            <div\n                v-if=\"isInSync && !noIssues && !isMinimized\"\n                class=\"sync-success\">\n                <h1>{{ $t('sync.filesNotSyncedErrorText') }}</h1>\n\n                <p class=\"description\">\n                    {{ $t('sync.filesNotSyncedErrorMessage') }}\n                </p>\n\n                <div class=\"buttons\">\n                    <p-button\n                        type=\"primary medium  green quarter-width\"\n                        :onClick=\"openWebsite\">\n                        {{ $t('sync.visitYourWebsite') }}\n                    </p-button>\n\n                    <p-button\n                        :onClick=\"close\"\n                        type=\"outline medium quarter-width \">\n                        {{ $t('ui.ok') }}\n                    </p-button>\n                </div>\n            </div>\n\n            <div\n                v-if=\"properConfig && !isInSync && !isMinimized\"\n                class=\"sync-todo\">\n                <div class=\"heading\">\n                    <h1>{{ $t('sync.websiteSynchronization') }}</h1>\n\n                    <p\n                        class=\"description\"\n                        v-pure-html=\"$t('sync.websiteSynchronizationInfo')\">\n                    </p>\n                </div>\n\n                <div class=\"progress-bars-wrapper\">\n                    <progress-bar\n                        :cssClasses=\"{ 'rendering-progress-bar': true }\"\n                        :color=\"renderingProgressColor\"\n                        :progress=\"renderingProgress\"\n                        :stopped=\"renderingProgressIsStopped\"\n                        :message=\"messageFromRenderer\" />\n\n                    <progress-bar\n                        v-if=\"!isManual && !renderingInProgress && (uploadInProgress || syncInProgress || isInSync || uploadError)\"\n                        :cssClasses=\"{ 'sync-progress-bar': true, 'is-in-progress': (uploadInProgress || syncInProgress), 'is-synced': isInSync, 'is-error': uploadError }\"\n                        :color=\"uploadingProgressColor\"\n                        :progress=\"uploadingProgress\"\n                        :stopped=\"uploadingProgressIsStopped\"\n                        :message=\"messageFromUploader\" />\n                </div>\n\n                <div class=\"buttons\">\n                    <p-button\n                        :onClick=\"startSync\"\n                        :type=\"syncInProgress ? 'disabled medium quarter-width': 'medium quarter-width'\"\n                        :disabled=\"syncInProgress\">\n                        {{ $t('sync.syncYourWebsite') }}\n                    </p-button>\n\n                    <p-button\n                        :onClick=\"cancelSync\"\n                        type=\"outline medium quarter-width\">\n                        {{ $t('ui.cancel') }}\n                    </p-button>\n                </div>\n            </div>\n\n            <div\n                v-if=\"noDomainConfig\"\n                class=\"sync-issues-to-resolve\">\n\n                <h1>{{ $t('sync.domainNameNotSetErrorText') }}</h1>\n                <p\n                    class=\"description\"\n                    v-pure-html=\"$t('sync.domainNameNotSetErrorMessage')\">\n                </p>\n\n                <div class=\"buttons\">\n                    <p-button\n                        type=\"medium  quarter-width\"\n                        :onClick=\"goToServerSettings\">\n                        {{ $t('sync.goToSettings') }}\n                    </p-button>\n\n                    <p-button\n                        :onClick=\"close\"\n                        type=\"outline medium  quarter-width\">\n                        {{ $t('ui.cancel') }}\n                    </p-button>\n                </div>\n            </div>\n\n            <div\n                v-if=\"!noDomainConfig && noServerConfig\"\n                class=\"sync-issues-to-resolve\">\n\n                <h1>{{ $t('sync.destinationServerNotConfiguredErrorText') }}</h1>\n                <p\n                    class=\"description\"\n                    v-pure-html=\"$t('sync.destinationServerNotConfiguredErrorMessage')\">\n                </p>\n\n                <div class=\"buttons\">\n                    <p-button\n                        type=\"medium  quarter-width\"\n                        :onClick=\"goToServerSettings\">\n                        {{ $t('sync.goToSettings') }}\n                    </p-button>\n\n                    <p-button\n                        type=\"outline medium  quarter-width\"\n                        :onClick=\"close\">\n                        {{ $t('ui.cancel') }}\n                    </p-button>\n                </div>\n            </div>\n\n            <!-- Minimized states -->\n            <div\n                v-if=\"properConfig && !isInSync && !isManual && isMinimized && !renderingInProgress\"\n                class=\"minimized-sync-in-progress\">\n                <progress-bar\n                    v-if=\"(uploadInProgress || syncInProgress || isInSync || uploadError)\"\n                    :cssClasses=\"{ 'sync-progress-bar': true, 'is-in-progress': (uploadInProgress || syncInProgress), 'is-synced': isInSync, 'is-error': uploadError }\"\n                    :color=\"uploadingProgressColor\"\n                    :progress=\"uploadingProgress\"\n                    :stopped=\"uploadingProgressIsStopped\"\n                    :message=\"messageFromUploader\" />\n            </div>\n\n            <!-- <div\n                v-if=\"properConfig && !isInSync && !isManual && isMinimized && !renderingInProgress && uploadError\"\n                class=\"minimized-sync-error\">\n                Error during sync\n            </div>\n\n            <div\n                v-if=\"isInSync && !noIssues && isMinimized\"\n                class=\"minimized-sync-issues\">\n                Issues during sync\n            </div> -->\n        </div>\n\n        <a\n            v-if=\"!isMinimized && uploadInProgress && !isManual\"\n            href=\"#\"\n            class=\"minimize-popup\"\n            @click.prevent.stop=\"minimizePopup\">\n            <icon\n                size=\"s\"\n                name=\"minimize\"/>\n            <span>{{ $t('ui.minimize') }}</span>\n        </a>\n    </div>\n</template>\n\n<script>\nimport Utils from './../helpers/utils.js';\n\nexport default {\n    name: 'sync-popup',\n    watch: {\n        'isVisible': function (newValue) {\n            if (newValue === false) {\n                this.$store.commit('setSyncStatus', false);\n            }\n        }\n    },\n    data () {\n        return {\n            isVisible: false,\n            isMinimized: false,\n            renderingInProgress: false,\n            uploadInProgress: false,\n            messageFromRenderer: 'true',\n            renderingProgress: 0,\n            renderingProgressColor: 'blue',\n            renderingProgressIsStopped: false,\n            messageFromUploader: '',\n            uploadingProgress: 0,\n            uploadingProgressColor: 'blue',\n            uploadingProgressIsStopped: false,\n            syncInProgress: false,\n            isInSync: false,\n            manualFilePath: '',\n            uploadError: false,\n            noIssues: true\n        };\n    },\n    computed: {\n        isGithubPages: function() {\n            let deploymentConfig = this.$store.state.currentSite.config.deployment;\n            return deploymentConfig && deploymentConfig.protocol === 'github-pages';\n        },\n        isGitlabPages: function() {\n            let deploymentConfig = this.$store.state.currentSite.config.deployment;\n            return deploymentConfig && deploymentConfig.protocol === 'gitlab-pages';\n        },\n        isManual: function() {\n            let deploymentConfig = this.$store.state.currentSite.config.deployment;\n            return deploymentConfig && deploymentConfig.protocol === 'manual';\n        },\n        properConfig: function() {\n            return !this.noServerConfig && !this.noDomainConfig;\n        },\n        noDomainConfig: function() {\n            let domainConfig = this.$store.state.currentSite.config.domain;\n\n            if(domainConfig == false || domainConfig === 'http://' || domainConfig === 'https://') {\n                return true;\n            }\n\n            return false;\n        },\n        noServerConfig: function() {\n            let deploymentConfig = this.$store.state.currentSite.config.deployment;\n\n            if (deploymentConfig) {\n                if (deploymentConfig.protocol === 's3' && this.checkS3Config(deploymentConfig)) {\n                    return false;\n                }\n\n                if (deploymentConfig.protocol === 'git' && this.checkGitConfig(deploymentConfig)) {\n                    return false;\n                }\n\n                if (deploymentConfig.protocol === 'github-pages' && this.checkGithubConfig(deploymentConfig)) {\n                    return false;\n                }\n\n                if (deploymentConfig.protocol === 'gitlab-pages' && this.checkGitlabConfig(deploymentConfig)) {\n                    return false;\n                }\n\n                if (deploymentConfig.protocol === 'netlify' && this.checkNetlify(deploymentConfig)) {\n                    return false;\n                }\n\n                if (deploymentConfig.protocol === 'google-cloud' && this.checkGoogleCloud(deploymentConfig)) {\n                    return false;\n                }\n\n                if (deploymentConfig.protocol === 'manual' && deploymentConfig.manual.output !== '') {\n                    return false;\n                }\n            }\n\n            if (\n                !deploymentConfig ||\n                deploymentConfig.server === '' ||\n                deploymentConfig.username === '' ||\n                deploymentConfig.protocol === '' ||\n                deploymentConfig.port === ''\n            ) {\n                return true;\n            }\n\n            return false;\n        }\n    },\n    mounted: function() {\n        this.$bus.$on('sync-popup-display', (config) => {\n            if (this.isVisible) {\n                return;\n            }\n\n            this.isVisible = true;\n            this.isMinimized = false;\n            this.messageFromRenderer = '';\n            this.renderingProgress = 0;\n            this.renderingProgressColor = 'blue';\n            this.renderingProgressIsStopped = false;\n            this.messageFromUploader = '';\n            this.uploadInProgress = false;\n            this.uploadingProgress = 0;\n            this.uploadingProgressColor = 'blue';\n            this.uploadingProgressIsStopped = false;\n            this.syncInProgress = false;\n            this.isInSync = false;\n            this.manualFilePath = '';\n            this.uploadError = false;\n            this.noIssues = true;\n        });\n\n        this.$bus.$on('sync-popup-maximize', this.maximizePopup);\n\n        mainProcessAPI.receive('app-rendering-progress', this.renderingProgressUpdate);\n\n        // Load the rendering error results (if exists)\n        mainProcessAPI.receive('app-deploy-render-error', (data) => {\n            this.$store.commit('setSidebarStatus', 'not-prepared');\n\n            if (data.message[0].message.translation) {\n                data.message[0].message = this.$t(data.message[0].message.translation);\n            }\n\n            if (data.message[0].desc.translation) {\n                data.message[0].desc = this.$t(data.message[0].desc.translation);\n            }\n\n            let errorsHTML = Utils.generateErrorLog(data);\n            let errorsText = Utils.generateErrorLog(data, true);\n\n            this.renderingProgress = 100;\n            this.renderingProgressColor = 'red';\n            this.renderingProgressIsStopped = true;\n            this.messageFromRenderer = this.$t('rendering.renderingErrorText');\n\n            setTimeout(() => {\n                this.close();\n                this.$bus.$emit('error-popup-display', {\n                    errors: errorsHTML,\n                    text: errorsText\n                });\n            }, 500);\n        });\n\n        // Load the rendering results\n        mainProcessAPI.receive('app-deploy-rendered', (data) => {\n            if (data.status) {\n                this.$store.commit('setSidebarStatus', 'prepared');\n                this.startUpload();\n            } else {\n                this.$store.commit('setSidebarStatus', 'not-prepared');\n            }\n        });\n\n        mainProcessAPI.receive('app-connection-in-progress', () => {\n            this.messageFromUploader = this.$t('sync.connectingToServer');\n        });\n\n        mainProcessAPI.stopReceive('app-connection-error', this.showError);\n        mainProcessAPI.receive('app-connection-error', this.showError);\n\n        mainProcessAPI.receive('app-connection-success', () => {\n            this.messageFromUploader = this.$t('sync.connectedToServer');\n        });\n\n        mainProcessAPI.receive('app-uploading-progress', this.uploadingProgressUpdate);\n        mainProcessAPI.receive('no-remote-files', this.askForContinueSync);\n        document.body.addEventListener('keydown', this.onDocumentKeyDown);\n    },\n    methods: {\n        goToServerSettings: function() {\n            let siteName = this.$store.state.currentSite.config.name;\n            this.$router.push('/site/' + siteName + '/settings/server/');\n            this.close();\n        },\n        openWebsite: function() {\n            let urlToOpen = Utils.getValidUrl(this.$store.state.currentSite.config.domain);\n\n            if (urlToOpen) {\n                mainProcessAPI.shellOpenExternal(urlToOpen);\n            } else {\n                alert(this.$t('sync.websiteLinkInvalidMsg'));\n            }\n\n            this.close();\n        },\n        showFolder: function() {\n            let folderPath = this.manualFilePath;\n            mainProcessAPI.shellShowItemInFolder(folderPath);\n            this.close();\n        },\n        close: function() {\n            this.isVisible = false;\n        },\n        startSync () {\n            if(!this.themeIsSelected) {\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('rendering.selectThemeBeforeCreatingPreviewMsg'),\n                    okLabel: this.$t('sync.goToSettings'),\n                    okClick: () => {\n                        let siteName = this.$route.params.name;\n                        this.$route.push('/site/' + siteName + '/settings/');\n                    }\n                });\n\n                return;\n            }\n\n            this.syncInProgress = true;\n            this.uploadInProgress = false;\n            this.renderingInProgress = false;\n\n            if (!this.uploadError) {\n                this.messageFromRenderer = '';\n                this.renderingProgress = 0;\n                this.renderingProgressColor = 'blue';\n                this.renderingProgressIsStopped = false;\n                this.messageFromUploader = '';\n                this.uploadingProgress = 0;\n                this.uploadingProgressColor = 'blue';\n                this.uploadingProgressIsStopped = false;\n                this.startRendering();\n            } else {\n                this.uploadError = false;\n                this.startUpload();\n                this.messageFromUploader = '';\n                this.uploadingProgress = 0;\n                this.uploadingProgressColor = 'blue';\n                this.uploadingProgressIsStopped = false;\n            }\n        },\n        askForContinueSync () {\n            this.$bus.$emit('confirm-display', {\n                hasInput: false,\n                message: this.$t('settings.continueSyncNoRemoteFiles'),\n                okClick: this.continueSync,\n                okLabel: this.$t('settings.continueSync'),\n                cancelLabel: this.$t('ui.cancel'),\n                cancelClick: this.cancelSync\n            });\n        },\n        continueSync () {\n            mainProcessAPI.send('app-deploy-continue');\n        },\n        cancelSync () {\n            if (this.renderingInProgress) {\n                mainProcessAPI.send('app-deploy-render-abort', {\n                    'site': this.$store.state.currentSite.config.name\n                });\n            }\n\n            if (this.syncInProgress) {\n                mainProcessAPI.send('app-deploy-abort', {\n                    'site': this.$store.state.currentSite.config.name\n                });\n            }\n\n            if (this.syncInProgress || this.renderingInProgress) {\n                mainProcessAPI.receiveOnce('app-deploy-aborted', () => {\n                    this.$store.commit('setSidebarStatus', 'not-synced');\n                    this.close();\n                });\n            } else {\n                this.close();\n            }\n        },\n        startRendering: function() {\n            this.renderingInProgress = true;\n            this.$store.commit('setSidebarStatus', 'preparing');\n            this.messageFromRenderer = '';\n            this.renderingProgress = 0;\n            this.renderingProgressColor = 'blue';\n            this.renderingProgressIsStopped = false;\n\n            mainProcessAPI.send('app-deploy-render', {\n                'site': this.$store.state.currentSite.config.name,\n                'theme': this.$store.state.currentSite.config.theme\n            });\n        },\n        renderingProgressUpdate: function(data) {\n            if (this.renderingProgress > data.progress) {\n                return;\n            }\n\n            if (data.message.translation) {\n                data.message = this.$t(data.message.translation);\n            }\n\n            this.messageFromRenderer = data.message + ' - ' + data.progress + '%';\n            this.renderingProgress = data.progress;\n\n            if(this.renderingProgress === 100) {\n                this.renderingProgressColor = 'green';\n                this.renderingProgressIsStopped = true;\n\n                if(this.isManual) {\n                    this.messageFromRenderer = this.$t('file.preparingFilesInOutputDir');\n                } else {\n                    this.messageFromRenderer = '';\n                }\n            }\n        },\n        uploadingProgressUpdate: function(data) {\n            if(this.uploadingProgress > data.progress) {\n                return;\n            }\n\n            this.uploadingProgress = data.progress;\n            this.messageFromUploader = this.$t('sync.uploadingWebsite');\n\n            if(data.operations) {\n                this.messageFromUploader = `${this.$t('sync.uploadingWebsite')} (${data.operations[0]} ${this.$t('ui.of')} ${data.operations[1]} ${this.$t('sync.operationsDone')})`;\n            }\n\n            if(data.message) {\n                if (data.message.translation) {\n                    data.message = this.$t(data.message.translation);\n                }\n                this.messageFromUploader = data.message;\n            }\n        },\n        showError: function(data) {\n            this.messageFromUploader = this.$t('sync.connectionToServerErrorText');\n            this.uploadError = true;\n            this.uploadingProgressColor = 'red';\n            this.uploadingProgress = 100;\n            this.uploadingProgressIsStopped = true;\n            this.syncInProgress = false;\n            this.$store.commit('setSidebarStatus', 'prepared');\n\n            if(data && data.additionalMessage ) {\n                if (data.additionalMessage.translation) {\n                    if (data.additionalMessage.translationVars) {\n                        data.additionalMessage = this.$t(data.additionalMessage.translation, data.message.translationVars);\n                    } else {\n                        data.additionalMessage = this.$t(data.additionalMessage.translation);\n                    }\n                }\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('sync.connectionToServerErrorAdditionalMessage') + data.additionalMessage\n                });\n            } else {\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('sync.connectionToServerErrorMessage')\n                });\n            }\n        },\n        startUpload: function() {\n            this.renderingInProgress = false;\n            this.uploadInProgress = true;\n            this.$store.commit('setSyncStatus', true);\n            this.$store.commit('setSidebarStatus', 'syncing');\n\n            if(\n                this.$store.state.currentSite.config.deployment.askforpassword &&\n                ['ftp', 'sftp', 'ftp+tls'].indexOf(this.$store.state.currentSite.config.deployment.protocol) > -1\n            ) {\n                let serverName = this.$store.state.currentSite.config.deployment.server;\n\n                this.$bus.$emit('confirm-display', {\n                    message: this.$t('sync.provideFTPPasswordForServer') + serverName,\n                    hasInput: true,\n                    inputIsPassword: true,\n                    okClick: (result) => {\n                        if(!result || result.trim() === '') {\n                            this.$bus.$emit('alert-display', {\n                                message: this.$t('sync.syncFTPNoPasswordMsg')\n                            });\n\n                            this.uploadingProgress = 0;\n                            this.messageFromUploader = '';\n                            this.syncInProgress = false;\n                            this.uploadError = true;\n                            return;\n                        }\n\n                        this.handleUploadEvents(result);\n                    },\n                    cancelClick: () => {\n                        this.$bus.$emit('alert-display', {\n                            message: this.$t('sync.syncFTPNoPasswordMsg')\n                        });\n\n                        this.uploadingProgress = 0;\n                        this.messageFromUploader = '';\n                        this.syncInProgress = false;\n                        this.uploadError = true;\n                    }\n                });\n            } else {\n                this.handleUploadEvents(false);\n            }\n        },\n        handleUploadEvents(askedPassword) {\n            // Send request for uploading the site\n            mainProcessAPI.send('app-deploy-upload', {\n                'site': this.$store.state.currentSite.config.name,\n                'password': askedPassword\n            });\n\n            // Load the deployment results\n            mainProcessAPI.receiveOnce('app-deploy-uploaded', (data) => {\n                if(data.type && data.path && this.isManual) {\n                    this.isInSync = true;\n                    this.manualFilePath = data.path;\n                }\n\n                this.uploadingProgress = 100;\n                this.uploadingProgressIsStopped = true;\n                this.syncInProgress = false;\n                this.uploadInProgress = false;\n\n                if (typeof data.issues !== 'undefined' && data.issues) {\n                    this.noIssues = false;\n                    this.uploadingProgressColor = 'orange';\n                    this.messageFromUploader = '';\n                } else {\n                    this.uploadingProgressColor = 'green';\n                    this.messageFromUploader = this.$t('sync.yourWebsiteIsInSync');\n                }\n\n                if (data.status) {\n                    mainProcessAPI.send('app-sync-is-done', {\n                        'site': this.$store.state.currentSite.config.name\n                    });\n\n                    this.$store.commit('setSyncDate', Date.now());\n                }\n            });\n\n            mainProcessAPI.receiveOnce('app-sync-is-done-saved', () => {\n                this.$store.commit('setSidebarStatus', 'synced');\n                this.isInSync = true;\n\n                if (this.isInSync && this.noIssues && this.isMinimized) {\n                    this.isVisible = false;\n                }\n            });\n        },\n        checkS3Config: function(deploymentConfig) {\n            if (deploymentConfig.s3 && deploymentConfig.s3.customProvider) {\n                return  deploymentConfig.s3.endpoint !== '' &&\n                        deploymentConfig.s3.id !== '' &&\n                        deploymentConfig.s3.key !== '' &&\n                        deploymentConfig.s3.bucket !== '';\n            } else if (deploymentConfig.s3) {\n                return  deploymentConfig.s3.region !== '' &&\n                        deploymentConfig.s3.id !== '' &&\n                        deploymentConfig.s3.key !== '' &&\n                        deploymentConfig.s3.bucket !== '';\n            }\n\n            return false;\n        },\n        checkGitConfig: function(deploymentConfig) {\n            if (\n                deploymentConfig.git &&\n                deploymentConfig.git.url !== '' &&\n                deploymentConfig.git.branch !== '' &&\n                deploymentConfig.git.user !== '' &&\n                deploymentConfig.git.password !== '' &&\n                deploymentConfig.git.commitAuthor !== '' &&\n                deploymentConfig.git.commitMessage !== ''\n            ) {\n                return true;\n            }\n\n            return false;\n        },\n        checkGithubConfig: function(deploymentConfig) {\n            if(\n                deploymentConfig.github &&\n                deploymentConfig.github.user !== '' &&\n                deploymentConfig.github.repo !== '' &&\n                deploymentConfig.github.branch !== ''\n            ) {\n                return true;\n            }\n\n            return false;\n        },\n        checkGitlabConfig: function(deploymentConfig) {\n            if(\n                deploymentConfig.gitlab &&\n                deploymentConfig.gitlab.server !== '' &&\n                deploymentConfig.gitlab.repo !== '' &&\n                deploymentConfig.gitlab.branch !== '' &&\n                deploymentConfig.gitlab.token !== ''\n            ) {\n                return true;\n            }\n\n            return false;\n        },\n        checkNetlify: function(deploymentConfig) {\n            if(\n                deploymentConfig.netlify &&\n                deploymentConfig.netlify.id !== '' &&\n                deploymentConfig.netlify.token !== ''\n            ) {\n                return true;\n            }\n\n            return false;\n        },\n        checkGoogleCloud: function(deploymentConfig) {\n            if(\n                deploymentConfig.google &&\n                deploymentConfig.google.projectId !== '' &&\n                deploymentConfig.google.key !== '' &&\n                deploymentConfig.google.bucket !== ''\n            ) {\n                return true;\n            }\n\n            return false;\n        },\n        themeIsSelected() {\n            return !(!this.$store.state.currentSite.config.theme || this.$store.state.currentSite.config.theme === '');\n        },\n        onDocumentKeyDown (e) {\n            if (e.code === 'Enter' && !event.isComposing && this.isVisible && !this.syncInProgress) {\n                this.onEnterKey();\n            }\n        },\n        onEnterKey () {\n            if (this.isInSync && this.noIssues && this.isManual) {\n                this.showFolder();\n            } else if (this.properConfig && !this.isInSync) {\n                this.startSync();\n            } else if (this.noDomainConfig || (!this.noDomainConfig && this.noServerConfig)) {\n                this.goToServerSettings();\n            }\n        },\n        maximizePopup () {\n            if (this.isMinimized) {\n                this.isMinimized = false;\n            }\n        },\n        minimizePopup () {\n            if (!this.isMinimized) {\n                this.isMinimized = true;\n            }\n        }\n    },\n    beforeDestroy: function() {\n        this.$bus.$off('sync-popup-display');\n        this.$bus.$off('sync-popup-maximize', this.maximizePopup);\n        mainProcessAPI.stopReceiveAll('app-preview-render-error');\n        mainProcessAPI.stopReceiveAll('app-rendering-progress');\n        mainProcessAPI.stopReceiveAll('app-connection-error', this.showError);\n        document.body.removeEventListener('keydown', this.onDocumentKeyDown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/popup-common.scss';\n\n.popup {\n    background: none;\n    max-width: $wrapper;\n    overflow: visible;\n    width: 100%;\n\n    .description {\n        color: var(--text-light-color);\n        font-size: 1.4rem;\n        line-height: 1.4;\n        margin: auto;\n        padding: 0 1rem;\n        text-align: center;\n\n        &.alert {\n            background: var(--highlighted);\n            border-radius: .2em;\n            color: var(--text-primary-color);\n            font-size: 1.4rem;\n            margin-bottom: 3rem;\n            padding: 1rem 2rem;\n            text-align: left;\n        }\n\n        strong {\n            color: var(--text-primary-color);\n        }\n    }\n}\n\n.sync {\n    svg {\n        display: block;\n        float: none;\n        margin: 2.6rem auto;\n    }\n}\n\n.message {\n    color: var(--text-primary-color);\n    font-weight: 400;\n    margin: 0;\n    padding: 4rem;\n    position: relative;\n    text-align: left;\n\n    &.text-centered {\n        text-align: center;\n    }\n}\n\n.buttons {\n    display: flex;\n    justify-content: center;\n    margin-top: 4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n}\n\n.progress-bars-wrapper {\n    margin-top: 7rem;\n    margin-bottom: -4rem;\n    position: relative;\n\n    .progress-wrapper + .progress-wrapper {\n        left: 0;\n        position: absolute;\n        top: 0;\n        width: 100%;\n        z-index: 10;\n    }\n}\n\n.minimize-popup {\n   align-items: center;\n   color: var(--icon-secondary-color);\n   display: flex;\n   position: absolute;\n   right: 3.2rem;\n   will-change: transform;\n\n   &:active,\n   &:focus,\n   &:hover {\n      color: var(--icon-tertiary-color);\n\n      svg {\n           transform: scale(.9);\n      }\n   }\n\n   & > svg {\n      transition: var(--transition);\n   }\n\n   & > span {\n      margin-left: .6rem;\n   }\n}\n\n.overlay {\n    transition: 0.5s cubic-bezier(.17,.67,.13,1.05) all;\n\n    &.is-minimized {\n        animation: minimized-popup .25s linear .25s forwards;\n        border-radius: 10px;\n        box-shadow: 0 0 160px rgba(0, 0, 0, .2);\n        cursor: pointer;\n        bottom: 56px;\n        left: 0;\n        opacity: 0;\n        overflow: visible;\n        padding: 0;\n        top: auto!important;\n        transform: translateY(10%) scale(.8);\n        z-index: 1;\n\n        & .progress-message, .minimized-sync-error {\n            color: white !important;\n        }\n\n        .popup {\n            animation: minimized-content .25s cubic-bezier(.17,.67,.13,1.05) .25s forwards;\n            margin-top: 2.6rem;\n            position: initial;\n            transform: none;\n            visibility: hidden;\n\n            .minimized-sync-in-progress {\n                max-width: 20rem;\n            }\n        }\n\n        @keyframes minimized-popup {\n\n            50% {opacity: 0;\n                transform: translateY(10%);\n            }\n            99% {\n                transform: translateY(10%);\n            }\n\n            100% {\n                box-shadow: none;\n                border-radius: 3px;\n                background: none;\n                height: 50px;\n                width: 240px;\n                opacity: 1;\n                transform: translate(40px, 0);\n            }\n        }\n\n        @keyframes minimized-content {\n            99% {visibility: hidden;}\n            100% {visibility: visible;}\n        }\n    }\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/TagForm.vue",
    "content": "<template>\n    <div\n        :key=\"'tag-view-' + tagData.id\"\n        :data-animate=\"formAnimation ? 'true' : 'false'\"\n        class=\"options-sidebar-container\">\n        <div class=\"options-sidebar\">\n            <h2>\n                <template v-if=\"tagData.id\">{{ $t('tag.editTag') }}</template>\n                <template v-if=\"!tagData.id\">{{ $t('tag.addNewTag') }}</template>\n            </h2>\n\n            <span\n                class=\"options-sidebar-close\"\n                name=\"sidebar-close\"\n                @click.prevent=\"close()\">\n                &times;\n            </span>\n\n             <div\n                v-if=\"!currentThemeHasSupportForTagPages\"\n                slot=\"note\"\n                class=\"msg msg-small msg-icon msg-alert\">\n                <icon name=\"warning\" size=\"m\" />\n                <p>{{ $t('settings.themeDoesNotSupportTagPages') }}</p>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'basic' }\"\n                    @click=\"openItem('basic')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-status\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.basicInformation') }}</span>\n                </div>\n\n                <div\n                    class=\"tag-settings\"\n                    style=\"max-height: none;\"\n                    ref=\"basic-content-wrapper\">\n                    <div\n                        class=\"tag-settings-content\"\n                        ref=\"basic-content\">\n                        <label :class=\"{ 'is-invalid': errors.indexOf('name') > -1 }\">\n                            <span>{{ $t('ui.name') }}:</span>\n                            <input\n                                v-model=\"tagData.name\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                @keyup=\"cleanError('name')\"\n                                type=\"text\">\n                        </label>\n\n                        <label>\n                            <span>{{ $t('ui.description') }}:</span>\n                            <text-area\n                                v-model=\"tagData.description\"\n                                :wysiwyg=\"true\"\n                                :miniEditorMode=\"true\"\n                                :simplifiedToolbar=\"true\"\n                                :rows=\"4\"></text-area>\n                        </label>\n\n                        <label class=\"tag-settings-hidden\">\n                            <switcher\n                                :key=\"'is-hidden-tag-' + tagData.id\"\n                                id=\"is-hidden\"\n                                v-model=\"tagData.additionalData.isHidden\"\n                                @click.native=\"toggleHiddenStatus\" />\n                            <icon\n                                    :title=\"$t('post.hidePost')\"\n                                    class=\"switcher-item-icon-helper\"\n                                    name=\"hidden-post\"\n                                    size=\"xs\"\n                                    strokeColor=\"color-6\" />\n                            <span :title=\"$t('tag.tagWillNotAppearInGeneratedTagLists')\">\n                                {{ $t('tag.hideTag') }}\n                            </span>\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'image' }\"\n                    @click=\"openItem('image')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-image\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.featuredImage') }}</span>\n                </div>\n\n                <div\n                    class=\"tag-settings\"\n                    ref=\"image-content-wrapper\">\n                    <div\n                        class=\"tag-settings-content\"\n                        ref=\"image-content\">\n                        <div\n                            v-if=\"!currentThemeHasSupportForTagImages\"\n                            slot=\"note\"\n                            class=\"msg msg-small msg-icon msg-alert\"><icon name=\"warning\" size=\"m\" />\n                            <p>{{ $t('tag.noSupportFoFeaturedImagesForTags') }}</p>\n                        </div>\n                        <label>\n                            <image-upload\n                                slot=\"field\"\n                                type=\"small\"\n                                id=\"featured-image\"\n                                :item-id=\"tagData.id\"\n                                ref=\"tag-featured-image\"\n                                imageType=\"tagImages\"\n                                :onRemove=\"() => { hasFeaturedImage = false }\"\n                                :onAdd=\"() => { hasFeaturedImage = true } \"\n                                v-model=\"tagData.additionalData.featuredImage\" />\n\n\n                            <div\n                                v-if=\"hasFeaturedImage\"\n                                class=\"image-uploader-settings-form\">\n                                <label>{{ $t('ui.alternativeText') }}\n                                    <text-input\n                                        ref=\"featured-image-alt\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        v-model=\"tagData.additionalData.featuredImageAlt\" />\n                                </label>\n\n                                <label>{{ $t('ui.caption') }}\n                                    <text-input\n                                        ref=\"featured-image-caption\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        v-model=\"tagData.additionalData.featuredImageCaption\" />\n                                </label>\n\n                                <label>{{ $t('ui.credits') }}\n                                    <text-input\n                                        ref=\"featured-image-credits\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        v-model=\"tagData.additionalData.featuredImageCredits\" />\n                                </label>\n                            </div>\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'seo' }\"\n                    @click=\"openItem('seo')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-seo\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.seo') }}</span>\n                </div>\n\n                <div\n                    class=\"tag-settings\"\n                    ref=\"seo-content-wrapper\">\n                    <div\n                        class=\"tag-settings-content\"\n                        ref=\"seo-content\">\n                        <label :class=\"{ 'is-invalid': errors.indexOf('slug') > -1 }\">\n                            <span>{{ $t('ui.slug') }}:</span>\n                            <div class=\"options-sidebar-item-slug\">\n                                <input\n                                    v-model=\"tagData.slug\"\n                                    @keyup=\"cleanError('slug')\"\n                                    spellcheck=\"false\"\n                                    type=\"text\">\n                                <p-button \n                                    :onClick=\"updateSlug\" \n                                    :title=\"$t('ui.updateSlug')\"\n                                    icon=\"refresh\"\n                                    type=\"secondary icon\">\n                                </p-button>\n                            </div>\n                        </label>\n\n                        <label class=\"with-char-counter\">\n                            <span>{{ $t('ui.pageTitle') }}:</span>\n                            <text-input\n                                v-model=\"tagData.additionalData.metaTitle\"\n                                type=\"text\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :placeholder=\"$t('ui.leaveBlankToUseDefaultPageTitle')\"\n                                :charCounter=\"true\"\n                                :preferredCount=\"70\" />\n                        </label>\n\n                        <label class=\"with-char-counter\">\n                            <span>{{ $t('ui.metaDescription') }}:</span>\n                            <text-area\n                                v-model=\"tagData.additionalData.metaDescription\"\n                                :placeholder=\"$t('ui.leaveBlankToUseDefaultPageTitle')\"\n                                :charCounter=\"true\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :preferredCount=\"160\"></text-area>\n                        </label>\n\n                        <label>\n                            {{ $t('ui.metaRobotsIndex') }}:\n                            <dropdown\n                                v-if=\"!tagData.additionalData.canonicalUrl\"\n                                id=\"tag-meta-robots\"\n                                v-model=\"tagData.additionalData.metaRobots\"\n                                :items=\"metaRobotsOptions\">\n                            </dropdown>\n                            <div v-else>\n                                <small>{{ $t('ui.ifCanonicalUrlIsSetMetaRobotsTagIsIgnored') }}</small>\n                            </div>\n                        </label>\n\n                        <label>\n                            {{ $t('ui.canonicalURL') }}:\n                            <input\n                                type=\"text\"\n                                v-model=\"tagData.additionalData.canonicalUrl\"\n                                spellcheck=\"false\"\n                                :placeholder=\"$t('tag.leaveBlankToUseDefaultTagPageURL')\" />\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'other' }\"\n                    @click=\"openItem('other')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-options\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.otherOptions') }}</span>\n                </div>\n\n                <div\n                    class=\"tag-settings\"\n                    ref=\"other-content-wrapper\">\n                    <div\n                        class=\"tag-settings-content\"\n                        ref=\"other-content\">\n                        <label>\n                            <span>{{ $t('ui.customTemplate') }}:</span>\n                            <dropdown\n                                v-if=\"currentThemeHasTagTemplates\"\n                                ref=\"template\"\n                                id=\"template\"\n                                v-model=\"tagData.additionalData.template\"\n                                :items=\"tagTemplates\"></dropdown>\n\n                            <text-input\n                                v-if=\"!currentThemeHasTagTemplates\"\n                                slot=\"field\"\n                                id=\"template\"\n                                :spellcheck=\"false\"\n                                :placeholder=\"$t('ui.notAvailableInYourTheme')\"\n                                :disabled=\"true\"\n                                :readonly=\"true\" />\n                        </label>\n\n                        <template v-if=\"dataSet\">\n                            <template v-for=\"(field, index) of tagViewThemeSettings\">\n                                <separator\n                                    v-if=\"displayField(field) && field.type === 'separator'\"\n                                    :label=\"field.label\"\n                                    :is-line=\"true\"\n                                    :key=\"'tag-view-field-' + index\"\n                                    :note=\"field.note\" />\n\n                                <label\n                                    v-if=\"displayField(field) && field.type !== 'separator'\"\n                                    :key=\"'tag-view-field-' + index\">\n                                    {{ field.label }}\n\n                                    <dropdown\n                                        v-if=\"!field.type || field.type === 'select'\"\n                                        :id=\"field.name + '-select'\"\n                                        class=\"tag-view-settings\"\n                                        v-model=\"tagData.additionalData.viewConfig[field.name]\"\n                                        :items=\"generateItems(field.options)\">\n                                        <option slot=\"first-choice\" value=\"\">{{ $t('settings.useGlobalConfiguration') }}</option>\n                                    </dropdown>\n\n                                    <text-input\n                                        v-if=\"field.type === 'text' || field.type === 'number'\"\n                                        :type=\"field.type\"\n                                        class=\"tag-view-settings\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        :placeholder=\"fieldPlaceholder(field)\"\n                                        v-model=\"tagData.additionalData.viewConfig[field.name]\" />\n\n                                    <text-area\n                                        v-if=\"field.type === 'textarea'\"\n                                        class=\"tag-view-settings\"\n                                        :placeholder=\"fieldPlaceholder(field)\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        v-model=\"tagData.additionalData.viewConfig[field.name]\" />\n\n                                    <color-picker\n                                        v-if=\"field.type === 'colorpicker'\"\n                                        class=\"tag-view-settings\"\n                                        v-model=\"tagData.additionalData.viewConfig[field.name]\"\n                                        :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\">\n                                    </color-picker>\n\n                                    <image-upload\n                                        v-if=\"field.type === 'image'\"\n                                        class=\"tag-view-settings\"\n                                        v-model=\"tagData.additionalData.viewConfig[field.name]\"\n                                        :item-id=\"tagData.id\"\n                                        imageType=\"tagImages\" />\n\n                                    <small\n                                        v-if=\"field.note\"\n                                        class=\"note\">\n                                        {{ field.note }}\n                                    </small>\n                                </label>\n                            </template>\n                        </template>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-buttons\">\n                <p-button\n                    type=\"secondary\"\n                    @click.native=\"save(false)\">\n                    <template v-if=\"tagData.id\">{{ $t('ui.saveChanges') }}</template>\n                    <template v-if=\"!tagData.id\">{{ $t('tag.addNewTag') }}</template>\n                </p-button>\n\n                <p-button\n                    :disabled=\"!tagData.id || currentTagIsHidden || !currentThemeHasSupportForTagPages\"\n                    type=\"primary\"\n                    class=\"options-sidebar-preview-button\"\n                    @click.native=\"saveAndPreview\">\n                    {{ $t('ui.saveAndPreview') }}\n                    <span>\n                        <icon\n                            size=\"s\"\n                            name=\"quick-preview\"/>\n                    </span>\n                </p-button>\n\n                <p-button\n                    @click.native=\"close\"\n                    type=\"outline\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n\n        </div>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'tag-form-sidebar',\n    props: [\n        'formAnimation'\n    ],\n    data: function() {\n        return {\n            errors: [],\n            hasFeaturedImage: false,\n            openedItem: 'basic',\n            currentTagIsHidden: false,\n            dataSet: false,\n            tagData: {\n                id: 0,\n                name: '',\n                slug: '',\n                description: '',\n                additionalData: {\n                    featuredImage: '',\n                    featuredImageAlt: '',\n                    featuredImageCaption: '',\n                    featuredImageCredits: '',\n                    isHidden: false,\n                    metaTitle: '',\n                    metaDescription: '',\n                    metaRobots: '',\n                    canonicalUrl: '',\n                    template: ''\n                }\n            }\n        };\n    },\n    computed: {\n        currentThemeHasSupportForTagImages () {\n            return this.$store.state.currentSite.themeSettings.supportedFeatures && this.$store.state.currentSite.themeSettings.supportedFeatures.tagImages;\n        },\n        currentThemeHasSupportForTagPages () {\n            if (this.$store.state.currentSite.themeSettings.supportedFeatures && this.$store.state.currentSite.themeSettings.supportedFeatures.tagPages === false) {\n                return false;\n            }\n\n            return this.$store.state.currentSite.themeSettings.renderer.createTagPages;\n        },\n        tagTemplates: function() {\n            return this.$store.getters.tagTemplates;\n        },\n        currentThemeHasTagTemplates: function() {\n            return Object.keys(this.tagTemplates).length > 1;\n        },\n        metaRobotsOptions () {\n            return {\n                '': this.$t('settings.useSiteGlobalSettings'),\n                'index, follow': this.$t('ui.indexFollow'),\n                'index, nofollow': this.$t('ui.indexNofollow'),\n                'index, follow, noarchive': this.$t('ui.indexFollowNoArchive'),\n                'index, nofollow, noarchive': this.$t('ui.indexNofollowNoArchive'),\n                'noindex, follow': this.$t('ui.noindexFollow'),\n                'noindex, nofollow': this.$t('ui.noindexNofollow')\n            };\n        },\n        tagViewThemeSettings () {\n            return this.$store.state.currentSite.themeSettings.tagConfig;\n        }\n    },\n    mounted () {\n        this.$bus.$on('show-tag-item-editor', (params, openedItem = false) => {\n            try {\n                if (typeof params.additionalData === 'string' && params.additionalData) {\n                    params.additionalData = JSON.parse(params.additionalData);\n                } else if (typeof params.additionalData !== 'object') {\n                    params.additionalData = {};\n                }\n            } catch (e) {\n                console.warn(this.$t('tag.tagDataParsingErrorMessage') + params.id);\n                params.additionalData = {};\n            }\n\n            this.openedItem = 'basic';\n            this.errors = [];\n            this.tagData.id = params.id || 0;\n            this.tagData.name = params.name || '';\n            this.tagData.slug = params.slug || '';\n            this.tagData.description = params.description || '';\n            this.tagData.additionalData = {};\n\n            if (typeof params.additionalData.viewConfig === 'object') {\n                this.tagData.additionalData.viewConfig = params.additionalData.viewConfig;\n            } else {\n                this.tagData.additionalData.viewConfig = {};\n            }\n\n            this.tagData.additionalData.featuredImage = params.additionalData.featuredImage || '';\n            this.tagData.additionalData.featuredImageAlt = params.additionalData.featuredImageAlt || '';\n            this.tagData.additionalData.featuredImageCaption = params.additionalData.featuredImageCaption || '';\n            this.tagData.additionalData.featuredImageCredits = params.additionalData.featuredImageCredits || '';\n            this.tagData.additionalData.isHidden = params.additionalData.isHidden || false;\n            this.tagData.additionalData.metaTitle = params.additionalData.metaTitle || '';\n            this.tagData.additionalData.metaDescription = params.additionalData.metaDescription || '';\n            this.tagData.additionalData.metaRobots = params.additionalData.metaRobots || '';\n            this.tagData.additionalData.canonicalUrl = params.additionalData.canonicalUrl || '';\n            this.tagData.additionalData.template = params.additionalData.template || '';\n            this.currentTagIsHidden = !!this.tagData.additionalData.isHidden;\n\n            if (this.tagData.additionalData && this.tagData.additionalData.featuredImage) {\n                this.hasFeaturedImage = true;\n            }\n\n            Vue.nextTick(() => {\n                this.dataSet = true;\n            });\n        });\n    },\n    methods: {\n        async save (showPreview = false) {\n            if (this.tagData.slug.trim() === '' && this.tagData.name.trim() !== '') {\n                this.tagData.slug = await mainProcessAPI.invoke('app-main-process-create-slug', this.tagData.name);\n            } else {\n                this.tagData.slug = await mainProcessAPI.invoke('app-main-process-create-slug', this.tagData.slug);\n            }\n\n            if (!this.validate()) {\n                return;\n            }\n\n            this.$bus.$emit('view-settings-before-save');\n\n            setTimeout(() => {\n                let tagData = Object.assign({}, this.tagData);\n                tagData.site = this.$store.state.currentSite.config.name;\n                tagData.imageConfigFields = this.tagViewThemeSettings.filter(field => field.type === 'image').map(field => field.name);\n                this.saveData(tagData, showPreview);\n            }, 500);\n        },\n        async saveAndPreview () {\n            await this.save(true);\n        },\n        validate () {\n            this.errors = [];\n\n            if (this.tagData.name.trim() === '') {\n                this.errors.push('name');\n            }\n\n            if (this.tagData.slug.trim() === '') {\n                this.errors.push('slug');\n            }\n\n            if (this.errors.length) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('ui.pleaseFillAllRequiredFields'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n            }\n\n            return this.errors.length === 0;\n        },\n        cleanError (field) {\n            let pos = this.errors.indexOf(field);\n\n            if (pos !== -1) {\n                this.errors.splice(pos, 1);\n            }\n        },\n        saveData(tagData, showPreview = false) {\n            mainProcessAPI.send('app-tag-save', tagData);\n\n            mainProcessAPI.receiveOnce('app-tag-saved', (data) => {\n                if (data.status !== false) {\n                    if(this.tagData.id === 0) {\n                        let newlyAddedTag = JSON.parse(JSON.stringify(data.tags.filter(tag => tag.id === data.tagID)[0]));\n                        this.showMessage(data.message);\n\n                        Vue.nextTick(() => {\n                            this.$bus.$emit('show-tag-item-editor', newlyAddedTag, this.openedItem);\n                        });\n                    } else {\n                        if (!showPreview) {\n                            this.$bus.$emit('hide-tag-item-editor');\n                            this.dataSet = false;\n                        }\n\n                        this.showMessage('success');\n\n                        if (showPreview) {\n                            this.$bus.$emit('rendering-popup-display', {\n                                tagOnly: true,\n                                itemID: this.tagData.id\n                            });\n                        }\n                    }\n\n                    this.$store.commit('setTags', data.tags);\n                    this.$bus.$emit('tags-list-updated');\n                    return;\n                }\n\n                this.showMessage(data.message);\n            });\n        },\n        close() {\n            this.$bus.$emit('hide-tag-item-editor');\n            this.dataSet = false;\n            \n            mainProcessAPI.send('app-tag-cancel', {\n                site: this.$store.state.currentSite.config.name,\n                id: this.tagData.id,\n                imageConfigFields: this.tagViewThemeSettings.filter(field => field.type === 'image').map(field => field.name)\n            });\n        },\n        showMessage(message) {\n            let messageConfig = {\n                message: this.$t('tag.newTagHasBeenCreated'),\n                type: 'success',\n                lifeTime: 3\n            };\n\n            if (this.tagData.id > 0) {\n                messageConfig.message = this.$t('tag.tagHasBeenEdited');\n            }\n\n            if(message !== 'success' && message !== 'tag-added') {\n                messageConfig.type = 'warning';\n            }\n\n            if(message === 'tag-duplicate-name') {\n                this.errors.push('name');\n                messageConfig.message = this.$t('tag.tagNameInUseErrorMessage');\n            } else if(message === 'tag-duplicate-slug') {\n                this.errors.push('slug');\n                messageConfig.message = this.$t('tag.tagNameSimilarInUseErrorMessage');\n            } else if(message === 'tag-empty-name') {\n                this.errors.push('name');\n                messageConfig.message = this.$t('tag.tagNameCannotBeEmptyErrorMessage');\n            } else if(message === 'tag-restricted-slug') {\n                this.errors.push('slug');\n                messageConfig.message = this.$t('tag.tagNameNotAllowedErrorMessage');\n            }\n\n            this.$bus.$emit('message-display', messageConfig);\n        },\n        openItem (itemName) {\n            if (this.openedItem === itemName) {\n                this.closeItem();\n                return;\n            }\n\n            this.closeItem();\n            this.openedItem = itemName;\n            let contentWrapper = this.$refs[this.openedItem + '-content-wrapper'];\n            let content = this.$refs[this.openedItem + '-content'];\n            contentWrapper.style.maxHeight = content.clientHeight + \"px\";\n\n            setTimeout(function() {\n                contentWrapper.style.maxHeight = 'none';\n            }, 300);\n        },\n        closeItem () {\n            if (this.openedItem === '') {\n                return;\n            }\n\n            let contentWrapper = this.$refs[this.openedItem + '-content-wrapper'];\n            let content = this.$refs[this.openedItem + '-content'];\n            this.openedItem = '';\n\n            if (content.classList.contains('post-editor-settings-content-tags')) {\n                contentWrapper.style.overflow = 'hidden';\n            }\n\n            contentWrapper.style.maxHeight = content.clientHeight + \"px\";\n\n            setTimeout(function () {\n                contentWrapper.style.maxHeight = 0;\n            }, 50);\n        },\n        toggleHiddenStatus () {\n            this.currentTagIsHidden = this.tagData.additionalData.isHidden;\n        },\n        displayField (field) {\n            if (!this.dataSet) {\n                return false;\n            }\n\n            if (!field.tagTemplates) {\n                return true;\n            }\n\n            if (field.tagTemplates.indexOf('!') === 0) {\n                return !(field.tagTemplates.replace('!', '').split(',').indexOf(this.tagData.additionalData.template) > -1);\n            }\n\n            return field.tagTemplates.split(',').indexOf(this.tagData.additionalData.template) > -1;\n        },\n        generateItems (arrayToConvert) {\n            let options = {};\n\n            for (let i = 0; i < arrayToConvert.length; i++) {\n                options[arrayToConvert[i].value] = arrayToConvert[i].label;\n            }\n\n            return options;\n        },\n        fieldPlaceholder (field) {\n            if (field.placeholder || field.placeholder === '') {\n                return field.placeholder;\n            }\n\n\t\t\treturn this.$t('theme.leaveBlankToUseDefault');\n        },\n        async updateSlug () {\n            if (this.tagData.name.trim() !== '') {\n                this.tagData.slug = await mainProcessAPI.invoke('app-main-process-create-slug', this.tagData.name);\n            }\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('show-tag-item-editor');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/options-sidebar.scss';\n@import '../scss/notifications.scss';\n\n.tag-settings {\n    max-height: 0;\n    overflow: hidden;\n    transition: max-height .25s ease-out;\n\n    &-content {\n        padding: 0 0 1rem;\n\n        .image-uploader {\n            margin-top: 0;\n        }\n\n        .msg {\n            margin: 0 0 2rem;\n        }\n    }\n\n    &-hidden {\n        font-size: 1.4rem !important;\n        font-weight: var(--font-weight-normal) !important;\n        line-height: 1.8 !important;\n        margin-top: 3rem;\n\n        svg {\n            margin: 0 .5rem 0 0;\n            position: relative;\n            top: .2rem;\n        }\n    }\n}\n\n.note {\n    position: relative;\n    z-index: 1;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Tags.vue",
    "content": "<template>\n    <section :class=\"{ 'content': true, 'tags-list-view': true, 'no-scroll': editorVisible }\">\n        <p-header\n            v-if=\"!showEmptyState\"\n            :title=\"$t('ui.tags')\">\n            <header-search\n                slot=\"search\"\n                :placeholder=\"$t('tag.filterOrSearchTags')\"\n                onChangeEventName=\"tags-filter-value-changed\" />\n\n            <p-button\n                :onClick=\"addTag\"\n                slot=\"buttons\"\n                type=\"primary icon\"\n                icon=\"add-site-mono\">\n                {{ $t('tag.addNewTag') }}\n            </p-button>\n        </p-header>\n\n        <collection\n            v-if=\"!emptySearchResults && hasTags\"\n            :itemsCount=\"4\">\n            <collection-header slot=\"header\">\n                <collection-cell>\n                    <checkbox\n                        value=\"all\"\n                        :checked=\"anyCheckboxIsSelected\"\n                        :onClick=\"toggleAllCheckboxes.bind(this, false)\" />\n                </collection-cell>\n\n                <collection-cell>\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('name')\">\n                        <template v-if=\"orderBy === 'name'\">\n                            <strong>{{ $t('ui.name') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.name') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'name' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'name' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell\n                    justifyContent=\"center\"\n                    textAlign=\"center\"\n                    min-width=\"100px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('postsCounter')\">\n                        <template v-if=\"orderBy === 'postsCounter'\">\n                            <strong>{{ $t('ui.posts') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.posts') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'postsCounter' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'postsCounter' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <collection-cell min-width=\"40px\">\n                    <span\n                        class=\"col-sortable-title\"\n                        @click=\"ordering('id')\">\n                        <template v-if=\"orderBy === 'id'\">\n                            <strong>{{ $t('ui.id') }}</strong>\n                        </template>\n                        <template v-else>{{ $t('ui.id') }}</template>\n\n                        <span class=\"order-descending\" v-if=\"orderBy === 'id' && order === 'ASC'\"></span>\n                        <span class=\"order-ascending\" v-if=\"orderBy === 'id' && order === 'DESC'\"></span>\n                    </span>\n                </collection-cell>\n\n                <div\n                    v-if=\"anyCheckboxIsSelected\"\n                    class=\"tools\">\n                    <p-button\n                        icon=\"trash\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkDelete\">\n                        {{ $t('ui.delete') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"selectedTagsAreNotHidden\"\n                        icon=\"hidden-post\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkHide\">\n                        {{ $t('ui.hide') }}\n                    </p-button>\n\n                    <p-button\n                        v-if=\"selectedTagsAreHidden\"\n                        icon=\"unhidden-post\"\n                        type=\"small light icon\"\n                        :onClick=\"bulkUnhide\">\n                        {{ $t('ui.unhide') }}\n                    </p-button>\n                </div>\n            </collection-header>\n\n            <collection-row\n                v-for=\"(item, index) in items\"\n                slot=\"content\"\n                :key=\"'collection-row-' + index\">\n                <collection-cell>\n                    <checkbox\n                        :value=\"item.id\"\n                        :checked=\"isChecked(item.id)\"\n                        :onClick=\"toggleSelection\"\n                        :key=\"'collection-row-checkbox-' + index\" />\n                </collection-cell>\n\n                <collection-cell type=\"titles\">\n                    <h2 class=\"title\">\n                        <a\n                            href=\"#\"\n                            @click.prevent.stop=\"editTag(item)\">\n                            {{ item.name }}\n\n                            <icon\n                                v-if=\"item.isHidden\"\n                                size=\"xs\"\n                                name=\"hidden-post\"\n                                strokeColor=\"color-7\"\n                                :title=\"$t('tag.thisTagIsHidden')\" />\n                        </a>\n                    </h2>\n\n                    <div\n                        v-if=\"showTagSlugs\"\n                        class=\"tag-slug\">\n                        {{ $t('tag.url') }}: /{{ item.slug }}<template v-if=\"!$store.state.currentSite.config.advanced.urls.cleanUrls\">.html</template>\n                    </div>\n                </collection-cell>\n\n                <collection-cell\n                    justifyContent=\"center\"\n                    textAlign=\"center\">\n                    <a\n                        @click.prevent.stop=\"showPostsConnectedWithTag(item.name)\"\n                        href=\"#\">\n                        {{ item.postsCounter }}\n                    </a>\n                </collection-cell>\n\n                <collection-cell>\n                    {{ item.id }}\n                </collection-cell>\n            </collection-row>\n        </collection>\n\n        <empty-state\n            v-if=\"emptySearchResults\"\n            :description=\"$t('tag.noTagsMatchingYourCriteria')\"></empty-state>\n\n        <empty-state\n            v-if=\"showEmptyState\"\n            imageName=\"tags.svg\"\n            imageWidth=\"344\"\n            imageHeight=\"286\"\n            :title=\"$t('tag.noTagsAvailable')\"\n            :description=\"$t('tag.createFirstTag')\">\n            <p-button\n                slot=\"button\"\n                icon=\"add-site-mono\"\n                type=\"icon\"\n                :onClick=\"addTag\">\n                {{ $t('tag.addNewTag') }}\n            </p-button>\n        </empty-state>\n\n        <transition>\n            <tag-form\n                v-if=\"editorVisible\"\n                :form-animation=\"formAnimation\" />\n        </transition>\n    </section>\n</template>\n\n<script>\nimport TagForm from './TagForm';\nimport CollectionCheckboxes from './mixins/CollectionCheckboxes.js';\n\nexport default {\n    name: 'tags',\n    mixins: [\n        CollectionCheckboxes\n    ],\n    components: {\n        'tag-form': TagForm\n    },\n    data: function() {\n        return {\n            formAnimation: false,\n            editorVisible: false,\n            filterValue: '',\n            orderBy: this.$store.state.ordering.tags.orderBy,\n            order: this.$store.state.ordering.tags.order,\n            selectedItems: []\n        };\n    },\n    watch: {\n        editorVisible (newValue, oldValue) {\n            if (newValue !== oldValue) {\n                this.formAnimation = true;\n\n                setTimeout(() => {\n                    this.formAnimation = false;\n                }, 500);\n            }\n        }\n    },\n    computed: {\n        items: function() {\n            return this.$store.getters.siteTags(this.filterValue, this.orderBy, this.order).map(item => {\n                if (item.additionalData) {\n                    if (typeof item.additionalData === 'string') {\n                        item.isHidden = item.additionalData.indexOf('\"isHidden\":true') > -1;\n                    } else {\n                        item.isHidden = item.additionalData.isHidden;\n                    }\n                }\n\n                return item;\n            });\n        },\n        hasTags: function() {\n            return this.$store.state.currentSite.tags && !!this.$store.state.currentSite.tags.length;\n        },\n        showEmptyState: function() {\n            return !this.hasTags;\n        },\n        emptySearchResults: function() {\n            return this.filterValue !== '' && !this.items.length;\n        },\n        selectedTagsAreNotHidden () {\n            let selectedTags = this.items.filter(item => this.selectedItems.indexOf(item.id) > -1);\n\n            if (!selectedTags.length) {\n                return false;\n            }\n\n            let notHiddenTags = selectedTags.filter(item => !item.isHidden);\n            return !!notHiddenTags.length;\n        },\n        selectedTagsAreHidden () {\n            let selectedTags = this.items.filter(item => this.selectedItems.indexOf(item.id) > -1);\n\n            if (!selectedTags.length) {\n                return false;\n            }\n\n            let hiddenTags = selectedTags.filter(item => item.isHidden);\n            return !!hiddenTags.length;\n        },\n        showTagSlugs () {\n            return this.$store.state.app.config.showPostSlugs;\n        }\n    },\n    beforeMount () {\n        mainProcessAPI.send('app-tags-load', {\n            \"site\": this.$store.state.currentSite.config.name\n        });\n\n        mainProcessAPI.receiveOnce('app-tags-loaded', (data) => {\n            this.$store.commit('setTags', data.tags);\n            this.$store.commit('setPostsTags', data.postsTags);\n        });\n    },\n    mounted: function() {\n        this.orderBy = this.$store.state.ordering.tags.orderBy;\n        this.order = this.$store.state.ordering.tags.order;\n\n        this.$bus.$on('tags-filter-value-changed', (newValue) => {\n            this.filterValue = newValue.trim().toLowerCase();\n        });\n\n        this.$bus.$on('hide-tag-item-editor', () => {\n            if (document.querySelector('.tags-list-view .item.is-edited')) {\n                document.querySelector('.tags-list-view .item.is-edited').classList.remove('is-edited');\n            }\n            \n            this.editorVisible = false;\n        });\n\n        this.$bus.$on('site-switched', () => {\n            setTimeout(() => {\n                this.saveOrdering(this.$store.state.ordering.tags.orderBy, this.$store.state.ordering.tags.order);\n            }, 500);\n        });\n\n        this.$bus.$on('app-settings-saved', newSettings => {\n            if (this.orderBy + ' ' + this.order !== newSettings.tagsOrdering) {\n                let order = newSettings.tagsOrdering.split(' ');\n                this.saveOrdering(order[0], order[1]);\n            }\n        });\n    },\n    methods: {\n        addTag () {\n            this.$bus.$on('show-tag-item-editor', () => ({\n                id: 0,\n                name: '',\n                slug: '',\n                description: '',\n                additionalData: {\n                    isHidden: false,\n                    metaTitle: '',\n                    metaDescription: '',\n                    template: ''\n                }\n            }));\n\n            this.editorVisible = true;\n        },\n        editTag (item) {\n            if (document.querySelector('.tags-list-view .item.is-edited')) {\n                document.querySelector('.tags-list-view .item.is-edited').classList.remove('is-edited');\n            }\n\n            document.querySelector('.tags-list-view .item input[value=\"' + parseInt(item.id, 10) + '\"]').parentNode.parentNode.classList.add('is-edited');\n            this.editorVisible = true;\n\n            setTimeout(() => {\n                this.$bus.$emit('show-tag-item-editor', item);\n            }, 100);\n        },\n        bulkDelete () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('tag.removeTagMessage'),\n                isDanger: true,\n                okClick: this.deleteSelected\n            });\n        },\n        bulkHide () {\n            this.changeStateForSelected('hidden');\n        },\n        bulkUnhide () {\n            this.changeStateForSelected('hidden', true);\n        },\n        changeStateForSelected (status, inverse = false) {\n            let itemsToChange = this.getSelectedItems();\n\n            this.$store.commit('changeTagsVisibility', {\n                tagsIDs: itemsToChange,\n                status: status,\n                inverse: inverse\n            });\n\n            mainProcessAPI.send('app-tags-status-change', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToChange,\n                \"status\": status,\n                \"inverse\": inverse\n            });\n\n            mainProcessAPI.receiveOnce('app-tags-status-changed', () => {\n                this.selectedItems = [];\n                this.$forceUpdate();\n            });\n\n            this.$bus.$emit('message-display', {\n                message: this.$t('tag.tagStatusChangeSuccessMessage'),\n                type: 'success',\n                lifeTime: 3\n            });\n        },\n        deleteSelected () {\n            let itemsToRemove = this.getSelectedItems();\n\n            mainProcessAPI.send('app-tag-delete', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"ids\": itemsToRemove\n            });\n\n            mainProcessAPI.receiveOnce('app-tag-deleted', () => {\n                this.$store.commit('removeTags', itemsToRemove);\n                this.selectedItems = [];\n\n                this.$bus.$emit('message-display', {\n                    message: this.$t('tag.removeTagSuccessMessage'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n            });\n        },\n        showPostsConnectedWithTag (name) {\n            let siteName = this.$store.state.currentSite.config.name;\n            localStorage.setItem('publii-posts-search-value', 'tag:' + name);\n            this.$router.push('/site/' + siteName + '/posts');\n        },\n        ordering (field) {\n            if (field !== this.orderBy) {\n                this.orderBy = field;\n                this.order = 'DESC';\n            } else {\n                if (this.order === 'DESC') {\n                    this.order = 'ASC';\n                } else {\n                    this.order = 'DESC';\n                }\n            }\n\n            this.saveOrdering(this.orderBy, this.order);\n        },\n        saveOrdering (orderBy, order) {\n            this.orderBy = orderBy;\n            this.order = order;\n\n            this.$store.commit('setOrdering', {\n                type: 'tags',\n                orderBy: this.orderBy,\n                order: this.order\n            });\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('tags-filter-value-changed');\n        this.$bus.$off('hide-tag-item-editor');\n        this.$bus.$off('show-tag-item-editor');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.content {\n    overflow-x: hidden!important;\n\n    &.no-scroll {\n        overflow: hidden!important;\n    }\n\n    .header {\n        .col {\n             align-items: center;\n             display: flex;\n\n             .col-sortable-title {\n                 cursor: pointer;\n             }\n        }\n    }\n\n    .order-ascending,\n    .order-descending {\n        margin-left: 3px;\n        position: relative;\n        &:after {\n             border-top: solid 5px var(--icon-secondary-color);\n             border-left: solid 5px transparent;\n             border-right: solid 5px transparent;\n             content: \"\";\n             cursor: pointer;\n             display: inline-block;\n             height: 4px;\n             left: 0;\n             line-height: 1.1;\n             opacity: 1;\n             padding: 0;\n             position: relative;\n             text-align: center;\n             top: 50%;\n             transform: translateY(-50%);\n             width: 8px;\n        }\n    }\n\n    .order-descending {\n        &:after {\n            border-top-color: transparent;\n            border-bottom: solid 5px var(--icon-secondary-color);\n        }\n    }\n\n    .tag-slug {\n        color: var(--gray-4);\n        font-size: 11px;\n        margin-top: .2rem;\n    }\n}\n\n.col > a > .icon {\n    margin-left: 10px;\n    position: relative;\n    top: 2px;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/ThemeSettings.vue",
    "content": "<template>\n    <section\n        class=\"content\"\n        ref=\"content\">\n        <div\n            class=\"theme-settings\"\n            v-if=\"siteHasTheme\">\n            <p-header :title=\"$t('theme.themeSettings')\">\n                <p-button\n                    @click.native=\"save\"\n                    slot=\"buttons\"\n                    type=\"secondary\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n\n                <btn-dropdown\n                    slot=\"buttons\"\n                    buttonColor=\"green\"\n                    :items=\"dropdownItems\"\n                    :disabled=\"!siteHasTheme || buttonsLocked\"\n                    localStorageKey=\"publii-preview-mode\"\n                    :previewIcon=\"true\"\n                    defaultValue=\"full-site-preview\" />\n            </p-header>\n\n            <fields-group :title=\"$t('settings.basicSettings')\">\n                <field\n                    id=\"name\"\n                    :label=\"$t('theme.postsPerPage')\">\n                    <text-input\n                        slot=\"field\"\n                        ref=\"name\"\n                        type=\"number\"\n                        min=\"-1\"\n                        step=\"1\"\n                        v-model=\"basic.postsPerPage\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('theme.postsPerPageInfo') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"name\"\n                    :label=\"$t('theme.tagsPostsPerPage')\">\n                    <text-input\n                        slot=\"field\"\n                        ref=\"name\"\n                        type=\"number\"\n                        min=\"-1\"\n                        step=\"1\"\n                        v-model=\"basic.tagsPostsPerPage\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('theme.tagsPostsPerPageInfo') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"name\"\n                    :label=\"$t('theme.authorsPostsPerPage')\">\n                    <text-input\n                        slot=\"field\"\n                        ref=\"name\"\n                        type=\"number\"\n                        min=\"-1\"\n                        step=\"1\"\n                        v-model=\"basic.authorsPostsPerPage\" />\n                    <small\n                        slot=\"note\"\n                        class=\"note\">\n                        {{ $t('theme.authorsPostsPerPageInfo') }}\n                    </small>\n                </field>\n\n                <field\n                    id=\"name\"\n                    :label=\"$t('theme.excerptLength')\">\n                    <text-input\n                        slot=\"field\"\n                        ref=\"name\"\n                        type=\"number\"\n                        min=\"0\"\n                        step=\"1\"\n                        v-model=\"basic.excerptLength\" />\n                </field>\n\n                <field\n                    id=\"name\"\n                    :label=\"$t('theme.websiteLogo')\">\n                    <image-upload\n                        slot=\"field\"\n                        v-model=\"basic.logo\"\n                        :addMediaFolderPath=\"true\"\n                        imageType=\"optionImages\" />\n                </field>\n            </fields-group>\n\n            <fields-group :title=\"$t('theme.customSettings')\">\n                <tabs\n                    ref=\"custom-settings-tabs\"\n                    id=\"custom-settings-tabs\"\n                    :items=\"customSettingsTabs\">\n                    <div\n                        v-for=\"(groupName, index) of customSettingsTabsNames\"\n                        :slot=\"'tab-' + index\"\n                        :key=\"'tab-' + index\">\n                        <div v-if=\"groupName !== $t('theme.postOptions') && groupName !== $t('theme.translations')\">\n                            <field\n                                v-for=\"(field, subindex) of getFieldsByGroupName(groupName)\"\n                                v-if=\"checkDependencies(field.dependencies)\"\n                                :label=\"getFieldLabel(field)\"\n                                :key=\"'tab-' + index + '-field-' + subindex\"\n                                :noLabelSpace=\"field.type === 'separator'\"\n                                :labelFullWidth=\"field.type === 'wysiwyg' || field.type === 'repeater'\">\n                                <range-slider\n                                    v-if=\"field.type === 'range'\"\n                                    :min=\"field.min\"\n                                    :max=\"field.max\"\n                                    :step=\"field.step\"\n                                    v-model=\"custom[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></range-slider>\n\n                                <separator\n                                    v-if=\"field.type === 'separator'\"\n                                    slot=\"field\"\n                                    :type=\"field.size\"\n                                    :label=\"field.label\"\n                                    :anchor=\"field.anchor\"\n                                    :note=\"field.note\"\n                                    :customCssClasses=\"field.customCssClasses\"></separator>\n\n                                <text-area\n                                    v-if=\"field.type === 'textarea'\"\n                                    slot=\"field\"\n                                    :rows=\"field.rows\"\n                                    v-model=\"custom[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    :cols=\"field.cols\"\n                                    :disabled=\"field.disabled\"\n                                    :customCssClasses=\"field.customCssClasses\"></text-area>\n\n                                <text-area\n                                    v-if=\"field.type === 'wysiwyg'\"\n                                    slot=\"field\"\n                                    :id=\"'theme-settings-' + index\"\n                                    v-model=\"custom[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    :wysiwyg=\"true\"\n                                    :miniEditorMode=\"true\"\n                                    :customCssClasses=\"field.customCssClasses\"></text-area>\n\n                                <image-upload\n                                    v-if=\"field.type === 'upload'\"\n                                    v-model=\"custom[field.name]\"\n                                    slot=\"field\"\n                                    :anchor=\"field.anchor\"\n                                    :addMediaFolderPath=\"true\"\n                                    imageType=\"optionImages\"\n                                    :customCssClasses=\"field.customCssClasses\"></image-upload>\n\n                                <small-image-upload\n                                    v-if=\"field.type === 'smallupload'\"\n                                    v-model=\"custom[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    imageType=\"optionImages\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></small-image-upload>\n\n                                <radio-buttons\n                                    v-if=\"field.type === 'radio'\"\n                                    :items=\"field.options\"\n                                    :name=\"field.name\"\n                                    v-model=\"custom[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <dropdown\n                                    v-if=\"field.type === 'select'\"\n                                    slot=\"field\"\n                                    :multiple=\"field.multiple\"\n                                    v-model=\"custom[field.name]\"\n                                    :id=\"field.anchor\"\n                                    :items=\"getDropdownOptions(field.options)\"\n                                    :customCssClasses=\"field.customCssClasses\"></dropdown>\n\n                                <switcher\n                                    v-if=\"field.type === 'checkbox'\"\n                                    v-model=\"custom[field.name]\"\n                                    :lower-zindex=\"true\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></switcher>\n\n                                <color-picker\n                                    v-if=\"field.type === 'colorpicker'\"\n                                    v-model=\"custom[field.name]\"\n                                    :data-field=\"field.name\"\n                                    :anchor=\"field.anchor\"\n                                    :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></color-picker>\n\n                                <posts-dropdown\n                                    v-if=\"field.type === 'posts-dropdown'\"\n                                    v-model=\"custom[field.name]\"\n                                    :allowed-post-status=\"field.allowedPostStatus || ['any']\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></posts-dropdown>\n\n                                <pages-dropdown\n                                    v-if=\"field.type === 'pages-dropdown'\"\n                                    v-model=\"custom[field.name]\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></pages-dropdown>\n\n                                <tags-dropdown\n                                    v-if=\"field.type === 'tags-dropdown'\"\n                                    v-model=\"custom[field.name]\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></tags-dropdown>\n\n                                <authors-dropdown\n                                    v-if=\"field.type === 'authors-dropdown'\"\n                                    v-model=\"custom[field.name]\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></authors-dropdown>\n\n                                <text-input\n                                    v-if=\"isNormalInput(field.type)\"\n                                    slot=\"field\"\n                                    :type=\"field.type\"\n                                    :min=\"field.min\"\n                                    :max=\"field.max\"\n                                    :size=\"field.size\"\n                                    :step=\"field.step\"\n                                    :pattern=\"field.pattern\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    v-model=\"custom[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    :disabled=\"field.disabled\"\n                                    :placeholder=\"field.placeholder\"\n                                    :customCssClasses=\"field.customCssClasses\"></text-input>\n\n                                <repeater\n                                    v-if=\"field.type === 'repeater'\" \n                                    slot=\"field\"\n                                    :structure=\"field.structure\"\n                                    v-model=\"custom[field.name]\"\n                                    :translations=\"field.translations\"\n                                    :maxCount=\"field.maxCount\"\n                                    :hasEmptyState=\"field.hasEmptyState\"\n                                    :hideLabels=\"field.hideLabels\"\n                                    :anchor=\"field.anchor\"\n                                    :settings=\"custom\"\n                                    imageType=\"optionImages\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <small\n                                    v-if=\"field.note && field.type !== 'separator'\"\n                                    slot=\"note\"\n                                    class=\"note\"\n                                    v-pure-html=\"field.note\">\n                                </small>\n                            </field>\n                        </div>\n\n                        <div v-if=\"groupName === $t('theme.postOptions')\">\n                            <field>\n                                <small\n                                    slot=\"note\"\n                                    class=\"note\">\n                                    {{ $t('theme.postOptionsInfo') }}<br><br>\n                                </small>\n                            </field>\n\n                            <field\n                                v-if=\"hasPostTemplates\"\n                                :label=\"$t('theme.defaultPostTemplate')\"\n                                key=\"tab-last-field-0\">\n                                <dropdown\n                                    :items=\"postTemplates\"\n                                    v-model=\"defaultTemplates.post\"\n                                    id=\"post-template\"\n                                    slot=\"field\">\n                                    <option\n                                        value=\"\"\n                                        slot=\"first-choice\">\n                                        {{ $t('theme.defaultTemplate') }}\n                                    </option>\n                                </dropdown>\n                            </field>\n\n                            <field\n                                v-for=\"(field, subindex) of postViewThemeSettings\"\n                                :label=\"field.label\"\n                                :key=\"'tab-' + index + '-field-' + subindex\">\n                                <dropdown\n                                    v-if=\"!field.type || field.type === 'select'\"\n                                    :id=\"field.name + '-select'\"\n                                    :items=\"getDropdownViewOptions(field.options)\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"\n                                    v-model=\"postView[field.name]\">\n                                </dropdown>\n\n                                <text-input\n                                    v-if=\"field.type === 'text' || field.type === 'number'\"\n                                    :type=\"field.type\"\n                                    slot=\"field\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    v-model=\"postView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <text-area\n                                    v-if=\"field.type === 'textarea'\"\n                                    slot=\"field\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    v-model=\"postView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <color-picker\n                                    v-if=\"field.type === 'colorpicker'\"\n                                    slot=\"field\"\n                                    v-model=\"postView[field.name]\"\n                                    :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\"\n                                    :customCssClasses=\"field.customCssClasses\">\n                                </color-picker>\n\n                                <image-upload\n                                    v-if=\"field.type === 'image'\"\n                                    slot=\"field\"\n                                    v-model=\"postView[field.name]\"\n                                    item-id=\"defaults\"\n                                    imageType=\"contentImages\" />\n\n                                <small\n                                    v-if=\"field.note\"\n                                    slot=\"note\"\n                                    class=\"note\">\n                                    {{ field.note }}\n                                </small>\n                            </field>\n                        </div>\n\n                        <div v-if=\"groupName === $t('theme.pageOptions')\">\n                            <field>\n                                <small\n                                    slot=\"note\"\n                                    class=\"note\">\n                                    {{ $t('theme.pageOptionsInfo') }}<br><br>\n                                </small>\n                            </field>\n\n                            <field\n                                v-if=\"hasPageTemplates\"\n                                :label=\"$t('theme.defaultPageTemplate')\"\n                                key=\"tab-last-field-0\">\n                                <dropdown\n                                    :items=\"pageTemplates\"\n                                    v-model=\"defaultTemplates.page\"\n                                    id=\"post-template\"\n                                    slot=\"field\">\n                                    <option\n                                        value=\"\"\n                                        slot=\"first-choice\">\n                                        {{ $t('theme.defaultTemplate') }}\n                                    </option>\n                                </dropdown>\n                            </field>\n\n                            <field\n                                v-for=\"(field, subindex) of pageViewThemeSettings\"\n                                :label=\"field.label\"\n                                :key=\"'tab-' + index + '-field-' + subindex\">\n                                <dropdown\n                                    v-if=\"!field.type || field.type === 'select'\"\n                                    :id=\"field.name + '-select'\"\n                                    :items=\"getDropdownViewOptions(field.options)\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"\n                                    v-model=\"pageView[field.name]\">\n                                </dropdown>\n\n                                <text-input\n                                    v-if=\"field.type === 'text' || field.type === 'number'\"\n                                    :type=\"field.type\"\n                                    slot=\"field\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    v-model=\"pageView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <text-area\n                                    v-if=\"field.type === 'textarea'\"\n                                    slot=\"field\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    v-model=\"pageView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <color-picker\n                                    v-if=\"field.type === 'colorpicker'\"\n                                    slot=\"field\"\n                                    v-model=\"pageView[field.name]\"\n                                    :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\"\n                                    :customCssClasses=\"field.customCssClasses\">\n                                </color-picker>\n\n                                <image-upload\n                                    v-if=\"field.type === 'image'\"\n                                    slot=\"field\"\n                                    v-model=\"pageView[field.name]\"\n                                    item-id=\"defaults\"\n                                    imageType=\"contentImages\" />\n\n                                <small\n                                    v-if=\"field.note\"\n                                    slot=\"note\"\n                                    class=\"note\">\n                                    {{ field.note }}\n                                </small>\n                            </field>\n                        </div>\n\n                        <div v-if=\"groupName === $t('theme.tagOptions')\">\n                            <field>\n                                <small\n                                    slot=\"note\"\n                                    class=\"note\">\n                                    {{ $t('theme.tagOptionsInfo') }}<br><br>\n                                </small>\n                            </field>\n\n                            <field\n                                v-for=\"(field, subindex) of tagViewThemeSettings\"\n                                :label=\"field.label\"\n                                :key=\"'tab-' + index + '-field-' + subindex\">\n                                <dropdown\n                                    v-if=\"!field.type || field.type === 'select'\"\n                                    :id=\"field.name + '-select'\"\n                                    :items=\"getDropdownViewOptions(field.options)\"\n                                    slot=\"field\"\n                                    v-model=\"tagView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\">\n                                </dropdown>\n\n                                <text-input\n                                    v-if=\"field.type === 'text' || field.type === 'number'\"\n                                    :type=\"field.type\"\n                                    slot=\"field\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    v-model=\"tagView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <text-area\n                                    v-if=\"field.type === 'textarea'\"\n                                    slot=\"field\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    v-model=\"tagView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <color-picker\n                                    v-if=\"field.type === 'colorpicker'\"\n                                    slot=\"field\"\n                                    v-model=\"tagView[field.name]\"\n                                    :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\"\n                                    :customCssClasses=\"field.customCssClasses\">\n                                </color-picker>\n\n                                <image-upload\n                                    v-if=\"field.type === 'image'\"\n                                    slot=\"field\"\n                                    v-model=\"tagView[field.name]\"\n                                    item-id=\"defaults\"\n                                    imageType=\"tagImages\" />\n\n                                <small\n                                    v-if=\"field.note\"\n                                    slot=\"note\"\n                                    class=\"note\">\n                                    {{ field.note }}\n                                </small>\n                            </field>\n                        </div>\n\n                        <div v-if=\"groupName === $t('theme.authorOptions')\">\n                            <field>\n                                <small\n                                    slot=\"note\"\n                                    class=\"note\">\n                                    {{ $t('theme.authorOptionsInfo') }}<br><br>\n                                </small>\n                            </field>\n\n                            <field\n                                v-for=\"(field, subindex) of authorViewThemeSettings\"\n                                :label=\"field.label\"\n                                :key=\"'tab-' + index + '-field-' + subindex\">\n                                <dropdown\n                                    v-if=\"!field.type || field.type === 'select'\"\n                                    :id=\"field.name + '-select'\"\n                                    :items=\"getDropdownViewOptions(field.options)\"\n                                    slot=\"field\"\n                                    v-model=\"authorView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\">\n                                </dropdown>\n\n                                <text-input\n                                    v-if=\"field.type === 'text' || field.type === 'number'\"\n                                    :type=\"field.type\"\n                                    slot=\"field\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    v-model=\"authorView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <text-area\n                                    v-if=\"field.type === 'textarea'\"\n                                    slot=\"field\"\n                                    :placeholder=\"field.placeholder ? field.placeholder : $t('theme.leaveBlankToUseDefault')\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    v-model=\"authorView[field.name]\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <color-picker\n                                    v-if=\"field.type === 'colorpicker'\"\n                                    slot=\"field\"\n                                    v-model=\"authorView[field.name]\"\n                                    :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\"\n                                    :customCssClasses=\"field.customCssClasses\">\n                                </color-picker>\n\n                                <image-upload\n                                    v-if=\"field.type === 'image'\"\n                                    slot=\"field\"\n                                    v-model=\"authorView[field.name]\"\n                                    item-id=\"defaults\"\n                                    imageType=\"authorImages\" />\n\n                                <small\n                                    v-if=\"field.note\"\n                                    slot=\"note\"\n                                    class=\"note\"\n                                    :customCssClasses=\"field.customCssClasses\">\n                                    {{ field.note }}\n                                </small>\n                            </field>\n                        </div>\n\n                        <div v-if=\"groupName === $t('theme.translations')\">\n                            <field>\n                                <small\n                                    slot=\"note\"\n                                    class=\"note\"\n                                    v-pure-html=\"$t('theme.translationsInfo')\">\n                                </small>\n                            </field>\n                        </div>\n                    </div>\n                </tabs>\n            </fields-group>\n\n            <p-footer>\n                <btn-dropdown\n                    slot=\"buttons\"\n                    buttonColor=\"green\"\n                    :items=\"dropdownItems\"\n                    :disabled=\"!siteHasTheme || buttonsLocked\"\n                    localStorageKey=\"publii-preview-mode\"\n                    :previewIcon=\"true\"\n                    :isReversed=\"true\"\n                    defaultValue=\"full-site-preview\" />\n\n                <p-button\n                    @click.native=\"save\"\n                    slot=\"buttons\"\n                    type=\"secondary\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n\n                <p-button\n                    @click.native=\"reset\"\n                    slot=\"buttons\"\n                    type=\"outline\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('theme.resetThemeSettings') }}\n                </p-button>\n            </p-footer>\n        </div>\n    </section>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'site-settings',\n    data: function() {\n        return {\n            buttonsLocked: false,\n            defaultTemplates: {\n                post: '',\n                page: ''\n            },\n            basic: {\n                postsPerPage: 4,\n                tagsPostsPerPage: 4,\n                authorsPostsPerPage: 4,\n                excerptLength: 45,\n                logo: ''\n            },\n            custom: {},\n            pageView: {},\n            postView: {},\n            tagView: {},\n            authorView: {}\n        };\n    },\n    computed: {\n        siteHasTheme () {\n            return !!this.$store.state.currentSite.config.theme;\n        },\n        customSettingsTabs () {\n            let tabs = [];\n\n            this.$store.state.currentSite.themeSettings.customConfig.forEach(item => {\n                if (tabs.indexOf(item.group) === -1 && !item.parentgroup) {\n                    tabs.push(item.group);\n                }\n            });\n\n            this.$store.state.currentSite.themeSettings.customConfig.forEach(item => {\n                if (\n                    item.parentgroup && \n                    !tabs.some((el) => Array.isArray(el) && el.length === 2 && el[0] === item.group && el[1] === item.parentgroup)\n                ) {\n                    let parentGroupIndex = tabs.indexOf(item.parentgroup);\n                    tabs.splice(parentGroupIndex + 1, 0, [item.group, item.parentgroup]);\n                }\n            });\n\n            tabs.push(this.$t('theme.authorOptions'));\n            tabs.push(this.$t('theme.postOptions'));\n            tabs.push(this.$t('theme.pageOptions'));\n            tabs.push(this.$t('theme.tagOptions'));\n            tabs.push(this.$t('theme.translations'));\n\n            // We need to reverse subgroups order\n            let finalTabsList = [];\n            let group = [];\n\n            tabs.forEach(item => {\n                if (Array.isArray(item)) {\n                    group.push(item);\n                } else {\n                    if (group.length > 0) {\n                        finalTabsList.push(...group.reverse());\n                        group = [];\n                    }\n\n                    finalTabsList.push(item);\n                }\n            });\n\n            if (group.length > 0) {\n                finalTabsList.push(...group.reverse());\n            }\n\n            return finalTabsList;\n        },\n        customSettingsTabsNames () {\n            return this.customSettingsTabs.map(tab => {\n                return Array.isArray(tab) ? tab[0] : tab;\n            });\n        },\n        postViewThemeSettings () {\n            return this.$store.state.currentSite.themeSettings.postConfig.filter(field => field.type !== 'separator');\n        },\n        pageViewThemeSettings () {\n            console.log(this.$store.state.currentSite.themeSettings);\n            return this.$store.state.currentSite.themeSettings.pageConfig.filter(field => field.type !== 'separator');\n        },\n        postTemplates () {\n            return this.$store.state.currentSite.themeSettings.postTemplates;\n        },\n        pageTemplates () {\n            return this.$store.state.currentSite.themeSettings.pageTemplates;\n        },\n        hasPostTemplates () {\n            return !!Object.keys(this.postTemplates).length;\n        },\n        hasPageTemplates () {\n            return !!Object.keys(this.pageTemplates).length;\n        },\n        tagViewThemeSettings () {\n            return this.$store.state.currentSite.themeSettings.tagConfig.filter(field => field.type !== 'separator');\n        },\n        authorViewThemeSettings () {\n            return this.$store.state.currentSite.themeSettings.authorConfig.filter(field => field.type !== 'separator');\n        },\n        dropdownItems () {\n            return [\n                {\n                    label: this.$t('ui.previewFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'full-site-preview',\n                    isVisible: () => true,\n                    icon: 'full-preview-monitor',\n                    onClick: this.saveAndPreview.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.renderFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'full-site-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    icon: 'full-render-monitor',\n                    onClick: this.saveAndRender.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.previewFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'homepage-preview',\n                    icon: 'quick-preview',\n                    isVisible: () => true,\n                    onClick: this.saveAndPreview.bind(this, 'homepage')\n                },\n                {\n                    label: this.$t('ui.renderFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'homepage-render',\n                    icon: 'quick-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    onClick: this.saveAndRender.bind(this, 'homepage')\n                }\n            ];\n        },\n        extensionsPath () {\n            return [\n                'file:///',\n                this.$store.state.currentSite.siteDir,\n                '/input/themes/',\n                this.$store.state.currentSite.config.theme,\n                '/'\n            ].join('');\n        }\n    },\n    mounted () {\n        setTimeout (() => {\n            this.loadAdditionalCss();\n            this.loadSettings();\n        }, 0);\n    },\n    methods: {\n        loadAdditionalCss () {\n            if (\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures.customThemeOptionsCss\n            ) {\n                let customCssPath = this.extensionsPath + 'theme-options.css?v=' + (+new Date());\n\n                if (!document.querySelector('#custom-theme-options-css')) {\n                    $(document.body).append($('<link rel=\"stylesheet\" id=\"custom-theme-options-css\" href=\"' + customCssPath + '\" />'));\n                }\n            }\n        },\n        loadSettings () {\n            this.loadBasicSettings();\n            this.loadCustomSettings();\n            this.loadPageViewSettings();\n            this.loadPostViewSettings();\n            this.loadTagViewSettings();\n            this.loadAuthorViewSettings();\n            this.loadDefaultTemplates();\n        },\n        loadBasicSettings () {\n            this.basic.postsPerPage = this.$store.state.currentSite.themeSettings.config.filter(field => field.name === 'postsPerPage')[0].value;\n            this.basic.tagsPostsPerPage = this.$store.state.currentSite.themeSettings.config.filter(field => field.name === 'tagsPostsPerPage')[0].value;\n            this.basic.authorsPostsPerPage = this.$store.state.currentSite.themeSettings.config.filter(field => field.name === 'authorsPostsPerPage')[0].value;\n            this.basic.excerptLength = this.$store.state.currentSite.themeSettings.config.filter(field => field.name === 'excerptLength')[0].value;\n            this.basic.logo = this.$store.state.currentSite.themeSettings.config.filter(field => field.name === 'logo')[0].value;\n        },\n        loadCustomSettings () {\n            let settings = this.$store.state.currentSite.themeSettings.customConfig.map(field => {\n                if (field.type !== 'separator') {\n                    return [field.name, field.value]\n                }\n\n                return false;\n            });\n\n            for (let setting of settings) {\n                if (setting) {\n                    Vue.set(this.custom, setting[0], setting[1]);\n                }\n            }\n        },\n        loadPageViewSettings () {\n            let settings = this.$store.state.currentSite.themeSettings.pageConfig.map(field => {\n                if (field.type !== 'separator') {\n                    return [field.name, field.value]\n                }\n\n                return false;\n            });\n\n            for (let setting of settings) {\n                if (setting) {\n                    Vue.set(this.pageView, setting[0], setting[1]);\n                }\n            }\n        },\n        loadPostViewSettings () {\n            let settings = this.$store.state.currentSite.themeSettings.postConfig.map(field => {\n                if (field.type !== 'separator') {\n                    return [field.name, field.value]\n                }\n\n                return false;\n            });\n\n            for (let setting of settings) {\n                if (setting) {\n                    Vue.set(this.postView, setting[0], setting[1]);\n                }\n            }\n        },\n        loadTagViewSettings () {\n            let settings = this.$store.state.currentSite.themeSettings.tagConfig.map(field => {\n                if (field.type !== 'separator') {\n                    return [field.name, field.value]\n                }\n\n                return false;\n            });\n\n            for (let setting of settings) {\n                if (setting) {\n                    Vue.set(this.tagView, setting[0], setting[1]);\n                }\n            }\n        },\n        loadAuthorViewSettings () {\n            let settings = this.$store.state.currentSite.themeSettings.authorConfig.map(field => {\n                if (field.type !== 'separator') {\n                    return [field.name, field.value]\n                }\n\n                return false;\n            });\n\n            for (let setting of settings) {\n                if (setting) {\n                    Vue.set(this.authorView, setting[0], setting[1]);\n                }\n            }\n        },\n        loadDefaultTemplates () {\n            this.defaultTemplates = {\n                post: this.$store.state.currentSite.themeSettings.defaultTemplates.post,\n                page: this.$store.state.currentSite.themeSettings.defaultTemplates.page\n            };\n        },\n        checkDependencies (dependencies) {\n            if (!dependencies || !dependencies.length) {\n                return true;\n            }\n\n            for (let i = 0; i < dependencies.length; i++) {\n                let dependencyName = dependencies[i].field;\n                let dependencyValue = dependencies[i].value;\n\n                if (\n                    Array.isArray(this.custom[dependencyName]) && \n                    dependencyValue.indexOf('=') > -1\n                ) {\n                    let dependencyData = this.custom[dependencyName];\n                    let fieldName = dependencyValue.split('=')[0];\n                    let acceptedValues = [];\n                    let isValidDependency = false;\n\n                    if (dependencyValue.split('=')[1]) {\n                        acceptedValues = dependencyValue.split('=')[1].split(',');\n                    }\n\n                    for (let i = 0; i < dependencyData.length; i++) {\n                        let dataRow = dependencyData[i];\n                        let valueToCompare = dataRow[fieldName];\n\n                        if (valueToCompare === true) {\n                            valueToCompare = 'true';\n                        } else if (valueToCompare === false) {\n                            valueToCompare = 'false';\n                        }\n\n                        if (acceptedValues.indexOf(valueToCompare) > -1) {\n                            isValidDependency = true;\n                        }\n                    }\n\n                    if (!isValidDependency) {\n                        return false;\n                    }\n\n                    continue;\n                }\n\n                if (dependencyValue === \"true\" && this.custom[dependencyName] !== true) {\n                    return false;\n                } else if (dependencyValue === \"true\") {\n                    continue;\n                }\n\n                if (dependencyValue === \"false\" && this.custom[dependencyName] !== false) {\n                    return false;\n                } else if (dependencyValue === \"false\") {\n                    continue;\n                }\n\n                if (typeof dependencyValue === 'string' && dependencyValue.indexOf(',') > -1) {\n                    let values = dependencyValue.split(',');\n                    let isValidDependency = false;\n\n                    for (let i = 0; i < values.length; i++) {\n                        if (this.custom[dependencyName] === values[i]) {\n                            isValidDependency = true;\n                        }\n                    }\n                    \n                    return isValidDependency;\n                }\n\n                if (dependencyValue !== this.custom[dependencyName]) {\n                    return false;\n                }\n            }\n\n            return true;\n        },\n        clearErrors (errorName) {\n            let pos = this.errors.indexOf(errorName);\n            this.errors.splice(pos, 1);\n        },\n        getFieldLabel (field) {\n            if (field.type === 'separator') {\n                return '';\n            }\n\n            if (field.label === '' || typeof field.label === 'undefined') {\n                return ' ';\n            }\n\n            return field.label;\n        },\n        getFieldsByGroupName (groupName) {\n            return this.$store.state.currentSite.themeSettings.customConfig.filter(field => field.group === groupName);\n        },\n        isNormalInput (type) {\n            return [\n                'separator',\n                'textarea',\n                'wysiwyg',\n                'radio',\n                'select',\n                'range',\n                'upload',\n                'smallupload',\n                'checkbox',\n                'colorpicker',\n                'posts-dropdown',\n                'pages-dropdown',\n                'authors-dropdown',\n                'tags-dropdown',\n                'repeater'\n            ].indexOf(type) === -1;\n        },\n        getDropdownOptions (inputOptions) {\n            let options = {};\n            let hasGroups = !!inputOptions.filter(option => typeof option.group !== 'undefined').length;\n\n            if (hasGroups) {\n                options.hasGroups = true;\n                let groups = {\n                    ungrouped: {}\n                };\n\n                for (let i = 0; i < inputOptions.length; i++) {\n                    let groupName = inputOptions[i].group;\n\n                    if (groupName && !groups[groupName]) {\n                        groups[groupName] = {};\n                    }\n                }\n\n                for (let i = 0; i < inputOptions.length; i++) {\n                    let inputGroupName = inputOptions[i].group;\n\n                    if (inputGroupName) {\n                        groups[inputGroupName][inputOptions[i].value] = {\n                            label: inputOptions[i].label,\n                            disabled: inputOptions[i].disabled\n                        };\n                    } else {\n                        groups['ungrouped'][inputOptions[i].value] = {\n                            label: inputOptions[i].label,\n                            disabled: inputOptions[i].disabled\n                        };\n                    }\n                }\n\n                options.groups = groups;\n            } else {\n                for (let i = 0; i < inputOptions.length; i++) {\n                    options[inputOptions[i].value] = {\n                        label: inputOptions[i].label,\n                        disabled: inputOptions[i].disabled\n                    };\n                }\n            }\n\n            return options;\n        },\n        getDropdownViewOptions (arrayToConvert) {\n            let options = {};\n\n            for (let i = 0; i < arrayToConvert.length; i++) {\n                options[arrayToConvert[i].value] = arrayToConvert[i].label;\n            }\n\n            return options;\n        },\n        goToSettings () {\n            let siteName = this.$route.params.name;\n            this.$router.push('/site/' + siteName + '/settings/');\n        },\n        save () {\n            this.$bus.$emit('theme-settings-before-save');\n\n            setTimeout(() => {\n                this.saveSettings(false);\n            }, 500);\n        },\n        saveAndPreview (renderingType = false) {\n            this.$bus.$emit('theme-settings-before-save');\n\n            setTimeout(() => {\n                this.saveSettings(true, renderingType, false);\n            }, 500);\n        },\n        saveAndRender (renderingType = false) {\n            this.$bus.$emit('theme-settings-before-save');\n\n            setTimeout(() => {\n                this.saveSettings(true, renderingType, true);\n            }, 500);\n        },\n        saveSettings(showPreview = false, renderingType = false, renderFiles) {\n            let newConfig = {\n                config: Object.assign({}, this.basic),\n                customConfig: Object.assign({}, this.custom),\n                postConfig: Object.assign({}, this.postView),\n                pageConfig: Object.assign({}, this.pageView),\n                tagConfig: Object.assign({}, this.tagView),\n                authorConfig: Object.assign({}, this.authorView),\n                defaultTemplates: Object.assign({}, this.defaultTemplates)\n            };\n\n            // Send request to the back-end\n            mainProcessAPI.send('app-site-theme-config-save', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"theme\": this.$store.state.currentSite.config.theme,\n                \"config\": newConfig\n            });\n\n            // Settings saved\n            mainProcessAPI.receiveOnce('app-site-theme-config-saved', async (data) => {\n                if (data.status === true) {\n                    await this.savedSettings(showPreview, renderingType, renderFiles);\n                    this.$store.commit('setThemeConfig', data);\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('theme.saveSettingsSuccessMessage'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n                }\n\n                this.loadSettings();\n            });\n        },\n        async savedSettings(showPreview = false, renderingType = false, renderFiles = false) {\n            if (showPreview) {\n                let previewLocationExists = await mainProcessAPI.existsSync(this.$store.state.app.config.previewLocation);\n\n                if (this.$store.state.app.config.previewLocation !== '' && !previewLocationExists) {\n                    this.$bus.$emit('confirm-display', {\n                        message: this.$t('sync.previewCatalogDoesNotExistInfo'),\n                        okLabel: this.$t('sync.goToAppSettings'),\n                        okClick: () => {\n                            this.$router.push(`/app-settings/`);\n                        }\n                    });\n                    return;\n                }\n\n                if (renderingType === 'homepage') {\n                    this.$bus.$emit('rendering-popup-display', {\n                        homepageOnly: true,\n                        showPreview: !renderFiles,\n                    });\n                } else {\n                    this.$bus.$emit('rendering-popup-display', {\n                        showPreview: !renderFiles\n                    });\n                }\n            }\n        },\n        reset () {\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('theme.settingsResetMessage'),\n                okClick: this.resetSettings\n            });\n        },\n        resetSettings () {\n            // Send request to the back-end\n            mainProcessAPI.send('app-site-theme-config-save', {\n                \"site\": this.$store.state.currentSite.config.name,\n                \"theme\": this.$store.state.currentSite.config.theme,\n                \"config\": {}\n            });\n\n            // Settings saved\n            mainProcessAPI.receiveOnce('app-site-theme-config-saved', async (data) => {\n                if (data.status === true) {\n                    await this.savedSettings(false);\n                    this.$store.commit('setThemeConfig', data);\n                }\n\n                this.loadSettings();\n\n                this.$bus.$emit('message-display', {\n                    message: this.$t('theme.settingsResetSuccessMessage'),\n                    type: 'success',\n                    lifeTime: 3\n                });\n            });\n        }\n    },\n    beforeDestroy () {\n        if (document.querySelector('#custom-theme-options-css')) {\n            $('#custom-theme-options-css').remove();\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.theme-settings {\n    margin: 0 auto;\n    max-width: $wrapper;\n    user-select: none;\n\n    .multiple-checkboxes {\n        label {\n            display: block;\n            margin-bottom: 1rem;\n        }\n    }\n\n    textarea {\n        height: 200px;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/ThemesList.vue",
    "content": "<template>\n    <div\n        @drop.stop.prevent=\"uploadTheme\"\n        @dragleave.stop.prevent=\"hideOverlay\"\n        @dragenter.stop.prevent=\"showOverlay\"\n        @dragover.stop.prevent=\"showOverlay\"\n        @drag.stop.prevent=\"showOverlay\"\n        @dragstart.stop.prevent\n        @dragend.stop.prevent\n        :class=\"{ 'themes': true, 'theme-is-over': themeIsOver }\">\n        <div\n            class=\"add-more-theme\">\n                <a href=\"https://marketplace.getpublii.com/\" target=\"_blank\" rel=\"noopener noreferrer\">\n                    <icon\n                        customWidth=\"50\"\n                        customHeight=\"46\"\n                        properties=\"not-clickable\"\n                        name=\"add\" />\n\n                    <h3>{{ $t('theme.getMoreThemes') }}</h3>\n                </a>\n        </div>\n\n        <theme-item\n            v-for=\"(theme, index) in themes\"\n            :themeData=\"theme\"\n            :key=\"'theme-item-' + index\" />\n\n        <overlay\n            v-if=\"themeIsOver\"\n            :hasBorder=\"true\"\n            :isBlue=\"true\">\n            <div>{{ $t('theme.dropYourThemeHere') }}</div>\n        </overlay>\n    </div>\n</template>\n\n<script>\nimport ThemesListItem from './ThemesListItem';\n\nexport default {\n    name: 'themes-list',\n    data: function() {\n        return {\n            themeIsOver: false\n        };\n    },\n    components: {\n        'theme-item': ThemesListItem\n    },\n    computed: {\n        themes () {\n            return this.$store.getters.themes;\n        }\n    },\n    methods: {\n        showOverlay (e) {\n            this.themeIsOver = true;\n        },\n        hideOverlay (e) {\n            if (e.target.classList.contains('themes')) {\n                this.themeIsOver = false;\n            }\n        },\n        async uploadTheme (e) {\n            this.themeIsOver = false;\n\n            mainProcessAPI.send('app-theme-upload', {\n                sourcePath: await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.dataTransfer.files[0]))\n            });\n\n            mainProcessAPI.receiveOnce('app-theme-uploaded', this.$parent.uploadedTheme);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.themes {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 3rem;\n    position: relative;\n    user-select: none;\n\n    &.theme-is-over {\n        & > * {\n            pointer-events: none;\n        }\n    }\n}\n\n.add-more-theme {\n    background-color: var(--bg-secondary);\n    border: 1px solid transparent;\n    border-radius: var(--border-radius);\n    box-shadow: var(--box-shadow-small);      \n    height: 100%;\n    transition: var(--transition);\n    text-align: center;\n\n    &:hover {\n         background: var(--bg-primary);\n         border-color: var(--color-primary);\n         box-shadow: 0 0 26px rgba(black, .07);\n\n         svg {\n             fill: var(--color-primary);\n         }\n\n         h3 {\n             color: var(--color-primary);\n         }\n    }\n\n    & > a {\n         align-items: center;\n         display: flex;\n         flex-direction: column;\n         height: 100%;\n         justify-content: center;\n         min-height: 29rem;\n         width: 100%;\n    }\n\n    h3 {\n         color: var(--text-primary-color);\n         font-size: $app-font-base;\n         font-weight: var(--font-weight-semibold);\n         margin-bottom: 0;\n         transition: inherit;\n    }\n\n    svg {\n         fill: var(--icon-primary-color);\n         transition: inherit;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/ThemesListItem.vue",
    "content": "<template>\n    <figure class=\"theme\">\n        <img\n            :src=\"thumbnail\"\n            class=\"theme-thumbnail\"\n            alt=\"\">\n\n        <figcaption class=\"theme-name\">\n            <h3>\n                {{ name }}\n                <span class=\"theme-version\">\n                    {{ version }}\n                </span>\n             </h3>\n            <a\n                href=\"#\"\n                class=\"theme-delete\"\n                :title=\"$t('theme.deleteTheme')\"\n                @click.stop.prevent=\"deleteTheme(name, directory)\">\n                    <icon\n                        size=\"xs\"\n                        properties=\"not-clickable\"\n                        name=\"trash\" />\n            </a>\n\n            <span \n                v-if=\"hasUpdateAvailable\"\n                class=\"theme-new-version-available\">\n                {{ $t('theme.newVersionAvailable') }}: <strong>{{ updateVersion }}</strong>\n            </span>\n        </figcaption>\n    </figure>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex';\nimport VersionComparator from '../helpers/version-comparator';\n\nexport default {\n    name: 'themes-list-item',\n    props: [\n        'themeData'\n    ],\n    computed: {\n        ...mapGetters([\n            'notifications'\n        ]),\n        thumbnail () {\n            return this.themeData.thumbnail;\n        },\n        name () {\n            return this.themeData.name;\n        },\n        directory () {\n            return this.themeData.directory;\n        },\n        version () {\n            return this.themeData.version;\n        },\n        updateVersion () {\n            let availableTheme = this.notifications.themes[this.directory];\n\n            if (!availableTheme) {\n                return '';\n            }\n\n            return availableTheme.version;\n        },\n        hasUpdateAvailable () {\n            if (!this.notifications || !this.notifications.themes) {\n                return false;\n            }\n\n            let availableTheme = this.notifications.themes[this.directory];\n\n            if (!availableTheme) {\n                return false;\n            }\n\n            return VersionComparator(availableTheme.version, this.version) === 1;\n        }\n    },\n    methods: {\n        deleteTheme: function(themeName, themeDirectory) {\n            let confirmConfig = {\n                message: this.$t('theme.removeThemeMessage', { themeName }),\n                isDanger: true,\n                okClick: function() {\n                    mainProcessAPI.send('app-theme-delete', {\n                        name: themeName,\n                        directory: themeDirectory\n                    });\n\n                    mainProcessAPI.receiveOnce('app-theme-deleted', (data) => {\n                        this.$bus.$emit('message-display', {\n                            message: this.$t('theme.removeThemeSuccessMessage'),\n                            type: 'success',\n                            lifeTime: 3\n                        });\n\n                        this.$store.commit('replaceAppThemes', data.themes);\n                        this.$store.commit('updateSiteThemes');\n                    });\n                }\n            };\n\n            this.$bus.$emit('confirm-display', confirmConfig);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.theme {\n    background-color: var(--bg-secondary);\n    border: 1px solid transparent;\n    border-radius: var(--border-radius);\n    box-shadow: var(--box-shadow-small);      \n    height: 100%;\n    margin: 0;\n    overflow: hidden;\n    padding: 1rem;\n    position: relative;\n    transition: var(--transition);\n    text-align: center;\n\n    &-thumbnail {\n        display: block;\n        height: auto;\n        max-width: 100%;\n    }\n\n    &-delete {\n        align-items: center;\n        background: var(--bg-primary);\n        border-radius: 50%;\n        height: 3rem;\n        justify-content: center;\n        display: inline-flex;\n        position: absolute;\n        right: 2rem;\n        text-align: center;\n        width: 3rem;\n\n        & > svg {\n             fill: var(--icon-secondary-color);\n             transform: scale(.9);\n             transition: var(--transition);\n        }\n\n        &:hover {\n             & > svg {\n                fill: var(--warning);\n                transform: scale(1);\n             }\n        }\n    }\n\n    &-name {\n        align-items: center;\n        background: var(--gray-1);\n        border-radius: 0 0 4px 4px;\n        display: flex;\n        justify-content: space-between;\n        padding: 0 2rem;\n        text-align: left;\n\n        & > h3 {\n             font-size: 1.4rem;\n             font-weight: var(--font-weight-semibold);\n             line-height: 1.4;\n             margin: 1.2rem 0;\n        }\n    }\n\n    &-version {\n        color: var(--text-light-color);\n        display: block;\n        font-size: 1.2rem;\n        font-weight: 400;\n        margin: 0 4rem 0 auto;\n    }\n\n    &-new-version-available {\n        background: var(--highlighted);\n        left: 1rem;\n        padding: 2rem;        \n        position: absolute;\n        right: 0;\n        top: 1rem;\n        width: calc(100% - 2rem);\n\n        strong {\n            color: var(--headings-color);\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/Tools.vue",
    "content": "<template>\n    <section class=\"content tools\">\n        <div class=\"tools-container\">\n            <p-header :title=\"$t('ui.tools')\" />\n\n            <div class=\"tools-list\">\n                <div \n                    v-for=\"(item, key) in items\"\n                    :class=\"{\n                        'is-core': item.type === 'core',\n                        'is-plugin': item.type !== 'core',\n                        'plugin-is-enabled': item.type !== 'core' && !!pluginsStatus[item.directory],\n                        'plugin-is-disabled': item.type !== 'core' && !pluginsStatus[item.directory]\n                    }\"\n                    class=\"tools-list-item\"\n                    :key=\"'item-' + key\">\n                    <template v-if=\"item.type === 'core'\">\n                        <router-link :to=\"getUrl(item.link, true)\">\n                            <icon\n                                :name=\"item.icon\"\n                                iconset=\"svg-map-tools\"\n                                customWidth=\"48\"\n                                customHeight=\"48\" />\n                            {{ item.name }}\n                        </router-link>\n                    </template>\n                    <template v-else>\n                        <router-link \n                            :to=\"getUrl(item.link, pluginsStatus[item.directory])\">\n                            <img :src=\"item.icon\" />\n                            {{ item.name }}\n\n                            <switcher class=\"tools-switcher\"\n                                @click.native.prevent.stop=\"togglePluginState(item.directory)\"\n                                v-model=\"pluginsStatus[item.directory]\" />\n                        </router-link>\n                    </template>\n                </div>\n            </div>\n        </div>\n    </section>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex';\nimport Vue from 'vue';\n\nexport default {\n    name: 'tools',\n    computed: {\n        ...mapGetters([\n            'sitePlugins'\n        ]),\n        siteName () {\n            return this.$route.params.name;\n        },\n        items () {\n            let coreItems = [{\n                type: 'core',\n                name: this.$t('file.backups'),\n                link: 'tools/backups',\n                icon: 'backup'\n            }, {\n                type: 'core',\n                name: this.$t('tools.css.customCSS'),\n                link: 'tools/custom-css',\n                icon: 'css'\n            }, {\n                type: 'core',\n                name: this.$t('tools.customHTML'),\n                link: 'tools/custom-html',\n                icon: 'code'\n            }, {\n                type: 'core',\n                name: this.$t('file.fileManager'),\n                link: 'tools/file-manager',\n                icon: 'file-manager'\n            }, {\n                type: 'core',\n                name: this.$t('tools.logViewer'),\n                link: 'tools/log-viewer',\n                icon: 'log'\n            }, {\n                type: 'core',\n                name: this.$t('tools.thumbnails.regenerateThumbnails'),\n                link: 'tools/regenerate-thumbnails',\n                icon: 'regenerate-thumbnails'\n            }, {\n                type: 'core',\n                name: this.$t('tools.wpImport.wpImporter'),\n                link: 'tools/wp-importer',\n                icon: 'importer'\n            }];\n\n            let pluginItems = this.sitePlugins.map(plugin => ({\n                type: 'plugin',\n                name: plugin.name,\n                directory: plugin.directory,\n                link: 'tools/plugins/' + plugin.directory,\n                icon: plugin.thumbnail\n            }));\n\n            return coreItems.concat(pluginItems);\n        }\n    },\n    data () {\n        return {\n            pluginsStatus: {}\n        }\n    },\n    mounted () {\n        this.getPluginsStatus();\n    },\n    methods: {\n        getUrl (link, status) {\n            if (!status) {\n                return '';\n            }\n\n            return '/site/' + this.siteName + '/' + link;\n        },\n        getPluginsStatus () {\n            mainProcessAPI.send('app-site-get-plugins-state', {\n                siteName: this.siteName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-plugins-state-loaded', status => {\n                this.pluginsStatus = status;\n            });\n        },\n        activatePlugin (pluginName) {\n            mainProcessAPI.send('app-site-plugin-activate', {\n                siteName: this.siteName,\n                pluginName: pluginName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-plugin-activated', status => {\n                if (status) {\n                    Vue.set(this.pluginsStatus, pluginName, true);\n                } else {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('tools.pluginActivationError'),\n                        buttonStyle: 'danger'\n                    });\n                }\n            });\n        },\n        deactivatePlugin (pluginName) {\n            mainProcessAPI.send('app-site-plugin-deactivate', {\n                siteName: this.siteName,\n                pluginName: pluginName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-plugin-deactivated', status => {\n                if (status) {\n                    Vue.set(this.pluginsStatus, pluginName, false);\n                } else {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('tools.pluginDeactivationError'),\n                        buttonStyle: 'danger'\n                    });\n                }\n            });\n        },\n        togglePluginState (pluginName) {\n            Vue.nextTick(() => {\n                if (this.pluginsStatus[pluginName]) {\n                    this.activatePlugin(pluginName);\n                } else {\n                    this.deactivatePlugin(pluginName);\n                }\n            });\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.tools {\n    &-container {\n        margin: 0 auto;\n        max-width: $wrapper;\n        user-select: none;\n        z-index: 1;\n    }\n\n    &-list {\n        display: grid;\n        grid-template-columns: repeat(3, 1fr);\n        grid-auto-rows: minmax(calc(8rem + 8vh), auto);\n        gap: 2rem;\n\n       &-item {\n            background-color: var(--bg-secondary);\n            border: 1px solid transparent;\n            border-radius: var(--border-radius);\n            box-shadow: var(--box-shadow-small);      \n            height: 100%;\n            transition: var(--transition);\n            text-align: center;\n\n            &:hover {\n               background: var(--bg-primary);\n               border-color: var(--color-primary);\n               box-shadow: var(--box-shadow-medium);  \n               cursor: pointer;\n\n               svg {\n                  fill: var(--color-primary);\n               }\n\n               a {\n                  color: var(--color-primary);\n               }\n            }\n\n            a {\n               color: var(--text-primary-color);\n               display: flex;\n               flex-direction: column;\n               justify-content: center;\n               font-weight: var(--font-weight-semibold);\n               height: 100%;\n               padding: 4rem 1rem;\n               position: relative;\n               width: 100%;\n            }\n\n            svg {\n               display: block;\n               fill: var(--icon-primary-color);\n               margin: 0 auto 1rem;\n               transition: inherit;\n            }\n\n            img {\n               display: block;\n               height: 48px;\n               margin: 0 auto 1.4rem;\n               width: auto;\n               max-width: 60%;\n            }\n\n            &.plugin-is-disabled {\n\n                a {\n                    color: var(--text-light-color);\n                    cursor: default;\n                }\n\n                &:hover {\n                   border-color: var(--border-light-color);\n                   box-shadow: var(--box-shadow-small);  \n                   \n                   .tools-switcher {\n                         animation: tools-switcher-animation 3s linear infinite;\n\n                   }\n                }\n            }\n        }      \n    }\n\n    &-switcher {\n       position: absolute;\n       left: 1.5rem;\n       bottom: 1rem;\n\n       &:hover {\n             animation: none !important;\n       }\n    }\n\n    &-description {\n        background: var(--option-sidebar-bg);\n        float: right;\n        padding: 2.4rem 3.6rem;\n        width: 45%;\n    }\n\n    &-tab {\n        h2 {\n            font-size: $app-font-base;\n            font-weight: 400;\n            margin: 1rem 0 0;\n            text-transform: none;\n        }\n\n        div {\n            font-size: 1.4rem;\n            font-style: italic;\n        }\n    }\n}\n\n@keyframes tools-switcher-animation {\n  0% {transform:scale(1)}\n  16% {transform:scale(.8)}\n  33% {transform:scale(1)}\n  50% {transform:scale(.8)}\n  67% {transform:scale(1)}\n  84% {transform:scale(.8)}\n}\n\n/*\n * Responsive improvements\n */\n\n @media (max-height: 900px) {\n    .tools {\n        &-list {\n            &-item {\n                svg,\n                img {\n                    transform: scale(0.9);\n                }\n            }\n        }\n    }\n}\n\n@media (max-width: 1400px) {\n    .tools {\n        &-list {\n            li {\n                flex-basis: calc(50% - 2rem);\n            }\n            &-item {\n                a {\n                    padding: 3rem 1rem 3.5rem;\n                }\n                svg, \n                img {\n                    transform: scale(0.9);\n                }\n            }\n        }\n    }\n}\n\n\n@media (min-width: 1920px) {\n    .tools {\n        &-list li {\n            flex-basis: calc(25% - 2rem);\n        }\n    }\n}\n\n@media (min-width: 2560px) {\n    .tools {\n        &-list li {\n            flex-basis: calc(20% - 2rem);\n        }\n    }\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/ToolsPlugin.vue",
    "content": "<template>\n    <section class=\"content\">\n        <div class=\"plugin-content\">\n            <p-header :title=\"pluginName\">\n                <p-button\n                    :onClick=\"goBack\"\n                    slot=\"buttons\"\n                    type=\"clean back\">\n                    {{ $t('ui.backToTools') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"pluginHasConfig && hasPluginCustomOptions && pluginStandardOptionsVisible\"\n                    :onClick=\"showPluginCustomOptions\"\n                    slot=\"buttons\"\n                    type=\"clean back icon\" \n                    icon=\"settings\">\n                    {{ $t('ui.goToPluginCustomOptions') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"pluginHasConfig && hasPluginCustomOptions && !pluginStandardOptionsVisible\"\n                    :onClick=\"showPluginStandardOptions\"\n                    slot=\"buttons\"\n                    type=\"clean back icon\" \n                    icon=\"settings\">\n                    {{ $t('ui.goToPluginStandardOptions') }}\n                </p-button>\n\n                <p-button\n                    v-if=\"pluginStandardOptionsVisible\"\n                    @click.native=\"save(false, false, false)\"\n                    slot=\"buttons\"\n                    type=\"secondary\"\n                    :disabled=\"buttonsLocked\">\n                    {{ $t('settings.saveSettings') }}\n                </p-button>\n\n                <btn-dropdown\n                    v-if=\"!previewNotRequired\"\n                    slot=\"buttons\"\n                    buttonColor=\"green\"\n                    :items=\"dropdownItems\"\n                    :disabled=\"!siteHasTheme || buttonsLocked\"\n                    localStorageKey=\"publii-preview-mode\"\n                    :previewIcon=\"true\"\n                    defaultValue=\"full-site-preview\" />\n            </p-header>\n\n            <supported-features-check\n                v-if=\"requiredFeatures\"\n                :featuresToCheck=\"requiredFeatures\" />\n\n            <div\n                v-if=\"hasMessage\"\n                :class=\"'msg msg-icon msg-' + messageInOptions.type\">\n                <icon\n                    :name=\"messageInOptions.type\"\n                    customWidth=\"28\"\n                    customHeight=\"28\" />\n                <div v-pure-html=\"messageInOptions.text\"></div>\n            </div>\n\n            <template v-if=\"!pluginHasConfig && !hasPluginCustomOptions\">\n                <p>{{ $t('toolsPlugin.thisPluginHasNoOptions') }}</p>\n            </template>\n\n            <template v-if=\"pluginHasConfig && pluginStandardOptionsVisible\">\n                <fields-group\n                    v-if=\"pluginSettingsDisplay === 'tabs'\"\n                    :title=\"pluginSettingsTabsLabel\">\n                    <tabs\n                        ref=\"custom-settings-tabs\"\n                        id=\"custom-settings-tabs\"\n                        :items=\"settingsGroups\">\n                        <div\n                            v-for=\"(groupName, index) of settingsGroups\"\n                            :slot=\"'tab-' + index\"\n                            :key=\"'tab-' + index\">\n                            <div>\n                                <template v-for=\"(field, subindex) of getFieldsByGroupName(groupName)\">\n                                    <field\n                                        v-if=\"checkDependencies(field.dependencies)\"\n                                        :label=\"getFieldLabel(field)\"\n                                        :labelHidden=\"getFieldLabel(field) === ' '\"\n                                        :key=\"'tab-' + index + '-field-' + subindex\"\n                                        :noLabelSpace=\"field.type === 'separator'\"\n                                        :labelFullWidth=\"field.type === 'wysiwyg'\">\n                                        <range-slider\n                                            v-if=\"field.type === 'range'\"\n                                            :min=\"field.min\"\n                                            :max=\"field.max\"\n                                            :step=\"field.step\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :anchor=\"field.anchor\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\"></range-slider>\n\n                                        <separator\n                                            v-if=\"field.type === 'separator'\"\n                                            slot=\"field\"\n                                            :type=\"field.size\"\n                                            :label=\"field.label\"\n                                            :anchor=\"field.anchor\"\n                                            :note=\"field.note\"\n                                            :customCssClasses=\"field.customCssClasses\"></separator>\n\n                                        <text-area\n                                            v-if=\"field.type === 'textarea'\"\n                                            slot=\"field\"\n                                            :rows=\"field.rows\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :anchor=\"field.anchor\"\n                                            :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                            :cols=\"field.cols\"\n                                            :customCssClasses=\"field.customCssClasses\"\n                                            :disabled=\"field.disabled\"></text-area>\n\n                                        <text-area\n                                            v-if=\"field.type === 'wysiwyg'\"\n                                            slot=\"field\"\n                                            :id=\"'theme-settings-' + index\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :anchor=\"field.anchor\"\n                                            :wysiwyg=\"true\"\n                                            :miniEditorMode=\"true\"\n                                            :customCssClasses=\"field.customCssClasses\"></text-area>\n\n                                        <image-upload\n                                            v-if=\"field.type === 'upload'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            slot=\"field\"\n                                            :anchor=\"field.anchor\"\n                                            imageType=\"pluginImages\"\n                                            :pluginDir=\"$route.params.pluginname\"\n                                            :addMediaFolderPath=\"false\"\n                                            :onBeforeRemove=\"removePluginImage\"\n                                            :customCssClasses=\"field.customCssClasses\"></image-upload>\n\n                                        <small-image-upload\n                                            v-if=\"field.type === 'smallupload'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :anchor=\"field.anchor\"\n                                            imageType=\"pluginImages\"\n                                            :pluginDir=\"$route.params.pluginname\"\n                                            slot=\"field\"\n                                            :onBeforeRemove=\"removePluginImage\"\n                                            :customCssClasses=\"field.customCssClasses\"></small-image-upload>\n\n                                        <radio-buttons\n                                            v-if=\"field.type === 'radio'\"\n                                            :items=\"field.options\"\n                                            :name=\"field.name\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :anchor=\"field.anchor\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\" />\n\n                                        <dropdown\n                                            v-if=\"field.type === 'select'\"\n                                            slot=\"field\"\n                                            :multiple=\"field.multiple\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :id=\"field.anchor\"\n                                            :items=\"getDropdownOptions(field.options)\"\n                                            :customCssClasses=\"field.customCssClasses\"></dropdown>\n\n                                        <switcher\n                                            v-if=\"field.type === 'checkbox'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :lower-zindex=\"true\"\n                                            :anchor=\"field.anchor\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\"></switcher>\n\n                                        <color-picker\n                                            v-if=\"field.type === 'colorpicker'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :data-field=\"field.name\"\n                                            :anchor=\"field.anchor\"\n                                            :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\"></color-picker>\n\n                                        <posts-dropdown\n                                            v-if=\"field.type === 'posts-dropdown'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :allowed-post-status=\"field.allowedPostStatus || ['any']\"\n                                            :multiple=\"field.multiple\"\n                                            :anchor=\"field.anchor\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\"></posts-dropdown>\n                                        \n                                        <pages-dropdown\n                                            v-if=\"field.type === 'pages-dropdown'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :multiple=\"field.multiple\"\n                                            :anchor=\"field.anchor\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\"></pages-dropdown>\n\n                                        <tags-dropdown\n                                            v-if=\"field.type === 'tags-dropdown'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :multiple=\"field.multiple\"\n                                            :anchor=\"field.anchor\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\"></tags-dropdown>\n\n                                        <authors-dropdown\n                                            v-if=\"field.type === 'authors-dropdown'\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :multiple=\"field.multiple\"\n                                            :anchor=\"field.anchor\"\n                                            slot=\"field\"\n                                            :customCssClasses=\"field.customCssClasses\"></authors-dropdown>\n\n                                        <text-input\n                                            v-if=\"isNormalInput(field.type)\"\n                                            slot=\"field\"\n                                            :type=\"field.type\"\n                                            :min=\"field.min\"\n                                            :max=\"field.max\"\n                                            :size=\"field.size\"\n                                            :step=\"field.step\"\n                                            :pattern=\"field.pattern\"\n                                            :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :anchor=\"field.anchor\"\n                                            :disabled=\"field.disabled\"\n                                            :placeholder=\"field.placeholder\"\n                                            :customCssClasses=\"field.customCssClasses\"></text-input>\n\n                                        <repeater\n                                            v-if=\"field.type === 'repeater'\" \n                                            slot=\"field\"\n                                            :structure=\"field.structure\"\n                                            v-model=\"settingsValues[field.name]\"\n                                            :translations=\"field.translations\"\n                                            :maxCount=\"field.maxCount\"\n                                            :hasEmptyState=\"field.hasEmptyState\"\n                                            :hideLabels=\"field.hideLabels\"\n                                            :anchor=\"field.anchor\"\n                                            :settings=\"settingsValues\"\n                                            :customCssClasses=\"field.customCssClasses\"\n                                            imageType=\"pluginImages\"\n                                            :pluginDir=\"$route.params.pluginname\" />\n\n                                        <small\n                                            v-if=\"field.note && field.type !== 'separator'\"\n                                            slot=\"note\"\n                                            class=\"note\"\n                                            v-pure-html=\"field.note\">\n                                        </small>\n                                    </field>\n                                </template>\n                            </div>\n                        </div>\n                    </tabs>\n                </fields-group>\n                \n                <template v-if=\"pluginSettingsDisplay === 'fieldsets'\">\n                    <fields-group\n                        v-for=\"(groupName, index) of settingsGroups\"\n                        :key=\"'settings-group-' + index\"\n                        :title=\"groupName\">\n                        <template v-for=\"(field, subindex) of getFieldsByGroupName(groupName)\">\n                            <field\n                                v-if=\"checkDependencies(field.dependencies)\"\n                                :label=\"getFieldLabel(field)\"\n                                :labelHidden=\"getFieldLabel(field) === ' '\"\n                                :key=\"'tab-' + index + '-field-' + subindex\"\n                                :noLabelSpace=\"field.type === 'separator'\"\n                                :labelFullWidth=\"field.type === 'wysiwyg'\">\n                                <range-slider\n                                    v-if=\"field.type === 'range'\"\n                                    :min=\"field.min\"\n                                    :max=\"field.max\"\n                                    :step=\"field.step\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></range-slider>\n\n                                <separator\n                                    v-if=\"field.type === 'separator'\"\n                                    slot=\"field\"\n                                    :type=\"field.size\"\n                                    :label=\"field.label\"\n                                    :anchor=\"field.anchor\"\n                                    :note=\"field.note\"\n                                    :customCssClasses=\"field.customCssClasses\"></separator>\n\n                                <text-area\n                                    v-if=\"field.type === 'textarea'\"\n                                    slot=\"field\"\n                                    :rows=\"field.rows\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    :disabled=\"field.disabled\"\n                                    :cols=\"field.cols\"\n                                    :customCssClasses=\"field.customCssClasses\"></text-area>\n\n                                <text-area\n                                    v-if=\"field.type === 'wysiwyg'\"\n                                    slot=\"field\"\n                                    :id=\"'theme-settings-' + index\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    :wysiwyg=\"true\"\n                                    :miniEditorMode=\"true\"\n                                    :customCssClasses=\"field.customCssClasses\"></text-area>\n\n                                <image-upload\n                                    v-if=\"field.type === 'upload'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    slot=\"field\"\n                                    :anchor=\"field.anchor\"\n                                    imageType=\"pluginImages\"\n                                    :pluginDir=\"$route.params.pluginname\"\n                                    :addMediaFolderPath=\"false\"\n                                    :onBeforeRemove=\"removePluginImage\"\n                                    :customCssClasses=\"field.customCssClasses\"></image-upload>\n\n                                <small-image-upload\n                                    v-if=\"field.type === 'smallupload'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    imageType=\"pluginImages\"\n                                    :pluginDir=\"$route.params.pluginname\"\n                                    slot=\"field\"\n                                    :onBeforeRemove=\"removePluginImage\"\n                                    :customCssClasses=\"field.customCssClasses\"></small-image-upload>\n\n                                <radio-buttons\n                                    v-if=\"field.type === 'radio'\"\n                                    :items=\"field.options\"\n                                    :name=\"field.name\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\" />\n\n                                <dropdown\n                                    v-if=\"field.type === 'select'\"\n                                    slot=\"field\"\n                                    :multiple=\"field.multiple\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :id=\"field.anchor\"\n                                    :items=\"getDropdownOptions(field.options)\"\n                                    :customCssClasses=\"field.customCssClasses\"></dropdown>\n\n                                <switcher\n                                    v-if=\"field.type === 'checkbox'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :lower-zindex=\"true\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></switcher>\n\n                                <color-picker\n                                    v-if=\"field.type === 'colorpicker'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :data-field=\"field.name\"\n                                    :anchor=\"field.anchor\"\n                                    :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></color-picker>\n\n                                <posts-dropdown\n                                    v-if=\"field.type === 'posts-dropdown'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :allowed-post-status=\"field.allowedPostStatus || ['any']\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></posts-dropdown>\n\n                                <pages-dropdown\n                                    v-if=\"field.type === 'pages-dropdown'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></pages-dropdown>\n\n                                <tags-dropdown\n                                    v-if=\"field.type === 'tags-dropdown'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></tags-dropdown>\n\n                                <authors-dropdown\n                                    v-if=\"field.type === 'authors-dropdown'\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :multiple=\"field.multiple\"\n                                    :anchor=\"field.anchor\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"field.customCssClasses\"></authors-dropdown>\n\n                                <text-input\n                                    v-if=\"isNormalInput(field.type)\"\n                                    slot=\"field\"\n                                    :type=\"field.type\"\n                                    :min=\"field.min\"\n                                    :max=\"field.max\"\n                                    :size=\"field.size\"\n                                    :step=\"field.step\"\n                                    :pattern=\"field.pattern\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking && field.spellcheck\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :anchor=\"field.anchor\"\n                                    :disabled=\"field.disabled\"\n                                    :placeholder=\"field.placeholder\"\n                                    :customCssClasses=\"field.customCssClasses\"></text-input>\n\n                                <repeater\n                                    v-if=\"field.type === 'repeater'\" \n                                    slot=\"field\"\n                                    :structure=\"field.structure\"\n                                    v-model=\"settingsValues[field.name]\"\n                                    :translations=\"field.translations\"\n                                    :maxCount=\"field.maxCount\"\n                                    :hasEmptyState=\"field.hasEmptyState\"\n                                    :hideLabels=\"field.hideLabels\"\n                                    :anchor=\"field.anchor\"\n                                    :settings=\"settingsValues\"\n                                    :customCssClasses=\"field.customCssClasses\"\n                                    imageType=\"pluginImages\"\n                                    :pluginDir=\"$route.params.pluginname\" />\n\n                                <small\n                                    v-if=\"field.note && field.type !== 'separator'\"\n                                    slot=\"note\"\n                                    class=\"note\"\n                                    v-pure-html=\"field.note\">\n                                </small>\n                            </field>\n                        </template>\n                    </fields-group>\n                </template>\n\n                <p-footer>\n                    <btn-dropdown\n                        v-if=\"!previewNotRequired\"\n                        slot=\"buttons\"\n                        buttonColor=\"green\"\n                        type=\"is-reversed\"\n                        :items=\"dropdownItems\"\n                        :disabled=\"!siteHasTheme || buttonsLocked\"\n                        localStorageKey=\"publii-preview-mode\"\n                        :previewIcon=\"true\"\n                        :isReversed=\"true\"\n                        defaultValue=\"full-site-preview\" />\n\n                    <p-button\n                        v-if=\"pluginStandardOptionsVisible\"\n                        @click.native=\"save(false, false, false)\"\n                        slot=\"buttons\"\n                        type=\"secondary\"\n                        :disabled=\"buttonsLocked\">\n                        {{ $t('settings.saveSettings') }}\n                    </p-button>\n                </p-footer>\n            </template>\n            <template v-else>\n                <iframe \n                    id=\"plugin-settings-root\"\n                    :src=\"this.pluginPath + '/options/index.html'\"></iframe>\n            </template>\n        </div>\n    </section>\n</template>\n\n<script>\nimport BackToTools from './mixins/BackToTools.js';\nimport SupportedFeaturesCheck from './basic-elements/SupportedFeaturesCheck.vue';\nimport Repeater from './basic-elements/Repeater';\nimport Vue from 'vue';\n\nexport default {\n    name: 'tools-plugin',\n    mixins: [\n        BackToTools\n    ],\n    components: {\n        'supported-features-check': SupportedFeaturesCheck,\n        'repeater': Repeater\n    },\n    computed: {\n        settingsGroups () {\n            let groups = [];\n\n            this.settings.forEach(item => {\n                if (groups.indexOf(item.group) === -1) {\n                    groups.push(item.group);\n                }\n            });\n\n            return groups;\n        },\n        pluginHasConfig () {\n            return !!this.settings.length;\n        },\n        dropdownItems () {\n            return [\n                {\n                    label: this.$t('ui.previewFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'full-site-preview',\n                    isVisible: () => true,\n                    icon: 'full-preview-monitor',\n                    onClick: this.saveAndPreview.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.renderFullWebsite'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'full-site-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    icon: 'full-render-monitor',\n                    onClick: this.saveAndRender.bind(this, 'full-site')\n                },\n                {\n                    label: this.$t('ui.previewFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndPreview'),\n                    value: 'homepage-preview',\n                    icon: 'quick-preview',\n                    isVisible: () => true,\n                    onClick: this.saveAndPreview.bind(this, 'homepage')\n                },\n                {\n                    label: this.$t('ui.renderFrontPageOnly'),\n                    activeLabel: this.$t('ui.saveAndRender'),\n                    value: 'homepage-render',\n                    icon: 'quick-render',\n                    isVisible: () => !!this.$store.state.app.config.enableAdvancedPreview,\n                    onClick: this.saveAndRender.bind(this, 'homepage')\n                }\n            ];\n        },\n        siteHasTheme () {\n            return !!this.$store.state.currentSite.config.theme;\n        }\n    },\n    data () {\n        return {\n            pluginName: '',\n            pluginPath: '',\n            settings: [],\n            settingsValues: {},\n            buttonsLocked: false,\n            hasMessage: false,\n            hasPluginCustomOptions: false,\n            messageInOptions: null,\n            requiredFeatures: null,\n            pluginStandardOptionsVisible: true,\n            pluginSettingsDisplay: 'fieldsets',\n            pluginSettingsTabsLabel: '',\n            previewNotRequired: false\n        };\n    },\n    async mounted () {\n        this.loadPluginConfig(this.$route.params.pluginname, this.$route.params.name);\n        document.getElementById('plugin-settings-root').addEventListener('load', async function () {\n            this.contentWindow.window.document.querySelector('html').setAttribute('data-theme', await window.app.getCurrentAppTheme());\n        }, false);\n    },\n    methods: {\n        loadPluginConfig (pluginName, siteName) {\n            mainProcessAPI.send('app-site-get-plugin-config', {\n                siteName,\n                pluginName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-get-plugin-config-retrieved', result => {\n                if (!result) {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('tools.pluginLoadError'),\n                        buttonStyle: 'danger'\n                    });\n                    return;\n                }\n\n                this.pluginName = result.pluginData.name;\n                this.hasPluginCustomOptions = !!result.pluginData.usePluginSettingsView;\n                this.hasMessage = !!result.pluginData.messageInOptions;\n                this.messageInOptions = result.pluginData.messageInOptions;\n                this.requiredFeatures = result.pluginData.requiredFeatures || [];\n                this.pluginSettingsDisplay = result.pluginData.settingsDisplay || 'fieldsets';\n                this.pluginSettingsTabsLabel = result.pluginData.tabsTitle || this.$t('toolsPlugin.tabsLabel');\n                this.previewNotRequired = result.pluginData.previewNotRequired || false;\n\n                if (this.hasPluginCustomOptions) {\n                    this.pluginStandardOptionsVisible = false;\n                    this.pluginPath = result.pluginData.path;\n                }\n\n                if (result.pluginData.useCustomCssForOptions) {\n                    this.pluginPath = result.pluginData.path;\n                    this.loadAdditionalCss();\n                }\n\n                this.loadSettings(result.pluginData.config, result.pluginConfig);\n            });\n        },\n        getFieldLabel (field) {\n            if (field.type === 'separator') {\n                return '';\n            }\n\n            if (field.label === '' || typeof field.label === 'undefined') {\n                return ' ';\n            }\n\n            return field.label;\n        },\n        checkDependencies (dependencies) {\n            if (!dependencies || !dependencies.length) {\n                return true;\n            }\n\n            for (let i = 0; i < dependencies.length; i++) {\n                let dependencyName = dependencies[i].field;\n                let dependencyValue = dependencies[i].value;\n\n                if (\n                    Array.isArray(this.settingsValues[dependencyName]) && \n                    dependencyValue.indexOf('=') > -1\n                ) {\n                    let dependencyData = this.settingsValues[dependencyName];\n                    let fieldName = dependencyValue.split('=')[0];\n                    let acceptedValues = [];\n                    let isValidDependency = false;\n\n                    if (dependencyValue.split('=')[1]) {\n                        acceptedValues = dependencyValue.split('=')[1].split(',');\n                    }\n\n                    for (let i = 0; i < dependencyData.length; i++) {\n                        let dataRow = dependencyData[i];\n                        let valueToCompare = dataRow[fieldName];\n\n                        if (valueToCompare === true) {\n                            valueToCompare = 'true';\n                        } else if (valueToCompare === false) {\n                            valueToCompare = 'false';\n                        }\n\n                        if (acceptedValues.indexOf(valueToCompare) > -1) {\n                            isValidDependency = true;\n                        }\n                    }\n\n                    if (!isValidDependency) {\n                        return false;\n                    }\n\n                    continue;\n                }\n\n                if (dependencyValue === \"true\" && this.settingsValues[dependencyName] !== true) {\n                    return false;\n                } else if (dependencyValue === \"true\") {\n                    continue;\n                }\n\n                if (dependencyValue === \"false\" && this.settingsValues[dependencyName] !== false) {\n                    return false;\n                } else if (dependencyValue === \"false\") {\n                    continue;\n                }\n\n                if (typeof dependencyValue === 'string' && dependencyValue.indexOf(',') > -1) {\n                    let values = dependencyValue.split(',');\n                    let isValidDependency = false;\n\n                    for (let i = 0; i < values.length; i++) {\n                        if (this.settingsValues[dependencyName] === values[i]) {\n                            isValidDependency = true;\n                        }\n                    }\n\n                    return isValidDependency;\n                }\n\n                if (dependencyValue !== this.settingsValues[dependencyName]) {\n                    return false;\n                }\n            }\n\n            return true;\n        },\n        isNormalInput (type) {\n            return [\n                'separator',\n                'textarea',\n                'wysiwyg',\n                'radio',\n                'select',\n                'range',\n                'upload',\n                'smallupload',\n                'checkbox',\n                'colorpicker',\n                'posts-dropdown',\n                'authors-dropdown',\n                'tags-dropdown',\n                'repeater'\n            ].indexOf(type) === -1;\n        },\n        getDropdownOptions (inputOptions) {\n            let options = {};\n            let hasGroups = !!inputOptions.filter(option => typeof option.group !== 'undefined').length;\n\n            if (hasGroups) {\n                options.hasGroups = true;\n                let groups = {\n                    ungrouped: {}\n                };\n\n                for (let i = 0; i < inputOptions.length; i++) {\n                    let groupName = inputOptions[i].group;\n\n                    if (groupName && !groups[groupName]) {\n                        groups[groupName] = {};\n                    }\n                }\n\n                for (let i = 0; i < inputOptions.length; i++) {\n                    let inputGroupName = inputOptions[i].group;\n\n                    if (inputGroupName) {\n                        groups[inputGroupName][inputOptions[i].value] = {\n                            label: inputOptions[i].label,\n                            disabled: inputOptions[i].disabled\n                        };\n                    } else {\n                        groups['ungrouped'][inputOptions[i].value] = {\n                            label: inputOptions[i].label,\n                            disabled: inputOptions[i].disabled\n                        };\n                    }\n                }\n\n                options.groups = groups;\n            } else {\n                for (let i = 0; i < inputOptions.length; i++) {\n                    options[inputOptions[i].value] = {\n                        label: inputOptions[i].label,\n                        disabled: inputOptions[i].disabled\n                    };\n                }\n            }\n\n            return options;\n        },\n        getFieldsByGroupName (groupName) {\n            return this.settings.filter(field => field.group === groupName);\n        },\n        loadAdditionalCss () {\n            let customCssPath = this.pluginPath + '/plugin-options.css?v=' + (+new Date());\n\n            if (!document.querySelector('#custom-plugin-options-css')) {\n                $(document.body).append($('<link rel=\"stylesheet\" id=\"custom-plugin-options-css\" href=\"' + customCssPath + '\" />'));\n            }\n        },\n        loadSettings (config, savedConfig) {\n            try {\n                this.settings = JSON.parse(JSON.stringify(config));\n                savedConfig = JSON.parse(savedConfig);\n            } catch (e) {\n                this.$bus.$emit('message-display', {\n                    message: this.$t('toolsPlugin.pluginSettingsLoadError'),\n                    type: 'warning',\n                    lifeTime: 3\n                });\n                return;\n            }\n\n            let settings = config.map(field => {\n                if (field.type !== 'separator') {\n                    if (savedConfig && typeof savedConfig[field.name] !== 'undefined') {\n                        return [field.name, savedConfig[field.name]];\n                    }\n\n                    return [field.name, field.value];\n                }\n\n                return false;\n            });\n\n            for (let setting of settings) {\n                if (setting) {\n                    Vue.set(this.settingsValues, setting[0], setting[1]);\n                }\n            }\n        },\n        saveAndPreview (renderingType = false) {\n            this.save(true, renderingType, false);\n        },\n        saveAndRender (renderingType = false) {\n            this.save(true, renderingType, true);\n        },\n        save (showPreview = false, renderingType = false, renderFiles = false) {\n            this.$bus.$emit('plugin-settings-before-save');\n\n            setTimeout(async () => {\n                await this.saveSettings(showPreview, renderingType, renderFiles);\n            }, 500);\n        },\n        async saveSettings (showPreview = false, renderingType = false, renderFiles = false) {\n            mainProcessAPI.send('app-site-save-plugin-config', {\n                siteName: this.$route.params.name,\n                pluginName: this.$route.params.pluginname,\n                newConfig: this.settingsValues\n            });\n\n            mainProcessAPI.receiveOnce('app-site-plugin-config-saved', async (result) => {\n                if (result === true) {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('toolsPlugin.pluginSettingsSaveSuccess'),\n                        type: 'success',\n                        lifeTime: 3\n                    });\n\n                    if (showPreview) {\n                        let previewLocationExists = await mainProcessAPI.existsSync(this.$store.state.app.config.previewLocation);\n\n                        if (this.$store.state.app.config.previewLocation !== '' && !previewLocationExists) {\n                            this.$bus.$emit('confirm-display', {\n                                message: this.$t('sync.previewCatalogDoesNotExistInfo'),\n                                okLabel: this.$t('sync.goToAppSettings'),\n                                okClick: () => {\n                                    this.$router.push(`/app-settings/`);\n                                }\n                            });\n                            return;\n                        }\n\n                        if (renderingType === 'homepage') {\n                            this.$bus.$emit('rendering-popup-display', {\n                                homepageOnly: true,\n                                showPreview: !renderFiles,\n                            });\n                        } else {\n                            this.$bus.$emit('rendering-popup-display', {\n                                showPreview: !renderFiles \n                            });\n                        }\n                    }\n                } else {\n                    this.$bus.$emit('message-display', {\n                        message: this.$t('toolsPlugin.pluginSettingsSaveError'),\n                        type: 'warning',\n                        lifeTime: 3\n                    });\n                }\n            });\n        },\n        showPluginCustomOptions () {\n            this.pluginStandardOptionsVisible = false;\n        },\n        showPluginStandardOptions () {\n            this.pluginStandardOptionsVisible = true;\n        },\n        removePluginImage (filePath) {\n            if (filePath) {\n                mainProcessAPI.send('app-image-upload-remove', filePath, this.$route.params.name);\n            }\n        }\n    },\n    beforeDestroy () {\n        if (document.querySelector('#custom-plugin-options-css')) {\n            $('#custom-plugin-options-css').remove();\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n@import '../scss/notifications.scss';\n\n#plugin-settings-root {\n    border: none;\n    height: calc(100vh - 200px);\n    width: 100%;\n}\n\n.plugin-content {\n    margin: 0 auto;\n    max-width: $wrapper; \n    user-select: none;\n}\n\n.msg {\n    background: var(--bg-secondary);\n    margin-bottom: 3rem;\n\n    & + .msg {\n        margin-top: -2rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/TopBar.vue",
    "content": "<template>\n    <div class=\"topbar\">\n        <topbar-appbar />\n\n        <div class=\"topbar-inner\">\n            <topbar-dropdown />\n        </div>\n    </div>\n</template>\n\n<script>\nimport VersionComparator from '../helpers/version-comparator';\nimport TopBarAppBar from './TopBarAppBar';\nimport TopBarDropDown from './TopBarDropDown';\n\nexport default {\n    name: 'topbar',\n    components: {\n        'topbar-appbar': TopBarAppBar,\n        'topbar-dropdown': TopBarDropDown\n    },\n    created: function() {\n        this.getNotifications();\n        this.scheduleNotificationsCheck();\n    },\n    mounted () {\n        this.$bus.$on('app-get-notifications', this.getNotifications);\n        this.$bus.$on('app-get-forced-notifications', this.forceGetNotifications);\n        this.$bus.$on('app-update-notifications-counters', this.updateNotificationsCounters);\n    },\n    methods: {\n        forceGetNotifications () {\n            this.getNotifications(true);\n        },\n        getNotifications(forced = false) {\n            if (this.$store.state.app.config.notificationsStatus !== 'accepted') {\n                return;\n            }\n\n            this.$bus.$emit('app-receiving-notifications');\n            let shouldGetNotifications = this.shouldRetrieveNotifications() || forced;\n            let lastRetrieveTime = localStorage.getItem('publii-notification-retrieve-timestamp');\n\n            if (forced && lastRetrieveTime && new Date().getTime() < parseInt(lastRetrieveTime, 10) + (60 * 1000)) {\n                shouldGetNotifications = false;\n            }\n\n            mainProcessAPI.send('app-notifications-retrieve', shouldGetNotifications);\n\n            mainProcessAPI.receiveOnce('app-notifications-retrieved', data => {\n                if (data.status === true) {\n                    this.setNotificationsData (data);\n                    this.updateNotificationsCounters();\n                }\n\n                if (data.status === false) {\n                    this.$bus.$emit('alert-display', {\n                        message: this.$t('ui.appIsUnableToGetNotifications') + data.error,\n                        okLabel: this.$t('ui.ok'),\n                    });\n                }\n\n                this.$bus.$emit('app-received-notifications');\n\n                if (data.downloaded === true) {\n                    localStorage.setItem('publii-notification-retrieve-timestamp', new Date().getTime());\n                }\n            });\n        },\n        shouldRetrieveNotifications() {\n            let currentTime = new Date().getTime();\n            let lastRetrieveTime = localStorage.getItem('publii-notification-retrieve-timestamp');\n\n            if (lastRetrieveTime !== null) {\n                if (currentTime > parseInt(lastRetrieveTime, 10) + (12 * 60 * 60 * 1000)) {\n                    return true;\n                }\n            } else {\n                return true;\n            }\n\n            return false;\n        },\n        setNotificationsData (data) {\n            this.$store.commit('setNotifications', data.notifications);\n        },\n        scheduleNotificationsCheck () {\n            setTimeout(() => {\n                this.getNotifications();\n                this.scheduleNotificationsCheck();\n            }, (12 * 60 * 60 * 1000) + 10000); // 12 hours + 10 seconds\n        },\n        updateNotificationsCounters () {\n            let updatesCount = 0;\n            let notificationsData = this.$store.state.app.notifications;\n            let notificationsReadStatus = this.$store.state.app.notificationsReadStatus;\n            notificationsReadStatus = notificationsReadStatus.split(';');\n\n            // Check if Publii version is newest one (check build number for specific OS)\n            let currentBuild = this.$store.state.app.versionInfo.build;\n            \n            if (\n                notificationsData.publii && parseInt(notificationsData.publii.build, 10) > parseInt(currentBuild, 10) &&\n                notificationsReadStatus.indexOf('PUBLII-' + notificationsData.publii.version + '-' + notificationsData.publii.build) === -1\n            ) {\n                updatesCount++;\n            }\n\n            // Check if there are any news which are not read yet\n            let currentDate = new Date().getTime();\n            \n            for (let notification of notificationsData.news || []) {\n                if (\n                    notification.id && \n                    notificationsReadStatus.indexOf(notification.id) === -1 &&\n                    currentDate >= new Date(notification.validFrom).getTime() &&\n                    currentDate <= new Date(notification.validTo).getTime()\n                ) {\n                    updatesCount++;\n                }\n            }\n\n            // Check if there are any updates for themes\n            let installedThemes = this.$store.state.themes;\n            let availableThemes = notificationsData.themes || {};\n\n            for (let theme of installedThemes) {\n                if (availableThemes[theme.directory]) {\n                    let result = VersionComparator(availableThemes[theme.directory].version, theme.version);\n                    \n                    if (result === 1 && notificationsReadStatus.indexOf('THEME-' + theme.directory + '-' + availableThemes[theme.directory].version) === -1) {\n                        updatesCount++;\n                    }\n                }\n            }\n\n            // Check if there are any updates for plugins\n            let installedPlugins = this.$store.state.plugins;\n            let availablePlugins = notificationsData.plugins || {};\n\n            for (let plugin of installedPlugins) {    \n                if (availablePlugins[plugin.directory]) {\n                    let result = VersionComparator(availablePlugins[plugin.directory].version, plugin.version);\n                    \n                    if (result === 1 && notificationsReadStatus.indexOf('PLUGIN-' + plugin.directory + '-' + availablePlugins[plugin.directory].version) === -1) {\n                        updatesCount++;\n                    }\n                }\n            }\n\n            this.$store.commit('setNotificationsCount', updatesCount);\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('app-get-notifciations', this.getNotifications);\n        this.$bus.$off('app-get-forced-notifications', this.forceGetNotifications);\n        this.$bus.$off('app-update-notifications-counters', this.updateNotificationsCounters);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.topbar {\n    background: var(--gray-1);\n    font-size: $app-font-base;\n    height: var(--topbar-height);\n    position: absolute;\n    top: 0;\n    -webkit-app-region: no-drag;\n    -webkit-user-select: none;\n    width: 100%;\n\n    & > .topbar-inner {\n        align-items: center;\n        display: flex;             \n        padding: 0;\n        position: absolute;\n        right: 0;\n        top: var(--topbar-height);\n        width: 40px;\n        z-index: 102;\n    }\n}\n    \n/*\n * Responsive improvements\n */\n\n@media (max-height: 900px) {\n    .topbar > .topbar-inner {\n        width: 35px;\n    }\n}\n\n@media (max-width: 1400px) {\n    .topbar > .topbar-inner {\n        width: 35px;\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .topbar {\n        height: 0;\n\n        & > .topbar-inner {\n            top: 0;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/TopBarAppBar.vue",
    "content": "<template>\n    <div class=\"appbar\">\n        <span\n            @click=\"appMinimize\"\n            ref=\"minimize\"\n            class=\"appbar-button appbar-minimize\">\n            <icon\n                size=\"xxxs\"\n                name=\"win-minimize\" />\n        </span>\n\n        <span\n            v-if=\"!$store.state.app.windowIsMaximized\"\n            @click=\"appMaximize\"\n            ref=\"maximize\"\n            class=\"appbar-button appbar-maximize\">\n            <icon\n                size=\"xxxs\"\n                name=\"win-maximize\" />\n        </span>\n\n        <span\n            v-if=\"$store.state.app.windowIsMaximized\"\n            @click=\"appUnmaximize\"\n            ref=\"unmaximize\"\n            class=\"appbar-button appbar-unmaximize\">\n            <icon\n                size=\"xxxs\"\n                name=\"win-expand\" />\n        </span>\n\n        <span\n            @click=\"appClose\"\n            ref=\"close\"\n            class=\"appbar-button appbar-close\">\n            <icon\n                size=\"xxxs\"\n                name=\"win-close\" />\n        </span>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'topbar-appbar',\n    methods: {\n        appMinimize: function() {\n            mainProcessAPI.invoke('app-window:minimize');\n        },\n        appMaximize: function() {\n            mainProcessAPI.invoke('app-window:maximize');\n            this.$store.commit('setWindowState', true);\n        },\n        appUnmaximize: function() {\n            mainProcessAPI.invoke('app-window:unmaximize');\n            this.$store.commit('setWindowState', false);\n        },\n        appClose: function() {\n            mainProcessAPI.invoke('app-window:close');\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.appbar {\n    -webkit-app-region: drag; // necessary for making window draggable\n    -webkit-user-select: none; // remove conflict with the text selection\n    background: var(--top-app-bar);\n    height: var(--topbar-height);\n    padding: 0;\n    position: relative;\n    text-align: right;\n    z-index: 999999;\n\n    &-button {\n        display: none;\n    }\n}\n\nbody[data-os=\"win\"] {\n    .appbar {        \n\n        &-button {\n            -webkit-app-region: no-drag; // Make the buttons clickable again\n            display: inline-block;\n            height: 2.2rem;\n            padding: 6px 0.75rem;\n            vertical-align: top;\n            width: 2.4rem;\n\n            & > svg {\n                display: block;\n                fill: var(--icon-tertiary-color);\n                margin: 0 auto;\n            }\n\n            &:hover {\n                background: var(--input-border-color);                \n            }\n        }\n\n        &-minimize {\n            & > svg {\n                position: relative;\n                top: 4px;\n            }\n        }\n\n        &-close {\n            &:hover {\n                background: var(--warning);\n\n                & > svg {\n                    fill: var(--white);\n                }\n            }\n        }\n    }\n}\n\nbody[data-os=\"linux\"] {\n  .appbar {\n    display: none;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/TopBarDropDown.vue",
    "content": "<template>\n      <div\n        @click=\"toggleSubmenu\"\n        class=\"topbar-app-settings\"\n        :title=\"$t('ui.moreItems')\">\n        \n        <!-- Bell icon when updates available AND menu is closed -->\n        <span v-if=\"insideWebsiteUI && !submenuIsOpen && notificationsStatus === 'accepted' && notificationsCount > 0\" \n            class=\"topbar-app-settings-bell\">\n            <icon\n                name=\"notification\"\n                customWidth=\"22\"\n                customHeight=\"22\" />\n            <span class=\"topbar-app-settings-bell-badge\">\n                {{ notificationsCount }}\n            </span>\n        </span>\n\n        <span v-else-if=\"insideWebsiteUI && notificationsStatus === false && !submenuIsOpen\" \n            class=\"topbar-app-settings-bell\">\n            <icon\n                name=\"notification\"\n                customWidth=\"22\"\n                customHeight=\"22\" />\n            <span class=\"topbar-app-settings-bell-badge\">\n                !\n            </span>\n        </span>\n\n        <!-- Three dots icon when menu is open OR no updates -->\n        <span v-else\n            class=\"topbar-app-settings-icon\"\n            :class=\"{ 'is-active': submenuIsOpen }\">\n        </span>\n\n        <ul\n            ref=\"submenu\"\n            :class=\"cssClasses\">\n            <topbar-dropdown-item\n                :label=\"$t('settings.appSettings')\"\n                :title=\"$t('ui.appConfiguration')\"\n                path=\"/app-settings\" />\n            <topbar-dropdown-item\n                :label=\"$t('theme.themes')\"\n                :title=\"$t('theme.goToThemesManager')\"\n                path=\"/app-themes\" />\n            <topbar-dropdown-item\n                :label=\"$t('plugins.plugins')\"\n                :title=\"$t('plugins.goToPluginsManager')\"\n                path=\"/app-plugins\" />\n            <topbar-dropdown-item\n                :label=\"$t('langs.languages')\"\n                :title=\"$t('langs.goToLanguagesManager')\"\n                path=\"/app-languages\" />\n            <topbar-dropdown-item\n                :hasBadge=\"notificationsCount > 0 || notificationsStatus === false || notificationsStatus === 'rejected'\"\n                :badgeValue=\"badgeValue\"\n                :badgeClass=\"notificationsStatus === 'rejected' ? 'is-notice' : 'is-warning'\"\n                :label=\"$t('notifications.notifications')\"\n                :title=\"$t('notifications.goToNotificationsCenter')\"\n                path=\"/notifications-center\" />\n            <topbar-dropdown-item \n                class=\"topbar-app-submenu-separator\"\n                :label=\"$t('ui.help')\"\n                :title=\"$t('ui.checkDocumentation')\"\n                path=\"https://getpublii.com/docs/\" />\n            <topbar-dropdown-item\n                :label=\"$t('ui.reportIssue')\"\n                :title=\"$t('ui.reportBugInSupportDesk')\"\n                path=\"https://github.com/GetPublii/Publii/discussions\" />\n            <topbar-dropdown-item\n                :label=\"$t('ui.githubRepository')\"\n                :title=\"$t('ui.publiiOnGithub')\"\n                path=\"https://github.com/getpublii/publii\" />\n            <topbar-dropdown-item\n                :label=\"$t('ui.donate')\"\n                :title=\"$t('ui.supportPublii')\"\n                path=\"https://getpublii.com/donate/\" />\n            <topbar-dropdown-item\n                :label=\"$t('ui.aboutPublii')\"\n                :title=\"$t('ui.moreInformationOnPublii')\"\n                path=\"/about\" />\n        </ul>\n    </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex';\nimport TopBarDropDownItem from './TopBarDropDownItem';\n\nexport default {\n    name: 'topbar-dropdown',\n    components: {\n        'topbar-dropdown-item': TopBarDropDownItem\n    },\n    data: function() {\n        return {\n            submenuIsOpen: false\n        };\n    },\n    computed: {\n        ...mapGetters([\n            'notificationsStatus', \n            'notificationsCount'\n        ]),\n        cssClasses: function() {\n            return {\n                'is-hidden': !this.submenuIsOpen,\n                'topbar-app-submenu': true\n            };\n        },\n        badgeValue () {\n            if (this.notificationsStatus === false) {\n                return '!';\n            }\n            \n            if (this.notificationsStatus === 'accepted') {\n                return this.notificationsCount > 99 ? '99+' : this.notificationsCount;\n            }\n\n            return this.$t('notifications.disabled');\n        },\n        insideWebsiteUI () {\n            return this.$route.path.indexOf('/site/') === 0;\n        }\n    },\n    mounted: function(e) {\n        this.$bus.$on('document-body-clicked', this.hideSubmenu);\n    },\n    methods: {\n        hideSubmenu () {\n            this.submenuIsOpen = false;\n        },\n        toggleSubmenu (e) {\n            e.stopPropagation();\n            this.submenuIsOpen = !this.submenuIsOpen;\n            this.$bus.$off('document-body-clicked', this.hideSubmenu);\n            this.$bus.$emit('document-body-clicked');\n            this.$bus.$on('document-body-clicked', this.hideSubmenu);\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('document-body-clicked', this.hideSubmenu);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.topbar {\n    &-app-settings {\n        color: var(--icon-secondary-color);\n        cursor: pointer;\n        display: block;\n        height: 5rem;\n        order: 3;\n        padding: 0 0 0 1.5rem;\n        width: 35px;\n\n        &:hover {\n            color: var(--icon-tertiary-color);\n        }\n\n        &-icon {\n            background: currentColor;\n            border-radius: 50%;\n            display: block;\n            height: 3px;\n            margin-top: -2px;\n            pointer-events: none;\n            position: relative;\n            right: -1px;\n            top: 50%;\n            width: 3px;\n\n            &:after,\n            &:before {\n                background: currentcolor;\n                border-radius: 50%;\n                content: \"\";\n                display: block;\n                height: 3px;\n                position: absolute;\n                top: -6px;\n                width: 3px;\n            }\n\n            &:before {\n                top: 6px;\n            }\n        }\n\n        &-bell {\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            position: relative;\n            height: 100%;\n            width: 35px;\n            margin-left: -1.7rem;\n \n\n            svg {\n                color: var(--icon-secondary-color);\n            }\n\n           &-badge {\n                align-items: center;\n                aspect-ratio: 1/1;\n                background: rgba(var(--warning-rgb), 1);\n                border: 2px solid var(--bg-site);\n                border-radius: 50%;\n                color: white;\n                display: flex;\n                font-size: 1rem;\n                font-weight: var(--font-weight-semibold);\n                height: 20px;\n                justify-content: center;\n                min-height: 20px;\n                min-width: 20px;\n                padding: 0 4px; \n                position: absolute;\n                right: 0;\n                top: 5px;\n                width: auto;   \n            }\n        }\n\n    }\n\n    &-app-settings {\n        -webkit-app-region: no-drag; // Make the buttons clickable again\n        font-weight: var(--font-weight-semibold);\n\n        & > svg {\n            height: 2.4rem;\n            position: relative;\n            top: .6rem;\n            width: 2.4rem;\n        }\n    }\n\n    &-app-submenu {\n        background: var(--popup-bg);\n        box-shadow: var(--box-shadow-medium);\n        border-radius: var(--border-radius);\n        cursor: default;\n        font-size: 1.4rem;\n        list-style-type: none;\n        padding: 2rem 0;\n        position: absolute;\n        right: 3.2rem;\n        top: 1.5rem;\n\n        &-separator {\n            border-bottom: 1px solid var(--border-light-color);\n            margin-bottom: 2rem;\n            padding-bottom: 1rem;\n        }\n    }\n}\n\n/*\n * Responsive improvements\n */\n\n@media (max-height: 900px) {\n    .topbar-app-submenu {\n        right: 2.8rem;\n    }\n}\n\n@media (max-width: 1400px) {\n    .topbar-app-submenu {\n        right: 2.8rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/TopBarDropDownItem.vue",
    "content": "<template>\n    <li :title=\"title\">\n        <a \n            v-if=\"!path\"\n            href=\"#\"\n            @click.prevent=\"onClick\">\n            {{ label }}\n            <span \n                v-if=\"hasBadge\" \n                :class=\"badgeCssClass\">\n                {{ badgeValue }}\n            </span>\n        </a>\n\n        <router-link\n            v-if=\"path && !isExternal\"\n            :to=\"path\">\n            {{ label }}\n            <span \n                v-if=\"hasBadge\" \n                :class=\"badgeCssClass\">\n                {{ badgeValue }}\n            </span>\n        </router-link>\n\n        <a\n            v-if=\"path && isExternal\"\n            :href=\"path\"\n            target=\"_blank\" \n            rel=\"noopener noreferrer\">\n            {{ label }}\n            <span \n                v-if=\"hasBadge\" \n                :class=\"badgeCssClass\">\n                {{ badgeValue }}\n            </span>\n        </a>\n    </li>\n</template>\n\n<script>\nexport default {\n    name: 'topbar-dropdown-item',\n    props: [\n        'hasBadge',\n        'badgeValue',\n        'badgeClass',\n        'onClick',\n        'label',\n        'path',\n        'title'\n    ],\n    computed: {\n        badgeCssClass () {\n            return this.badgeClass ? `badge ${this.badgeClass}` : 'badge';\n        },\n        isExternal () {\n            return this.path.indexOf('http://') === 0 || this.path.indexOf('https://') === 0;\n        }\n    },\n    methods: {\n        openExternalLink (e) {\n            e.preventDefault();\n            mainProcessAPI.shellOpenExternal(this.path);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\nli {\n    & > a {\n        color: var(--link-invert-color);\n        display: block;\n        font-weight: 400;\n        padding: .5rem 3rem;\n        white-space: nowrap;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--link-invert-color-hover);\n        }\n\n        &:has(.badge) {\n            align-items: center;\n            display: flex;\n        }\n\n        .badge {\n            align-items: center;\n            aspect-ratio: 1/1;\n            border-radius: 50%;\n            display: inline-flex;\n            font-size: 1rem;\n            font-weight: var(--font-weight-semibold);\n            height: 21px;\n            justify-content: center;\n            min-height: 21px;\n            min-width: 21px;\n            padding: 6px; \n            position: relative;\n            right: -4px;\n            top: -2px;\n            width: auto;   \n            z-index: 2;\n\n            &.is-warning {\n                background: rgba(var(--warning-rgb), 1);\n                border: 2px solid var(--bg-site);\n                color: white;\n            }\n\n            &.is-notice {\n                aspect-ratio: unset;\n                background: var(--gray-6);\n                border-radius: 6px;\n                color: var(--text-primary-color);\n                font-size: 1rem;\n                top: -2px;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/WPImport.vue",
    "content": "<template>\n    <section class=\"content\">\n        <div class=\"wp-import\">\n            <p-header :title=\"$t('tools.wpImport.wpImporter')\">\n                <p-button\n                    :onClick=\"goBack\"\n                    slot=\"buttons\"\n                    type=\"clean back\">\n                    {{ $t('ui.backToTools') }}\n                </p-button>\n            </p-header>\n\n            <fields-group>\n                <field\n                    id=\"wxr-file\"\n                    :label=\"$t('tools.wpImport.selectWXRFileLabel')\"\n                    :labelFullWidth=\"true\">\n                    <file-select\n                        id=\"wxr-file\"\n                        :placeholder=\"$t('tools.wpImport.selectWXRFilePlaceholder')\"\n                        value=\"\"\n                        ref=\"wxr-file\"\n                        :disabled=\"uploadDisabled\"\n                        :onChange=\"selectedFileChanged\"\n                        slot=\"field\" />\n                </field>\n\n                <small\n                    v-if=\"!stats\"\n                    class=\"note\">\n                    {{ $t('tools.wpImport.importNote') }}\n                </small>\n\n                <div\n                    v-if=\"checkingFile\"\n                    class=\"import-check-results\">\n                    {{ $t('tools.wpImport.checkingWXRFile') }}&hellip;\n                </div>\n\n                <div\n                    v-if=\"errorMessage\"\n                    class=\"import-check-results is-error\">\n                    {{ errorMessage }}\n                </div>\n\n                <wp-import-stats\n                    v-if=\"stats\"\n                    :stats=\"stats\" />\n\n                <div\n                    v-if=\"configVisible\"\n                    :class=\"importConfigCssClasses\">\n                    <div class=\"import-config-section\">\n                        <strong>{{ $t('tools.wpImport.importSelectedTypesOfPosts') }}</strong>\n\n                        <field\n                            id=\"import-cpt-post\"\n                            :label=\"$t('post.posts')\"\n                            :labelSeparated=\"false\"\n                            :noLabelSpace=\"true\"\n                            spacing=\"small\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"import-cpt-post\"\n                                ref=\"import-cpt-post\"\n                                :checked=\"true\" />\n                        </field>\n\n                        <field\n                            id=\"import-cpt-page\"\n                            :label=\"$t('tools.wpImport.pages')\"\n                            :labelSeparated=\"false\"\n                            :noLabelSpace=\"true\"\n                            spacing=\"small\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"import-cpt-page\"\n                                ref=\"import-cpt-page\"\n                                :checked=\"true\" />\n                        </field>\n\n                        <field\n                            v-for=\"(cpt, index) in customPostTypes\"\n                            :id=\"'import-cpt-' + cpt\"\n                            :label=\"cpt\"\n                            :labelSeparated=\"false\"\n                            :noLabelSpace=\"true\"\n                            :key=\"'custom-post-type-item-' + index\"\n                            spacing=\"small\">\n                            <switcher\n                                slot=\"field\"\n                                :id=\"'import-cpt-' + cpt\"\n                                :ref=\"'import-cpt-' + cpt\"\n                                :checked=\"true\" />\n                        </field>\n                    </div>\n\n                    <div class=\"import-config-section\">\n                        <strong>{{ $t('tools.wpImport.usedTaxonomyForPosts') }}</strong>\n\n                        <radio-buttons\n                            name=\"taxonomy\"\n                            :items=\"radioTaxonomyItems\"\n                            selected=\"tags\"\n                            ref=\"taxonomy\" />\n                    </div>\n\n                    <div class=\"import-config-section\">\n                        <strong>{{ $t('tools.wpImport.postAuthors') }}</strong>\n\n                        <radio-buttons\n                            name=\"authors\"\n                            :items=\"radioAuthorItems\"\n                            selected=\"publii-author\"\n                            ref=\"authors\" />\n                    </div>\n\n                    <div class=\"import-config-section\">\n                        <strong>{{ $t('tools.wpImport.contentFormatting') }}</strong>\n\n                        <field\n                            id=\"use-autop\"\n                            :label=\"$t('tools.wpImport.addTagsToContentAutomatically')\"\n                            :labelSeparated=\"false\"\n                            :noLabelSpace=\"true\"\n                            spacing=\"small\">\n                            <switcher\n                                slot=\"field\"\n                                id=\"use-autop\"\n                                ref=\"use-autop\"\n                                :checked=\"false\" />\n                        </field>\n                    </div>\n                </div>\n\n                <p-button\n                    v-if=\"configVisible\"\n                    :onClick=\"importFile\"\n                    :disabled=\"importInProgress\"\n                    type=\"primary\">\n                    <template v-if=\"!importInProgress\">{{ $t('tools.wpImport.importData') }}</template>\n                    <template v-if=\"importInProgress\">{{ $t('tools.wpImport.importingData') }}&hellip;</template>\n                </p-button>\n\n                <span\n                    v-if=\"configVisible && progressInfo\"\n                    id=\"import-progress\">\n                    {{ progressInfo }}\n                </span>\n            </fields-group>\n        </div>\n    </section>\n</template>\n\n<script>\nimport BackToTools from './mixins/BackToTools.js';\nimport WPImportStats from './WPImportStats';\n\nexport default {\n    name: 'wp-import',\n    mixins: [\n        BackToTools\n    ],\n    components: {\n        'wp-import-stats': WPImportStats\n    },\n    data: function() {\n        return {\n            filePath: '',\n            uploadDisabled: false,\n            configVisible: false,\n            checkingFile: false,\n            customPostTypes: [],\n            errorMessage: '',\n            stats: false,\n            progressInfo: '',\n            importInProgress: false,\n            radioAuthorItems: [\n                {\n                    value: \"publii-author\",\n                    label: this.$t('tools.wpImport.useMainAuthor')\n                },\n                {\n                    value: \"wp-authors\",\n                    label: this.$t('tools.wpImport.importAuthors')\n                }\n            ],\n            radioTaxonomyItems: [\n                {\n                    value: \"tags\",\n                    label: this.$t('ui.tags')\n                },\n                {\n                    value: \"categories\",\n                    label: this.$t('tools.wpImport.categories')\n                }\n            ]\n        };\n    },\n    computed: {\n        importConfigCssClasses: function() {\n            return {\n                'import-config': true,\n                'is-inactive': this.importInProgress\n            };\n        }\n    },\n    methods: {\n        selectedFileChanged: function(filePath) {\n            if (filePath === '') {\n                this.resetState();\n                return;\n            }\n\n            this.filePath = filePath;\n            this.fileSelected();\n        },\n        fileSelected: function() {\n            mainProcessAPI.send('app-wxr-check', {\n                siteName: this.$store.state.currentSite.config.name,\n                filePath: this.filePath\n            });\n\n            this.uploadDisabled = true;\n            this.checkingFile = true;\n            mainProcessAPI.receiveOnce('app-wxr-checked', (data) => {\n                this.checkFile(data);\n            });\n        },\n        checkFile: function(data) {\n            this.uploadDisabled = false;\n            this.checkingFile = false;\n\n            if(data.status === 'error') {\n                if (data.message.translation) {\n                    this.errorMessage = this.$t(data.message.translation, data.message.translationVars);\n                } else {\n                    this.errorMessage = data.message;\n                }\n            }\n\n            if(data.status === 'success') {\n                this.configVisible = true;\n\n                for(let postType of Object.keys(data.message.types)) {\n                    if(['post', 'page', 'image'].indexOf(postType) !== -1) {\n                        continue;\n                    }\n\n                    this.customPostTypes.push(postType);\n                }\n\n                this.stats = data.message;\n            }\n        },\n        importFile: function() {\n            this.progressInfo = '';\n            this.importInProgress = true;\n            this.uploadDisabled = true;\n            let selectedPostTypes = [];\n            let allRefs = Object.keys(this.$refs);\n\n            for(let i = 0; i < allRefs.length; i++) {\n                if(allRefs[i].indexOf('import-cpt-') === 0 && this.$refs[allRefs[i]].isChecked) {\n                    selectedPostTypes.push(allRefs[i].replace('import-cpt-', ''));\n                }\n            }\n\n            mainProcessAPI.send('app-wxr-import', {\n                siteName: this.$store.state.currentSite.config.name,\n                filePath: this.filePath,\n                importAuthors: this.$refs['authors'].content,\n                usedTaxonomy: this.$refs['taxonomy'].content,\n                autop: this.$refs['use-autop'].isChecked,\n                postTypes: selectedPostTypes\n            });\n\n            this.bindedFileImported = this.fileImported.bind(this);\n            mainProcessAPI.receiveOnce('app-wxr-imported', this.bindedFileImported);\n\n            this.bindedFileImportProgress = this.fileImportProgress.bind(this);\n            mainProcessAPI.receive('app-wxr-import-progress', this.bindedFileImportProgress);\n        },\n        fileImportProgress(data) {\n            this.progressInfo = this.$t(data.message.translation, data.message.translationVars);\n        },\n        fileImported: function(data) {\n            let siteName = this.$store.state.currentSite.config.name;\n            this.importInProgress = false;\n\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('tools.wpImport.wpImportGoToRegenerateMsg'),\n                okLabel: this.$t('tools.goToTools'),\n                cancelLabel: this.$t('ui.ok'),\n                okClick: () => {\n                    let currentSite = this.$route.params.name;\n                    this.$router.push(this.$route.path.replace('wp-importer', 'regenerate-thumbnails'));\n                }\n            });\n\n            this.resetState();\n\n            mainProcessAPI.send('app-site-reload', {\n                siteName: siteName\n            });\n\n            mainProcessAPI.receiveOnce('app-site-reloaded', (result) => {\n                this.$store.commit('setSiteConfig', result);\n                this.$store.commit('switchSite', result.data);\n            });\n        },\n        resetState() {\n            this.filePath = '';\n            this.configVisible = false;\n            this.uploadDisabled = false;\n            this.checkingFile = false;\n            this.customPostTypes = [];\n            this.errorMessage = '';\n            this.stats = false;\n            this.progressInfo = '';\n            this.importInProgress = false;\n        }\n    },\n    beforeDestroy: function() {\n        if(this.bindedFileImported) {\n            mainProcessAPI.stopReceive('app-wxr-imported', this.bindedFileImported);\n        }\n\n        if(this.bindedFileImportProgress) {\n            mainProcessAPI.stopReceive('app-wxr-import-progress', this.bindedFileImportProgress);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.wp-import {\n    margin: 0 auto;\n    max-width: $wrapper;\n    user-select: none;\n\n    p {\n        margin-top: 0;\n    }\n\n    .note {\n        clear: both;\n        color: var(--text-light-color);\n        display: block;\n        font-size: 1.35rem;\n        font-style: italic;\n        line-height: 1.4;\n        padding: .5rem 0 1rem 0;\n        user-select: text;\n    }\n\n    #import-data {\n        margin-top: 2rem;\n        width: auto;\n    }\n\n    #import-progress {\n        color: var(--gray-4);\n        display: inline-block;\n        font-style: italic;\n        margin: 2rem;\n    }\n\n    .import-check-results {\n        clear: both;\n        padding: 1rem 0 0 0;\n\n        &.is-error {\n            color: var(--warning);\n        }\n    }\n\n    .import-config {\n        margin: 0 0 2rem 0;\n\n        &-section {\n            border-bottom: 2px solid var(--gray-1);\n            font-weight: var(--font-weight-semibold);\n            margin: 0;\n            padding: 3rem 0;\n        }\n\n        & > p {\n            margin: 0;\n        }\n\n        label {\n            display: inline-block;\n            font-weight: 400;\n            margin: 1rem 2rem 0 0;\n        }\n\n        &.is-inactive {\n            opacity: .5;\n            pointer-events: none;\n        }\n    }\n\n    .import-config-section {\n        strong {\n            display: block;\n            padding-bottom: 1rem;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/WPImportStats.vue",
    "content": "<template>\n    <div\n        v-if=\"stats\"\n        class=\"import-stats\">\n        <strong>{{ $t('tools.wpImport.duringWXRAnalyzeWeHaveFound') }}:</strong>\n        <p>{{ $t('ui.posts') }}: <strong>{{ stats.types.post }}</strong></p>\n        <p>{{ $t('ui.pages') }}: <strong>{{ stats.types.page }}</strong></p>\n        <p>{{ $t('image.images') }}: <strong>{{ stats.types.image }}</strong></p>\n        <p v-if=\"countCpt().length\">{{ $t('post.customPostTypes') }}:</p>\n        <p v-for=\"cpt in countCpt()\"> - {{ cpt.type }}: <strong>{{ cpt.count }}</strong></p>\n        <p>{{ $t('ui.tags') }}: <strong>{{ stats.tags }}</strong></p>\n        <p>{{ $t('tools.wpImport.categories') }}: <strong>{{ stats.categories }}</strong></p>\n        <p>{{ $t('ui.authors') }}: <strong>{{ stats.authors }}</strong></p>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'wp-import-stats',\n    props: {\n        stats: {\n            default: false,\n            type: [Object, Boolean]\n        }\n    },\n    methods: {\n        countCpt: function() {\n            let result = [];\n\n            for(let postType of Object.keys(this.stats.types)) {\n                if(['post', 'page', 'image'].indexOf(postType) !== -1) {\n                    continue;\n                }\n\n                result.push({\n                    type: postType,\n                    count: this.stats.types[postType]\n                });\n            }\n\n            return result;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../scss/variables.scss';\n\n.import-stats {\n    border-bottom: 2px solid var(--gray-1);\n    padding: 4rem 0 3rem 0;\n\n    & > strong {\n        display: block;\n        font-weight: 600;\n        margin-bottom: 2rem;\n    }\n\n    & > p {\n        margin: .25rem 0;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Alert.vue",
    "content": "<template>\n    <div class=\"overlay\" v-if=\"isVisible\">\n        <div class=\"popup\">\n            <p\n                :class=\"cssClasses\"\n                v-pure-html=\"message\">\n            </p>\n\n            <div class=\"buttons\">\n                <p-button\n                    :type=\"buttonType\"\n                    :onClick=\"onOk\">\n                    {{ buttonText }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'alert',\n    data: function() {\n        return {\n            isVisible: false,\n            textCentered: false,\n            message: '',\n            buttonStyle: 'normal',\n            buttonText: this.$t('ui.ok'),\n            okClick: () => false\n        };\n    },\n    computed: {\n        cssClasses: function() {\n            return {\n                'message': true,\n                'text-centered': this.textCentered\n            };\n        },\n        buttonType: function() {\n            let types = 'medium no-border-radius full-width';\n\n            if(this.buttonStyle === 'normal') {\n                return types;\n            }\n\n            return types + ' ' + this.buttonStyle;\n        }\n    },\n    mounted: function() {\n        this.$bus.$on('alert-display', (config) => {\n            document.body.classList.add('has-popup-visible');\n\n            setTimeout(() => {\n                this.isVisible = true;\n                this.message = config.message;\n                this.textCentered = config.textCentered || false;\n                this.buttonStyle = config.buttonStyle || 'normal';\n\n                if (config.okLabel) {\n                    this.buttonText = config.okLabel;\n                } else {\n                    this.buttonText = this.$t('ui.ok');\n                }\n\n                if (config.okClick) {\n                    this.okClick = config.okClick;\n                } else {\n                    this.okClick = () => false;\n                }\n            }, 0);\n        });\n\n        document.body.addEventListener('keydown', this.onDocumentKeyDown);\n    },\n    methods: {\n        onOk: function() {\n            this.isVisible = false;\n            document.body.classList.remove('has-popup-visible');\n            this.okClick();\n        },\n        onDocumentKeyDown (e) {\n            if (e.code === 'Enter' && !event.isComposing && this.isVisible) {\n                this.onEnterKey();\n            }\n        },\n        onEnterKey () {\n            this.onOk();\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('alert-display');\n        document.body.removeEventListener('keydown', this.onDocumentKeyDown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100006;\n}\n\n.popup {\n    max-width: 60rem;\n    min-width: 40rem;\n\n    p {\n        max-height: 400px;\n        overflow: auto;\n        -webkit-user-select: text;\n        user-select: text;\n    }\n}\n\n.buttons {\n    display: flex;\n    margin: .5rem 0 0 0;\n    position: relative;\n    text-align: center;\n    top: 1px;\n    width: 100%;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/AuthorsDropDown.vue",
    "content": "<template>\n<div>\n    <v-select\n        ref=\"dropdown\"\n        :options=\"authors\"\n        v-model=\"selectedAuthor\"\n        :custom-label=\"authorLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        @select=\"closeDropdown()\"\n        :multiple=\"multiple\"\n        :id=\"anchor\"\n        :class=\"customCssClasses.replace(/[^a-z0-9\\-\\_\\s]/gmi, '')\"\n        :placeholder=\"placeholder\"></v-select>\n</div>\n</template>\n\n<script>\nexport default {\n    name: 'authors-dropdown',\n    props: {\n        multiple: {\n            type: Boolean,\n            default: false\n        },\n        value: {},\n        anchor: {\n            type: String,\n            default: '',\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    data () {\n        return {\n            selectedAuthor: ''\n        };\n    },\n    computed: {\n        authors () {\n            return [''].concat(this.$store.state.currentSite.authors.slice().sort((a, b) => {\n                return a.username.localeCompare(b.username);\n            }).map(author => author.id));\n        },\n        placeholder () {\n            return this.$t('author.selectAuthor');\n        }\n    },\n    watch: {\n        value: function (newValue, oldValue) {\n            this.selectedAuthor = newValue;\n        },\n        selectedAuthor: function (newValue, oldValue) {\n            this.$emit('input', this.selectedAuthor);\n        }\n    },\n    mounted () {\n        if (this.value) {\n            this.selectedAuthor = this.value;\n        }\n    },\n    methods: {\n        authorLabels (value) {\n            return this.$store.state.currentSite.authors.filter(author => author.id === value).map(author => author.name)[0];\n        },\n        closeDropdown () {\n            this.$refs['dropdown'].isOpen = false;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n.multiselect {\n    line-height: 2;\n}\n\n.multiselect,\n.multiselect__tags {\n    min-height: 49px;\n}\n\n.multiselect__tags {\n    padding: 0 4rem 0 1.8rem;\n}\n\n.multiselect__input {\n    max-width: 100%;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Button.vue",
    "content": "<template>\n    <span\n        :class=\"cssClasses\"\n        @click=\"onClick\"\n        :title=\"title\">\n        <icon\n            v-if=\"icon\"\n            size=\"s\"\n            properties=\"not-clickable\"\n            :name=\"icon\" />\n\n        <slot v-if=\"!isPreloader\"></slot>\n\n        <span\n            v-if=\"isPreloader\"\n            class=\"preloader\"></span>\n    </span>\n</template>\n\n<script>\nexport default {\n    name: 'p-button',\n    props: {\n        'icon': {\n            default: '',\n            type: String\n        },\n        'type': {\n            default: '',\n            type: String\n        },\n        'disabled': {\n            default: false,\n            type: Boolean\n        },\n        'onClick': {\n            default: () => false,\n            type: Function\n        },\n        'title': {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        isPreloader: function() {\n            return this.type.split(' ').indexOf('preloader') > -1;\n        },\n        cssClasses: function() {\n            let types = [];\n\n            if(this.type) {\n                types = this.type.split(' ');\n            }\n\n            return {\n                'button': true,\n                'button-primary': types.indexOf('primary') > -1,\n                'button-secondary': types.indexOf('secondary') > -1,\n                'button-success': types.indexOf('success') > -1,\n                'button-danger': types.indexOf('danger') > -1,\n                'button-error': types.indexOf('error') > -1,\n                'button-green': types.indexOf('green') > -1,\n                'button-outline': types.indexOf('outline') > -1,\n                'button-muted': types.indexOf('muted') > -1,\n                'button-cancel-popup': types.indexOf('cancel-popup') > -1,\n                'button-icon': types.indexOf('icon') > -1,\n                'button-only-icon': types.indexOf('only-icon') > -1,\n                'button-only-icon-color': types.indexOf('only-icon-color') > -1,\n                'button-icon-smaller': types.indexOf('icon-smaller') > -1,\n                'button-bottom': types.indexOf('bottom') > -1,\n                'button-medium': types.indexOf('medium') > -1,\n                'button-small': types.indexOf('small') > -1,\n                'button-full-width': types.indexOf('full-width') > -1,\n                'button-half-width': types.indexOf('half-width') > -1,\n                'button-quarter-width': types.indexOf('quarter-width') > -1,\n                'button-no-border-radius': types.indexOf('no-border-radius') > -1,\n                'button-disabled': types.indexOf('disabled') > -1 || this.disabled,\n                'button-disabled-with-events': types.indexOf('disabled-with-events') > -1,\n                'button-preloader': this.isPreloader,\n                'button-light': types.indexOf('light') > -1,\n                'button-active': types.indexOf('active') > -1,\n                'button-delete': types.indexOf('delete') > -1,\n                'button-clean': types.indexOf('clean') > -1,\n                'button-clean-invert': types.indexOf('clean-invert') > -1,\n                'button-back': types.indexOf('back') > -1,\n            }\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n/*\n * Basic button\n */\n\n.button {\n    background: var(--button-bg);\n    border: none;\n    border-radius: var(--border-radius);\n    box-shadow: none;\n    color: var(--white);\n    cursor: pointer;\n    display: inline-block;\n    font-size: 1.3rem;\n    font-family: var(--font-base);\n    font-weight: var(--font-weight-semibold);  \n    height: 4.4rem;\n    line-height: 4.3rem;\n    padding: 0 1.3rem;\n    position: relative;\n    transition: var(--transition);\n    user-select: none;\n    white-space: nowrap;\n\n    &:focus {\n        outline: none;\n    }\n\n    &:active,\n    &:focus,\n    &:hover,\n    &.button-active {\n        background: var(--button-bg-hover);\n        color: var(--white);\n    }\n\n    & > svg {\n        display: inline-block;\n        fill: var(--white);\n        left: 1.8rem;\n        position: absolute;\n        top: 50%;\n        transform: translateY(-50%);\n    }\n\n    &-link {\n       background: var(--bg-primary);\n        color: var(--color-primary);\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--gray-1);\n            color: var(--color-primary);\n        }\n    }\n\n    &-primary,\n    &-success {\n        background: var(--button-tertiary-bg);\n        color: var(--white);\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--button-tertiary-bg-hover);\n            color: var(--white);\n        }\n    }\n\n    &-secondary {\n        background: var(--button-secondary-bg);\n        color: var(--button-secondary-color);\n\n        & > svg {\n           fill: var(--button-secondary-color);\n        }\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--button-secondary-bg-hover);\n            color: var(--button-secondary-color-hover);\n\n            & > svg {\n                fill: var(--button-secondary-color-hover);\n            }\n        }  \n    }\n\n    &-cancel-popup {\n        background: var(--popup-btn-cancel-bg);\n        border: none;\n        border-top: 1px solid var(--input-border-color);\n        color: var(--popup-btn-cancel-color);\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--popup-btn-cancel-bg-hover);\n            color: var(--popup-btn-cancel-hover-color);\n        }\n    }\n\n    &-danger,\n    &-error {\n        background: var(--button-red-bg);\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--button-red-bg-hover);\n            color: var(--white);\n        }\n    }\n\n    &-green {\n        background: var(--success); \n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n             background: var(--success); \n        }\n    }\n\n    &-muted {\n        background: var(--gray-4);\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--color-primary);\n            color: var(--white);\n        }\n    }\n\n    &-outline {\n        background: transparent;\n        box-shadow: inset 0 0 0 2px var(--input-border-color);\n        color: var(--text-primary-color);\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: transparent;\n            box-shadow: inset 0 0 0 2px var(--gray-3);\n            color: var(--text-primary-color);\n\n            &:disabled {\n                box-shadow: inset 0 0 0 2px var(--input-border-color);\n            }\n        }\n    }\n\n    &-clean {\n        background: transparent;\n        box-shadow: none;\n        color: var(--link-primary-color);\n        font: {\n           size: 1.3rem;\n           weight: 400;\n        }\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: transparent;\n            box-shadow: none;\n            color: var(--link-primary-color-hover);\n        }\n    }\n\n    &-clean-invert {\n        background: transparent;\n        box-shadow: none;\n        color: var(--link-primary-color-hover);\n        font: {\n            size: 1.3rem;\n            weight: 400;\n        }\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: transparent;\n            box-shadow: none;\n            color: var(--link-primary-color);\n        }\n    }\n\n    &-back {\n        & + .button {\n            margin-left: 2rem !important;\n        }\n    }\n\n    &-bottom {\n        background: var(--button-tertiary-bg);\n        border-radius: 0 0 3px 3px;\n        display: block;\n        font-size: 1.3rem;\n        height: 5.6rem;\n        line-height: 5.6rem;\n        padding: 0 2rem;\n        text-align: center;\n        width: 100%;\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--button-tertiary-bg-hover);\n        }\n\n        & > svg {\n            left: -1rem!important;\n            margin-top: -.5rem;\n            position: relative;\n            top: 4px;\n            transform: none;\n        }\n\n        &.button-outline {\n            background: transparent;\n            box-shadow: inset 0 0 0 2px var(--input-border-color);\n            color: var(--text-primary-color);\n\n            &:active,\n            &:focus,\n            &:hover,\n            &.button-active {\n                background: transparent;\n                box-shadow: inset 0 0 0 2px var(--gray-3);\n                color: var(--text-primary-color);\n\n                &:disabled {\n                    box-shadow: inset 0 0 0 2px var(--input-border-color);\n                }\n            }\n        }\n    }\n\n    &-medium {\n        font-size: 1.3rem;\n        font-weight: var(--font-weight-semibold);\n        height: 5.6rem;\n        line-height: 5.5rem;\n        padding: 0 2rem;\n    }\n\n    &-small {\n        font-size: 1.3rem;\n        font-weight: var(--font-weight-normal);\n        height: 3.8rem;\n        line-height: 3.8rem;\n        padding: 0 1.4rem;\n    }\n\n    &-quarter-width {\n        width: 25%;\n    }\n\n    &-half-width {\n        margin: 0!important;\n        width: 50%;\n    }\n\n    &-full-width {\n        margin: 0!important;\n        width: 100%;\n    }\n\n    &-no-border-radius {\n        border-radius: 0;\n    }\n\n    &-icon {\n        padding-left: 4.3rem;\n        padding-right: 1.3rem;\n\n        & > svg {\n            left: 1.2rem;\n        }\n\n        &.button-small {\n            padding-left: 3.8rem;\n\n            & > svg {\n                height: 16px;\n                width: 16px;\n            }\n        }\n\n        &.button-outline {\n            & > svg {\n                fill: var(--icon-primary-color);\n                transition: var(--transition);\n            }\n\n            &:active,\n            &:focus,\n            &:hover {\n\n               & > svg {\n                fill: var(--icon-tertiary-color);\n               }\n            }\n        }\n\n        &.button-clean,\n        &.button-clean-invert {\n            & > svg {\n                fill: currentColor;\n            }\n        }\n\n        &.button-only-icon {\n            padding: 0;\n            width: 48px;\n        }\n\n        &.button-only-icon {\n            padding: 0;\n            width: 48px;\n        }\n\n        &.button-only-icon-color {\n            padding: 0;\n            width: 48px;\n\n             & > svg {\n                fill: var(--color-primary);\n            }\n        }\n\n        &.button-icon-smaller {\n            & > svg {\n                transform: translateY(-50%) scale(0.8);\n            }\n        }\n    }\n\n    &-preloader {\n        .preloader {\n            animation: rotate .6s infinite linear;\n            border: .2rem solid var(--input-border-color);\n            border-top: .2rem solid var(--gray-4);\n            border-radius: 50%;\n            clear: both;\n            display: block;\n            height: 2rem;\n            margin: 1.3rem auto;\n            width: 2rem;\n\n            &-white {\n                border-color: rgba(255, 255, 255, .5);\n                border-top-color: rgba(255, 255, 255, 1);\n            }\n        }\n\n        & > svg {\n            display: none;\n        }\n\n        &.button-small {\n            .preloader {\n                margin-top: 1rem;\n            }\n        }\n    }\n\n    &-light {\n        background: var(--bg-primary);\n        color: var(--text-light-color);\n        font-weight: var(--font-weight-semibold);\n        padding-left: 3.8rem;\n\n        & > svg {\n            fill: var(--icon-secondary-color);\n            transition: var(--transition);\n        }\n\n        &:active,\n        &:focus,\n        &:hover,\n        &.button-active {\n            background: var(--gray-1);\n            color: var(--text-primary-color);\n\n            & > svg {\n                fill: var(--icon-tertiary-color);\n            }\n        }\n    }\n\n    &.button-disabled,\n    &.button-disabled-with-events {\n        background-color: var(--popup-btn-cancel-bg-hover);\n        border-color: var(--input-border-color);\n        color: var(--popup-btn-cancel-color);\n        cursor: not-allowed;\n        pointer-events: none;\n    }\n\n    &.button-disabled-with-events {\n        pointer-events: auto;\n    }\n\n    & + .button,\n    & + button {\n        margin-left: 2.5 * $spacing;\n    }\n}\n\n@keyframes rotate {\n    from {\n        transform: rotate(0deg);\n    }\n\n    to {\n        transform: rotate(359deg);\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/ButtonDropdown.vue",
    "content": "<template>\n    <div :class=\"{\n        'button': true,\n        'is-green': isGreen,\n        'has-icon': hasIcon,\n        'has-icon-preview': previewIcon,\n        'is-reversed': isReversed,\n        'disabled': disabled\n    }\">\n        <span\n            class=\"button-trigger\"\n            :style=\"'min-width:' + minWidth + 'px;'\"\n            @click.stop=\"doCurrentAction()\">\n            <icon\n                v-if=\"buttonIcon\"\n                size=\"s\"\n                properties=\"not-clickable\"\n                :name=\"buttonIcon\" />\n\n            {{ currentLabel }}\n       \n\n        <span\n            v-if=\"previewIcon && currentIcon\"\n            class=\"button-trigger-icon\">\n            <icon\n                size=\"s\"\n                properties=\"not-clickable\"\n                :name=\"currentIcon\" />\n        </span>\n\n         </span>\n\n        <span\n            class=\"button-toggle\"\n            @click.stop=\"toggleDropdown()\">\n        </span>\n\n        <div\n            v-if=\"dropdownVisible\"\n            class=\"button-dropdown\">\n            <div\n                v-for=\"(item, index) of filteredItems\"\n                :key=\"'button-dropdown-' + index\"\n                class=\"button-dropdown-item\"\n                @click=\"doAction(item.value)\">\n                {{ item.label }}\n\n                <div\n                    v-if=\"previewIcon\"\n                    class=\"button-dropdown-item-icon\">\n                    <icon\n                        size=\"s\"\n                        properties=\"not-clickable\"\n                        :name=\"item.icon\" />\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'btn-dropdown',\n    props: {\n        'items': {\n            default: '',\n            type: Array\n        },\n        'defaultValue': {\n            default: '',\n            type: String\n        },\n        'buttonColor': {\n            default: 'blue',\n            type: String\n        },\n        'buttonIcon': {\n            default: '',\n            type: String\n        },\n        'minWidth': {\n            default: 150,\n            type: Number\n        },\n        'disabled': {\n            default: false,\n            type: Boolean\n        },\n        'previewIcon': {\n            default: false,\n            type: Boolean\n        },\n        'localStorageKey': {\n            default: false,\n            type: [String, Boolean]\n        },\n        'isReversed': {\n            default: false,\n            type: Boolean\n        }\n    },\n    computed: {\n        filteredItems () {\n            return this.items.filter(item => item.isVisible());\n        },\n        currentLabel () {\n            let foundedItem = this.items.filter(item => item.value === this.value);\n\n            if (foundedItem.length) {\n                if (foundedItem[0].activeLabel) {\n                    return foundedItem[0].activeLabel;\n                }\n\n                return foundedItem[0].label;\n            }\n\n            return '';\n        },\n        currentIcon () {\n            let foundedItem = this.items.filter(item => item.value === this.value);\n\n            if (foundedItem.length && foundedItem[0].icon) {\n                return foundedItem[0].icon;\n            }\n\n            return false;\n        }\n    },\n    data () {\n        return {\n            value: '',\n            hasIcon: false,\n            isGreen: false,\n            dropdownVisible: false\n        };\n    },\n    mounted () {\n        if (this.localStorageKey) {\n            let retrievedValue = localStorage.getItem(this.localStorageKey);\n            let values = this.filteredItems.map(item => item.value);\n\n            if (retrievedValue && values.indexOf(retrievedValue) > -1) {\n                this.setValue(retrievedValue);\n            } else {\n                this.setValue(this.defaultValue);\n            }\n        } else {\n            this.setValue(this.defaultValue);\n        }\n\n        this.$bus.$on('document-body-clicked', this.hideDropdown);\n\n        if (this.buttonColor === 'green') {\n            this.isGreen = true;\n        }\n\n        if (this.buttonIcon) {\n            this.hasIcon = true;\n        }\n    },\n    methods: {\n        doAction (actionName) {\n            this.value = actionName;\n            this.items.filter(item => item.value === this.value)[0].onClick();\n\n            if (this.localStorageKey) {\n                localStorage.setItem(this.localStorageKey, actionName);\n            }\n\n            this.hideDropdown();\n        },\n        doCurrentAction () {\n            this.items.filter(item => item.value === this.value)[0].onClick();\n        },\n        toggleDropdown () {\n            this.dropdownVisible = !this.dropdownVisible;\n        },\n        hideDropdown () {\n            this.dropdownVisible = false;\n        },\n        setValue (newValue) {\n            this.value = newValue;\n        },\n        getValue () {\n            return this.value;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('document-body-clicked', this.hideDropdown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.button {\n    background: var(--button-bg);\n    border: none;\n    border-radius: var(--border-radius);\n    box-shadow: none;\n    color: var(--white);\n    cursor: pointer;\n    display: inline-flex;\n    font-size: 1.3rem;\n    font-weight: var(--font-weight-semibold);\n    height: 4.4rem;\n    line-height: 4.3rem;\n    padding: 0;\n    position: relative;\n    text-align: left;\n    transition: var(--transition);\n    user-select: none;\n    white-space: nowrap;\n    width: auto;\n\n    &-trigger {\n        border-radius: var(--border-radius);\n        display: block;\n        height: 4.4rem;\n        left: 0;\n        padding-left: 1.3rem;\n        padding-right: 6rem;\n        position: relative;\n        text-align: left;\n        top: 0;\n        transition: var(--transition);\n\n        &:hover {\n            background: var(--button-bg-hover);\n        }\n\n        &-icon {\n            align-items: center;\n            display: flex;\n            height: inherit;\n            justify-content: center;\n            position: absolute;\n            top: 0;\n            right: 4.4rem;\n            width: 4.2rem;\n\n            .icon {\n                color: var(--white)\n            }\n        }\n    }\n\n    &-toggle {\n        background: var(--button-bg-hover);\n        border-left: 1px solid var(--button-bg);\n        border-radius: 0 var(--border-radius) var(--border-radius) 0;\n        cursor: pointer;\n        height: 100%;\n        position: absolute;\n        right: 0;\n        top: 0;\n        transition: var(--transition);\n        width: 4.4rem;\n\n        &::before {\n            content: \"\";\n            border-radius: 0 var(--border-radius) var(--border-radius) 0;\n            pointer-events: none;\n            height: 100%;\n            left: 0;\n            position: absolute;\n            transition: var(--transition);\n            width: 100%;\n        }\n\n        &::after {\n            border-color: var(--white) transparent transparent;\n            border-style: solid;\n            border-width: 5px;\n            content: \"\";\n            pointer-events: none;\n            left: 50%;\n            position: absolute;\n            top: 50%;\n            transform: translateX(-50%) translateY(-2.5px);\n        }\n\n        &:hover {\n            background: var(--button-bg-hover);\n\n            &::before {\n               background: rgba(black, .1);\n            }\n        }\n    }\n\n    &-dropdown {\n        background: var(--bg-secondary);\n        border-radius: var(--border-radius) var(--border-radius);\n        box-shadow: var(--box-shadow-medium);\n        overflow: hidden;\n        position: absolute;\n        right: 0;\n        text-align: left;\n        top: 5.3rem;\n        min-width: 100%;\n        z-index: 10;\n\n        &-item {\n            border-top: 1px solid var(--border-light-color);\n            color: var(--text-primary-color);\n            padding: .2rem 2rem;\n            position: relative;\n            text-align: left;\n            transition: var(--transition);\n\n            &:hover {\n                background: var(--gray-1);\n\n                .button-dropdown-item-icon .icon {\n                    color: var(--icon-tertiary-color);\n                }\n            }\n\n            &:first-child {\n                border-top: none;\n            }\n\n            &-icon {\n                align-items: center;\n                bottom: 0;\n                display: flex;\n                justify-content: center;\n                position: absolute;\n                right: 0;\n                top: 0;\n                width: 4.4rem;\n\n                .icon {\n                    color: var(--icon-secondary-color);\n                    transition: var(--transition);\n                }\n            }\n        }\n    }\n\n    &.has-icon {\n        .button-trigger {\n            padding-left: 4.3rem;\n\n            & > svg {\n                display: inline-block;\n                fill: var(--white);\n                left: 1.4rem;\n                position: absolute;\n                top: 50%;\n                transform: translateY(-50%);\n            }\n        }\n    }\n\n    &.is-green {\n        background-color: var(--button-tertiary-bg);\n\n        .button-trigger {\n            &:hover {\n                background: var(--button-tertiary-bg-hover);\n            }\n        }\n\n        .button-toggle {\n            background: var(--button-tertiary-bg-hover);\n            border-left: 1px solid var(--button-tertiary-bg);\n\n            &:hover {\n                background: var(--button-tertiary-bg-hover);\n\n                &::before {\n                    background: rgba(black, .1);\n                }\n            }\n        }\n\n        &.disabled {\n            background-color: var(--popup-btn-cancel-bg-hover);\n            color: var(--popup-btn-cancel-color);\n            cursor: not-allowed;\n            pointer-events: none;\n\n            &:hover {\n                background-color: var(--popup-btn-cancel-bg-hover);\n                color: var(--popup-btn-cancel-color);\n            }\n\n            .button-toggle {\n                background: var(--popup-btn-cancel-bg-hover);\n                border-left: 1px solid var(--popup-btn-cancel-bg-hover);\n\n                &:hover {\n                    background-color: var(--popup-btn-cancel-bg-hover);\n                    color: var(--popup-btn-cancel-color);\n                }\n            }\n\n            .button-trigger-icon {\n                background: var(--popup-btn-cancel-bg-hover);\n            }\n        }\n    }\n\n    &.has-icon-preview {\n        .button-trigger {\n            padding-right: 8.4rem;\n        }\n\n        .button-dropdown-item {\n            padding: .2rem 4rem .2rem 2rem;\n        }\n    }\n\n    &.is-reversed {\n        .button-toggle::after {\n            border-color: transparent transparent var(--white);\n            transform: translateX(-50%) translateY(-8px);\n        }\n\n        .button-dropdown {\n            border-radius: var(--border-radius);\n            bottom: 5.3rem;\n            box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.125);\n            top: unset;\n        }\n    }\n}\n</style>"
  },
  {
    "path": "app/src/components/basic-elements/CharCounter.vue",
    "content": "<template>\n    <div :class=\"cssClasses\">\n        {{ stringLength }} / {{ preferredCount }} chars\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'char-counter',\n    props: {\n        value: {\n            type: String,\n            required: true\n        },\n        preferredCount: {\n            type: Number,\n            required: true\n        }\n    },\n    data: function () {\n        return {\n            stringLength: 0\n        };\n    },\n    computed: {\n        cssClasses () {\n            let classes = {\n                'char-counter': true\n            };\n\n            if (this.stringLength > this.preferredCount) {\n                classes['is-too-long'] = true;\n            }\n\n            return classes;\n        }\n    },\n    watch: {\n        value: function (newValue) {\n            this.stringLength = newValue.length;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.char-counter {\n    color: var(--text-light-color);\n    display: block;\n    font-size: 1.2rem;\n    line-height: 1.6;\n    margin: .5rem 0;\n    text-align: right;\n    width: 100%;\n\n    &.is-too-long {\n        color: var(--warning);\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Checkbox.vue",
    "content": "<template>\n    <input\n        type=\"checkbox\"\n        :id=\"id || value\"\n        :value=\"value\"\n        :checked=\"checked\"\n        :class=\"{ 'is-checked': checked }\"\n        @click.prevent.stop=\"onClick(value)\" />\n</template>\n\n<script>\nexport default {\n    name: 'checkbox',\n    props: {\n        id: {\n            default: false,\n            type: [String, Number, Boolean]\n        },\n        value: {\n            default: '',\n            type: [String, Number]\n        },\n        checked: {\n            default: false,\n            type: Boolean\n        },\n        onClick: {\n            default: () => false,\n            type: Function\n        }\n    },\n    computed: {\n        isChecked () {\n            return this.checked;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\ninput[type=\"checkbox\"] {\n    -webkit-appearance: none;\n    background: var(--bg-primary);\n    border: 1px solid var(--input-border-color);\n    border-radius: 4px;\n    height: 1.9rem;\n    line-height: 1.6rem;\n    margin: 0 .5rem 0 0;\n    outline: none;\n    position: relative;\n    text-align: center;\n    vertical-align: middle;\n    width: 1.9rem;\n    z-index: 1;\n\n    &:hover {\n        border: 1px solid var(--color-primary);\n        cursor: pointer;\n    }\n\n    &.is-checked {\n        background: var(--color-primary);\n        border-color: var(--color-primary);\n\n        &:before {\n            color: var(--white);\n            font-weight: bold;\n            content: '\\2713';\n            -webkit-margin-start: 0;\n            margin-left: 2px;\n            font-size: 0.9em;\n            left: -1px;\n            position: relative;\n            top: 0;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/CodeMirrorEditor.vue",
    "content": "<template>\n    <textarea\n        :id=\"id\"\n        spellcheck=\"false\"\n        ref=\"textarea\"></textarea>\n</template>\n\n<script>\nimport beautify from 'js-beautify';\nimport CodeMirror from 'codemirror';\nimport cssMode from '../../assets/vendor/js/codemirror/css.js';\nimport xmlMode from '../../assets/vendor/js/codemirror/xml.js';\n\nimport CodeMirrorJumpToLine from '../../../node_modules/codemirror/addon/search/jump-to-line.js';\nimport CodeMirrorSearch from '../../../node_modules/codemirror/addon/search/search.js';\nimport CodeMirrorMatchHighlighter from '../../../node_modules/codemirror/addon/search/match-highlighter.js';\nimport CodeMirrorMatchesOnScrollbar from '../../../node_modules/codemirror/addon/search/matchesonscrollbar.js';\nimport CodeMirrorSearchCursor from '../../../node_modules/codemirror/addon/search/searchcursor.js';\nimport CodeMirrorAnnotateScrollbar from '../../../node_modules/codemirror/addon/scroll/annotatescrollbar.js';\nimport CodeMirrorScrollPastend from '../../../node_modules/codemirror/addon/scroll/scrollpastend.js';\nimport CodeMirrorSimpleScrollbars from '../../../node_modules/codemirror/addon/scroll/simplescrollbars.js';\nimport CodeMirrorPanel from '../../../node_modules//codemirror/addon/display/panel.js';\nimport CodeMirrorAdvancedDialog from '../../../node_modules/codemirror-advanceddialog/dist/advanced-dialog.js';\nimport CodeMirrorRevisedSearch from '../../../node_modules/codemirror-revisedsearch/dist/revised-search.js';\n\nexport default {\n    name: 'codemirroreditor',\n    props: {\n        id: {\n            default: '',\n            type: String\n        },\n        mode: {\n            default: 'text',\n            type: String\n        },\n        readonly: {\n            default: false,\n            type: Boolean\n        },\n        editorLoadedEventName: {\n            default: 'editor-loaded',\n            type: String\n        }\n    },\n    data: function() {\n        return {\n            editor: null\n        };\n    },\n    mounted: function() {\n        setTimeout(() => {\n            this.initEditor();\n        }, 0);\n\n        this.$bus.$on('source-code-editor-beautify-code', this.beautifyCode);\n    },\n    methods: {\n        initEditor () {\n            this.editor = CodeMirror.fromTextArea(\n                this.$refs['textarea'],\n                {\n                    lineWrapping: true,\n                    lineNumbers: true,\n                    matchBrackets: true,\n                    styleActiveLine: true,\n                    autoRefresh: true,\n                    mode: this.mode,\n                    htmlMode: this.mode === 'xml',\n                    readOnly: this.readonly,\n                    smartIndent: !!this.$store.state.currentSite.config.advanced.editors.codemirrorAutoIndent,\n                    autoIndent: !!this.$store.state.currentSite.config.advanced.editors.codemirrorAutoIndent,\n                    indentUnit: +this.$store.state.currentSite.config.advanced.editors.codemirrorTabSize,\n                    tabSize: +this.$store.state.currentSite.config.advanced.editors.codemirrorTabSize\n                }\n            );\n\n            this.$bus.$emit(this.editorLoadedEventName, true);\n        },\n        beautifyCode () {\n            let editorContent = this.editor.getValue();\n            editorContent = beautify.html(editorContent, { indent_with_tabs: true });\n            this.editor.setValue(editorContent);\n            this.editor.refresh();\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('source-code-editor-beautify-code', this.beautifyCode);\n    }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../scss/variables.scss';\n@import '../../assets/vendor/css/codemirror.css';\n@import '../../../node_modules/codemirror-advanceddialog/dist/dialog.css';\n\n.CodeMirror {\n    border: 1px solid var(--input-border-color);\n}\n\n// Search + Search and Replace popup in Code Mirror\n.CodeMirror-advanced-dialog {\n    background: var(--input-bg-light);\n    border: none;\n    bottom: 0;\n    box-shadow: 0 0 0 1px var(--input-border-color);\n    display: block;\n    left: 0;\n    padding: 2rem 5rem 1rem;\n    position: fixed;\n    width: 100%;\n    z-index: 102;\n\n    & > .row {\n        clear: left;\n        display: block;\n        float: left;\n        width: calc(100% - 340px);\n\n        & > label {\n            float: left;\n            padding: 9px 0;\n            width: 80px;\n        }\n\n        & > input {\n            background: var(--input-bg);\n            border: 1px solid var(--input-border-color);\n            border-radius: 30px;\n            box-shadow: none!important;            \n            float: left;\n            font-family: var(--font-base);\n            margin: 0;\n            padding: 1rem 2rem;\n            width: calc(100% - 80px);\n        }\n\n        & > .CodeMirror-search-hint {\n            float: right;\n            font-size: 11px;\n            margin: 5px 0;\n        }\n\n        & > .CodeMirror-search-count {\n            display: block;\n            float: left;\n            font-size: 11px;\n            margin: 5px 0 5px 80px;\n        }\n    }\n\n    .buttons {\n        position: absolute;\n        right: 5rem;\n        text-align: right;\n        width: 350px;\n\n        button {\n            background: var(--button-secondary-bg);\n            border: none;\n            border-radius: var(--border-radius);\n            box-shadow: none;\n            color: var(--button-secondary-color);\n            cursor: pointer;\n            display: inline-block;\n            font: {\n                size: 1.4rem;\n                family: var(--font-base);\n                weight: 500;\n            }\n            height: 3.8rem;\n            line-height: 3.8rem;\n            margin: 2px 2px 30px;\n            padding: 0 1rem;\n            position: relative;\n            transition: var(--transition);\n            user-select: none;\n            white-space: nowrap;\n\n            &:focus {\n                outline: none;\n            }\n\n            &:active,\n            &:focus,\n            &:hover {\n                background: var(--button-secondary-bg-hover);\n                color: var(--button-secondary-color-hover);\n            }\n\n            &:last-child {\n                background: transparent;\n                box-shadow: inset 0 0 0 2px var(--input-border-color);\n                color: var(--text-primary-color);\n\n                &:active,\n                &:focus,\n                &:hover {\n                    background: transparent;\n                    box-shadow: inset 0 0 0 2px var(--gray-3);\n                    color: var(--text-primary-color);\n                }\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Collection.vue",
    "content": "<template>\n    <div class=\"collection-wrapper\">\n        <div\n            :class=\"cssClasses\"\n            :style=\"gridLayout\">\n            <slot name=\"header\"></slot>\n\n            <div class=\"content\">\n                <slot name=\"content\"></slot>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'collection',\n    props: {\n        formIsOpened: {\n            default: false,\n            type: Boolean\n        },\n        itemsCount: {\n            default: 3,\n            type: Number\n        }\n    },\n    computed: {\n        cssClasses: function() {\n            return {\n                'collection': true,\n                'is-add-form-opened': this.formIsOpened\n            };\n        },\n        gridLayout: function() {\n            let column = ' auto';\n            let templateColumns = `auto 1fr${column.repeat(Math.max(0, this.itemsCount - 2))}`;\n\n            return `grid-template-columns: ${templateColumns}`;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n/*\n * Collection element\n */\n .collection-wrapper {\n     &:after {\n        background: linear-gradient(transparent, var(--bg-site));\n        bottom: 0;\n        content: \"\";\n        height: 4rem;\n        left: 0;\n        pointer-events: none;\n        position: absolute;\n        right: 5px;\n        z-index: 9999;\n    }\n }\n\n.collection {\n    border-top: 1px solid var(--gray-2);\n    border-collapse: collapse;\n    bottom: 0;\n    display: grid;\n    grid-auto-rows: max-content;\n    overflow: auto;\n    padding-bottom: 3rem;\n    position: absolute;\n    top: 12.5rem;\n    width: calc(100% - 8rem);\n\n    &.is-add-form-opened {\n        top: 64.75rem;\n    }\n\n    .content {\n        display: contents;\n    }\n\n    /*\n    &.authors-collection{\n        .item,\n        & > .heading {\n            grid-template-columns: 30px auto 50px;\n            grid-template-areas: \"col col col\"\n                                 \"form form form\";\n        }\n    }\n\n    &.backups-collection {\n        .item,\n        & > .heading {\n            grid-template-columns: 30px auto 100px 175px 200px;\n            grid-template-areas: \"col col col col col\";\n        }\n\n        .item {\n            transition: background .75s ease-out;\n\n            &.is-newest {\n                background: lighten($color-helper-6, 35);\n            }\n        }\n    }*/\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/CollectionCell.vue",
    "content": "<template>\n    <div\n        :class=\"cssClasses\"\n        :style=\"cellStyle\">\n        <slot></slot>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'collectioncell',\n    props: {\n        minWidth: {\n            default: 'auto',\n            type: String\n        },\n        width: {\n            default: 'auto',\n            type: String\n        },\n        textAlign: {\n            default: 'left',\n            type: String\n        },\n        justifyContent: {\n            default: 'left',\n            type: String\n        },\n        type: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        cssClasses: function() {\n            let classes = {\n                'col': true\n            };\n\n            if(this.type !== '') {\n                classes[this.type] = true;\n            }\n\n            return classes;\n        },\n        cellStyle: function() {\n            let styles = [\n                'width: ' + this.width,\n                'min-width: ' + this.minWidth,\n                'text-align: ' + this.textAlign,\n                'justify-content: ' + this.justifyContent\n            ];\n\n            return styles.join(';');\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.col {\n    align-items: center;\n    background: var(--collection-bg);\n    box-sizing: content-box;\n    border-bottom: 1px solid var(--border-light-color);\n    display: grid;\n    font-weight: var(--font-weight-normal);\n    justify-content: left;\n    padding: 1.4rem 1.8rem;\n    text-align: left;\n\n    &:first-child {\n        padding-right: 0;\n    }\n\n    a {\n        color: var(--link-invert-color);\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--link-invert-color-hover);\n        }\n    }\n\n    &.checkbox {\n        .featured-icon {\n            fill: $color-8;\n            height: 20px;\n            left: -2px;\n            position: relative;\n            top: 2px;\n            width: 20px;\n        }\n    }\n\n    &.titles {\n        flex-wrap: wrap;\n\n        .title {\n            font-size: $app-font-base;\n            font-weight: var(--font-weight-semibold);\n            margin: 0 0 -.3rem;\n            text-transform: none;\n            width: 100%;\n        }\n\n        .tag {\n            color: var(--text-light-color);\n            font-size: 1.2rem;\n            font-weight: var(--font-weight-normal);\n\n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--link-primary-color);\n            }\n        }\n\n        svg {\n            left: 0.3rem;\n            position: relative;\n            top: .1rem;\n        }\n        \n    }\n\n    &.names {\n        .name {\n            font-size: $app-font-base;\n            font-weight: var(--font-weight-normal);\n            margin: 0;\n            text-transform: none;\n        }\n\n        .is-main-author {\n            color: var(--text-light-color);\n        }\n    }\n\n    &.publish-dates {\n        flex-wrap: wrap;\n\n        .publish-date,\n        .modify-date {\n            display: block;\n            width: 100%;\n        }\n\n        .modify-date {\n            color: var(--text-light-color);\n            font-size: 1.2rem;\n            margin: 0;\n        }\n    }\n\n    &-buttons {\n        display:  flex !important;\n    }\n\n    &.authors {\n        a {\n            overflow: hidden;\n            display: inline-block;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n             max-width: 15rem;\n        }\n    }\n}\n\n.item {\n    &:hover,\n    &.is-edited {\n        .col {\n           background: var(--collection-bg-hover);\n\n           &:first-child {\n               box-shadow: inset 3px 0 0 var(--color-primary);\n           }\n        }\n    }\n\n    &[data-is-draft=\"true\"] {\n        .title,\n        .tags,\n        .authors {\n            a {\n                color: var(--text-light-color);\n\n                &:active,\n                &:focus,\n                &:hover {\n                    color: var(--link-primary-color);\n                }\n            }\n        }\n    }\n}\n\nbody[data-os=\"win\"] {\n    .col {\n        &.titles {\n            .title {\n                margin: 0 0 -.3rem 0;\n            }\n        }\n    }\n}\n\n/*\n * Responsive improvements\n */\n\n@media (min-width: 1920px) {\n    .col.authors a {\n        max-width: 100%;\n    }\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/CollectionHeader.vue",
    "content": "<template>\n    <div class=\"header\">\n        <slot></slot>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'collection-header'\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.header {\n    display: contents;\n\n    .tools {\n        background: var(--bg-site);\n        display: flex;\n        gap: .75rem;\n        margin-left: 5rem;\n        min-width: 200px;\n        padding: .55rem 0;\n        position: fixed;\n        z-index: 5;\n\n        .button {\n            background: none;\n            position: relative;\n            z-index: 0;\n\n            &::before {\n                content: \"\";\n                background: var(--popup-bg);  \n                border-radius: var(--border-radius);\n                display: block;\n                left: -2px;\n                opacity: 0;\n                position: absolute;\n                right: 0;\n                height: 100%;\n                top: 0;\n                transition: all .15s cubic-bezier(0.4,0.0,0.2,1);\n                transform: scale(.5);\n                width: calc(100% + 2px);\n                z-index: -1;\n            }\n            \n            & + .button {\n               margin: 0;\n               position: relative;\n            }\n\n            &:hover {\n                background: none;\n\n                &::before {\n                    opacity: 1;\n                    transform: scale(1);\n                }\n            }\n\n            &.button-active {\n                 background: var(--popup-bg);  \n            }\n        }\n    }\n\n    .col {\n        background: var(--bg-site);\n        color: var(--headings-color);\n        position: sticky;\n        top: 0;\n        z-index: 2;\n    }\n}\n\n@media (max-width: 1350px) {\n    .header {\n        .tools {\n            .button {\n                &-small {\n                    font-size: 1.3rem;\n                    padding: 0 .75rem;\n\n                    &.button-icon {\n                        padding-left: 3.6rem;\n\n                        & > svg {\n                            display: none;\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/CollectionRow.vue",
    "content": "<template>\n    <div :class=\"'item ' + cssClasses\">\n        <slot></slot>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'collection-row',\n    props: {\n        'cssClasses': {\n            default: ''\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.item {\n    display: contents;\n    font-size: 1.3rem;\n\n\n    .add-form {\n        margin-top: 0;\n    }\n\n    .single-post-status {\n        pointer-events: none;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/ColorPicker.vue",
    "content": "<template>\n    <div \n        :id=\"anchor\"\n        :class=\"cssClasses\">\n        <div\n            class=\"color-picker-preview\"\n            @click=\"togglePicker\"\n            :style=\"'background-color: ' + content\"></div>\n        <input\n            v-model=\"content\"\n            :readonly=\"true\"\n            @click=\"togglePicker\"\n            spellcheck=\"false\"\n            type=\"colorpicker\" />\n\n        <chrome-picker\n            @click.native=\"stopEventPropagation\"\n            :class=\"{ 'is-visible': pickerVisible }\"\n            v-model=\"pickerContent\"\n            ref=\"colorpicker\" />\n    </div>\n</template>\n\n<script>\nimport { Chrome } from 'vue-color';\n\nexport default {\n    name: 'color-picker',\n    components: {\n        'chrome-picker': Chrome\n    },\n    props: {\n        value: {\n            default: '',\n            type: String\n        },\n        outputFormat: {\n            default: 'RGBAorHEX',\n            type: String\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        cssClasses () {\n            let cssClasses = { \n                'color-picker': true,\n                'has-disabled-toggle': this.outputFormat === 'RGBA' || this.outputFormat === 'HSLA'\n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    },\n    data () {\n        return {\n            content: '',\n            pickerContent: '',\n            pickerVisible: false\n        };\n    },\n    watch: {\n        value (newValue) {\n            this.content = newValue;\n        },\n        content (newValue) {\n            this.$emit('input', this.content);\n        }\n    },\n    mounted: function() {\n        if (this.outputFormat === 'RGBA') {\n            this.$refs['colorpicker'].fieldsIndex = 1;\n        } else if (this.outputFormat === 'HSLA') {\n            this.$refs['colorpicker'].fieldsIndex = 2;\n        }\n\n        this.$watch('value', (color) => {\n            this.content = color;\n            this.pickerContent = color;\n        }, { immediate: true });\n\n        this.$watch('pickerContent', (color) => {\n            if (typeof color === 'string') {\n                return;\n            }\n\n            if (this.outputFormat === 'RGBAorHEX') {\n                if (color.a !== 1) {\n                    this.content = 'rgba(' + color.rgba.r + ',' + color.rgba.g + ',' + color.rgba.b + ',' + color.rgba.a + ')';\n                } else {\n                    this.content = color.hex;\n                }\n            } else if (this.outputFormat === 'RGBA') {\n                this.content = 'rgba(' + color.rgba.r + ',' + color.rgba.g + ',' + color.rgba.b + ',' + color.rgba.a + ')';\n            } else if (this.outputFormat === 'HSLA') {\n                this.content = 'hsla(' + parseInt(color.hsl.h, 10) + ', ' + parseInt(color.hsl.s * 100, 10) + '%, ' + parseInt(color.hsl.l * 100, 10) + '%, ' + color.hsl.a + ')';\n            } else if (this.outputFormat === 'HEX') {\n                this.content = color.hex;   \n            } else {\n                this.content = color.hex;\n            }\n        }, { immediate: true });\n\n        this.pickerContent = this.value;\n        this.$bus.$on('document-body-clicked', this.hide);\n    },\n    methods: {\n        getValue () {\n            return this.content;\n        },\n        togglePicker (e) {\n            e.stopPropagation();\n            this.pickerVisible = !this.pickerVisible;\n\n            if (this.pickerVisible) {\n                this.$bus.$off('document-body-clicked', this.hide);\n                this.$bus.$emit('document-body-clicked');\n                this.$bus.$on('document-body-clicked', this.hide);\n            }\n        },\n        stopEventPropagation (e) {\n            e.stopPropagation();\n        },\n        hide () {\n            this.pickerVisible = false;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('document-body-clicked', this.hide);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.color-picker {\n    position: relative;\n\n    &-preview {\n        border-radius: 50%;\n        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);\n        cursor: pointer;\n        float: left;\n        height: 20px;\n        left: 14px;\n        margin-right: -30px;\n        pointer-events: none;\n        position: relative;\n        top: 14px;\n        width: 20px!important;\n\n        & + input {\n            cursor: pointer;\n            text-indent: 25px;\n            width: 100%;\n        }\n    }\n\n    .vc-chrome {\n        display: none;\n        position: absolute;\n        top: 60px;\n        z-index: 10;\n\n        &.is-visible {\n            display: block;\n        }  \n\n        ::v-deep .vc-chrome-body {\n            background-color: var(--bg-primary);\n        }   \n        \n        ::v-deep .vc-input__input {\n            background-color: var(--input-bg);\n            box-shadow: inset 0 0 0 1px var(--input-border-color);\n            color: var(--text-primary-color);\n        }\n        \n        ::v-deep .vc-chrome-toggle-icon {\n            \n            path {\n               fill: var(--icon-primary-color);\n            }\n            \n            &:hover {\n               path {\n                  fill: var(--icon-tertiary-color);\n               }\n            }\n        }\n        \n        ::v-deep .vc-chrome-toggle-icon-highlight {\n            background: var(--input-border-color);\n        }\n    }\n\n    &.has-disabled-toggle {\n        .vc-chrome {\n            ::v-deep .vc-chrome-toggle-btn {\n                display: none;\n                pointer-events: none;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Confirm.vue",
    "content": "<template>\n    <div\n        v-if=\"isVisible\"\n        class=\"overlay\">\n        <div class=\"popup\">\n            <p\n                :class=\"cssClasses\"\n                v-pure-html=\"message\">\n            </p>\n\n            <text-input\n                v-if=\"hasInput\"\n                :type=\"inputIsPassword ? 'password' : 'text'\"\n                :value=\"defaultText\"\n                :spellcheck=\"false\"\n                ref=\"input\" />\n\n            <div class=\"buttons\">\n                <p-button\n                    :type=\"isDanger ? 'medium no-border-radius half-width danger' : 'medium no-border-radius half-width'\"\n                    :onClick=\"onOk\"\n                    ref=\"okButton\">\n                    {{ okLabel }}\n                </p-button>\n\n                <p-button\n                    type=\"medium no-border-radius half-width cancel-popup\"\n                    :onClick=\"onCancel\">\n                    {{ cancelLabel }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'confirm',\n    data () {\n        return {\n            isVisible: false,\n            hasInput: false,\n            inputIsPassword: false,\n            message: '',\n            textCentered: false,\n            okClick: () => false,\n            cancelClick: () => false,\n            okLabel: this.$t('ui.ok'),\n            isDanger: false,\n            cancelLabel: this.$t('ui.cancel'),\n            defaultText: '',\n            cancelNotClosePopup: false\n        };\n    },\n    computed: {\n        cssClasses () {\n            return {\n                'message': true,\n                'text-centered': this.textCentered\n            };\n        }\n    },\n    mounted () {\n        this.$bus.$on('confirm-display', (config) => {\n            document.body.classList.add('has-popup-visible');\n\n            setTimeout(() => {\n                this.isVisible = true;\n                this.message = config.message;\n                this.textCentered = config.textCentered || false;\n                this.hasInput = config.hasInput || false;\n                this.inputIsPassword = config.inputIsPassword || false;\n                this.okLabel = config.okLabel || this.$t('ui.ok');\n                this.cancelLabel = config.cancelLabel || this.$t('ui.cancel');\n                this.defaultText = config.defaultText || \"\";\n                this.isDanger = config.isDanger || false;\n                this.cancelNotClosePopup = config.cancelNotClosePopup || false;\n\n                if(config.okClick) {\n                    this.okClick = config.okClick;\n                } else {\n                    this.okClick = () => false;\n                }\n\n                if(config.cancelClick) {\n                    this.cancelClick = config.cancelClick\n                } else {\n                    this.cancelClick = () => false;\n                }\n\n                setTimeout(() => {\n                    if (config.hasInput) {\n                        this.$refs.input.$el.querySelector('input').focus();\n                    }\n                }, 100);\n            }, 0);\n        });\n\n        document.body.addEventListener('keydown', this.onDocumentKeyDown);\n    },\n    methods: {\n        onOk () {\n            this.isVisible = false;\n            document.body.classList.remove('has-popup-visible');\n\n            if(this.hasInput) {\n                this.okClick(this.$refs.input.content);\n            } else {\n                this.okClick();\n            }\n        },\n        onCancel () {\n            if (!this.cancelNotClosePopup) {\n                this.isVisible = false;\n                document.body.classList.remove('has-popup-visible');\n            }\n\n            this.cancelClick();\n        },\n        onDocumentKeyDown (e) {\n            if (e.code === 'Enter' && !event.isComposing && this.isVisible) {\n                this.onEnterKey();\n            }\n        },\n        onEnterKey () {\n            this.onOk();\n\n            setTimeout(() => {\n                this.isVisible = false;\n                document.body.classList.remove('has-popup-visible');\n            }, 100);\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('confirm-display');\n        document.body.removeEventListener('keydown', this.onDocumentKeyDown);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100005;\n}\n\n.popup {\n    max-width: 60rem;\n    min-width: 60rem;\n    padding: 4rem;\n}\n\n.message {\n    padding: 0;\n\n    & + * {\n        margin-top: 2rem;\n    }\n}\n\n.buttons {\n    display: flex;\n    margin: 4rem -4rem -4rem -4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/DirSelect.vue",
    "content": "<template>\n    <div\n        class=\"box\"\n        @click=\"selectDir\">\n        <text-input\n            icon=\"folder\"\n            ref=\"input\"\n            :id=\"id\"\n            :placeholder=\"placeholder\"\n            :value=\"value\"\n            :readonly=\"readonly\"\n            :spellcheck=\"false\"\n            properties=\"keyboard-blocked\" />\n\n        <span\n            v-if=\"fieldValue\"\n            class=\"clear\"\n            @click.stop=\"clear\">\n            &times;\n        </span>\n    </div>\n</template>\n\n\n<script>\nexport default {\n    name: 'dirselect',\n    props: {\n        id: {\n            default: '',\n            required: true,\n            type: String\n        },\n        placeholder: {\n            default: '',\n            required: false,\n            type: String\n        },\n        value: {\n            default: '',\n            required: true,\n            type: String\n        },\n        readonly: {\n            default: false,\n            type: Boolean\n        },\n        onChange: {\n            default: () => false,\n            type: Function\n        }\n    },\n    data: function() {\n        return {\n            fieldValue: this.value\n        };\n    },\n    watch: {\n        value (newValue, oldValue) {\n            this.fieldValue = newValue;\n            this.$emit('input', this.fieldValue);\n        }\n    },\n    methods: {\n        clear () {\n            this.$refs.input.content = '';\n            this.fieldValue = '';\n            this.$emit('input', '');\n        },\n        async selectDir () {\n            await mainProcessAPI.invoke('app-main-process-select-directory', 'dir-select');\n\n            mainProcessAPI.receiveOnce('app-directory-selected', (data) => {\n                if (data.path === undefined || !data.path.filePaths.length) {\n                    return;\n                }\n\n                this.$refs.input.content = data.path.filePaths[0];\n                this.fieldValue = data.path.filePaths[0];\n                this.onChange(this.fieldValue);\n                this.$emit('input', this.fieldValue);\n            });\n        },\n        getValue () {\n            return this.fieldValue;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n// @ToDo: move \".clear\" to a separate component - ClearButton\n\n.box {\n    position: relative;\n\n    input {\n        float: left;\n        padding-left: 4.5rem!important;\n        width: 100%!important;\n    }\n\n    .clear {\n        border-radius: 50%;\n        color: var(--warning);\n        cursor: pointer;\n        font-size: 2.4rem;\n        font-weight: 300;\n        height: 3rem; \n        line-height: 1.1;\n        position: absolute;\n        right: 1.5rem;\n        text-align: center;\n        transition: var(--transition);           \n        top: 50%;\n        transform: translateY(-50%); \n        width: 3rem;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--icon-tertiary-color);\n        }\n        \n        &:hover {\n            background: var(--input-border-color);\n        }      \n    }\n\n    svg {\n        left: .5rem;\n        position: absolute;\n        top: .75rem;\n        z-index: 1;\n    }\n\n    & ~ small.note {\n        color: var(--warning);\n        padding: 1rem 0;\n        width: 100%;\n\n        svg {\n            fill: var(--warning);\n            height: 1.8rem!important;\n            margin-left: 1.3rem;\n            position: relative;\n            top: 3px;\n            width: 1.8rem!important;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Dropdown.vue",
    "content": "<template>\n    <select\n        :id=\"id\"\n        :multiple=\"multiple\"\n        :disabled=\"disabled\"\n        :readonly=\"readonly\"\n        :class=\"cssClasses\"\n        @change=\"onChangeEvent\">\n        <slot name=\"first-choice\"></slot>\n        <template v-if=\"Array.isArray(items)\">\n            <option\n                v-for=\"(item, index) in items\"\n                :value=\"item.value\"\n                :key=\"id + '-dropdown-item-' + index\"\n                :disabled=\"(disabledValues.indexOf(item.value) > -1 && item.value !== selectedValue && item.value !== '') || item.disabled\"\n                :selected=\"item.value == selectedValue\">\n                {{item.label}}\n            </option>\n        </template>\n\n        <template v-if=\"!Array.isArray(items) && !items.hasGroups\">\n            <option \n                v-for=\"(item, key) in items\"\n                :value=\"key\"\n                :key=\"key\"\n                :disabled=\"(disabledValues.indexOf(key) > -1 && key !== selectedValue && key !== '') || item.disabled\"\n                :selected=\"key == selectedValue\">\n                <template v-if=\"item.label\">\n                {{ item.label }}\n                </template>\n                <template v-else>\n                {{ item }}\n                </template>\n            </option>\n        </template>\n\n        <template v-if=\"!Array.isArray(items) && items.hasGroups\"> \n            <option \n                v-for=\"(item, key) in items.groups.ungrouped\"\n                :value=\"key\"\n                :key=\"'ungrouped-' + key\"\n                :disabled=\"(disabledValues.indexOf(key) > -1 && key !== selectedValue && key !== '') || item.disabled\"\n                :selected=\"key == selectedValue\">\n                {{item.label}}\n            </option>\n\n            <template v-for=\"(groupName, index) of Object.keys(items.groups)\">\n                <optgroup \n                    v-if=\"groupName !== 'ungrouped'\"\n                    :key=\"groupName + '-' + index\"\n                    :label=\"groupName\">\n                    <option \n                        v-for=\"(item, key) in items.groups[groupName]\"\n                        :value=\"key\"\n                        :key=\"key\"\n                        :disabled=\"(disabledValues.indexOf(key) > -1 && key !== selectedValue && key !== '') || item.disabled\"\n                        :selected=\"key == selectedValue\">\n                        {{item.label}}\n                    </option>\n                </optgroup>\n            </template>\n        </template>\n    </select>\n</template>\n\n<script>\nexport default {\n    name: 'dropdown',\n    props: {\n        id: {\n            default: 'dropdown-default',\n            type: String\n        },\n        items: {\n            required: true,\n            type: [Object, Array]\n        },\n        value: {\n            default: '',\n            type: [String, Number]\n        },\n        selected: {\n            default: '',\n            type: String\n        },\n        onChange: {\n            default: () => false,\n            type: Function\n        },\n        multiple: {\n            default: false,\n            type: Boolean\n        },\n        readonly: {\n            default: false,\n            type: Boolean\n        },\n        disabled: {\n            default: false,\n            type: Boolean\n        },\n        noBorder: {\n            default: false,\n            type: Boolean\n        },\n        disabledValues: {\n            default: () => [],\n            type: Array\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        cssClasses () {\n            let cssClasses = { \n                'no-border': this.noBorder \n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    },\n    data: function() {\n        return {\n            selectedValue: ''\n        };\n    },\n    watch: {\n        value (newValue, oldValue) {\n            this.selectedValue = newValue;\n        }\n    },\n    mounted: function() {\n        setTimeout(() => {\n            if (this.value) {\n                this.selectedValue = this.value;\n            } else {\n                this.selectedValue = this.selected;\n            }\n        }, 0);\n    },\n    methods: {\n        onChangeEvent (e) {\n            this.selectedValue = e.target.value;\n            this.$emit('input', this.selectedValue);\n            this.onChange(this.selectedValue);\n        },\n        getValue () {\n            return this.selectedValue;\n        },\n        setValue (newValue) {\n            this.selectedValue = newValue;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\nselect {\n    -webkit-appearance: none;\n    max-width: 100%;\n    min-width: 100px;\n    min-height: 46px;\n    position: relative;\n    width: 100%;\n\n    &.no-border {\n        background-color: transparent!important;\n        box-shadow: none; \n        padding: 0 12px 0 0;\n\n        &:focus {\n            box-shadow: none;\n        }\n    }\n\n    &[disabled],\n    &[readonly] {\n        opacity: .5;\n        cursor: not-allowed;\n\n        &:focus {\n            box-shadow: inset 0 0 0 1px var(--input-border-color);\n        }\n    }\n\n    select[multiple] {\n        padding: baseline(6);\n        width: 100%;\n\n        &:hover {\n            border-color: var(--input-border-color);\n        }\n\n        &:focus {\n            border-color: var(--input-border-color);\n        }\n\n        &[disabled] {\n            background-color: var(--gray-1);\n            cursor: not-allowed;\n            &:hover {\n                border-color: var(--input-border-color);\n            }\n        }\n    }\n\n    &:not([multiple]) {\n        background: url('data:image/svg+xml;utf8,<svg fill=\"%238e929d\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 6\"><polygon points=\"10 0 5 0 0 0 5 6 10 0\"/></svg>') no-repeat calc(100% - 2rem) 50%;\n        background-color: var(--input-bg);\n        background-size: 10px;\n        padding-right: 3rem;\n    }\n\n    &.invalid {\n        border: 1px solid var(--warning);\n\n        &:focus {\n            border: none;\n        }\n    }\n}\n\n/*\n * Special rules for Windows\n */\n\nbody[data-os=\"win\"] {\n    select:not([multiple]) {\n        height: 4.8rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/EmbedConsentsGroups.vue",
    "content": "<template>\n    <div class=\"embed-consents-groups\">\n        <div\n            v-if=\"content.length\"\n            class=\"embed-consents-groups-header\">\n            <div class=\"embed-consents-groups-header-cell\">{{ $t('gdpr.embedConsents.groupRule') }}</div>\n            <div class=\"embed-consents-groups-header-cell\">{{ $t('gdpr.embedConsents.groupCookieGroup') }}</div>\n            <div class=\"embed-consents-groups-header-cell\">{{ $t('gdpr.embedConsents.groupButtonLabel') }}</div>\n        </div>\n\n        <div\n            v-for=\"(group, index) of content\"\n            class=\"embed-consents-group\"\n            :key=\"'embed-consents-group-' + index\">\n            <text-input\n                :spellcheck=\"false\"\n                v-model=\"group.rule\"\n                :placeholder=\"$t('gdpr.embedConsents.groupRulePlaceholder')\" />\n\n            <dropdown\n                v-model=\"group.cookieGroup\"\n                :items=\"availableCookieGroups\" />\n\n            <text-input\n                :spellcheck=\"false\"\n                v-model=\"group.buttonLabel\"\n                :placeholder=\"$t('gdpr.embedConsents.groupButtonLabel')\" />\n\n            <a\n                href=\"#\"\n                class=\"embed-consents-group-btn delete\"\n                tabindex=\"-1\"\n                @click.stop.prevent=\"removeRule(index)\">\n                <icon\n                    name=\"trash\"\n                    size=\"xs\" />\n            </a>\n\n            <text-area\n                v-model=\"group.text\"\n                :placeholder=\"$t('gdpr.embedConsents.groupTextPlaceholder')\"\n                :rows=\"3\"></text-area>\n        </div>\n\n        <p-button\n            icon=\"add-site-mono\"\n            type=\"secondary icon\"\n            @click.native=\"addRule\">\n            {{ $t('gdpr.embedConsents.addRule') }}\n        </p-button>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'embed-consents',\n    props: [\n        'value',\n        'cookieGroups'\n    ],\n    computed: {\n        availableCookieGroups () {\n            if (!this.cookieGroups) {\n                return [{\n                    label: 'None',\n                    value: ''\n                }];\n            }\n\n            return [{\n                label: 'None',\n                value: ''\n            }].concat(this.cookieGroups.filter(group => group.id !== '' && group.id !== '-').map(group => ({\n                label: group.id,\n                value: group.id\n            })));\n        }\n    },\n    data () {\n        return {\n            content: []\n        };\n    },\n    watch: {\n        value (newValue) {\n            this.content = newValue;\n        },\n        content (newValue) {\n            this.$emit('input', newValue);\n        },\n        cookieGroups: {\n            handler: function (newValue) {\n                if (!newValue) {\n                    return;\n                }\n\n                let availableGroups = newValue.map(group => group.id);\n\n                for (let i = 0; i < this.content.length; i++) {\n                    if (availableGroups.indexOf(this.content[i].cookieGroup) === -1) {\n                        Vue.set(this.content[i], 'cookieGroup', '');\n                    }\n                }\n            },\n            deep: true\n        }\n    },\n    mounted: function() {\n        setTimeout(() => {\n            this.content = this.value;\n        }, 0);\n    },\n    methods: {\n        addRule () {\n            this.content.push({\n                rule: '',\n                buttonLabel: '',\n                cookieGroup: '',\n                text: ''\n            });\n        },\n        removeRule (index) {\n            this.content.splice(index, 1);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.embed-consents-groups {\n    border-radius: 3px;\n    padding-top: 1.75rem;\n\n    .embed-consents-groups-header {\n        display: flex;\n        margin-top: 1rem;\n\n        &-cell {\n            font-size: 1.4rem;\n            font-weight: bold;\n            margin: 0 0 1rem 0;\n            width: calc((100% / 3) - 15px);\n        }\n    }\n\n    .embed-consents-group {\n        align-items: center;\n        display: flex;\n        flex-wrap: wrap;\n        padding: .25rem 0;\n\n        &-btn {\n            align-items: center;\n            background: var(--gray-1);\n            position: relative;\n            border-radius: 50%;\n            display: flex;\n            height: 30px;\n            justify-content: center;\n            margin: 0 2px;\n            position: relative;\n            text-align: center;\n            width: 30px;\n\n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--headings-color);\n            }\n\n            &:hover {\n\n                & > svg {\n                    fill: var(--icon-tertiary-color);\n                    transform: scale(1);\n                }\n            }\n\n            svg {\n                fill: var(--icon-secondary-color);\n                height: 1.6rem;\n                pointer-events: none;\n                transform: scale(.9);\n                transition: var(--transition);\n                width: 1.6rem;\n            }\n\n            &.delete {\n\n                &:hover {\n\n                    & > svg {\n                        fill: var(--warning);\n                    }\n                }\n            }\n        } \n\n\n        .input-wrapper,\n        select {\n            padding-right: 1rem;\n            text-align: left;\n            width: calc((100% / 3) - 15px);\n        }\n\n        select {\n            margin-right: 1rem;\n            width: calc((100% / 3) - 15px - 1rem);\n        }\n\n        div:last-child {\n            margin-bottom: 3rem;\n            margin-top: 1rem;\n            width: calc(100% - 56px);\n        }\n    }\n\n    .button {\n        margin: 1rem 0;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/EmptyState.vue",
    "content": "<template>\n    <div \n        v-if=\"theme\"\n        class=\"empty-state\">\n        <img\n            v-if=\"imageName\"\n            :src=\"imagePath\"\n            :height=\"imageHeight\"\n            :width=\"imageWidth\"\n            alt=\"\">\n\n        <h3 v-if=\"title\">\n            {{ title }}\n        </h3>\n\n        <p v-if=\"description\">\n            {{ description }}\n        </p>\n\n        <slot\n            v-if=\"hasButtonSlot\"\n            name=\"button\" />\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'emptystate',\n    props: {\n        imageName: {\n            default: '',\n            type: String\n        },\n        imageWidth: {\n            default: '',\n            type: String\n        },\n        imageHeight: {\n            default: '',\n            type: String\n        },\n        title: {\n            default: '',\n            type: String\n        },\n        description: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        hasButtonSlot () {\n            return !!this.$slots['button'];\n        },\n        imagePath () {\n            return '../src/assets/svg/' + this.theme + '/' + this.imageName;\n        }\n    },\n    data () {\n        return {\n            theme: ''\n        };\n    },\n    async mounted () {\n        this.theme = await this.$root.getCurrentAppTheme();\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/empty-states.scss';\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Field.vue",
    "content": "<template>\n    <div :class=\"cssClasses\">\n        <!-- Fields with separated label (most cases) -->\n        <label\n            v-if=\"!labelHidden && labelSeparated && label\"\n            :for=\"id\">\n            {{ label }}\n        </label>\n\n        <div v-if=\"labelSeparated\">\n            <slot name=\"field\"></slot>\n            <slot name=\"second-label\"></slot>\n            <slot name=\"note\"></slot>\n        </div>\n\n        <!-- Fields with label around the field (mainly switchers) -->\n        <label\n            :class=\"labelCssClasses\"\n            v-if=\"!labelHidden && !labelSeparated\"\n            :for=\"id\">\n            <slot name=\"field\"></slot>\n            {{ label }}\n            <slot name=\"note\"></slot>\n        </label>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'field',\n    props: {\n        id: {\n            default: '',\n            type: String\n        },\n        label: {\n            default: '',\n            type: String\n        },\n        labelHidden: {\n            default: false,\n            type: Boolean\n        },\n        labelSeparated: {\n            default: true,\n            type: Boolean\n        },\n        labelFullWidth: {\n            default: false,\n            type: Boolean\n        },\n        noLabelSpace: {\n            default: false,\n            type: Boolean\n        },\n        spacing: {\n            default: 'normal',\n            type: String\n        },\n        withCharCounter: {\n            default: false,\n            type: Boolean\n        }\n    },\n    computed: {\n        cssClasses: function() {\n            let containsSwitcher = false;\n\n            if (this.$slots && this.$slots['field']) {\n                this.$slots['field'].forEach(item => {\n                    if (item && item.tag && item.tag.substr(-9) === '-switcher') {\n                        containsSwitcher = true;\n                        return;\n                    }\n                });\n            }\n\n            return {\n                'field': true,\n                'field-label-full-width': this.labelFullWidth,\n                'field-small-spacing': this.spacing === 'small',\n                'field-with-char-counter': this.withCharCounter,\n                'field-with-switcher': containsSwitcher\n            };\n        },\n        labelCssClasses: function() {\n            return {\n                'is-not-separated': true,\n                'has-no-label-space': this.noLabelSpace,\n                'no-spaces': this.spacing === 'small'\n            };\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\nlabel {\n    color: var(--label-color);\n    cursor: pointer;\n    font-weight: 400;\n}\n\n.field {\n    display: table;\n    margin: 2rem 0;\n    table-layout: fixed;\n    width: 100%;\n\n    &.field-small-spacing {\n        margin: 1rem 0;\n    }\n\n    &:first-child {\n        margin-top: 0;\n\n        .separator {\n            padding-top: 0;\n        }\n    }\n\n    & > label {\n        &:first-child {\n            cursor: default;\n            display: table-cell;\n            line-height: 1.4;\n            padding: 1.5rem 1rem 0 0;\n            vertical-align: top;\n            width: 23rem;\n\n            sup {\n                color: var(--warning);\n                font-size: 1.8rem;\n                font-weight: 400;\n                position: relative;\n                top: 1px;\n            }\n        }\n\n        &.is-not-separated {\n            padding-left: 23rem;\n\n            &.has-no-label-space {\n                padding-left: 0;\n            }\n        }\n\n        &.no-spaces {\n            padding-top: 0;\n        }\n    }\n\n    &.fields-2 {\n        label ~ label {\n            display: table-cell;\n            padding: 0 2rem;\n            text-align: right;\n        }\n    }\n\n    &.no-label {\n        padding-left: 23rem;\n        grid-template-columns: auto;\n    }\n\n    &.is-hidden {\n        display: none;\n    }\n\n    &:last-child {\n        margin-bottom: 0;\n    }\n\n    & > div {\n        display: table-cell;\n        padding: 0;\n        vertical-align: bottom;\n        width: 100%;\n\n        & > strong {\n            display: inline-block;\n            width: 50%;\n\n            & + select {\n                width: 50%;\n            }\n        }\n\n        .switcher {\n            margin-top: 1.5rem;\n        }\n    }\n\n    & > div > select,\n    & > div > textarea,\n    & > div > input[type=\"text\"],\n    & > div > input[type=\"number\"],\n    & > div > input[type=\"colorpicker\"],\n    & > div > input[type=\"password\"] {\n        width: 100%;\n    }\n\n    .note {\n        clear: both;\n        color: var(--text-light-color);\n        display: block;\n        font-size: 1.35rem;\n        font-style: italic;\n        line-height: 1.4;        \n        padding: .5rem 0 1rem 0;\n        user-select: text;\n\n        svg {\n            display: inline-block;\n            height: 1.4rem;\n            margin-right: .5rem;\n            width: 1.4rem;\n        }\n\n        &.is-invalid {\n            color: var(--warning);\n        }\n        \n        a {\n            color: var(--link-primary-color);\n            \n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--link-primary-color-hover);\n            }\n        }\n    }\n\n    label + .note {\n        padding-top: 1.5rem;\n    }\n\n    .checkbox ~ .note {\n        margin-bottom: 1.5rem;\n    }\n\n    .range-wrapper {\n        position: relative;\n        z-index: 1;\n\n        & + .note {\n            padding-top: 1.5rem;\n        }\n    }\n\n    .separator {\n        clear: both;\n        display: block;\n        position: relative;\n        width: 100%;\n\n        &.line > .separator-wrapper::before {\n            border: 1px solid var(--gray-1);\n            content: \"\";\n            left: 0;\n            position: absolute;\n            top: 50%;\n            width: 100%;\n        }\n\n        & > .separator-wrapper {\n            height: 2rem;\n            position: relative;\n\n            & > label {\n                background: var(--bg-primary);\n                color: var(--color-primary);\n                font-size: 1.4rem;\n                font-weight: var(--font-weight-semibold);\n                padding-right: .5rem;\n                position: absolute;\n                text-transform: uppercase;\n                top: 50%;\n                transform: translateY(-50%);\n                width: auto;\n            }\n        }\n\n        & > .note {\n            padding: 0;\n        }\n\n        &.line > .note {\n            padding: 2.5rem 0 0 0;\n        }\n    }\n\n    &.field-label-full-width {\n        display: block;\n        margin: 0;\n\n        & > label,\n        & > div {\n            display: block;\n            width: 100%;\n        }\n\n        & > label {\n            padding: 0;\n        }\n\n        & > div {\n            padding: .85rem 0;\n\n            textarea {\n                margin: 0;\n            }\n        }\n    }\n\n    &.field-with-char-counter {\n        .note {\n            margin-top: -3rem;\n            width: 70%;\n        }\n    }\n\n    &.field-with-switcher {\n        & > label {\n            &:first-child {\n                padding-top: 0;\n            }\n        }   \n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/FieldsGroup.vue",
    "content": "<template>\n    <fieldset>\n        <legend\n            v-if=\"title\" \n            :class=\"{ \n                'is-danger': type === 'danger' \n            }\">\n            {{ title }}\n        </legend>\n\n        <slot />\n    </fieldset>\n</template>\n\n<script>\nexport default {\n    name: 'fields-group',\n    props: {\n        'title': {\n            default: '',\n            type: String\n        },\n        'type': {\n            default: '',\n            type: String\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\nfieldset {\n    background-color: var(--bg-secondary);\n    border: none;\n    border-radius: var(--border-radius);\n    box-shadow: var(--box-shadow-small);\n    border: none;\n    margin: 0 0 3rem;\n    position: relative;\n    padding: 3rem 3rem 3rem;\n\n    & > legend {\n        color: var(--headings-color);\n        font-size: 1.6rem;\n        font-weight: 600;\n        position: absolute;\n        top: 3rem;\n\n        &.is-danger {\n            color: var(--warning);\n        }\n\n        & + * {\n            margin-top: 5rem !important;\n        }\n\n        & + p {\n            margin-top: 0;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/FileSelect.vue",
    "content": "<template>\n    <div\n        class=\"box\"\n        @click=\"selectFile\">\n        <text-input\n            icon=\"folder\"\n            ref=\"input\"\n            :id=\"id\"\n            :placeholder=\"placeholder\"\n            :disabled=\"disabled\"\n            :value=\"value\"\n            :spellcheck=\"false\"\n            properties=\"keyboard-blocked\" />\n\n        <span\n            v-if=\"fieldValue && !disabled\"\n            class=\"clear\"\n            @click.stop=\"clear\">\n            &times;\n        </span>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'fileselect',\n    props: {\n        id: {\n            default: '',\n            required: true,\n            type: String\n        },\n        placeholder: {\n            default: '',\n            required: false,\n            type: String\n        },\n        disabled: {\n            default: false,\n            type: Boolean\n        },\n        value: {\n            default: '',\n            required: true,\n            type: String\n        },\n        onChange: {\n            default: () => false,\n            type: Function\n        }\n    },\n    data: function() {\n        return {\n            fieldValue: this.value\n        };\n    },\n    watch: {\n        value (newValue, oldValue) {\n            this.fieldValue = newValue;\n            this.$emit('input', this.fieldValue);\n        }\n    },\n    methods: {\n        clear () {\n            this.$refs.input.content = '';\n            this.fieldValue = '';\n            this.onChange('');\n        },\n        async selectFile () {\n            await mainProcessAPI.invoke('app-main-process-select-file', 'file-select');\n\n            mainProcessAPI.receiveOnce('app-file-selected', (data) => {\n                if (data.path === undefined || !data.path.filePaths.length) {\n                    return;\n                }\n\n                this.$refs.input.content = data.path.filePaths[0];\n                this.fieldValue = data.path.filePaths[0];\n                this.$emit('input', this.fieldValue);\n                this.onChange(this.fieldValue);\n            });\n        },\n        getValue () {\n            return this.fieldValue;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n// @ToDo: move \".clear\" to a separate component - ClearButton\n\n.box {\n    position: relative;\n\n    input {\n        float: left;\n        padding-left: 4.5rem!important;\n        width: 100%!important;\n    }\n\n    .clear {\n        border-radius: 50%;\n        color: var(--warning);\n        cursor: pointer;\n        font-size: 2.4rem;\n        font-weight: 300;\n        height: 3rem; \n        line-height: 1.1;\n        position: absolute;\n        right: 1.5rem;\n        text-align: center;\n        transition: var(--transition);           \n        top: 50%;\n        transform: translateY(-50%); \n        width: 3rem;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--icon-tertiary-color);\n        }\n        \n        &:hover {\n            background: var(--input-border-color);\n        }   \n    }\n\n    svg {\n        left: .5rem;\n        position: absolute;\n        top: .75rem;\n        z-index: 1;\n    }\n\n    & ~ small.note {\n        color: var(--warning);\n        padding: 1rem 0;\n        width: 100%;\n\n        svg {\n            fill: var(--warning);\n            height: 1.8rem!important;\n            margin-left: 1.3rem;\n            position: relative;\n            top: 3px;\n            width: 1.8rem!important;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Footer.vue",
    "content": "<template>\n    <div class=\"footer\">\n        <div\n            v-if=\"hasButtonsSlot\"\n            class=\"buttons\">\n            <slot name=\"buttons\"></slot>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'p-footer',\n    computed: {\n        hasButtonsSlot () {\n            return !!this.$slots['buttons']\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.footer {\n    @include clearfix;\n\n    .buttons {\n        float: right;\n\n        button {\n            position: relative;\n            top: -0.75rem;\n        }\n\n        .button {\n            float: right;\n\n            & + .button {\n              margin-right: 1rem;\n            }\n\n            & > span.progress {\n                display: inline-block;\n                width: 3.2rem;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/GConsentModeGroups.vue",
    "content": "<template>\n    <div class=\"g-consent-mode-groups\">\n        <div\n            v-if=\"content.length\"\n            class=\"g-consent-mode-groups-header\">\n            <div class=\"g-consent-mode-groups-header-cell\">\n                {{ $t('settings.gConsentMode.cookieGroup') }}\n            </div>\n        </div>\n\n        <div\n            v-for=\"(group, index) of content\"\n            class=\"g-consent-mode-group\"\n            :key=\"'g-consent-mode-group-' + index\">\n            <dropdown\n                v-model=\"group.cookieGroup\"\n                :items=\"availableCookieGroups\" />\n\n            <a\n                href=\"#\"\n                class=\"g-consent-mode-group-btn delete\"\n                tabindex=\"-1\"\n                @click.stop.prevent=\"removeRule(index)\">\n                <icon\n                    name=\"trash\"\n                    size=\"xs\" />\n            </a>\n\n            <div  \n                class=\"g-consent-mode-group-switchers\">\n                <switcher \n                    :key=\"'gcm-switcher-1-' + index\"\n                    v-model=\"group.ad_storage\"\n                    label=\"ad_storage\" />\n                \n                <switcher \n                    :key=\"'gcm-switcher-2-' + index\"\n                    v-model=\"group.ad_personalization\"\n                    label=\"ad_personalization\" />\n\n                <switcher \n                    :key=\"'gcm-switcher-3-' + index\"\n                    v-model=\"group.ad_user_data\"\n                    label=\"ad_user_data\" />\n\n                <switcher \n                    :key=\"'gcm-switcher-4-' + index\"\n                    v-model=\"group.analytics_storage\"\n                    label=\"analytics_storage\" />\n\n                <switcher \n                    :key=\"'gcm-switcher-5-' + index\"\n                    v-model=\"group.personalization_storage\"\n                    label=\"personalization_storage\" />\n\n                <switcher \n                    :key=\"'gcm-switcher-6-' + index\"\n                    v-model=\"group.functionality_storage\"\n                    label=\"functionality_storage\" />\n\n                <switcher \n                    :key=\"'gcm-switcher-7-' + index\"\n                    v-model=\"group.security_storage\"\n                    label=\"security_storage\" />\n            </div>\n        </div>\n\n        <p-button\n            icon=\"add-site-mono\"\n            type=\"secondary icon\"\n            @click.native=\"addRule\">\n            {{ $t('settings.gConsentMode.addGroup') }}\n        </p-button>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'g-consent-mode-groups',\n    props: [\n        'value',\n        'cookieGroups'\n    ],\n    computed: {\n        availableCookieGroups () {\n            if (!this.cookieGroups) {\n                return [{\n                    label: 'None',\n                    value: ''\n                }];\n            }\n\n            return [{\n                label: 'None',\n                value: ''\n            }].concat(this.cookieGroups.filter(group => group.id !== '' && group.id !== '-').map(group => ({\n                label: group.id,\n                value: group.id\n            })));\n        }\n    },\n    data () {\n        return {\n            content: []\n        };\n    },\n    watch: {\n        value (newValue) {\n            this.content = newValue;\n        },\n        content (newValue) {\n            this.$emit('input', newValue);\n        },\n        cookieGroups: {\n            handler: function (newValue) {\n                if (!newValue) {\n                    return;\n                }\n\n                let availableGroups = newValue.map(group => group.id);\n\n                for (let i = 0; i < this.content.length; i++) {\n                    if (availableGroups.indexOf(this.content[i].cookieGroup) === -1) {\n                        Vue.set(this.content[i], 'cookieGroup', '');\n                    }\n                }\n            },\n            deep: true\n        }\n    },\n    mounted: function() {\n        setTimeout(() => {\n            this.content = this.value;\n        }, 0);\n    },\n    methods: {\n        addRule () {\n            this.content.push({\n                cookieGroup: '',\n                ad_storage: false,\n                ad_personalization: false,\n                ad_user_data: false,\n                analytics_storage: false,\n                personalization_storage: false,\n                functionality_storage: false,\n                security_storage: false\n            });\n        },\n        removeRule (index) {\n            this.content.splice(index, 1);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.g-consent-mode-groups {\n    border-radius: 3px;\n    padding-top: 1.75rem;\n\n    &-header {\n        display: flex;\n        margin-top: 1rem;\n\n        &-cell {\n            font-size: 1.4rem;\n            font-weight: bold;\n            margin: 0 0 1rem 0;\n            width: calc(100% - 15px);\n        }\n    }\n\n    .button {\n        margin: 1rem 0;\n    }\n    \n}\n\n.g-consent-mode-group {\n    align-items: center;\n    display: flex;\n    flex-wrap: wrap;\n    margin-bottom: 20px;\n    padding: .25rem 0;\n\n    &-switchers {\n        display: grid;\n        grid-template-columns: repeat(3, auto);\n        gap: .8rem 1rem;\n        margin: 1.5rem 0 2rem .5rem;\n        width: calc(100% - 51px);\n    }\n\n    &-btn {\n        align-items: center;\n        background: var(--gray-1);\n        position: relative;\n        border-radius: 50%;\n        display: flex;\n        height: 30px;\n        justify-content: center;\n        margin: 0 2px;\n        position: relative;\n        text-align: center;\n        width: 30px;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--headings-color);\n        }\n\n        &:hover {\n\n            & > svg {\n                fill: var(--icon-tertiary-color);\n                transform: scale(1);\n            }\n        }\n\n        svg {\n            fill: var(--icon-secondary-color);\n            height: 1.6rem;\n            pointer-events: none;\n            transform: scale(.9);\n            transition: var(--transition);\n            width: 1.6rem;\n        }\n\n        &.delete {\n\n            &:hover {\n\n                & > svg {\n                    fill: var(--warning);\n                }\n            }\n        }\n    } \n\n\n    .input-wrapper,\n    select {\n        padding-right: 1rem;\n        text-align: left;\n        width: calc(100% - 15px);\n    }\n\n    select {\n        margin-right: 1rem;\n        width: calc(100% - 56px);\n    }\n\n    .has-label {\n        align-items: center;\n        display: flex;\n        font-size: 1.4rem;\n        letter-spacing: -.01em;\n        margin: 0;\n\n        ::v-deep .switcher {\n            top: 0;\n            transform: scale(.88);\n        }\n    }\n    \n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/GdprGroups.vue",
    "content": "<template>\n    <div class=\"gdpr-groups\">\n        <div\n            v-if=\"content.length\"\n            class=\"gdpr-groups-header\">\n            <div class=\"gdpr-groups-header-cell\">{{ $t('gdpr.groupName') }}</div>\n            <div class=\"gdpr-groups-header-cell\">{{ $t('gdpr.groupID') }}</div>\n        </div>\n\n        <div\n            v-for=\"(group, index) of content\"\n            class=\"gdpr-group\"\n            :key=\"'gdpr-group-' + index\">\n            <text-input\n                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                v-model=\"group.name\"\n                :placeholder=\"$t('gdpr.groupName')\" />\n\n            <text-input\n                :spellcheck=\"false\"\n                v-model=\"group.id\"\n                :placeholder=\"$t('gdpr.groupID')\" />\n\n            <a\n                href=\"#\"\n                class=\"gdpr-group-btn delete\"\n                tabindex=\"-1\"\n                @click.stop.prevent=\"removeGroup(index)\">\n                <icon\n                    name=\"trash\"\n                    size=\"xs\" />\n            </a>\n\n            <text-area\n                v-model=\"group.description\"\n                :placeholder=\"$t('gdpr.groupDescriptionPlaceholder')\"\n                :rows=\"3\"></text-area>\n        </div>\n\n        <p-button\n            icon=\"add-site-mono\"\n            type=\"secondary icon\"\n            @click.native=\"addGroup\">\n            {{ $t('gdpr.addGroup') }}\n        </p-button>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'gdpr-groups',\n    props: ['value'],\n    data () {\n        return {\n            content: []\n        };\n    },\n    watch: {\n        value (newValue) {\n            this.content = newValue;\n        },\n        content (newValue) {\n            this.$emit('input', newValue);\n        }\n    },\n    mounted: function() {\n        setTimeout(() => {\n            this.content = this.value;\n\n            for (let i = 0; i < this.content.length; i++) {\n                if (typeof this.content[i].description === 'undefined') {\n                    this.content[i].description = '';\n                }\n            }\n        }, 0);\n    },\n    methods: {\n        addGroup () {\n            this.content.push({\n                name: \"\",\n                id: \"\",\n                description: \"\"\n            });\n        },\n        removeGroup (index) {\n            this.content.splice(index, 1);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.gdpr-groups {\n    border-radius: 3px;\n    padding-top: 1.75rem;\n\n    .gdpr-groups-header {\n        display: flex;\n        margin-top: 1rem;\n\n        &-cell {\n            font-size: 1.4rem;\n            font-weight: bold;\n            margin: 0 0 1rem 0;\n            width: calc(50% - 23px);\n\n            &:last-child {\n                width: 80px;\n            }\n        }\n    }\n\n    .gdpr-group {\n        align-items: center;\n        display: flex;\n        flex-wrap: wrap;\n        padding: .25rem 0;\n\n        &-btn {\n            align-items: center;\n            background: var(--gray-1);\n            position: relative;\n            border-radius: 50%;\n            display: flex;\n            height: 30px;\n            justify-content: center;\n            margin: 0 2px;\n            position: relative;\n            text-align: center;\n            width: 30px;\n\n            &:active,\n            &:focus,\n            &:hover {\n                color: var(--headings-color);\n            }\n\n            &:hover {\n\n                & > svg {\n                    fill: var(--icon-tertiary-color);\n                    transform: scale(1);\n                }\n            }\n\n            svg {\n                fill: var(--icon-secondary-color);\n                height: 1.6rem;\n                pointer-events: none;\n                transform: scale(.9);\n                transition: var(--transition);\n                width: 1.6rem;\n            }\n\n            &.delete {\n\n                &:hover {\n\n                    & > svg {\n                        fill: var(--warning);\n                    }\n                }\n            }\n        } \n\n        .input-wrapper {\n            padding-right: 1rem;\n            text-align: left;\n            width: calc(50% - 23px);\n        }\n\n        div:last-child {\n            margin-bottom: 3rem;\n            margin-top: 1rem;\n            width: calc(100% - 56px);\n        }\n    }\n\n    .button {\n        margin: 1rem 0;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Header.vue",
    "content": "<template>\n    <div class=\"heading\">\n        <h1 class=\"title\">\n            {{ title }}\n        </h1>\n\n        <slot\n            v-if=\"hasSearchSlot\"\n            name=\"search\" />\n\n        <div class=\"buttons\">\n            <slot name=\"buttons\"></slot>\n        </div>\n\n        <slot\n            v-if=\"hasFiltersSlot\"\n            name=\"filters\" />\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'p-header',\n    props: {\n        title: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        hasFiltersSlot () {\n            return !!this.$slots['filters']\n        },\n        hasSearchSlot () {\n            return !!this.$slots['search']\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.heading {\n    display: flex;\n    margin-bottom: 12 * $spacing;\n    user-select: none;\n    width: 100%;\n\n    @include clearfix;\n\n    .title {    \n        line-height: 4.3rem;\n        margin: 0;\n        padding: 0 2rem 0 0;\n    }\n}\n\n.buttons {\n    display: inline-flex;\n    margin-left: auto;\n    white-space: nowrap;\n\n    button {\n        position: relative;\n        top: -0.75rem;\n    }\n\n    .button {\n        margin-left: auto;\n\n        & + .button {\n          margin-left: 1rem;\n        }\n\n        & > span.progress {\n            display: inline-block;\n            width: 3.2rem;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/HeaderSearch.vue",
    "content": "<template>\n    <div :class=\"cssClasses\">\n        <icon\n            size=\"xs\"\n            name=\"magnifier-small\"\n            @click.native=\"open\" />\n\n        <input\n            type=\"search\"\n            v-model=\"value\"\n            :placeholder=\"placeholder\"\n            :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n            ref=\"input-field\"\n            @keyup=\"updateValue\" />\n\n        <span\n            v-if=\"isOpen\"\n            @click.stop=\"close\">\n            &times;\n        </span>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'header-search',\n    props: {\n        placeholder: {\n            default: '',\n            type: String\n        },\n        onChangeEventName: {\n            default: 'header-search-value-changed',\n            type: String\n        }\n    },\n    data: function() {\n        return {\n            value: '',\n            isOpen: false\n        };\n    },\n    computed: {\n        cssClasses: function() {\n            return {\n                'search': true,\n                'is-opened': this.isOpen\n            };\n        }\n    },\n    methods: {\n        open: function() {\n            if(this.isOpen) {\n                return;\n            }\n\n            this.isOpen = true;\n            this.value = '';\n            this.$bus.$emit(this.onChangeEventName, '');\n            this.$refs['input-field'].focus();\n        },\n        close: function() {\n            if(!this.isOpen) {\n                return;\n            }\n\n            this.isOpen = false;\n            this.value = '';\n            this.$bus.$emit(this.onChangeEventName, '');\n            this.$refs['input-field'].blur();\n        },\n        updateValue: function() {\n            this.$bus.$emit(this.onChangeEventName, this.value);\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.search {\n    max-width: 700px;\n    padding-left: 1rem;\n    position: relative;\n    width: 100%;\n\n    & > svg {\n        cursor: pointer;\n        fill: var(--icon-primary-color);\n        left: 1.5rem;\n        position: absolute;\n        top: 1.4rem;\n        transition: var(--transition);\n        z-index: 1;\n        \n        &:hover {\n            fill: var(--icon-tertiary-color);\n        }\n    }\n\n    & > input { \n        border: 0;\n        border-radius: 30px;\n        box-shadow: none;\n        font-size: $app-font-base;\n        height: 4.4rem;\n        opacity: 0;\n        padding: 0 5rem 0 6rem;\n        pointer-events: none;\n        position: relative;\n        top: -0.125rem;\n        transition: var(--transition);\n        transform: scaleX(.25);\n        transform-origin: left center;\n        width: calc(100% - 3rem); \n    }\n\n    & > span {\n        animation: close-delay .3s ease-out .3s forwards;\n        border-radius: 50%;\n        color: var(--icon-secondary-color);\n        cursor: pointer;\n        font-size: 2.4rem;\n        font-weight: 300;\n        height: 3rem;\n        line-height: 1;       \n        opacity: 0;\n        padding: 0;\n        position: absolute;\n        right: 4.4rem;\n        text-align: center;       \n        transition: all .3s ease-out;\n        transition-delay: .3s;  \n        top: 50%;\n        transform: translate(0, -50%);       \n        width: 3rem;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--icon-tertiary-color);\n        }\n        \n        &:hover {\n            background: var(--input-border-color);\n        }\n    }\n\n    &.is-opened {\n        & > svg {\n            left: 3rem;\n            cursor: default;\n            \n            &:hover {\n                fill: var(--icon-primary-color);\n            }\n        }\n\n        & > input {\n            background: var(--input-bg-lightest);           \n            opacity: 1;\n            pointer-events: auto;\n            transform: scaleX(1);\n        }\n\n        & > span {\n             transition-delay: 0s;\n\n             @at-root {\n                  @keyframes close-delay {\n                     from {\n                          opacity: 0;\n                     }\n                     to {\n                          opacity: 1;\n                     }\n                 }\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Icon.vue",
    "content": "<template>\n    <svg\n        :class=\"cssClasses\"\n        :style=\"style\">\n        <use :xlink:href=\"iconPath\" />\n    </svg>\n</template>\n\n<script>\nimport SassColors from './../../helpers/sass-colors.js';\n\nexport default {\n    props: {\n        'name': {\n            default: '',\n            type: String\n        },\n        'size': {\n            default: '',\n            type: String\n        },\n        'iconset': {\n            default: 'svg-map',\n            type: String\n        },\n        'primaryColor': {\n            default: '',\n            type: String\n        },\n        'strokeColor': {  // Changed from primaryStroke to strokeColor\n            default: '',\n            type: String\n        },\n        'properties': {\n            default: '',\n            type: String\n        },\n        'customCssClasses': {\n            default: '',\n            type: String\n        },\n        'customWidth': {\n            default: '',\n            type: String\n        },\n        'customHeight': {\n            default: '',\n            type: String\n        }\n    },\n    name: 'icon',\n    computed: {\n        cssClasses: function() {\n            let classes = {\n                'icon': true,\n                'size-xxxs': this.size === 'xxxs',\n                'size-xxs': this.size === 'xxs',\n                'size-xs': this.size === 'xs',\n                'size-s': this.size === 's',\n                'size-m': this.size === 'm',\n                'size-l': this.size === 'l',\n                'size-xl': this.size === 'xl',\n                'size-xxl': this.size === 'xxl',\n                'size-xxxl': this.size === 'xxxl',\n                'not-clickable': this.properties.indexOf('not-clickable') > -1\n            };\n\n            if(this.customCssClasses !== '') {\n                let additionalCssClasses = this.customCssClasses.split(' ');\n\n                for(let cssClass of additionalCssClasses) {\n                    classes[cssClass] = true;\n                }\n            }\n\n            return classes;\n        },\n        style: function() {\n            let style = [];\n\n            if(this.primaryColor !== '' && SassColors[this.primaryColor]) {\n                style.push(`fill: ${SassColors[this.primaryColor]}`);\n            }\n\n            if(this.strokeColor !== '' && SassColors[this.strokeColor]) {\n                style.push(`color: ${SassColors[this.strokeColor]}`);\n            }\n\n            if(this.customWidth !== '') {\n                style.push(`width: ${this.customWidth}px`);\n            }\n\n            if(this.customHeight !== '') {\n                style.push(`height: ${this.customHeight}px`);\n            }\n\n            return style.join(';')\n        },\n        iconPath: function() {\n            return `../src/assets/svg/${this.iconset}.svg#${this.name}`;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.icon {\n    &.size-xxxs {\n        height: 10px;\n        width: 10px;\n    }\n\n    &.size-xxs {\n        height: 12px;\n        width: 12px;\n    }\n\n    &.size-xs {\n        height: 16px;\n        width: 16px;\n    }\n\n    &.size-s {\n        height: 20px;\n        width: 20px;\n    }\n\n    &.size-m {\n        height: 24px;\n        width: 24px;\n    }\n\n    &.size-l {\n        height: 32px;\n        width: 32px;\n    }\n\n    &.size-xl {\n        height: 50px;\n        width: 50px;\n    }\n\n    &.size-xxl {\n        height: 100px;\n        width: 100px;\n    }\n\n    &.size-xxxl {\n        height: 200px;\n        width: 200px;\n    }\n\n    &.not-clickable {\n        pointer-events: none;\n    }\n\n    &.file {\n        fill: var(--gray-1);\n        margin: -2px 1rem -2px 0;\n        position: relative!important;\n        top: 4px!important;\n    }\n\n    &.file--txt {\n        color: #42a5f5;\n    }\n\n    &.file--code {\n        color: #2a2e30;\n    }\n\n    &.file--img {\n        color: #40b771;\n    }\n\n    &.file--video {\n        color: #ef5350;\n    }\n\n    &.file--zip {\n        color: #2a2e30;\n    }\n\n    &.file--music {\n        color: #2a2e30;\n    }\n\n    &.file--pdf {\n        color: #ef5350;\n    }\n}\n\n.directory-link {\n    .icon {\n        margin-right: 1rem;\n        position: relative;\n        top: 3px;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/ImageUpload.vue",
    "content": "<template>\n    <div\n        :id=\"anchor\"\n        :class=\"wrapperCssClasses\">\n        <div\n            :class=\"inputCssClasses\"\n            :data-path=\"filePath\"\n            :style=\"backgroundImage\"\n            @drag=\"stopEvents\"\n            @dragstart=\"stopEvents\"\n            @dragend=\"stopEvents\"\n            @dragover=\"dragOver\"\n            @dragenter=\"stopEvents\"\n            @dragleave=\"dragLeave\"\n            @drop=\"drop\">\n            <div class=\"upload-overlay\">\n                <icon\n                    name=\"blank-image\"\n                    customWidth=\"75\"\n                    customHeight=\"62\" />\n\n               <div> {{ labelText }}</div>\n                <input\n                    ref=\"input\"\n                    type=\"file\"\n                    class=\"upload-image-input\"\n                    spellcheck=\"false\"\n                    @change=\"valueChanged\">\n\n                <div\n                    v-if=\"isUploading\"\n                    class=\"upload-uploading-overlay\">\n                    <div>\n                        <div class=\"loader\"><span></span></div>\n                        {{ $t('ui.uploadInProgress') }}\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <a\n            v-if=\"!isEmpty\"\n            href=\"#\"\n            class=\"upload-remove\"\n            @click=\"remove\">\n            {{ $t('image.removeImage') }}\n        </a>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'image-upload',\n    props: {\n        value: {\n            default: '',\n            type: String\n        },\n        type: {\n            default: '',\n            type: String\n        },\n        'item-id': {\n            type: [String, Number]\n        },\n        onRemove: {\n            default: () => false,\n            type: Function\n        },\n        onBeforeRemove: {\n            default: () => false,\n            type: Function\n        },\n        onAdd: {\n            default: () => false,\n            type: Function\n        },\n        addMediaFolderPath: {\n            default: false,\n            type: Boolean\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        imageType: {\n            default: 'pluginImages',\n            type: String\n        },\n        pluginDir: {\n            default: '',\n            type: String\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    data () {\n        return {\n            isEmpty: true,\n            filePath: '',\n            isUploading: false,\n            isHovered: false\n        }\n    },\n    watch: {\n        value: async function (newValue, oldValue) {\n            if (newValue && typeof newValue === 'string') {\n                if (newValue.indexOf('https://') === 0 || newValue.indexOf('http://') === 0) {\n                    this.filePath = newValue;\n                } else {\n                    this.filePath = await this.mediaPath + newValue;\n                }\n\n                this.isEmpty = false;\n            }\n        },\n        filePath: function(newValue) {\n            if (newValue === '') {\n                this.$emit('input', '');\n            } else {\n                if (newValue.indexOf('http://') === 0 || newValue.indexOf('https://') === 0) {\n                    this.$emit('input', newValue);\n                } else {\n                    if (this.addMediaFolderPath) {\n                        this.$emit('input', 'media/website/' + newValue.split('/').pop());\n                    } else {\n                        this.$emit('input', newValue.split('/').pop());\n                    }\n                }\n            }\n        }\n    },\n    mounted () {\n        setTimeout(async () => {\n            if (this.value && typeof this.value === 'string') {\n                if (this.value.indexOf('https://') === 0 || this.value.indexOf('http://') === 0) {\n                    this.filePath = this.value;\n                } else {\n                    this.filePath = await this.mediaPath + this.value;\n                }\n\n                this.isEmpty = false;\n            }\n        }, 0);\n    },\n    computed: {\n        labelText () {\n            let label = this.$t('image.dropToUploadPhotoOr');\n\n            if ((this.itemId || this.itemId === 0) && this.imageType !== 'tagImages' && this.imageType !== 'authorImages') {\n                label = this.$t('image.dropFeaturedImageOr');\n            }\n\n            return label;\n        },\n        wrapperCssClasses () {\n            let cssClasses = {\n                'is-small': this.type.indexOf('small') > -1,\n                'upload-image-wrapper': true,\n                'is-empty': this.isEmpty\n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        },\n        inputCssClasses () {\n            return {\n                'upload-image': true,\n                'is-empty': this.isEmpty,\n                'is-hovered': this.isHovered,\n                'is-small': this.type.indexOf('small') > -1\n            };\n        },\n        backgroundImage () {\n            if (this.filePath !== '') {\n                if (this.filePath.indexOf('https://') === 0 || this.filePath.indexOf('http://') === 0) {\n                    return 'background-image: url(\\'' + this.filePath + '\\');';\n                }\n\n                return 'background-image: url(\\'file:///' + this.filePath + '\\');';\n            }\n\n            return false;\n        },\n        async mediaPath () {\n            if (this.itemId && this.imageType === 'tagImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/tags/' + this.itemId + '/';\n            } else if (this.itemId && this.imageType === 'authorImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/authors/' + this.itemId + '/';\n            } else if (this.itemId === 0 && this.imageType === 'tagImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/tags/temp/';\n            } else if (this.itemId === 0 && this.imageType === 'authorImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/authors/temp/';\n            } else if (this.imageType === 'pluginImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/plugins/' + this.pluginDir + '/';\n            } else if (this.itemId === 0) {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/posts/temp/';\n            } else if (this.itemId === 'defaults' && this.imageType === 'contentImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/posts/defaults/';\n            } else if (this.itemId === 'defaults' && this.imageType === 'authorImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/authors/defaults/';\n            } else if (this.itemId === 'defaults' && this.imageType === 'tagImages') {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/tags/defaults/';\n            } else if (this.itemId) {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/posts/' + this.itemId + '/';\n            }\n\n            if (this.addMediaFolderPath) {\n                return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/';\n            }\n\n            return await mainProcessAPI.normalizePath(this.$store.state.currentSite.siteDir) + '/input/media/website/';\n        }\n    },\n    methods: {\n        stopEvents (e) {\n            e.preventDefault();\n            e.stopPropagation();\n        },\n        dragOver (e) {\n            this.stopEvents(e);\n            this.isHovered = true;\n        },\n        dragLeave (e) {\n            this.stopEvents(e);\n            this.isHovered = false;\n        },\n        async drop (e) {\n            this.stopEvents(e);\n            let sourcePath = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.dataTransfer.files[0]));\n            this.uploadImage(sourcePath);\n        },\n        remove (e) {\n            e.preventDefault();\n            this.onBeforeRemove(this.filePath);\n            this.filePath = '';\n            this.$refs['input'].value = '';\n            this.isEmpty = true;\n            this.onRemove();\n        },\n        async valueChanged (e) {\n            if(!e.target.files.length) {\n                return;\n            }\n\n            let sourcePath = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.target.files[0]));\n            this.uploadImage(sourcePath);\n        },\n        uploadImage (sourcePath) {\n            this.isUploading = true;\n\n            let uploadData = {\n                id: 'website',\n                site: this.$store.state.currentSite.config.name,\n                path: sourcePath,\n                imageType: 'optionImages'\n            };\n\n            if (this.itemId && this.itemId === 'defaults') {\n                uploadData.id = this.itemId;\n                uploadData.imageType = this.imageType;\n            } else if ((this.itemId || this.itemId === 0) && this.imageType === 'tagImages') {\n                uploadData.id = this.itemId;\n                uploadData.imageType = 'tagImages';\n            } else if ((this.itemId || this.itemId === 0) && this.imageType === 'authorImages') {\n                uploadData.id = this.itemId;\n                uploadData.imageType = 'authorImages';\n            } else if (this.imageType === 'pluginImages') {\n                uploadData.imageType = 'pluginImages';\n                uploadData.pluginDir = this.pluginDir;\n            } else if ((this.itemId || this.itemId === 0) && this.imageType === 'contentImages') {\n                uploadData.id = this.itemId;\n                uploadData.imageType = 'contentImages';\n            } else if ((this.itemId || this.itemId === 0)) {\n                uploadData.id = this.itemId;\n                uploadData.imageType = 'featuredImages';\n            } \n\n            mainProcessAPI.send('app-image-upload', uploadData);\n\n            mainProcessAPI.receiveOnce('app-image-uploaded', async (data) => {\n                this.isEmpty = false;\n                this.isHovered = false;\n                this.filePath = await mainProcessAPI.normalizePath(data.baseImage.newPath);\n                this.isUploading = false;\n                this.onAdd();\n            });\n        },\n        async setImage (newPath, addMedia = false) {\n            this.filePath = newPath;\n\n            if (addMedia) {\n                this.filePath = await this.mediaPath + newPath;\n            }\n\n            if (newPath !== '') {\n                this.isEmpty = false;\n            } else {\n                this.isEmpty = true;\n            }\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.upload {\n    &-image {\n        background-clip: padding-box;\n        background-position: center;\n        background-repeat: no-repeat;\n        border: 2px dashed var(--input-border-color);\n        border-radius: var(--border-radius);\n        color: var(--gray-3);\n        display: block;\n        font-size: $app-font-base;\n        font-weight: var(--font-weight-normal);\n        line-height: 1.5;\n        margin: 0 0 -40px 0;\n        text-align: center;\n        padding: 3rem 5rem;\n        position: relative;\n        width: 100%;\n\n        &.is-small {\n        }\n\n        &-wrapper {\n            display: block;\n            padding: 0 0 40px 0;\n\n            &:not(.is-empty):not(.is-hovered) {\n                background-color: var(--bg-secondary);\n                background-clip: content-box;\n                background-image:   linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa),\n                                    linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa);\n                background-size:36px 36px;\n                background-position:0 0, 18px 18px;\n            }\n        }\n\n        &.is-empty {\n            box-shadow: inset 0 0 0 5px var(--bg-primary);\n            container-type: inline-size;\n\n            .upload-overlay {\n                display: block;\n            }\n\n            @container (max-width: 200px) {\n                .upload-overlay svg {\n                    display: none;\n                }\n            }\n\n            @container (max-width: 160px) {\n                .upload-overlay div {\n                    display: none;\n                }\n                .upload-image-input {\n                    margin-top: 0 !important;\n                }\n            }\n        }\n\n        &-input {\n            clear: both;\n            color: transparent; // hack to remove the phrase \"no file selected\" from the file input\n            display: block;\n            line-height: 1.6!important;\n            margin: 2rem auto 0 auto!important;\n\n            span {\n                    display: none;\n                }\n\n            &::-webkit-file-upload-button {\n                -webkit-appearance: none;\n                background: var(--button-secondary-bg);\n                border: 1px solid var(--button-secondary-bg);\n                border-radius: var(--border-radius);\n                color: var(--button-secondary-color);\n                cursor: pointer;\n                display: inline-block;\n                font-size: 1.4rem;\n                font-weight: var(--font-weight-semibold);\n                left: 50%;\n                padding: .75rem 1.5rem;\n                position: relative;\n                transform: translate(-50%, 0);\n                outline: none;\n                \n                &:hover {\n                    background: var(--button-secondary-bg-hover);\n                    border-color: var(--button-secondary-bg-hover);\n                    color: var(--button-secondary-color-hover);\n                }\n            }\n        }\n\n        &.is-hovered {\n            border-color: var(--color-primary);\n        }\n\n        &:not(.is-empty):not(.is-hovered) {\n            background-color: transparent;\n            background-position: center center;\n            background-repeat: no-repeat;\n            background-size: contain;\n            border: none;\n            padding: 10rem;\n\n            &.is-small {\n                padding: 9rem;\n            }\n        }\n    }\n\n    &-remove {\n        color: var(--warning);\n        display: block;\n        font-size: 13px;\n        margin: 10px 0;\n        position: relative;\n        text-align: center;\n        top: 40px;\n        width: 100%;\n\n        &.is-hidden {\n            display: none;\n        }\n    }\n\n    &-overlay {\n        color: var(--gray-3);\n        display: none;\n\n        svg {\n            display: block;\n            fill: var(--icon-quaternary-color);\n            margin: 0 auto 1.5rem;\n\n        }\n    }\n\n    &-uploading-overlay {\n        background: var(--gray-1);\n        height: 100%;\n        left: 0;\n        position: absolute;\n        top: 0;\n        width: 100%;\n\n        & > div {\n            color: var(--gray-3)!important;\n            left: 50%;\n            position: absolute;\n            top: 50%;\n            transform: translateX(-50%) translateY(-50%);\n            width: 100%;\n        }\n\n        .loader {\n            display: block;\n            height: 2.8rem;\n            margin: 0 auto 1rem;\n            width: 2.8rem;\n\n            & > span {\n                animation: spin .9s infinite linear;\n                border-top: 2px solid rgba(var(--color-primary-rgb), .2);\n                border-right: 2px solid rgba(var(--color-primary-rgb), .2);\n                border-bottom: 2px solid rgba(var(--color-primary-rgb), .2);\n                border-left: 2px solid var(--color-primary);\n                border-radius: 50%;\n                display: block;\n                height: 2.5rem;\n                width: 2.5rem;\n\n                &::after {\n                    border-radius: 50%;\n                    content: \"\";\n                    display: block;\n                }\n\n                @at-root {\n                    @keyframes spin {\n                       100% {\n                          transform: rotate(360deg);\n                       }\n                    }\n                }\n          }\n       }\n    }\n}\n\n.settings-basic {\n    .upload {\n        display: block;\n        margin-bottom: 40px;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/LogoCreator.vue",
    "content": "<template>\n    <div class=\"logo-creator\">\n        <div\n            class=\"logo-creator-preview\"\n            :data-icon=\"activeIcon\">\n            <icon\n                size=\"xl\"\n                :name=\"icons[activeIcon - 1]\" \n                iconset=\"svg-map-site\"/>\n        </div>\n\n        <ul class=\"logo-creator-icon\">\n            <li\n                v-for=\"(icon, index) in icons\"\n                :data-status=\"isActiveIcon(index + 1)\"\n                @click=\"changeIcon(index + 1)\"\n                class=\"logo-creator-icon-block\" >\n                <icon                   \n                    size=\"m\"\n                    properties=\"not-clickable\"\n                    :name=\"icon\" \n                    iconset=\"svg-map-site\"/>\n            </li>\n        </ul>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'logo-creator',\n    data () {\n        return {\n            activeIcon: 1,\n            activeColor: 1,\n            icons: []\n        };\n    },\n    computed: {\n        colors: function() {\n            return Array(16).fill().map((e, i) => i+1);\n        }\n    },\n    mounted () {\n        Vue.nextTick(() => {\n            this.icons = [\n                'web-pizza',\n                'web-ice-cream',\n                'web-school',\n                'web-leaf',\n                'web-bag',\n                'web-shirt',\n                'web-note',\n                'web-mic',\n                'web-headphone',\n                'web-american-football',\n                'web-racket',\n                'web-car',\n                'web-camera',\n                'web-doc',\n                'web-credit-card',\n                'web-gear',\n                'web-palette',\n                'web-boat',\n                'web-cut',\n                'web-film',\n                'web-flask',\n                'web-heart',\n                'web-journal',\n                'web-rocket',\n                'web-glass',\n                'web-pie',\n                'web-paw',\n                'web-fire',\n                'web-planet',\n                'web-watch',\n                'web-idea',\n                'web-pulse',\n                'web-bell',\n                'web-briefcase',\n                'web-clipboard',\n                'web-command',\n                'web-cpu',\n                'web-droplet',\n                'web-dollar-sign',\n                'web-edit-2',\n                'web-eye',\n                'web-feather',\n                'web-file-text',\n                'web-flag',\n                'web-home',\n                'web-image',\n                'web-moon',\n                'web-percent',\n                'web-power',\n                'web-shield',\n                'web-smartphone',\n                'web-speaker',\n                'web-sun',\n                'web-tv',\n                'web-umbrella',\n                'web-radio',\n                'web-layers'\n            ];\n        });\n    },\n    methods: {\n        changeIcon: function(newIndex) {\n            if (typeof newIndex === 'string') {\n                newIndex = this.icons.indexOf(newIndex) + 1;\n            }\n\n            this.activeIcon = newIndex;\n        },\n        changeColor: function(newIndex) {\n            this.activeColor = newIndex;\n        },\n        isActiveColor: function(colorIndex) {\n            if(colorIndex === this.activeColor) {\n                return 'active';\n            }\n\n            return 'inactive';\n        },\n        isActiveIcon: function(iconIndex) {\n            if(iconIndex === this.activeIcon) {\n                return 'active';\n            }\n\n            return 'inactive';\n        },\n        getActiveColor: function() {\n            return this.activeColor;\n        },\n        getActiveIcon: function() {\n            return this.icons[this.activeIcon - 1];\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.logo-creator {\n    display: flex;\n    margin-bottom: 32px;\n    max-width: 100%;\n    overflow: hidden;\n    text-align: center;\n\n    &-preview {          \n        line-height: 100%;\n        min-width: 23rem;\n        overflow: hidden;\n        padding: 1rem;\n        text-align: left;       \n    }\n\n    &-color {        \n        margin: 0 0 1rem;\n        padding: 0;       \n\n        &-block {  \n            border-radius: 50%;\n            cursor: pointer;\n            display: block;\n            float: left;\n            height: 3.2rem;\n            list-style-type: none;\n            margin: 0 0.15rem;\n            padding: 0;\n            transition: all .2s ease-out;\n            width: 3.2rem;\n\n            &[data-status=\"active\"] {\n                transform: scale(1);\n            }\n\n            &[data-status=\"inactive\"] {\n                transform: scale(.75); \n            }\n        }\n    }\n\n    &-icon {\n        float: right;\n        margin: 0;\n        padding: 0;\n\n        &-block {\n            color: var(--icon-secondary-color);\n            cursor: pointer;\n            display: block;\n            float: left;\n            height: 3rem;\n            list-style-type: none;\n            padding: 0 1px 0 0;\n            transition: all .2s ease-out;  \n            \n            &:hover {\n                color: var(--icon-tertiary-color);\n            }\n\n            & > svg {                 \n                margin: .5rem;\n            }\n\n            &[data-status=\"active\"] {\n                color: var(--icon-tertiary-color);\n                transform: scale(1);\n            }\n\n            &[data-status=\"inactive\"] {               \n                transform: scale(.75);\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Overlay.vue",
    "content": "<template>\n    <div :class=\"cssClasses\">\n        <slot />\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'overlay',\n    props: {\n        hasBorder: {\n            default: false,\n            type: Boolean\n        },\n        isBlue: {\n            default: false,\n            type: Boolean\n        }\n    },\n    computed: {\n        cssClasses: function() {\n            return {\n                'overlay': true,\n                'has-border': this.hasBorder,\n                'is-blue': this.isBlue\n            };\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n/*\n * Overlay\n */\n.overlay {\n    align-items: center;\n    background: var(--overlay);\n    bottom: 0;\n    color: var(--gray-4);\n    display: flex;   \n    font-weight: bold;\n    justify-content: center;\n    left: 0;\n    position: absolute;\n    right: 0;\n    top: 0;\n\n    &-icon {\n        font-size: 2rem;\n        left: 50%;\n        position: absolute;\n        top: 50%;\n        transform: translateX(-50%) translateY(-50%);\n    }\n\n    &.has-border {\n        border: 3px dashed var(--gray-1);\n        border-radius: 10px;\n    }\n\n    &.is-blue {\n        border: 3px solid var(--color-primary);\n        background: rgba(var(--color-primary-rgb), .17);\n        \n        & > div {\n            box-shadow: 0 0 3px rgba(black, .2);\n            background: var(--color-primary);\n            border-radius: 3px;\n            color: var(--white);\n            font-size: $app-font-base;\n            font-weight: var(--font-weight-semibold);\n            height: auto;\n            left: 50%;\n            line-height: 1.5;                \n            padding: 1.4rem 3rem 1.4rem 3rem;\n            position: absolute;\n            top: 50%; \n            transform: translateX(-50%) translateY(-50%);\n            width: auto;              \n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/PagesDropDown.vue",
    "content": "<template>\n    <v-select\n        ref=\"dropdown\"\n        :class=\"'pages-dropdown ' + customCssClasses.replace(/[^a-z0-9\\-\\_\\s]/gmi, '')\"\n        :id=\"anchor\"\n        :options=\"pages\"\n        v-model=\"selectedPage\"\n        :custom-label=\"pageLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        :multiple=\"multiple\"\n        @select=\"closeDropdown()\"\n        :placeholder=\"placeholder\"></v-select>\n</template>\n\n<script>\nexport default {\n    name: 'pages-dropdown',\n    props: {\n        multiple: {\n            type: Boolean,\n            default: false\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        value: {},\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    data () {\n        return {\n            selectedPage: ''\n        };\n    },\n    computed: {\n        pages () {\n            return [''].concat(this.$store.state.currentSite.pages.slice().sort((a, b) => {\n                return a.title.localeCompare(b.title);\n            }).map(page => page.id));\n        },\n        placeholder () {\n            return this.$t('page.selectPage');\n        }\n    },\n    watch: {\n        value: function (newValue, oldValue) {\n            this.selectedPage = newValue;\n        },\n        selectedPage: function (newValue, oldValue) {\n            this.$emit('input', newValue);\n        }\n    },\n    mounted () {\n        if (this.value) {\n            this.selectedPage = this.value;\n        }\n    },\n    methods: {\n        pageLabels (value) {\n            return this.$store.state.currentSite.pages.filter(page => page.id === value).map(page => page.title)[0];\n        },\n        closeDropdown () {\n            this.$refs['dropdown'].isOpen = false;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n.multiselect {\n    line-height: 2;\n}\n\n.multiselect,\n.multiselect__tags {\n    min-height: 49px;\n}\n\n.multiselect__tags {\n    padding: 0 4rem 0 1.8rem;\n}\n\n.pages-dropdown .multiselect__input {\n    max-width: 100%;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/PostsDropDown.vue",
    "content": "<template>\n    <v-select\n        ref=\"dropdown\"\n        :class=\"'posts-dropdown ' + customCssClasses.replace(/[^a-z0-9\\-\\_\\s]/gmi, '')\"\n        :id=\"anchor\"\n        :options=\"postPages\"\n        v-model=\"selectedPost\"\n        :custom-label=\"postLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        :multiple=\"multiple\"\n        @select=\"closeDropdown()\"\n        :placeholder=\"placeholder\"></v-select>\n</template>\n\n<script>\nexport default {\n    name: 'posts-dropdown',\n    props: {\n        multiple: {\n            type: Boolean,\n            default: false\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        value: {},\n        allowedPostStatus: {\n            default: ['any']\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    data () {\n        return {\n            selectedPost: ''\n        };\n    },\n    computed: {\n        postPages () {\n            return [''].concat(this.$store.state.currentSite.posts.filter(post => {\n                let postStatuses = post.status.split(',');\n\n                if (postStatuses.indexOf('trashed') > -1) {\n                    return false;\n                }\n\n                if (this.allowedPostStatus[0] === 'any') {\n                    return true;\n                }\n\n                return this.allowedPostStatus.some(status => postStatuses.includes(status));\n            }).sort((a, b) => {\n                return a.title.localeCompare(b.title);\n            }).map(post => post.id));\n        },\n        placeholder () {\n            return this.$t('post.selectPostPage');\n        }\n    },\n    watch: {\n        value: function (newValue, oldValue) {\n            this.selectedPost = newValue;\n        },\n        selectedPost: function (newValue, oldValue) {\n            this.$emit('input', newValue);\n        }\n    },\n    mounted () {\n        if (this.value) {\n            this.selectedPost = this.value;\n        }\n    },\n    methods: {\n        postLabels (value) {\n            return this.$store.state.currentSite.posts.filter(post => post.id === value).map(post => post.title)[0];\n        },\n        closeDropdown () {\n            this.$refs['dropdown'].isOpen = false;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n.multiselect {\n    line-height: 2;\n}\n\n.multiselect,\n.multiselect__tags {\n    min-height: 49px;\n}\n\n.multiselect__tags {\n    padding: 0 4rem 0 1.8rem;\n}\n\n.posts-dropdown .multiselect__input {\n    max-width: 100%;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/ProgressBar.vue",
    "content": "<template>\n    <div :class=\"cssWrapperClasses\">\n        <div class=\"progress\">\n            <div\n                :class=\"cssBarClasses\"\n                :style=\"'width: ' + progress + '%'\">\n            </div>\n        </div>\n\n        <p\n            v-if=\"message\"\n            class=\"progress-message\">\n            {{ message }}\n        </p>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'progress-bar',\n    props: {\n        progress: {\n            default: 0,\n            type: Number\n        },\n        color: {\n            default: 'blue',\n            type: String\n        },\n        stopped: {\n            default: false,\n            type: Boolean\n        },\n        message: {\n            default: '',\n            type: String\n        },\n        cssClasses: {\n            default: () => ({}),\n            type: Object\n        }\n    },\n    computed: {\n        cssWrapperClasses () {\n            let defaultCss = { 'progress-wrapper': true };\n            return Object.assign(defaultCss, this.cssClasses);\n        },\n        cssBarClasses () {\n            return {\n                'progress-bar': true,\n                'is-stopped': this.stopped,\n                'is-error': this.color === 'red',\n                'is-success': this.color === 'green',\n                'is-warning': this.color === 'orange'\n            };\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.progress {\n    background: var(--input-border-color);\n    border: none;\n    border-radius: 3px;\n    height: 6px;\n    margin: 0 auto;\n    padding: 0;\n    position: relative;\n    width: 100%;\n\n    &-bar {\n        background: var(--input-border-focus);\n        border-radius: 3px;\n        height: 6px;\n        margin: 0;\n        max-width: 100%;\n        position: relative;\n        transition: width .2s ease-out;\n        width: 0;\n       \n        @at-root {\n            .sync-progress-bar {               \n                .progress-bar {\n                     background: rgba(var(--yellow), 1);\n                }\n            }\n        }\n\n        &.is-stopped {}\n\n        &.is-success {\n            background: var(--success);\n        }\n\n        &.is-error {\n            background: var(--warning);\n        }\n\n        &.is-warning {\n            background: $color-helper-6;\n        }\n    }\n\n    &-wrapper {\n        padding: 0 0 7rem;\n        position: relative;\n    }\n\n    &-message {\n        color: var(--text-light-color);\n        font-size: 1.3rem;\n        padding: 0;\n        position: absolute;\n        text-align: center;\n        bottom: 2rem;\n        width: 100%;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/RadioButton.vue",
    "content": "<template>\n    <div \n        :id=\"anchor\"\n        :class=\"cssClasses\">\n        <template v-for=\"(item, index) in items\">\n            <input\n                type=\"radio\"\n                :name=\"name\"\n                :id=\"name + '-' + item.value\"\n                :value=\"item.value\"\n                :key=\"'radio-item-' + index\"\n                :disabled=\"item.disabled\"\n                v-model=\"content\" />\n            <label\n                class=\"radio\"\n                :for=\"name + '-' + item.value\"\n                :key=\"'radio-label-' + index\">\n                {{ item.label }}\n            </label>\n        </template>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'radio-buttons',\n    props: {\n        name: {\n            default: '',\n            type: String\n        },\n        items: {\n            default: [],\n            type: Array\n        },\n        selected: {\n            default: '',\n            type: String\n        },\n        value: {\n            default: '',\n            type: [String, Number]\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        cssClasses () {\n            let cssClasses = {\n                'radio-buttons': true \n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    },\n    data: function() {\n        return {\n            content: ''\n        };\n    },\n    watch: {\n        value (newValue, oldValue) { \n            this.content = newValue;\n        },\n        content: function(newValue) {\n            this.$emit('input', newValue);\n        }\n    },\n    mounted () {\n        if (this.value) {\n            this.content = this.value;\n        } else {\n            this.content = this.selected;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n/*\n * Radio buttons\n */\n\ninput[type=\"radio\"] {\n    display: none;\n}\n\ninput[type=\"radio\"] + label {\n    color: var(--label-color);\n    display: inline-block;\n    cursor: pointer;\n    top: 0;\n    margin-right: 1rem;\n}\n\nlabel.radio:before {\n    border: 1px solid var(--input-border-color);\n    border-radius: 2px;\n    content: \"\";\n    display: inline-block;\n    height: 1.8rem;\n    line-height: 1.8rem;\n    margin-right: .5rem;\n    vertical-align: sub;\n    text-align: center;\n    width: 1.8rem;\n}\n\nlabel.radio:before {\n    border-radius: 50%;\n}\n\ninput[type=\"radio\"]:checked + label.radio:before {\n    background: var(--input-border-focus);\n    border: 1px solid var(--input-border-focus);\n    box-shadow: inset 0 0 0 4px var(--input-bg);\n    content: \"\";\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/RangeSlider.vue",
    "content": "<template>\n    <div \n        :id=\"anchor\"\n        :class=\"'input-range-wrapper ' + cssClass.replace(/[^a-z0-9\\-\\_\\s]/gmi, '')\">\n        <input\n            type=\"range\"\n            v-model=\"content\"\n            :readonly=\"readonly\"\n            :disabled=\"disabled\"\n            :min=\"min\"\n            :max=\"max\"\n            :step=\"step\" />\n        <span>\n            <template v-if=\"contentModifier\">\n                {{ contentModifier(content) }}\n            </template>\n            <template v-else>\n                {{ content }}\n            </template>\n        </span>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'text-input',\n    data: function() {\n        return {\n            content: ''\n        };\n    },\n    props: {\n        'readonly': {\n            default: false,\n            type: Boolean\n        },\n        'disabled': {\n            default: false,\n            type: Boolean\n        },\n        'value': {\n            default: '',\n            type: [String, Number]\n        },\n        'min': {\n            default: '',\n            type: [String, Number]\n        },\n        'max': {\n            default: '',\n            type: [String, Number]\n        },\n        'step': {\n            default: '',\n            type: [String, Number]\n        },\n        'anchor': {\n            default: '',\n            type: String\n        },\n        'cssClass': {\n            default: '',\n            type: String\n        },\n        'contentModifier': {\n            default: false,\n            type: [Boolean, Function]\n        }\n    },\n    watch: {\n        value (newValue) { \n            this.content = newValue;\n        },\n        content (newValue) {\n            if (this.changeEventName) {\n                this.$bus.$emit(this.changeEventName, newValue);\n            }\n\n            this.$emit('input', this.content);\n        }\n    },\n    mounted: function() {\n        setTimeout(() => {\n            this.content = this.value;\n        }, 0);\n    },\n    methods: {\n        getValue: function () {\n            return this.content;\n        },\n        setValue: function (newValue) {\n            this.content = newValue;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.input-range-wrapper {\n    margin-bottom: 3.6rem;\n\n    input[type=\"range\"] {\n        -webkit-appearance: none;\n        background: transparent;\n        float: left;\n        margin: 1rem 0;\n        position: relative;\n        top: 1.6rem;\n        width: 80%!important;\n\n        & + span {\n            float: right;\n            position: relative;\n            text-align: center;\n            top: 1.6rem;\n            width: 20%!important;\n        }\n\n        &::-webkit-slider-thumb {\n            -webkit-appearance: none;\n            background: var(--color-primary);\n            border: none;\n            border-radius: 50%;\n            box-shadow: 1px 1px 1px rgba(0, 0, 0, .15);\n            cursor: pointer;\n            height: 2rem;\n            margin-top: -1rem;\n            outline: none;\n            width: 2rem;\n        }\n\n        &::-webkit-slider-runnable-track {\n            background: var(--input-border-color);\n            border: none;\n            cursor: pointer;\n            height: 2px;\n            outline: none;\n            width: 80%;\n        }\n\n        &:focus::-webkit-slider-runnable-track {\n            background: rgba(var(--color-primary-rgb), .5);\n            outline: none;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Repeater.vue",
    "content": "<template>\n    <div \n        :id=\"anchor\"\n        :class=\"cssClasses\">\n        <div \n            v-if=\"!content.length && hasEmptyState\"\n            class=\"publii-repeater-empty-state\">\n            <template v-if=\"translations.emptyState\">\n                {{ translation('empty') }}\n            </template>\n            <template v-else>\n                {{ $t('repeater.emptyState') }}\n            </template>\n        </div>\n\n        <draggable\n            tag=\"div\"\n            group=\"publii-repeater\"\n            chosenClass=\"is-chosen\"\n            ghostClass=\"is-ghost\"\n            handle=\".move\"\n            class=\"publii-repeater\"\n            @change=\"listUpdated\"\n            v-model=\"content\">\n            <div \n                v-for=\"(row, index) of content\"\n                :key=\"'publii-repeater-row-' + index\"\n                class=\"publii-repeater-item\">\n                <span \n                    v-if=\"content.length > 1\"\n                    class=\"move\">\n                    <icon\n                        name=\"more\"\n                        size=\"xs\" />\n                </span>\n\n                <template v-for=\"(field, subindex) of itemConfig\">\n                    <div \n                        v-if=\"isVisible(itemConfig[subindex], index)\"\n                        :key=\"'publii-repeater-row-' + index + '-' + subindex\"\n                        class=\"publii-repeater-item-field\"\n                        :style=\"'width: ' + itemConfig[subindex].width + '%;'\">\n                        <label>\n                            <span v-if=\"itemConfig[subindex].type !== 'checkbox' && (!hideLabels || (hideLabels && index === 0))\">\n                                {{ itemConfig[subindex].label }}:\n                            </span>\n\n                            <dropdown\n                                v-if=\"itemConfig[subindex].type === 'dropdown'\"\n                                :items=\"itemConfig[subindex].options\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\">\n                            </dropdown>\n\n                            <text-input\n                                v-if=\"itemConfig[subindex].type === 'text' || itemConfig[subindex].type === 'number' || itemConfig[subindex].type === 'url' || itemConfig[subindex].type === 'email'\"\n                                :type=\"itemConfig[subindex].type\"\n                                :spellcheck=\"itemConfig[subindex].spellcheck\"\n                                :placeholder=\"itemConfig[subindex].placeholder\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :min=\"itemConfig[subindex].min\"\n                                :max=\"itemConfig[subindex].max\"\n                                :step=\"itemConfig[subindex].step\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\" />\n\n                            <range-slider\n                                v-if=\"itemConfig[subindex].type === 'range'\"\n                                :min=\"itemConfig[subindex].min\"\n                                :max=\"itemConfig[subindex].max\"\n                                :step=\"itemConfig[subindex].step\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\"></range-slider>\n\n                            <text-area\n                                v-if=\"itemConfig[subindex].type === 'textarea'\"\n                                :spellcheck=\"itemConfig[subindex].spellcheck\"\n                                :placeholder=\"itemConfig[subindex].placeholder\"\n                                :rows=\"itemConfig[subindex].rows\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\" />\n\n                            <text-area\n                                v-if=\"itemConfig[subindex].type === 'wysiwyg'\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :key=\"'wysiwyg-' + index + '-' + subindex\"\n                                :ref=\"'wysiwyg-' + index + '-' + subindex\"\n                                :wysiwyg=\"true\"\n                                :miniEditorMode=\"true\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\"></text-area>\n\n                            <color-picker\n                                v-if=\"itemConfig[subindex].type === 'colorpicker'\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :outputFormat=\"itemConfig[subindex].outputFormat ? itemConfig[subindex].outputFormat : 'RGBAorHEX'\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\">\n                            </color-picker>\n\n                            <switcher\n                                v-if=\"itemConfig[subindex].type === 'checkbox'\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :lower-zindex=\"true\"\n                                :label=\"itemConfig[subindex].label\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\"></switcher>\n\n                            <radio-buttons\n                                v-if=\"itemConfig[subindex].type === 'radio'\"\n                                :items=\"itemConfig[subindex].options\"\n                                :name=\"itemConfig[subindex].name + '-' + index + '-' + subindex\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\" />\n\n                            <posts-dropdown\n                                v-if=\"itemConfig[subindex].type === 'posts-dropdown'\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :allowed-post-status=\"itemConfig[subindex].allowedPostStatus || ['any']\"\n                                :multiple=\"itemConfig[subindex].multiple\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\"></posts-dropdown>\n\n                            <pages-dropdown\n                                v-if=\"itemConfig[subindex].type === 'pages-dropdown'\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :multiple=\"itemConfig[subindex].multiple\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\"></pages-dropdown>\n\n                            <tags-dropdown\n                                v-if=\"itemConfig[subindex].type === 'tags-dropdown'\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :multiple=\"itemConfig[subindex].multiple\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\"></tags-dropdown>\n\n                            <authors-dropdown\n                                v-if=\"itemConfig[subindex].type === 'authors-dropdown'\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :multiple=\"itemConfig[subindex].multiple\"\n                                :customCssClasses=\"itemConfig[subindex].customCssClasses\"></authors-dropdown>\n\n                            <template v-if=\"pluginDir\">\n                                <image-upload\n                                    v-if=\"itemConfig[subindex].type === 'upload'\"\n                                    slot=\"field\"\n                                    v-model=\"content[index][itemConfig[subindex].name]\"\n                                    :imageType=\"imageType\"\n                                    :pluginDir=\"pluginDir\"\n                                    :addMediaFolderPath=\"false\" />\n\n                                <small-image-upload\n                                    v-if=\"itemConfig[subindex].type === 'smallupload'\"\n                                    v-model=\"content[index][itemConfig[subindex].name]\"\n                                    :anchor=\"itemConfig[subindex].anchor\"\n                                    :imageType=\"imageType\"\n                                    :pluginDir=\"$route.params.pluginname\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"itemConfig[subindex].customCssClasses\"></small-image-upload>\n                            </template>\n                            <template v-else>\n                                <image-upload\n                                    v-if=\"itemConfig[subindex].type === 'upload'\"\n                                    slot=\"field\"\n                                    v-model=\"content[index][itemConfig[subindex].name]\"\n                                    :imageType=\"imageType\"\n                                    :addMediaFolderPath=\"true\" />   \n\n                                <small-image-upload\n                                    v-if=\"itemConfig[subindex].type === 'smallupload'\"\n                                    v-model=\"content[index][itemConfig[subindex].name]\"\n                                    :anchor=\"itemConfig[subindex].anchor\"\n                                    :imageType=\"imageType\"\n                                    slot=\"field\"\n                                    :customCssClasses=\"itemConfig[subindex].customCssClasses\"></small-image-upload>\n                            </template>\n\n                            <vue-select\n                                v-if=\"itemConfig[subindex].type === 'file-dropdown'\"\n                                slot=\"field\"\n                                :ref=\"'file-dropdown-' + index\"\n                                :options=\"filesList\"\n                                v-model=\"content[index][itemConfig[subindex].name]\"\n                                :close-on-select=\"true\"\n                                :show-labels=\"false\"\n                                :placeholder=\"$t('file.selectFileFromFileManager')\"\n                                @select=\"closeFileDropdown('file-dropdown-' + index)\"></vue-select>\n                            \n                            <small\n                                v-if=\"itemConfig[subindex].note && (!hideLabels || (hideLabels && index === 0))\"\n                                slot=\"note\"\n                                class=\"note\">\n                                {{ itemConfig[subindex].note }}\n                            </small>\n                        </label>\n                    </div>\n                </template>\n\n                <div class=\"publii-repeater-item-ui\">\n                    <a\n                        href=\"#\"\n                        :class=\"{ \n                            'publii-repeater-item-ui-btn': true,\n                            'duplicate': true,\n                            'is-disabled': maxCount !== -1 && content.length >= maxCount\n                        }\"\n                        :title=\"translation('duplicate')\"\n                        tabindex=\"-1\"\n                        @click.stop.prevent=\"duplicateItem(index)\">\n                        <icon\n                            name=\"duplicate\"\n                            size=\"xs\" />\n                    </a>\n\n                    <a\n                        href=\"#\"\n                        class=\"publii-repeater-item-ui-btn delete\"\n                        :title=\"translation('remove')\"\n                        tabindex=\"-1\"\n                        @click.stop.prevent=\"removeItem(index)\">\n                        <icon\n                            name=\"trash\"\n                            size=\"xs\" />\n                    </a>\n                </div>\n            </div>\n        </draggable>\n\n        <p-button\n            v-if=\"maxCount === -1 || content.length < maxCount\"\n            :onClick=\"addItem\"\n            type=\"secondary icon\"\n            icon=\"add-site-mono\">\n            {{ translation('add') }}\n        </p-button>\n    </div>\n</template>\n\n<script>\nimport Draggable from 'vuedraggable';\nimport vSelect from 'vue-multiselect/dist/vue-multiselect.min.js';\n\nexport default {\n    name: 'repeater',\n    props: {\n        structure: {\n            default: () => ([]),\n            type: Array\n        },\n        maxCount: {\n            default: -1,\n            type: Number\n        },\n        translations: {\n            default: false,\n            type: [Boolean, Object]\n        },\n        hasEmptyState: {\n            default: true,\n            type: Boolean\n        },\n        hideLabels: {\n            default: true,\n            type: Boolean\n        },\n        value: {\n            default: () => ([]),\n            type: Array\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        settings: {\n            default: () => ({}),\n            type: Object\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        },\n        imageType: {\n            default: 'optionImages',\n            type: String\n        },\n        pluginDir: {\n            default: '',\n            type: String\n        }\n    },\n    components: {\n        'draggable': Draggable,\n        'vue-select': vSelect\n    },\n    computed: {\n        cssClasses () {\n            let cssClasses = {};\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        },\n        itemStructure () {\n            let output = {};\n            let keys = this.structure.map(item => item.name);\n            let values = this.structure.map(item => item.value);\n\n            for (let i = 0; i < keys.length; i++) {\n                output[keys[i]] = values[i];\n            }\n\n            return output;\n        },\n        itemConfig () {\n            return this.structure;\n        },\n        hasFileManagerField () {\n            return this.itemConfig.filter(item => item.type === 'file-dropdown').length > 0;\n        }\n    },\n    data () {\n        return {\n            content: this.value,\n            filesList: ['']\n        };\n    },\n    mounted () {\n        this.content = this.value;\n\n        if (this.hasFileManagerField) {\n            mainProcessAPI.send('app-file-manager-list', {\n                siteName: this.$store.state.currentSite.config.name,\n                dirPath: 'root-files'\n            });\n\n            mainProcessAPI.receiveOnce('app-file-manager-listed', (data) => {\n                this.filesList = data.map(file => file.name);\n\n                mainProcessAPI.send('app-file-manager-list', {\n                    siteName: this.$store.state.currentSite.config.name,\n                    dirPath: 'media/files'\n                }); \n\n                mainProcessAPI.receiveOnce('app-file-manager-listed', (data) => {\n                    this.filesList = this.filesList.concat(data.map(file => 'media/files/' + file.name));\n                });\n            });\n        }\n    },\n    watch: {\n        value (newValue, oldValue) {\n            this.content = newValue;\n        },\n        content: {\n            handler () {\n                this.$emit('input', this.content);\n            },\n            deep: true\n        }\n    },\n    methods: {\n        addItem () {\n            let structureCopy = JSON.parse(JSON.stringify(this.itemStructure));\n            this.content.push(structureCopy);\n            this.refreshWysiwygs();\n        },\n        duplicateItem (index) {\n            let itemCopy = JSON.parse(JSON.stringify(this.content[index]));\n            this.content.splice(index + 1, 0, itemCopy);\n            this.refreshWysiwygs();\n        },\n        removeItem (index) {\n            this.content.splice(index, 1);\n            this.refreshWysiwygs();\n        },\n        translation (key) {\n            if (key === 'add') {\n                if (this.translations && this.translations.addItem) {\n                    return this.translations.addItem;\n                }\n\n                return this.$t('repeater.addItem');\n            } else if (key === 'duplicate') {\n                if (this.translations && this.translations.duplicateItem) {\n                    return this.translations.duplicateItem;\n                }\n\n                return this.$t('repeater.duplicateItem');\n            } else if (key === 'empty') {\n                if (this.translations && this.translations.emptyState) {\n                    return this.translations.emptyState;\n                }\n\n                return this.$t('repeater.emptyState');\n            } else if (key === 'remove') {\n                if (this.translations && this.translations.removeItem) {\n                    return this.translations.removeItem;\n                }\n\n                return this.$t('repeater.removeItem');\n            }\n        },\n        isVisible (itemConfig, index) {\n            if (!itemConfig.dependencies || !itemConfig.dependencies.length) {\n                return true;\n            }\n\n            for (let i = 0; i < itemConfig.dependencies.length; i++) {\n                let dependencyName = itemConfig.dependencies[i].field;\n                let dependencyType = itemConfig.dependencies[i].type;\n                let dependencyValue = itemConfig.dependencies[i].value;\n                let valueToCompare;\n\n                if (dependencyType === 'externalOption') {\n                    valueToCompare = this.settings[dependencyName];\n                } else {\n                    valueToCompare = this.content[index][dependencyName];\n                }\n\n                if (dependencyValue === \"true\" && valueToCompare !== true) {\n                    return false;\n                } else if (dependencyValue === \"true\") {\n                    continue;\n                }\n\n                if (dependencyValue === \"false\" && valueToCompare !== false) {\n                    return false;\n                } else if (dependencyValue === \"false\") {\n                    continue;\n                }\n\n                if (typeof dependencyValue === 'string' && dependencyValue.indexOf(',') > -1) {\n                    let values = dependencyValue.split(',');\n\n                    for (let i = 0; i < values.length; i++) {\n                        if (valueToCompare === values[i]) {\n                            return true;\n                        }\n                    }\n                    \n                    return false;\n                }\n\n                if (dependencyValue !== valueToCompare) {\n                    return false;\n                }\n            }\n\n            return true;\n        },\n        closeFileDropdown (refID) {\n            if (this.$refs[refID] && this.$refs[refID][0]) {\n                this.$refs[refID][0].isOpen = false;\n            }\n        },\n        listUpdated () {\n            this.refreshWysiwygs();\n        },\n        refreshWysiwygs() {\n            if (this.$refs) {\n                return Object.keys(this.$refs)\n                    .filter(refName => refName.startsWith('wysiwyg-'))\n                    .forEach(refName => {\n                        let index = refName.split('-')[1];\n                        let subindex = refName.split('-')[2];\n                        let newContent = this.content[index][this.itemConfig[subindex].name];\n                        this.$refs[refName][0].removeEditor();\n                        this.$refs[refName][0].$forceUpdate();\n                        this.$refs[refName][0].setContent(newContent);\n                        this.$refs[refName][0].initWysiwyg();\n                    });\n            }\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n@import '../../scss/editor/post-editors-common.scss';\n@import '../../scss/editor/editor-overrides.scss';\n\n.publii-repeater {\n    &-item {\n        display: flex;\n        flex-wrap: wrap;\n        position: relative;\n        width: calc(100% - 68px);\n\n        &-ui {\n            display: flex;\n            position: absolute;\n            right: -68px;\n            width: 68px;\n\n            &-btn {\n                align-items: center;\n                background: var(--gray-1);\n                position: relative;\n                border-radius: 50%;\n                display: flex;\n                height: 30px;\n                justify-content: center;\n                margin: 0 2px;\n                position: relative;\n                text-align: center;\n                width: 30px;\n\n                &:active,\n                &:focus,\n                &:hover {\n                    color: var(--headings-color);\n                }\n\n                &:hover {\n\n                    & > svg {\n                       fill: var(--icon-tertiary-color);\n                       transform: scale(1);\n                    }\n                }\n\n                svg {\n                    fill: var(--icon-secondary-color);\n                    height: 1.6rem;\n                    pointer-events: none;\n                    transform: scale(.9);\n                    transition: var(--transition);\n                    width: 1.6rem;\n                }\n\n                &.delete {\n\n                    &:hover {\n\n                        & > svg {\n                           fill: var(--warning);\n                        }\n                    }\n                }\n\n                &.is-disabled {\n                    opacity: 0.25;\n                    pointer-events: none;\n                }\n            } \n        }\n\n        &-field {\n            label {\n                padding-right: 10px;\n\n                & > span {\n                    display: block;\n                    font-size: 1.4rem;\n                    font-weight: bold;\n                    margin: 0 0 1rem 0;\n                }\n\n                & > * {\n                    width: calc(100% - 10px)!important;\n                }\n            }\n\n            .note {\n                clear: both;\n                color: var(--text-light-color);\n                display: block;\n                font-size: 1.35rem;\n                font-style: italic;\n                line-height: 1.4;        \n                padding: .5rem 0 1rem 0;\n                user-select: text;\n\n                svg {\n                    display: inline-block;\n                    height: 1.4rem;\n                    margin-right: .5rem;\n                    width: 1.4rem;\n                }\n                \n                a {\n                    color: var(--link-primary-color);\n                    \n                    &:active,\n                    &:focus,\n                    &:hover {\n                        color: var(--link-primary-color-hover);\n                    }\n                }\n            }\n\n            label + .note {\n                padding-top: 1.5rem;\n            }\n\n            .checkbox ~ .note {\n                margin-bottom: 1.5rem;\n            }\n\n            .range-wrapper {\n                position: relative;\n                z-index: 1;\n\n                & + .note {\n                    padding-top: 1.5rem;\n                }\n            }\n        }\n\n        .move {\n            cursor: move;\n            left: -20px;\n            position: absolute;\n            width: 20px;\n\n            &:hover {\n\n                & > svg {\n                      fill: var(--icon-tertiary-color);\n                    }\n                }\n\n            svg {\n                fill: var(--icon-secondary-color);\n                height: 1.6rem;\n                pointer-events: none;\n                vertical-align: middle;\n                width: 1.6rem;\n            }\n        }\n\n        &.is-ghost {\n            &::before { \n                background-color: var(--collection-bg-hover);                \n                border: 1px dashed var(--input-border-focus);\n                border-radius: var(--border-radius);\n                content: \"\";\n                display: block;   \n                position: absolute;\n                left: -1.5rem;\n                right: -7rem;\n                bottom: .75rem;\n                top: -1.5rem;\n            }\n            .publii-repeater-item-field, \n            .publii-repeater-item-ui,\n            .move {\n                opacity: 0;\n            }\n        }\n\n        &:first-child {\n            .move {\n                top: 3.24rem\n            }\n            .publii-repeater-item-ui {\n                top: 3.24rem\n            }\n        }\n    }\n\n    &-empty-state {\n        margin-bottom: 1.2rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Separator.vue",
    "content": "<template>\n    <div :class=\"cssClasses\" :id=\"anchor\">\n        <div \n        class=\"separator-wrapper\"\n        :class=\"{ 'has-label': label }\">\n            <label v-if=\"label\">{{ label }}</label>\n        </div>\n\n        <small\n            v-if=\"note\"\n            class=\"note\"\n            v-pure-html=\"note\">        \n        </small>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'separator',\n    props: {\n        'label': {\n            default: '',\n            type: [String, Boolean]\n        },\n        'anchor': {\n            default: '',\n            type: String\n        },\n        'type': {\n            default: '',\n            type: String\n        },\n        'is-line': {\n            default: true,\n            type: Boolean\n        },\n        'note': {\n            default: '',\n            type: String\n        },\n        'customCssClasses': {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        cssClasses () {\n            let cssClasses = {\n                'separator': true,\n                'line': this.isLine,\n                'no-line': this.type.indexOf('no-line') > -1,\n                'empty': this.type.indexOf('empty') > -1,\n                'thin': this.type.indexOf('thin') > -1,\n                'small': this.type.indexOf('small') > -1,\n                'medium': this.type.indexOf('medium') > -1,\n                'big': this.type.indexOf('big') > -1,\n                'ultra': this.type.indexOf('ultra') > -1\n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.separator {\n    clear: both;\n    display: block;\n    position: relative;\n    width: 100%;\n\n    &.small,\n    &.medium,\n    &.big,\n    &.ultra {\n        padding-bottom: 2rem;\n\n        & > .separator-wrapper {\n\n            & > label {\n               margin-bottom: -2rem;\n            }\n        }\n    }\n\n    &.small {\n        padding-top: 2rem;\n    }\n\n    &.medium {\n        padding-top: 3rem;\n    }\n\n    &.big {\n        padding-top: 4rem;\n    }\n\n    &.ultra {\n        padding-top: 5rem;\n    }\n\n    & > .separator-wrapper {\n        position: relative;\n\n        & > label {\n            color: var(--headings-color);\n            display: block;\n            font-size: 1.6rem;\n            font-weight: 600;\n            padding: 3.5rem 0 0;\n\n        }\n    }\n\n    & > .note {\n        padding: 0;\n        clear: both;\n        display: block;\n        font-size: 1.4rem;\n        font-style: italic;\n        line-height: 1.4;\n        opacity: .75;\n    }\n\n    &.line {\n        & > .separator-wrapper::before {\n            border-top: 4px solid var(--bg-site);\n            content: \"\";\n            left: 0;\n            position: absolute;\n            top: 0;\n            width: 100%;\n        }\n\n        & > .note {\n           padding: 2.5rem 0 0 0;\n        }\n    }\n\n    &.no-line {\n        padding: 0 0 2rem;\n        & > .separator-wrapper::before {     \n            content: none; \n        }\n        & > .separator-wrapper {\n            padding: 0;\n        }\n\n        & > .note {\n           padding: 0;\n        }\n    }\n\n    &.empty {\n        & > .separator-wrapper {\n            padding-bottom: .25rem;\n        }\n    }\n\n    &.thin {\n          & > .separator-wrapper { \n\n            &::before {\n                border-top: 2px solid var(--bg-site);\n            }\n\n             & > label {\n                 font-size: 1.6rem;\n                padding-top: 2rem;\n            }\n        }\n    }\n\n    & + &.thin {\n        & > .separator-wrapper { \n\n            &::before {\n                display: none;\n            }\n        }\n    }\n}\n\n.field {\n    &:first-child {\n\n        .separator-wrapper {\n            \n            & > label {\n                padding-top: .5rem;\n            }\n\n            &::before {\n                display: none;\n            }\n        }\n    }\n}\n\n.site-settings .tab  {\n\n    .separator:first-child {\n\n        .separator-wrapper {\n            \n            & > label {\n                padding-top: .5rem;\n            }\n\n            &::before {\n                display: none;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/SmallImageUpload.vue",
    "content": "<template>\n    <div\n        :id=\"anchor\"\n        :class=\"cssClasses\">\n        <span class=\"upload-path\">\n            <template v-if=\"!isUploading\">{{ fileName }}</template>\n            <template v-if=\"isUploading\">{{ $t('image.loadingImage') }}</template>\n        </span>\n\n        <input\n            v-if=\"isEmpty\"\n            type=\"file\"\n            ref=\"input\"\n            spellcheck=\"false\"\n            class=\"upload-input\"\n            @change=\"valueChanged\">\n\n        <a\n            v-if=\"!isEmpty\"\n            href=\"#\"\n            class=\"upload-remove\"\n            :title=\"$t('image.removeImage')\"\n            @click=\"remove\">\n            &times;\n        </a>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'small-image-upload',\n    props: {\n        value: {\n            default: '',\n            type: String\n        },\n        onRemove: {\n            default: () => false,\n            type: Function\n        },\n        onBeforeRemove: {\n            default: () => false,\n            type: Function\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        imageType: {\n            default: 'optionImages',\n            type: String\n        },\n        pluginDir: {\n            default: '',\n            type: String\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        cssClasses () {\n            let cssClasses = { \n                'small-image-upload': true, \n                'is-uploading': this.isUploading \n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    },\n    data () {\n        return {\n            isEmpty: true,\n            fileName: '',\n            isUploading: false\n        }\n    },\n    watch: {\n        value (newValue, oldValue) {\n            if (newValue !== '') {\n                this.fileName = newValue;\n                this.isEmpty = false;\n            }\n        },\n        fileName: function(newValue) {\n            if (newValue === '') {\n                this.$emit('input', '');\n            } else {\n                this.$emit('input', newValue);\n            }\n        }\n    },\n    mounted () {\n        setTimeout(() => {\n            if (this.value !== '') {\n                this.fileName = this.value;\n                this.isEmpty = false;\n            }\n        }, 0);\n    },\n    methods: {\n        remove (e) {\n            e.preventDefault();\n            this.onBeforeRemove(this.fileName);\n            this.fileName = '';\n            this.isEmpty = true;\n            this.onRemove();\n        },\n        async valueChanged (e) {\n            if(!e.target.files.length) {\n                return;\n            }\n\n            let sourcePath = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(e.target.files[0]));\n            this.uploadImage(sourcePath);\n        },\n        uploadImage (sourcePath) {\n            this.isUploading = true;\n\n            let uploadData = {\n                id: 'website',\n                site: this.$store.state.currentSite.config.name,\n                path: sourcePath,\n                imageType: this.imageType,\n                pluginDir: this.pluginDir\n            };\n\n            mainProcessAPI.send('app-image-upload', uploadData);\n\n            mainProcessAPI.receiveOnce('app-image-uploaded', async (data) => {\n                let dir = 'media/website/';\n\n                if (this.imageType === 'pluginImages') {\n                    dir = 'media/plugins/' + this.pluginDir + '/';\n                }\n\n                this.isEmpty = false;\n                let newPath = await mainProcessAPI.normalizePath(data.baseImage.newPath);\n                this.fileName = dir + newPath.split('/').pop();\n                this.isUploading = false;\n            });\n        },\n        setImage (newFileName) {\n            this.fileName = newFileName;\n            this.isEmpty = newFileName === '';\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.small-image-upload {\n    min-height: 48px;\n    position: relative;\n\n    &.is-uploading {\n        opacity: .85;\n        pointer-events: none;\n    }\n\n    .upload-remove {\n        border-radius: 50%;\n        color: var(--warning);\n        cursor: pointer;\n        font-size: 2.4rem;\n        font-weight: 300;\n        height: 3rem;\n        line-height: 1.1;\n        padding: 0;\n        position: absolute;\n        right: 1.5rem;\n        text-align: center;\n        top: 50%;\n        transition: var(--transition);\n        transform: translateY(-50%);\n        width: 3rem;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--icon-tertiary-color);\n        }\n\n        &:hover {\n            background: var(--input-border-color);\n        }\n    }\n\n    .upload-input {\n        display: block;\n        overflow: hidden;\n        position: absolute;\n        right: .6rem;\n        top: .6rem;\n        width: 12rem!important;\n\n        &::-webkit-file-upload-button {\n            -webkit-appearance: none;\n            background: var(--button-secondary-bg);\n            border: 1px solid var(--button-secondary-bg);\n            border-radius: var(--border-radius);\n            color: var(--button-secondary-color);\n            cursor: pointer;\n            font-weight: var(--font-weight-semibold);\n            font-size: 1.4rem;\n            padding: .6rem;\n            text-align: center;\n            width: 12rem;\n            outline: none;\n\n            &:hover {\n                background: var(--button-secondary-bg-hover);\n                border-color: var(--button-secondary-bg-hover);\n                color: var(--button-secondary-color-hover);\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/SupportedFeaturesCheck.vue",
    "content": "<template>\n    <div \n        v-if=\"unavailableFeatures.length\"\n        class=\"supported-features-check\">\n        <div\n            v-for=\"(feature, index) of unavailableFeatures\"\n            :key=\"'feature-to-check-' + index\"\n            class=\"msg msg-icon msg-alert\">\n            <icon name=\"warning\" customWidth=\"28\" customHeight=\"28\" />\n            <p>{{ $t('supportedFeatures.yourThemeDoNotSupport', { featureName: featureNames[feature] }) }} </p>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'supported-features-check',\n    props: {\n        featuresToCheck: {\n            required: true,\n            type: Array,\n            default: () => ([])\n        }\n    },\n    computed: {\n        unavailableFeatures () {\n            return this.featuresToCheck.filter(feature => !this.isSupported(feature));\n        }\n    },\n    data () {\n        return {\n            featureNames: {\n                authorImages: this.$t('supportedFeatures.featureNames.authorImages'),\n                authorPages: this.$t('supportedFeatures.featureNames.authorPages'),\n                blockEditor: this.$t('supportedFeatures.featureNames.blockEditor'),\n                customComments: this.$t('supportedFeatures.featureNames.customComments'),\n                customSearch: this.$t('supportedFeatures.featureNames.customSearch'),\n                errorPage: this.$t('supportedFeatures.featureNames.errorPage'),\n                searchPage: this.$t('supportedFeatures.featureNames.searchPage'),\n                tagImages: this.$t('supportedFeatures.featureNames.tagImages'),\n                tagsList: this.$t('supportedFeatures.featureNames.tagsList'),\n                tagPages: this.$t('supportedFeatures.featureNames.tagPages')\n            }\n        }\n    },\n    methods: {\n        isSupported (featureName) {\n            return this.$store.state.currentSite.themeSettings && \n                this.$store.state.currentSite.themeSettings.supportedFeatures &&\n                this.$store.state.currentSite.themeSettings.supportedFeatures[featureName];\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/notifications.scss';\n\n.msg {\n    background: var(--bg-secondary);\n    margin-bottom: 1rem;\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Switcher.vue",
    "content": "<template>\n    <span \n        :id=\"anchor\"\n        :class=\"wrapperCssClasses\">\n        <span\n            :class=\"cssClasses\"\n            @click=\"toggle\"></span>\n        {{ label }}\n    </span>\n</template>\n\n<script>\nexport default {\n    name: 'switcher',\n    props: {\n        value: {\n            type: [Boolean, Number]\n        },\n        label: {\n            default: '',\n            type: String\n        },\n        checked: {\n            default: false,\n            type: Boolean\n        },\n        onToggle: {\n            default: () => false,\n            type: Function\n        },\n        lowerZindex: {\n            default: false,\n            type: Boolean\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        disabled: {\n            default: false,\n            type: Boolean\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    data: function() {\n        return {\n            isChecked: this.checked\n        };\n    },\n    computed: {\n        cssClasses: function() {\n            return {\n                'switcher': true,\n                'is-checked': this.isChecked,\n                'lower-zindex': this.lowerZindex,\n                'has-label': this.label,\n                'is-disabled': this.disabled\n            };\n        },\n        wrapperCssClasses () {\n            let cssClasses = { \n                'has-label': this.label \n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    },\n    watch: {\n        value: function (newValue, oldValue) {\n            this.isChecked = !!newValue;\n        }\n    },\n    mounted () {\n        if (this.value) {\n            this.isChecked = !!this.value;\n        }\n    },\n    methods: {\n        toggle: function() {\n            this.isChecked = !this.isChecked;\n            this.$emit('input', this.isChecked);\n            this.onToggle(this.isChecked);\n        },\n        getValue: function() {\n            return !!this.isChecked;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n.has-label {\n    font-size: $app-font-base;\n    margin-right: 10px;\n}\n\n.switcher {\n    background: var(--input-border-dark);\n    border-radius: 20px;\n    cursor: pointer;\n    display: inline-block;\n    height: 18px;\n    margin-right: .5rem;\n    position: relative;\n    top: 3px;\n    transition: all .28s ease;\n    width: 32px;\n    z-index: 1;\n\n    &.lower-zindex {\n        z-index: 0;\n    }\n\n    &:after {\n        position: absolute;\n        left: 2px;\n        top: 2px;\n        display: block;\n        width: 14px;\n        height: 14px;\n        border-radius: 50%;\n        background: var(--input-bg-lightest);\n        content: '';\n        transition: all .28s ease;\n        \n    }\n\n    &:active:after {\n        transform: scale(0.8);\n    }\n\n    &.is-checked {\n        background: var(--input-border-focus);\n\n        &:after {\n            left: 16px;\n            background: var(--input-bg-lightest);\n        }\n    }\n\n    &.is-disabled {\n        opacity: .5;\n        pointer-events: none;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/Tabs.vue",
    "content": "<template>\n    <div\n        :class=\"{\n            'tabs': true,\n            'tabs-horizontal': isHorizontal\n        }\"\n        @click=\"detectInternalNavigation\">\n        <div>\n            <ul>\n                <li\n                    v-for=\"(item, index) in items\"\n                    :key=\"'tab-item-' + index\"\n                    :class=\"{ \n                        'active': Array.isArray(item) ? item[0] === activeItem : item === activeItem,\n                        'active-parent': item === activeParentItem,\n                        'subtab': Array.isArray(item)\n                    }\"\n                    @click=\"toggle(item, index)\">\n                    <template v-if=\"Array.isArray(item)\">\n                        {{ item[0] }}\n                    </template>\n                    <template v-else>\n                        {{ item }}\n                    </template>\n                </li>\n            </ul>\n        </div>\n\n        <div class=\"content\">\n            <div\n                v-for=\"(item, index) in items\"\n                :key=\"'tab-item-content-' + index\"\n                :class=\"{ \n                    'tab': true, \n                    'active': Array.isArray(item) ? item[0] === activeItem : item === activeItem,\n                }\">\n                <slot :name=\"'tab-' + index\"></slot>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'tabs',\n    props: {\n        id: {\n            default: '',\n            type: String\n        },\n        items: {\n            default: [],\n            type: Array\n        },\n        onToggle: {\n            default: () => false,\n            type: Function\n        },\n        isHorizontal: {\n            defalt: false,\n            type: Boolean\n        }\n    },\n    data () {\n        return {\n            activeItem: false,\n            activeParentItem: false,\n            activeIndex: 0\n        }\n    },\n    mounted () {\n        let lastOpenedTab = window.sessionStorage.getItem(this.id);\n\n        if(lastOpenedTab && this.items.indexOf(lastOpenedTab) > -1) {\n            this.activeItem = lastOpenedTab;\n            this.activeIndex = this.items.indexOf(lastOpenedTab);\n        } else {\n            this.activeItem = this.items[0] || false;\n        }\n    },\n    methods: {\n        detectInternalNavigation (e) {\n            if (e.target.tagName === 'A' && e.target.getAttribute('data-internal-link')) {\n                e.preventDefault();\n\n                let linkData = e.target.getAttribute('data-internal-link');\n                linkData = linkData.split('#');\n\n                if (linkData.length === 2) {\n                    this.toggle(linkData[0], linkData[1]);\n                } else {\n                    this.toggle(linkData[0]);\n                }\n            }\n        },\n        toggle (newActiveItem, newIndex) {\n            if (Array.isArray(newActiveItem)) {\n                this.activeItem = newActiveItem[0];\n                this.activeParentItem = newActiveItem[1];\n            } else {\n                this.activeItem = newActiveItem;\n                this.activeParentItem = false;\n            }\n\n            this.activeIndex = newIndex;\n\n            if (this.id) {\n                window.sessionStorage.setItem(this.id, newActiveItem);\n            }\n\n            this.onToggle();\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.tabs {\n    display: flex;\n    justify-content: space-between;\n\n    @include clearfix;\n\n    &.tabs-horizontal {\n        flex-direction: column;\n\n        & > div {\n\n            & > ul {\n                border-bottom: 2px solid var(--input-border-color);\n                position: relative;\n                top: initial;\n                text-align: left;\n                width: 100%;\n\n                & > li {\n                    color: var(--text-light-color);\n                    border-bottom: 2px solid transparent;\n                    display: inline-block;\n                    margin: 0 2rem;\n                    padding: 0 0 1.7rem 0;\n                    top: 2px;\n                    width: auto;\n\n                    &.active {\n                        background: none!important;\n                        border-bottom: 2px solid var(--button-tertiary-bg);\n                        border-radius: 0;\n                        color: var(--tab-color);\n                    }\n\n                    &:hover {\n                        background: none;\n                        border-radius: 0;\n                        color: var(--tab-color);\n                    }\n\n                    &:first-child {\n                        margin-left: 0;\n                    }\n                }\n            }\n        }\n\n        & > .content {\n            border: none;\n            margin-top: 3rem;\n            padding-left: 0;\n            width: 100%;\n        }\n    }\n\n    & > div {\n\n        & > ul {\n            list-style-type: none;\n            margin: 0;\n            padding: 0;\n            position: sticky;\n            top: 0;\n            user-select: none;\n            width: 18rem;\n\n            & > li {\n                border-radius: var(--border-radius);\n                color: var(--tab-color);\n                cursor: pointer;\n                padding: 0.8rem 1.2rem;\n                position: relative;\n                transition: var(--transition);\n                width: 100%;\n\n                &.active {\n                    background: var(--tab-active-bg)!important;\n                    border-radius: var(--border-radius);\n                    color: var(--tab-active-color) !important;\n                    transition: all .125s ease-out;\n                }\n\n                &.subtab {\n                    padding: 0.6rem .6rem 0.6rem 3rem;\n\n                    &::before {\n                        border-radius: 0 0 0 2px;\n                        content: '';\n                        display: block;\n                        width: 8px;\n                        height: 100%;\n                        border-left: 1px solid var(--input-border-dark);\n                        border-bottom: 1px solid var(--input-border-dark);\n                        position: absolute;\n                        left: 1.2rem;\n                        top: 0;\n                        transform: translate(0, -46%);\n                    }\n\n                    &.active {\n                        background: none !important;\n                        font-weight: var(--font-weight-semibold);\n                    }\n                }\n\n                // Add selector for the first `.subtab` in the group\n                & + .subtab:not(.subtab + .subtab) {\n                    margin-top: 0.6rem;\n\n                    &::before {\n                        height: 60%;\n                        top: 23%;\n                    }\n                }\n\n                &:hover {\n                    color: var(--tab-color-hover);\n                }\n\n                &:last-child {\n                    border-bottom: none;\n                }\n\n                &.active-parent {\n                    background: var(--tab-parent-active-bg);\n                }\n                \n            }\n        }\n    }\n\n    & > .content {\n        border-left: 5px solid var(--bg-site);\n        margin-left: auto;\n        padding-left: 4rem;\n        width: calc( 100% - 22rem);\n\n        & > .tab {\n            display: none;\n\n            &.active {\n                display: block;\n            }\n\n            .msg {\n                margin: 2rem 0;\n            }\n\n            .separator:first-child {\n                padding-top: 0 !important;\n            }\n        }\n    }\n}\n\n/*\n * Responsive improvements\n */\n@media (max-height: 900px) {\n\n    .tabs > div > ul {\n        width: 15rem;\n    }\n\n    .tabs > div > ul > li {\n        font-size: 1.4rem;\n    }\n\n    .tabs > .content {\n        padding-left: 3rem;\n        width: calc(100% - 18rem);\n    }\n}\n\n@media (max-width: 1400px) {\n\n    .tabs > div > ul {\n        width: 15rem;\n    }\n\n    .tabs > div > ul > li {\n        font-size: 1.4rem;\n    }\n\n    .tabs > .content {\n        padding-left: 3rem;\n        width: calc(100% - 18rem);\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/TagsDropDown.vue",
    "content": "<template>\n    <v-select\n        ref=\"dropdown\"\n        :options=\"tagPages\"\n        v-model=\"selectedTag\"\n        :custom-label=\"tagLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        @select=\"closeDropdown()\"\n        :multiple=\"multiple\"\n        :id=\"anchor\"\n        :class=\"customCssClasses.replace(/[^a-z0-9\\-\\_\\s]/gmi, '')\"\n        :placeholder=\"placeholder\"></v-select>\n</template>\n\n<script>\nexport default {\n    name: 'tags-dropdown',\n    props: {\n        multiple: {\n            type: Boolean,\n            default: false\n        },\n        value: {},\n        anchor: {\n            default: '',\n            type: String\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    data () {\n        return {\n            selectedTag: ''\n        };\n    },\n    computed: {\n        tagPages () {\n            return [''].concat(this.$store.state.currentSite.tags.slice().sort((a, b) => {\n                return a.name.localeCompare(b.name);\n            }).map(tag => tag.id));\n        },\n        placeholder () {\n            return this.$t('tag.selectTag');\n        },\n    },\n    watch: {\n        value: function (newValue, oldValue) {\n            this.selectedTag = newValue;\n        },\n        selectedTag: function (newValue, oldValue) {\n            this.$emit('input', newValue);\n        }\n    },\n    mounted () {\n        if (this.value) {\n            this.selectedTag = this.value;\n        }\n    },\n    methods: {\n        tagLabels (value) {\n            return this.$store.state.currentSite.tags.filter(tag => tag.id === value).map(tag => {\n                if (tag.additionalData.indexOf('\"isHidden\":true') > -1) {\n                    return tag.name + ' (' + this.$t('tag.thisTagIsHidden') + ')';\n                } else {\n                    return tag.name;\n                }\n            })[0];\n        },\n        closeDropdown () {\n            this.$refs['dropdown'].isOpen = false;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n.multiselect {\n    line-height: 2;\n}\n\n.multiselect,\n.multiselect__tags {\n    min-height: 49px;\n}\n\n.multiselect__tags {\n    padding: 0 4rem 0 1.8rem;\n}\n\n.multiselect__input {\n    max-width: 100%;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/TextArea.vue",
    "content": "<template>\n    <div \n        :id=\"anchor\"\n        :class=\"cssClasses\">\n        <textarea\n            :id=\"editorID\"\n            :data-id=\"editorID\"\n            :rows=\"rows\"\n            :cols=\"cols\"\n            class=\"publii-textarea\"\n            :spellcheck=\"spellcheck\"\n            :placeholder=\"placeholder\"\n            v-model=\"content\"></textarea>\n        <char-counter\n            v-if=\"charCounter\"\n            v-model=\"content\"\n            :preferredCount=\"preferredCount\" />\n    </div>\n</template>\n\n<script>\nimport Utils from './../../helpers/utils';\nimport Vue from 'vue';\n\nexport default {\n    name: 'text-area',\n    props: {\n        id: {\n            default: 't-editor',\n            type: String\n        },\n        value: {\n            default: '',\n            type: [String, Boolean]\n        },\n        wysiwyg: {\n            default: false,\n            type: Boolean\n        },\n        rows: {\n            default: false,\n            type: [String, Number, Boolean]\n        },\n        cols: {\n            default: false,\n            type: [String, Number, Boolean]\n        },\n        charCounter: {\n            default: false,\n            type: Boolean\n        },\n        preferredCount: {\n            default: 0,\n            type: Number\n        },\n        spellcheck: {\n            default: true,\n            type: Boolean\n        },\n        anchor: {\n            default: '',\n            type: String\n        },\n        placeholder: {\n            default: '',\n            type: String\n        },\n        miniEditorMode: {\n            default: false,\n            type: Boolean\n        },\n        simplifiedToolbar: {\n            default: false,\n            type: Boolean\n        },\n        customCssClasses: {\n            default: '',\n            type: String\n        }\n    },\n    computed: {\n        cssClasses () {\n            let cssClasses = { \n                'wysiwyg-mini-editor': this.miniEditorMode, \n                'has-simplified-toolbar': this.simplifiedToolbar \n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    },\n    data: function() {\n        return {\n            editorID: this.id,\n            content: this.value\n        };\n    },\n    async mounted () {\n        setTimeout(async () => {\n            this.content = this.value;\n\n            if (this.wysiwyg) {\n                this.editorID = this.generateID();\n\n                setTimeout(async () => {\n                    await this.initWysiwyg();\n                }, 0);\n            }\n\n            this.$bus.$on('theme-settings-before-save', () => {\n                if (this.wysiwyg) {\n                    tinymce.triggerSave();\n\n                    setTimeout(() => {\n                        this.content = tinymce.get(this.editorID).getContent()\n                    }, 250);\n                }\n            });\n\n            this.$bus.$on('view-settings-before-save', () => {\n                if (this.wysiwyg) {\n                    tinymce.triggerSave();\n\n                    setTimeout(() => {\n                        this.content = tinymce.get(this.editorID).getContent();\n                    }, 250);\n                }\n            });\n\n            this.$bus.$on('plugin-settings-before-save', () => {\n                if (this.wysiwyg) {\n                    tinymce.triggerSave();\n\n                    setTimeout(() => {\n                        if (this.editorID && tinymce.get(this.editorID)) {\n                            this.content = tinymce.get(this.editorID).getContent();\n                        }\n                    }, 250);\n                }\n            });\n        }, 0);\n    },\n    watch: {\n        value: function (newValue, oldValue) {\n            this.content = newValue;\n        },\n        content: function(newValue) {\n            this.$emit('input', this.content);\n        }\n    },\n    methods: {\n        async initWysiwyg () {\n            let self = this;\n            let customFormats = this.loadCustomFormatsFromTheme();\n            let pluginsList = \"autolink link lists paste code\";\n            let firstToolbarStructure = \"bold italic link unlink forecolor blockquote alignleft aligncenter alignright bullist numlist formatselect removeformat code\";\n            let secondToolbarStructure = \"\";\n\n            if (customFormats.length) {\n                secondToolbarStructure = \"styleselect formatselect removeformat undo redo code\";\n            }\n\n            if (this.$store.state.wysiwygTranslation) {\n                tinymce.addI18n('custom', this.$store.state.wysiwygTranslation);\n            }\n\n            if (this.simplifiedToolbar) {\n                pluginsList = \"autolink link paste code\";\n                firstToolbarStructure = \"bold italic link unlink forecolor alignleft aligncenter alignright removeformat code\";\n                secondToolbarStructure = \"\";\n            }\n\n            tinymce.init({\n                selector: 'textarea[data-id=\"' + this.editorID + '\"]',\n                language: this.$store.state.wysiwygTranslation ? 'en' : 'custom',\n                content_css: this.getTinyMCECSSFiles(),\n                plugins: pluginsList,\n                toolbar1: firstToolbarStructure,\n                toolbar2: secondToolbarStructure,\n                toolbar3: \"\",\n                icons: 'publii',   \n                preview_styles: false,\n                resize: true,\n                menubar: false,\n                forced_root_block: \"\",\n                force_br_newlines: false,\n                force_p_newlines: true,\n                paste_as_text: true,\n                element_format : 'html',\n                fix_list_elements : true,\n                image_caption: true,\n                extended_valid_elements: \"a[*],altGlyph[*],altGlyphDef[*],altGlyphItem[*],animate[*],animateColor[*],animateMotion[*],animateTransform[*],circle[*],clipPath[*],color-profile[*],cursor[*],defs[*],desc[*],discard[*],ellipse[*],feBlend[*],feColorMatrix[*],feComponentTransfer[*],feComposite[*],feConvolveMatrix[*],feDiffuseLighting[*],feDisplacementMap[*],feDistantLight[*],feDropShadow[*],feFlood[*],feFuncA[*],feFuncB[*],feFuncG[*],feFuncR[*],feGaussianBlur[*],feImage[*],feMerge[*],feMergeNode[*],feMorphology[*],feOffset[*],fePointLight[*],feSpecularLighting[*],feSpotLight[*],feTile[*],feTurbulence[*],filter[*],font[*],font-face[*],font-face-format[*],font-face-name[*],font-face-src[*],font-face-uri[*],foreignObject[*],g[*],glyph[*],glyphRef[*],hatch[*],hatchpath[*],hkern[*],iframe[*],image[*],line[*],linearGradient[*],marker[*],mask[*],mesh[*],meshgradient[*],meshpatch[*],meshrow[*],metadata[*],missing-glyph[*],mpath[*],path[*],pattern[*],polygon[*],polyline[*],radialGradient[*],rect[*],set[*],solidcolor[*],stop[*],style[*],svg[*],switch[*],symbol[*],text[*],textPath[*],title[*],tref[*],tspan[*],unknown[*],use[*],view[*],vkern[*],publii-amp,publii-non-amp,script[*],i[*],input[*]\",\n                formats: {\n                    alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},\n                    aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},\n                    alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},\n                    alignjustify: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-justify'}\n                },\n                height: 320,\n                entity_encoding: \"raw\",\n                allow_script_urls: true,\n                convert_urls: false,\n                style_formats: customFormats,\n                contextmenu: false,\n                browser_spellcheck: this.$store.state.currentSite.config.spellchecking,\n                setup: async function (editor) {\n                    editor.on('init', async function () {\n                        let iframe = document.querySelector('#' + self.editorID + '_ifr');\n                        let htmlElement = iframe.contentWindow.window.document.querySelector('html');\n                        htmlElement.setAttribute('data-theme', await self.$root.getCurrentAppTheme());\n                        htmlElement.setAttribute('style', self.$root.overridedCssVariables);\n                    });\n\n                    editor.on('input', Utils.debouncedFunction(() => {\n                        self.content = editor.getContent();\n                    }, 1000));\n                }\n            });\n        },\n        removeEditor () {\n            let editorToRemove = tinymce.get(this.editorID);\n\n            if (editorToRemove) {\n                editorToRemove.remove();\n            }\n        },\n        generateID () {\n            return 't-' + Math.ceil(Math.random() * 10000000).toString();\n        },\n        loadCustomFormatsFromTheme() {\n            let output = [];\n            let customElements = [];\n            let inlineElements = [\n                'a', 'b', 'abbr', 'acronym', 'cite', 'dfn', 'kbd',\n                'samp', 'time', 'var', 'bdo', 'br', 'big', 'code',\n                'i', 'em', 'small','strong','span', 'tt', 'img',\n                'map', 'object', 'q', 'script', 'sub', 'sup', 'button',\n                'input', 'label', 'select', 'textarea'\n            ];\n\n            // Detect mode\n            if(\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.customElementsMode &&\n                this.$store.state.currentSite.themeSettings.customElementsMode === 'advanced'\n            ) {\n                output = JSON.parse(JSON.stringify(this.$store.state.currentSite.themeSettings.customElements));\n                return output;\n            }\n\n            // Load custom elements\n            if(\n                this.$store.state.currentSite.themeSettings &&\n                this.$store.state.currentSite.themeSettings.customElements\n            ) {\n                customElements = this.$store.state.currentSite.themeSettings.customElements;\n            }\n\n            if(customElements && customElements.length) {\n                for(let i = 0; i < customElements.length; i++) {\n                    if(!customElements[i]) {\n                        continue;\n                    }\n\n                    if(!customElements[i].tag && !customElements[i].selector) {\n                        continue;\n                    }\n\n                    if(!customElements[i].themeSettings) {\n                        continue;\n                    }\n\n                    let style = {\n                        title: customElements[i].label,\n                        classes: customElements[i].cssClasses\n                    };\n\n                    if(customElements[i].selector) {\n                        style.selector = customElements[i].selector;\n                    } else {\n                        if (inlineElements.indexOf(customElements[i].tag)) {\n                            style.inline = customElements[i].tag;\n                        } else {\n                            style.block = customElements[i].tag;\n                        }\n                    }\n\n                    output.push(style);\n                }\n            }\n\n            return output;\n        },\n        extensionsPath () {\n            return [\n                'file:///',\n                this.$store.state.currentSite.siteDir,\n                '/input/themes/',\n                this.$store.state.currentSite.config.theme,\n                '/'\n            ].join('');\n        },\n        getTinyMCECSSFiles () {\n            let pathToEditorCSS = this.extensionsPath() + 'assets/css/editor.css';\n            let customEditorCSS = pathToEditorCSS;\n\n            return [\n                'css/editor-options.css?v=0711',\n                customEditorCSS\n            ].join(',');\n        },\n        setContent (newContent) {\n            Vue.set(this, 'content', newContent);\n            this.$emit('input', this.content);\n        }\n    },\n    beforeDestroy () {\n        if (this.wysiwyg) {\n            let editorToRemove = tinymce.get(this.editorID);\n\n            if (editorToRemove) {\n                editorToRemove.remove();\n            }\n            \n            this.$bus.$off('theme-settings-before-save');\n            this.$bus.$off('plugin-settings-before-save');\n            this.$bus.$off('view-settings-before-save');\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n@import '../../scss/editor/post-editors-common.scss';\n@import '../../scss/editor/editor-overrides.scss';\n\n.publii-textarea {\n    background-color: var(--input-bg);\n    border: none;\n    border-radius: var(--border-radius);\n    box-shadow: inset 0 0 0 1px var(--input-border-color);\n    color: var(--text-primary-color);\n    display: block;\n    font: 400 #{$app-font-base}/1.5 var(--font-base);\n    max-width: 100%;\n    overflow: auto;\n    outline: none;\n    padding: 12px 18px;\n    resize: vertical;\n    width: 100%;\n\n    &:focus {\n        box-shadow: inset 0 0 2px 1px var(--input-border-focus);\n    }\n\n    &[disabled],\n    &[readonly] {\n        opacity: .5;\n        cursor: not-allowed;\n\n        &:focus {\n            box-shadow: inset 0 0 0 1px var(--input-border-color);\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/TextInput.vue",
    "content": "<template>\n    <div\n        :id=\"anchor\"\n        :class=\"cssClasses\">\n        <icon\n            v-if=\"icon\"\n            size=\"s\"\n            :name=\"icon\" />\n\n        <input\n            :type=\"fieldType\"\n            v-model=\"content\"\n            :id=\"id\"\n            :readonly=\"readonly\"\n            :disabled=\"disabled\"\n            :placeholder=\"placeholder\"\n            :min=\"min\"\n            :max=\"max\"\n            :step=\"step\"\n            :spellcheck=\"spellcheck\"\n            ref=\"input\"\n            :pattern=\"pattern\"\n            @keyup=\"keyboardEvent\"\n            @keydown=\"keyboardEvent\"\n            @keypress=\"keyboardEvent\" />\n\n        <char-counter\n            v-if=\"charCounter\"\n            v-model=\"content\"\n            :preferredCount=\"preferredCount\" />\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'text-input',\n    data: function() {\n        return {\n            content: '',\n            fieldType: ''\n        };\n    },\n    props: {\n        'id': {\n            default: '',\n            type: String\n        },\n        'anchor': {\n            default: '',\n            type: String\n        },\n        'icon': {\n            default: '',\n            type: String\n        },\n        'placeholder': {\n            default: '',\n            type: String\n        },\n        'type': {\n            default: 'text',\n            type: String\n        },\n        'properties': {\n            default: '',\n            type: String\n        },\n        'readonly': {\n            default: false,\n            type: Boolean\n        },\n        'disabled': {\n            default: false,\n            type: Boolean\n        },\n        'value': {\n            default: '',\n            type: [String, Number]\n        },\n        'min': {\n            default: '',\n            type: String\n        },\n        'max': {\n            default: '',\n            type: String\n        },\n        'step': {\n            default: '',\n            type: String\n        },\n        'pattern': {\n            default: '',\n            type: String\n        },\n        'changeEventName': {\n            default: '',\n            type: String\n        },\n        'customCssClasses': {\n            default: '',\n            type: String\n        },\n        'charCounter': {\n            default: false,\n            type: Boolean\n        },\n        'preferredCount': {\n            default: 0,\n            type: Number\n        },\n        'spellcheck': {\n            default: true,\n            type: Boolean\n        }\n    },\n    computed: {\n        cssClasses: function() {\n            let properties = [];\n            let cssClasses = {};\n\n            if(this.properties) {\n                properties = this.properties.split(' ');\n            }\n\n            cssClasses = {\n                'input-wrapper': true,\n                'is-small': properties.indexOf('is-small') > -1,\n                'has-padding': properties.indexOf('has-padding') > -1,\n                'has-icon': !!this.icon,\n                'is-number': this.type === 'number'\n            };\n\n            if (this.customCssClasses && this.customCssClasses.trim() !== '') {\n                this.customCssClasses.split(' ').forEach(item => {\n                    item = item.replace(/[^a-z0-9\\-\\_\\s]/gmi, '');\n                    cssClasses[item] = true;\n                });\n            }\n\n            return cssClasses;\n        }\n    },\n    watch: {\n        value (newValue, oldValue) {\n            this.content = newValue;\n        },\n        content: function(newValue) {\n            if(this.changeEventName) {\n                this.$bus.$emit(this.changeEventName, newValue);\n            }\n\n            this.$emit('input', this.content);\n        }\n    },\n    mounted: function() {\n        setTimeout(() => {\n            this.content = this.value;\n            this.fieldType = this.type;\n        }, 0);\n    },\n    methods: {\n        keyboardEvent: function (e) {\n            if(this.properties.indexOf('keyboard-blocked') > -1) {\n                e.preventDefault();\n            }\n        },\n        getValue: function () {\n            return this.content;\n        },\n        setValue: function (newValue) {\n            this.content = newValue;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\n/*\n * Text input field\n */\n\n.input-wrapper {\n    position: relative;\n\n    &.is-number {\n        display: inline-block;\n        width: 12rem;\n    }\n\n    svg {\n        fill: var(--icon-secondary-color);\n        left: 2rem;\n        position: absolute;\n        top: 50%;\n        transform: translateY(-50%);\n        z-index: 1;\n    }\n\n    input {\n        background-color: var(--input-bg);\n        border: none;\n        box-shadow: inset 0 0 0 1px var(--input-border-color);\n        color: var(--text-primary-color);\n        display: inline-block;\n        font: 400 #{$app-font-base}/1.5 var(--font-base);\n        outline: none;\n        padding: 12px 18px;\n        width: 100%;\n\n        &:focus {\n            box-shadow: inset 0 0 2px 1px var(--input-border-focus);\n        }\n\n        &[disabled],\n        &[readonly] {\n            opacity: .5;\n            cursor: not-allowed;\n\n            &:focus {\n                box-shadow: inset 0 0 0 1px var(--input-border-color);\n            }\n        }\n    }\n\n    &.is-small {\n        input {\n            padding: 6px 9px;\n        }\n    }\n\n    &.is-invalid,\n    &.has-error {\n        input {\n            box-shadow: inset 0 0 0 1px var(--warning);\n        }\n    }\n\n    &.has-padding {\n        padding: 1rem 2rem;\n\n        &.has-icon {\n            svg {\n                left: 3rem;\n            }\n        }\n    }\n\n    &.has-icon {\n        input {\n            padding-left: 5.5rem;\n        }\n\n        &.is-small {\n            input {\n                padding-left: 4rem;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/basic-elements/ThemesDropdown.vue",
    "content": "<template>\n    <select\n        :id=\"id\"\n        @change=\"onChangeEvent\">\n        <option value=\"\">{{ $t('theme.selectTheme') }}</option>\n        <optgroup\n            v-for=\"key in themesGroups()\"\n            :label=\"themesGroupName(key)\"\n            :key=\"'themes-group-' + key\">\n            <option\n                v-for=\"(item, value) in themesGroup(key)\"\n                :value=\"value\"\n                :key=\"'themes-group-' + key + '-' + value\">\n                {{ item }}\n            </option>\n        </optgroup>\n    </select>\n</template>\n\n<script>\nexport default {\n    name: 'themes-dropdown',\n    props: {\n        id: {\n            default: '',\n            required: true,\n            type: String\n        },\n        value: {\n            default: '',\n            type: String\n        }\n    },\n    data: function() {\n        return {\n            selectedValue: ''\n        };\n    },\n    computed: {\n        themes () {\n            return this.$store.getters.themeSelect;\n        }\n    },\n    methods: {\n        onChangeEvent (e) {\n            this.selectedValue = e.target.value;\n            this.$emit('input', this.selectedValue);\n        },\n        themesGroups () {\n            return Object.keys(this.themes);\n        },\n        themesGroupName (key) {\n            return this.themes[key].name;\n        },\n        themesGroup (key) {\n            return this.themes[key].items;\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n\nselect {\n    -webkit-appearance: none;\n    max-width: 100%;\n    min-width: 100px;\n    min-height: 46px;\n    position: relative;\n    width: 100%;\n\n    &:not([multiple]) {\n        background: url('data:image/svg+xml;utf8,<svg fill=\"%238e929d\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 6\"><polygon points=\"10 0 5 0 0 0 5 6 10 0\"/></svg>') no-repeat calc(100% - 2rem) 50%;\n        background-color: var(--input-bg);\n        background-size: 10px;\n        padding-right: 3rem;\n    }\n}\n\n/*\n * Special rules for Windows\n */\n\nbody[data-os=\"win\"] {\n    select:not([multiple]) {\n        height: 4.8rem;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/PubliiBlockEditor.vue",
    "content": "<template>\n  <div id=\"publii-block-editor\">\n    <div\n      id=\"post-title\"\n      ref=\"post-title\"\n      class=\"post-editor-form-title\"\n      contenteditable=\"true\"\n      @paste.prevent=\"pasteTitle\"\n      @keydown=\"detectEnterInTitle\"\n      @input=\"updateTitle\"\n      :data-translation=\"itemType === 'post' ? $t('post.addPostTitle') : $t('page.addPageTitle')\"></div>\n\n    <block-editor\n        ref=\"block-editor\" />\n    <textarea\n        id=\"post-editor\"></textarea>\n    <input\n        name=\"image\"\n        id=\"post-editor-fake-image-uploader\"\n        class=\"is-hidden\"\n        type=\"file\" />\n    <input\n        name=\"image\"\n        id=\"post-editor-fake-multiple-images-uploader\"\n        class=\"is-hidden\"\n        type=\"file\"\n        multiple />\n  </div>\n</template>\n\n<script>\nimport Vue from 'vue';\nimport BlockEditor from './components/BlockEditor.vue';\n\nexport default {\n  name: 'publii-block-editor',\n  props: [\n    'itemType'\n  ],\n  components: {\n    BlockEditor\n  },\n  mounted () {\n    this.$bus.$on('block-editor-set-post-id', this.setPostID);\n    this.$bus.$on('block-editor-set-post-text', this.setPostText);\n    this.$bus.$on('block-editor-set-post-title', this.setPostTitle);\n    this.$bus.$on('block-editor-post-save', this.postSave);\n    this.$bus.$on('block-editor-set-current-site-data', this.setCurrentSiteData);\n    mainProcessAPI.receive('block-editor-undo', this.undoAction);\n    mainProcessAPI.receive('block-editor-redo', this.redoAction);\n    this.$refs['post-title'].focus();\n  },\n  methods: {\n    setPostID (postID) {\n      this.$refs['block-editor'].setPostID(postID);\n    },\n    setPostText (postText) {\n      document.getElementById('post-editor').value = postText;\n\n      setTimeout(() => {\n        this.$bus.$emit('publii-block-editor-load');\n      }, 0);\n    },\n    detectEnterInTitle (event) {\n      if (event.key === 'Enter') {\n        event.preventDefault();\n        let firstBlockID = this.$refs['block-editor'].content[0].id;\n        this.$refs['block-editor'].$refs['block-' + firstBlockID][0].focus();\n      }\n    },\n    setPostTitle (postTitle) {\n      this.$refs['post-title'].innerText = postTitle;\n    },\n    updateTitle () {\n      let title = this.$refs['post-title'].innerText.replace(/\\n/gmi, ' ');\n      this.$bus.$emit('block-editor-title-updated', title);\n    },\n    pasteTitle (e) {\n      let text = (e.originalEvent || e).clipboardData.getData('text/plain').replace(/\\n/gmi, '');\n      document.execCommand('insertHTML', false, text);\n    },\n    postSave () {\n      this.$bus.$emit('publii-block-editor-save');\n\n      setTimeout(() => {\n        this.$bus.$emit('block-editor-post-saved');\n      }, 500);\n    },\n    setCurrentSiteData (currentSiteData) {\n      this.$refs['block-editor'].currentSiteData = currentSiteData;\n    },\n    undoAction () {\n      this.$refs['block-editor'].undo();\n    },\n    redoAction () {\n\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-set-post-id', this.setPostID);\n    this.$bus.$off('block-editor-set-post-text', this.setPostText);\n    this.$bus.$off('block-editor-set-post-title', this.setPostTitle);\n    this.$bus.$off('block-editor-post-save', this.postSave);\n    this.$bus.$off('block-editor-set-current-site-data', this.setCurrentSiteData);\n    mainProcessAPI.stopReceiveAll('block-editor-undo', this.undoAction);\n    mainProcessAPI.stopReceiveAll('block-editor-redo', this.redoAction);\n  }\n}\n</script>\n\n<style>\n#post-title {\n  border: none;\n  box-shadow: none;\n  color: var(--headings-color);\n  display: block;\n  font-family: -apple-system, BlinkMacSystemFont, Arial, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  font-size: 36px;\n  font-weight: var(--font-weight-bold);\n  letter-spacing: var(--letter-spacing);\n  line-height: 1.2;\n  margin: 0 10% 1.6rem;\n  outline: none;\n  padding: 0;\n  text-align: center;\n  width: 80%;\n}\n\n#post-editor {\n  display: none;\n}\n\n#post-title:empty:before {\n  content: attr(data-translation);\n  color: var(--gray-4);\n}\n\n#post-title:empty:focus:before {\n  content: \"\";\n}\n\n@media (min-width: 1800px) {\n    #post-title {\n        margin: 0 auto 1.6rem;\n        max-width: calc(100vw - 880px);\n        width: 100%;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/assets/prism-theme.scss",
    "content": "/**\n * Publii theme based on okaidia theme for JavaScript, CSS and HTML\n * Loosely based on Monokai textmate theme by http://www.monokai.nl/\n * @author Publii Team\n */\n\n.editor > .editor-inner {\n    code[class*=\"language-\"],\n    pre[class*=\"language-\"] {\n        color: var(--pre-color);\n        background: none;\n        text-shadow: 0 1px rgba(0, 0, 0, 0.3);\n        font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace !important;\n        font-size: 1em;\n        text-align: left;\n        white-space: pre;\n        word-spacing: normal;\n        word-break: normal;\n        word-wrap: normal;\n        line-height: 1.5;\n\n        -moz-tab-size: 4;\n        -o-tab-size: 4;\n        tab-size: 4;\n\n        -webkit-hyphens: none;\n        -moz-hyphens: none;\n        -ms-hyphens: none;\n        hyphens: none;\n    }\n\n    /* Code blocks */\n    pre[class*=\"language-\"] {\n        padding: 1em;\n        margin: .5em 0;\n        overflow: auto;\n        border-radius: 0.3em;\n    }\n\n    :not(pre) > code[class*=\"language-\"],\n    pre[class*=\"language-\"] {\n        background: #1e2128;\n    }\n\n    /* Inline code */\n    :not(pre) > code[class*=\"language-\"] {\n        padding: .1em;\n        border-radius: .3em;\n        white-space: normal;\n    }\n\n    .token.comment,\n    .token.prolog,\n    .token.doctype,\n    .token.cdata {\n        color: rgba(var(--white-rgb), .45);\n    }\n\n    .token.punctuation {\n        color: #b3b9c5;\n    }\n\n    .namespace {\n        opacity: .7;\n    }\n\n    .token.property,\n    .token.tag,\n    .token.constant,\n    .token.symbol,\n    .token.deleted {\n        color: #6ab0f3;\n    }\n\n    .token.boolean,\n    .token.number {\n        color: #e1a6f2;\n    }\n\n    .token.selector,\n    .token.attr-name,\n    .token.string,\n    .token.char,\n    .token.builtin,\n    .token.inserted {\n        color: #ffd479;\n    }\n\n    .token.operator,\n    .token.entity,\n    .token.url,\n    .language-css .token.string,\n    .style .token.string,\n    .token.variable {\n        color: #ac8d58;\n    }\n\n    .token.atrule,\n    .token.attr-value,\n    .token.function,\n    .token.class-name {\n        color: #92d192;\n    }\n\n    .token.keyword {\n        color: #ffeead;\n    }\n\n    .token.regex,\n    .token.important {\n        color: #fd971f;\n    }\n\n    .token.important,\n    .token.bold {\n        font-weight: bold;\n    }\n    .token.italic {\n        font-style: italic;\n    }\n\n    .token.entity {\n        cursor: help;\n    }\n\n    .prism-editor-wrapper {\n        max-height: 515px;\n    }\n}\n\n@media (max-height: 700px) {\n    .editor > .editor-inner { \n        .prism-editor-wrapper {\n            max-height: 395px;\n        }\n    }\n}"
  },
  {
    "path": "app/src/components/block-editor/assets/typography.scss",
    "content": "@import '../../../scss/vendor/modularscale';\n@import '../../../scss/variables.scss';\n@import '../../../scss/mixins.scss';\n\n\n.editor > .editor-inner {\n    font-size: var(--editor-font-size);\n\n    & > .wrapper > div {\n        color: var(--text-primary-color);\n        -webkit-font-smoothing: antialiased;\n        font-family: var(--editor-font-family);\n        font-size: 1em;\n        font-weight: var(--font-weight-normal);\n        line-height: $line-height;\n       \n        a:not(.btn):not(.publii-block-toc a) {\n            @include links (var(--link-invert-color), var(--link-invert-color), var(--link-invert-color), var(--link-invert-color));\n        }\n\n        p,\n        ul,\n        ol,\n        dl,\n        table,\n        hr,\n        blockquote,\n        figure,\n        pre,\n        table,\n        script {\n            margin: 0;\n        }\n\n\n        h1,\n        h2,\n        h3,\n        h4,\n        h5,\n        h6 {\n            color: var(--headings-color);\n            font-family: var(--font-base);\n            font-weight: var(--font-weight-bold);\n            letter-spacing: var(--letter-spacing);\n            line-height: 1.3;\n            margin: 0;\n            text-transform: none;\n        }\n\n        h1 {\n            font-size: $h1;\n            margin: 0;\n        }\n        \n        h2 {\n            font-size: $h2;\n        }\n        \n        h3 {\n            font-size: $h3;\n        }\n        \n        h4 {\n            font-size: $h4;\n        }\n        \n        h5 {\n            font-size: $h5;\n        }\n        \n        h6 {\n            font-size: $h6;\n        }\n        \n\n        b,\n        strong {\n            color: var(--headings-color);\n            font-weight: var(--font-weight-bold);\n        }\n\n        blockquote {\n            border-left: 2px solid var(--gray-3);\n            font-family: var(--font-serif);\n            font-style: italic;\n            padding: 0 0 0 baseline(5,em);\n        }\n\n        ul,\n        ol {\n            padding: 0 0 0 2em;\n\n            & > li {\n                list-style: inherit;\n                padding-left: baseline(2,em);\n            }\n\n            ul,\n            ol {\n                margin-bottom: baseline(2,em);\n                margin-top: baseline(2,em);\n            }\n        }\n\n        code {\n            background-color: rgba(var(--warning-rgb), .15);\n            border-radius: 2px;\n            color: var(--warning);\n            font-size: ms(-1);\n            font-family: var(--font-monospace);\n            padding: baseline(1,em) baseline(2,em);\n        }\n\n        pre {\n            font-size: ms(-1);\n            padding: baseline(6,em);\n            white-space: pre-wrap;\n            word-wrap: break-word;\n            width: 100%;\n\n            code {\n                color: inherit !important;\n            }\n        }\n\n        figcaption {\n            color: var(--text-light-color);\n            font-size: ms(-1);\n            text-align: center;\n        }\n        \n        label {\n            color: var(--label-color);\n            font-weight: var(--font-weight-normal);\n        }\n        \n        .msg {\n            border: 1px solid transparent;\n            border-radius: 3px;\n            padding: baseline(4,em);\n\n            &--highlight {\n                background-color: rgba(var(--yellow), .1);\n                border-color: rgba(var(--yellow), .5);\n            }\n        \n            &--info {\n                background-color: rgba(var(--color-primary-rgb), .1);\n                border-color: rgba(var(--color-primary-rgb), .5);\n            }\n        \n            &--success {\n                background-color: rgba(var(--success-rgb), .1);\n                border-color: rgba(var(--success-rgb), .51);\n            }\n        \n            &--warning {\n                background-color: rgba(var(--warning-rgb), .1);\n                border-color: rgba(var(--warning-rgb), .5);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/components/block-editor/available-blocks.json",
    "content": "[\n    {\n        \"blockName\": \"publii-paragraph\",\n        \"icon\": \"paragraph\",\n        \"label\": \"editor.paragraph\"\n    },\n    {\n        \"blockName\": \"publii-header\",\n        \"icon\": \"headings\",\n        \"label\": \"editor.header\"\n    },\n    {\n        \"blockName\": \"publii-image\",\n        \"icon\": \"image\",\n        \"label\": \"image.image\"\n    },\n    {\n        \"blockName\": \"publii-gallery\",\n        \"icon\": \"gallery\",\n        \"label\": \"editor.gallery\"\n    },\n    {\n        \"blockName\": \"publii-list\",\n        \"icon\": \"unordered-list\",\n        \"label\": \"editor.list\"\n    },\n    {\n        \"blockName\": \"publii-quote\",\n        \"icon\": \"quote\",\n        \"label\": \"editor.quote\"\n    },\n    {\n        \"blockName\": \"publii-code\",\n        \"icon\": \"code\",\n        \"label\": \"editor.code\"\n    },\n    {\n        \"blockName\": \"publii-html\",\n        \"icon\": \"html\",\n        \"label\": \"editor.html\"\n    },\n    {\n        \"blockName\": \"publii-separator\",\n        \"icon\": \"separator\",\n        \"label\": \"editor.separator\"\n    },\n    {\n        \"blockName\": \"publii-readmore\",\n        \"icon\": \"readmore\",\n        \"label\": \"editor.readMoreBlockName\"\n    },\n    {\n        \"blockName\": \"publii-toc\",\n        \"icon\": \"toc\",\n        \"label\": \"editor.toc\"\n    }\n]\n"
  },
  {
    "path": "app/src/components/block-editor/blocks-mapping.js",
    "content": "module.exports = {\n  'publii-code': require('./components/default-blocks/publii-code/render.js'),\n  'publii-embed': require('./components/default-blocks/publii-embed/render.js'),\n  'publii-gallery': require('./components/default-blocks/publii-gallery/render.js'),\n  'publii-header': require('./components/default-blocks/publii-header/render.js'),\n  'publii-html': require('./components/default-blocks/publii-html/render.js'),\n  'publii-image': require('./components/default-blocks/publii-image/render.js'),\n  'publii-list': require('./components/default-blocks/publii-list/render.js'),\n  'publii-paragraph': require('./components/default-blocks/publii-paragraph/render.js'),\n  'publii-quote': require('./components/default-blocks/publii-quote/render.js'),\n  'publii-readmore': require('./components/default-blocks/publii-readmore/render.js'),\n  'publii-separator': require('./components/default-blocks/publii-separator/render.js'),\n  'publii-toc': require('./components/default-blocks/publii-toc/render.js')\n};\n"
  },
  {
    "path": "app/src/components/block-editor/components/Block.vue",
    "content": "<script>\nimport AdvancedConfig from './mixins/AdvancedConfig.vue';\nimport Utils from './utils/Utils.js';\n\nexport default {\n  name: 'Block',\n  mixins: [\n    AdvancedConfig\n  ],\n  props: [\n    'id',\n    'inputContent',\n    'inputConfig',\n    'editor'\n  ],\n  computed: {\n    isEmpty () {\n      if (typeof this.content === 'string') {\n        return this.content === '';\n      } else if (typeof this.content === 'boolean') {\n        return false;\n      }\n\n      // When content is an object\n      let keys = Object.keys(this.content);\n\n      for (let i = 0; i < keys.length; i++) {\n        let field = this.content[keys[i]];\n\n        if (typeof field === 'string' && field !== '') {\n          return false;\n        }\n\n        if (typeof field === 'object' && field.length > 0) {\n          return false;\n        }\n      }\n\n      return true;\n    }\n  },\n  data () {\n    return {\n      caretIsAtStart: false,\n      caretIsAtEnd: false,\n      textIsHighlighted: false\n    };\n  },\n  mounted () {\n    this.config = Utils.deepMerge(this.config, this.inputConfig);\n    this.$on('block-save', this.save);\n    this.$bus.$on('block-editor-trigger-block-save', this.saveIsNeeded);\n    this.$bus.$on('block-editor-clear-text-selection', this.clearTextSelection);\n  },\n  methods: {\n    focus (cursorPosition = 'end') {\n      let focusElement = this.getFocusableElement();\n\n      if (\n        document.activeElement === this.$refs[focusElement] ||\n        document.activeElement.parentNode === this.$refs[focusElement].parentNode\n      ) {\n        return;\n      }\n\n      if (!this.$refs[focusElement].focus) {\n        return;\n      }\n\n      this.$refs[focusElement].focus();\n\n      if (cursorPosition === 'none') {\n        return;\n      }\n\n      if (cursorPosition === 'end') {\n        this.setCursorAtEndOfElement(focusElement, this.isContentEditable(focusElement));\n      }\n\n      if (typeof cursorPosition === 'number') {\n        this.setCursorAtPosition(cursorPosition);\n      }\n    },\n    isContentEditable (el) {\n      let formElements = [\n        'INPUT',\n        'SELECT',\n        'TEXTAREA'\n      ];\n\n      if (formElements.indexOf(this.$refs[el].tagName) > -1) {\n        return false;\n      }\n\n      return true;\n    },\n    getFocusableElement () {\n      if (this.focusable) {\n        let focusElement = this.focusable[0];\n        return focusElement;\n      } else {\n        return 'block';\n      }\n    },\n    setCursorAtPosition (position) {\n      let el = this.$refs[this.getFocusableElement()];\n      let range = document.createRange();\n      let sel = document.getSelection();\n      range.setStart(el.firstChild, position);\n      range.setEnd(el.firstChild, position);\n      sel.removeAllRanges();\n      sel.addRange(range);\n    },\n    getCaretPosition (node) {\n      let range = window.getSelection().getRangeAt(0);\n      let preCaretRange = range.cloneRange();\n      let caretPosition;\n      let tmp = document.createElement('div');\n\n      preCaretRange.selectNodeContents(node);\n      preCaretRange.setEnd(range.endContainer, range.endOffset);\n      tmp.appendChild(preCaretRange.cloneContents());\n      caretPosition = tmp.innerHTML.length;\n\n      return caretPosition;\n    },\n    getHtmlCaretPosition (node) {\n      let textPosition = this.getCaretPosition(node);\n      let htmlContent = node.innerHTML;\n      let textIndex = 0;\n      let htmlIndex = 0;\n      let insideHtml = false;\n      let htmlBeginChars = ['&', '<'];\n      let htmlEndChars = [';', '>'];\n\n      if (textPosition === 0) {\n        return 0;\n      }\n\n      while (textIndex < textPosition) {\n        htmlIndex++;\n\n        while (htmlBeginChars.indexOf(htmlContent.charAt(htmlIndex)) > -1) {\n          insideHtml = true;\n\n          while (insideHtml) {\n            if (htmlEndChars.indexOf(htmlContent.charAt(htmlIndex)) > -1) {\n              if (htmlContent.charAt(htmlIndex) === ';') {\n                htmlIndex--;\n              }\n\n              insideHtml = false;\n            }\n\n            htmlIndex++;\n          }\n        }\n\n        textIndex++;\n      }\n\n      return htmlIndex;\n    },\n    getCursorPosition (id) {\n      return this.getHtmlCaretPosition(this.$refs[id]);\n    },\n    setCursorAtEndOfElement (id = 'block', contenteditable = true) {\n      if (contenteditable) {\n        let range = document.createRange();\n        range.selectNodeContents(this.$refs[id]);\n        range.collapse(false);\n\n        let sel = document.getSelection();\n        sel.removeAllRanges();\n        sel.addRange(range);\n      } else {\n        let val = this.$refs[id].value;\n\n        setTimeout(() => {\n          if (this.$refs[id].focus) {\n            this.$refs[id].focus();\n            this.$refs[id].setSelectionRange(val.length, val.length);\n          }\n        }, 0);\n      }\n    },\n    handleCaret (e, blockRefName = 'block') {\n      let cursorPosition = this.getCursorPosition(blockRefName);\n\n      if (\n        e.code === 'ArrowUp' && (\n          cursorPosition <= 2 ||\n          cursorPosition === this.$refs[blockRefName].innerHTML.length ||\n          (cursorPosition <= 15 && this.$options.name === 'List')\n        )\n      ) {\n        if (!this.caretIsAtStart) {\n          this.caretIsAtStart = true;\n          return;\n        }\n\n        let previousBlockID = this.findPreviousBlockID();\n\n        if (previousBlockID) {\n          this.editor.$refs['block-wrapper-' + previousBlockID][0].blockClick();\n\n          if (this.editor.$refs['block-' + previousBlockID][0].focus) {\n            this.editor.$refs['block-' + previousBlockID][0].focus();\n          }\n        }\n      }\n\n      if (\n        e.code === 'ArrowDown' && (\n          cursorPosition >= this.$refs[blockRefName].innerHTML.length - 5 ||\n          (cursorPosition === 0 && this.$options.name === 'Separator') ||\n          (cursorPosition === 0 && this.$options.name === 'ReadMore')\n        )\n      ) {\n        if (!this.caretIsAtEnd) {\n          this.caretIsAtEnd = true;\n          return;\n        }\n\n        let nextBlockID = this.findNextBlockID();\n\n        if (nextBlockID) {\n          this.editor.$refs['block-wrapper-' + nextBlockID][0].blockClick();\n          this.editor.$refs['block-' + nextBlockID][0].focus('none');\n        }\n      }\n\n      if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {\n        this.caretIsAtStart = false;\n        this.caretIsAtEnd = false;\n      }\n    },\n    getFocusFromTab (e) {\n      if (e.code === 'Tab') {\n        this.focus();\n      }\n    },\n    pastePlainText (e) {\n      e.preventDefault();\n      let text = (e.originalEvent || e).clipboardData.getData('text/plain');\n      // eslint-disable-next-line\n      text = text.replace(/\\n/gmi, '<br>');\n      document.execCommand('insertHTML', false, text);\n    },\n    saveIsNeeded (id) {\n      if (this.id === id) {\n        this.save();\n      }\n    },\n    findNextBlockID () {\n      let currentBlockIndex = this.editor.content.findIndex(el => el.id === this.id);\n      currentBlockIndex++;\n\n      if (currentBlockIndex < this.editor.content.length) {\n        return this.editor.content[currentBlockIndex].id;\n      }\n\n      return false;\n    },\n    findPreviousBlockID () {\n      let currentBlockIndex = this.editor.content.findIndex(el => el.id === this.id);\n\n      if (currentBlockIndex < 1) {\n        return false;\n      }\n\n      currentBlockIndex--;\n\n      return this.editor.content[currentBlockIndex].id;\n    },\n    clearTextSelection (blockID) {\n      if (this.id === blockID) {\n        window.getSelection().removeAllRanges();\n        this.textIsHighlighted = false;\n      }\n    },\n    clearContentHtml (refID) {\n      this.$refs[refID].innerHTML = this.$refs[refID].innerText;\n    },\n    updateCurrentBlockID () {\n      this.$bus.$emit('publii-block-editor-update-current-block-id', this.id);\n    },\n    debouncedSave: Utils.debounce(function (newValue) {\n      this.save();\n    }, 500)\n  },\n  beforeDestroy () {\n    this.$off('block-save', this.save);\n    this.$bus.$off('block-editor-trigger-block-save', this.saveIsNeeded);\n    this.$bus.$off('block-editor-clear-text-selection', this.clearTextSelection);\n  }\n}\n</script>\n"
  },
  {
    "path": "app/src/components/block-editor/components/BlockAdvancedConfig.vue",
    "content": "<template>\n  <div\n    @click.stop=\"hide()\"\n    :class=\"{ 'block-advanced-config-overlay': true, 'is-visible': isVisible }\">\n    <div\n      @click.prevent.stop\n      class=\"block-advanced-config\"\n      ref=\"form\">\n      <div\n        v-for=\"(field, index) of configForm\"\n        :key=\"'field-' + index\"\n        class=\"block-advanced-config-field\">\n        <label\n          :for=\"'advanced-config-field-' + index\"\n          :title=\"field.tooltip !== '' ? $t(field.tooltip) : ''\">\n          {{ $t(field.label) }}\n          <span\n            v-if=\"field.tooltip !== ''\"\n            class=\"block-advanced-config-field-help\">?</span>\n        </label>\n        <input\n          v-if=\"field.type === 'text'\"\n          :id=\"'advanced-config-field-' + index\"\n          :key=\"'field-input-' + index\"\n          type=\"text\"\n          :disabled=\"getDisabledState(field.disabled)\"\n          :spellcheck=\"field.spellcheck || false\"\n          v-model=\"config[field.name]\" />\n        <switcher\n          v-if=\"field.type === 'checkbox'\"\n          :id=\"'advanced-config-field-' + index\"\n          :key=\"'field-input-' + index\"\n          :disabled=\"getDisabledState(field.disabled)\"\n          v-model=\"config[field.name]\" />\n        <select\n          v-if=\"field.type === 'select'\"\n          :id=\"'advanced-config-field-' + index\"\n          :key=\"'field-input-' + index\"\n          blockData.config.advanced.style\n          v-model=\"config[field.name]\">\n          <option value=\"\">{{ $t('editor.defaultSelect') }}</option>\n          <option\n            v-for=\"(option, index) of field.values\"\n            :key=\"'option-' + index\"\n            :value=\"option.value\">\n            {{ $t(option.label) }}\n          </option>\n        </select>\n      </div>\n\n      <div class=\"block-advanced-config-buttons\">\n        <button @click=\"save()\">\n          {{ $t('ui.save') }}\n        </button>\n        <button @click=\"hide()\" class=\"outline\">\n          {{ $t('ui.cancel') }}\n        </button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Switcher from './elements/Switcher.vue';\n\nexport default {\n  name: 'block-advanced-config',\n  components: {\n    'switcher': Switcher\n  },\n  data () {\n    return {\n      isVisible: false,\n      currentBlockID: '',\n      config: {},\n      configForm: []\n    };\n  },\n  mounted () {\n    this.$bus.$on('block-editor-show-advanced-config', this.show);\n  },\n  methods: {\n    show (blockID, config, configForm) {\n      this.isVisible = true;\n      this.currentBlockID = blockID;\n      this.config = JSON.parse(JSON.stringify(config));\n      this.configForm = configForm;\n\n      setTimeout(() => {\n        this.$refs['form'].querySelector('.block-advanced-config-field').querySelector('input').focus();\n      }, 100);\n    },\n    hide () {\n      this.isVisible = false;\n\n      setTimeout(() => {\n        this.currentBlockID = '';\n        this.config = {};\n        this.configForm = [];\n      }, 500);\n    },\n    save () {\n      this.$bus.$emit('block-editor-save-advanced-config', this.currentBlockID, this.config);\n      this.hide();\n    },\n    getDisabledState (fieldDisabledRules) {\n      if (typeof fieldDisabledRules === 'undefined') {\n        return false;\n      }\n\n      for (let rule of fieldDisabledRules) {\n        if (this.config[rule.field] === rule.value) {\n          return true;\n        }\n      }\n\n      return false;\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-show-advanced-config', this.show);\n  }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../../../scss/variables.scss';\n\n.block-advanced-config {\n   background: var(--popup-bg);\n  border-radius: 6px;\n  box-shadow: 0 0 32px var(--shadow);\n  padding: 4rem;\n  transform: scale(.5);\n  transition: all .24s cubic-bezier(0, 0, 0.25, 0.99);\n  user-select: none;\n  width: 580px;\n\n  &-overlay {\n    align-items: center;\n    background: var(--overlay);\n    display: flex;\n    height: 100%;\n    justify-content: center;\n    left: 0;\n    opacity: 0;\n    pointer-events: none;\n    position: fixed;\n    top: 0;\n    transition: all .3s ease-out;\n    width: 100%;\n    z-index: 999991;\n\n    &.is-visible {\n      opacity: 1;\n      pointer-events: auto;\n\n      .block-advanced-config {\n        transform: scale(1);\n      }\n    }\n  }\n\n  &-field {\n    margin: 0 0 16px 0;\n\n    &-help {\n      align-items: center;\n      background: var(--color-primary);\n      border-radius: 50%;\n      color: var(--white);\n      cursor: help;\n      display: inline-flex;\n      font-size: 10px;\n      font-weight: var(--font-weight-bold);\n      height: 14px;\n      justify-content: center;\n      position: relative;\n      top: -1px;\n      width: 14px;\n    }\n\n    label {\n      color: var(--label-color);\n      display: block;\n      font-size: 14px;\n      padding-bottom: 8px;\n    }\n\n    input,\n    select {  \n      display: block;\n      width: 100%;\n\n      &[disabled] {\n        opacity: .5;\n        pointer-events: none;\n      }\n    }\n\n    select {\n      appearance: none;\n      max-width: 100%;\n      margin: 0;\n    }\n  }\n\n  &-buttons {\n    display: flex;\n    margin: 3rem -4rem -4rem;\n\n    button {\n      background: var(--button-bg);\n      border: none;\n      box-shadow: none;\n      border-bottom-left-radius: 6px;\n      border-top: 1px solid var(--button-bg);\n      color: var(--white);\n      cursor: pointer;\n      font-size: 15px;\n      font-weight: var(--font-weight-semibold);\n      line-height: 1;\n      width: 50%;\n      padding: 18px;\n      transition: all .25s ease-out;\n\n      &:hover {\n        background: var(--button-bg-hover);\n        border-color: var(--button-bg-hover);\n      }\n\n      &.outline {\n        background: var(--popup-btn-cancel-bg);\n        border: none;\n        border-top: 1px solid var(--input-border-color);\n        border-bottom-right-radius: 6px;\n        color: var(--popup-btn-cancel-color);\n\n        &:hover {\n           background: var(--popup-btn-cancel-bg-hover);\n           color: var(--popup-btn-cancel-hover-color);\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/BlockEditor.vue",
    "content": "<template>\n  <div\n    class=\"editor\"\n    data-ui-opened-block=\"\"\n    ref=\"editor-main\"\n    @click=\"$bus.$emit('block-editor-deselect-blocks')\">\n    <div :class=\"{ 'editor-inner': true }\">\n      <block-wrapper\n        v-for=\"block of content\"\n        :id=\"block.id\"\n        :block-type=\"block.type\"\n        :key=\"'block-wrapper-' + block.id\"\n        :ref=\"'block-wrapper-' + block.id\"\n        :editor=\"editorInstance\">\n        <component\n          :is=\"block.type\"\n          :id=\"block.id\"\n          :inputConfig=\"block.config\"\n          :inputContent=\"block.content\"\n          :key=\"'block-' + block.id\"\n          :ref=\"'block-' + block.id\"\n          :editor=\"editorInstance\" />\n      </block-wrapper>\n      <div\n        class=\"editor-inner-trigger\"\n        @click=\"addNewParagraphAtEnd\"></div>\n    </div>\n\n    <block-advanced-config />\n    <block-link-popup />\n    <blocks-list\n      :content=\"simplifiedContent\" />\n  </div>\n</template>\n\n<script>\n// core elements\nimport Vue from 'vue';\nimport Block from './Block.vue';\nimport BlockAdvancedConfig from './BlockAdvancedConfig.vue';\nimport BlockLinkPopup from './BlockLinkPopup.vue';\nimport BlocksList from './BlocksList.vue';\nimport BlockWrapper from './BlockWrapper.vue';\nimport ContentEditableImprovements from './helpers/ContentEditableImprovements.vue';\nimport { compileToFunctions } from 'vue-template-compiler';\nimport Icon from './elements/EditorIcon.vue';\n// default blocks\nimport PubliiCode from './default-blocks/publii-code/block.vue';\n// import PubliiEmbed from './default-blocks/publii-embed/block.vue';\nimport PubliiHeader from './default-blocks/publii-header/block.vue';\nimport PubliiHtml from './default-blocks/publii-html/block.vue';\nimport PubliiImage from './default-blocks/publii-image/block.vue';\nimport PubliiGallery from './default-blocks/publii-gallery/block.vue';\nimport PubliiList from './default-blocks/publii-list/block.vue';\nimport PubliiParagraph from './default-blocks/publii-paragraph/block.vue';\nimport PubliiReadmore from './default-blocks/publii-readmore/block.vue';\nimport PubliiSeparator from './default-blocks/publii-separator/block.vue';\nimport PubliiToc from './default-blocks/publii-toc/block.vue';\nimport PubliiQuote from './default-blocks/publii-quote/block.vue';\n// extensions\nimport ConversionHelpers from './extensions/ConversionHelpers.js';\nimport ShortcutManager from './extensions/ShortcutManager.js';\nimport UndoManager from './extensions/UndoManager.js';\n\nexport default {\n  name: 'BlockEditor',\n  components: {\n    'block-advanced-config': BlockAdvancedConfig,\n    'block-link-popup': BlockLinkPopup,\n    'block-wrapper': BlockWrapper,\n    'blocks-list': BlocksList,\n    'icon': Icon,\n    'publii-code': PubliiCode,\n    // 'publii-embed': PubliiEmbed,\n    'publii-header': PubliiHeader,\n    'publii-html': PubliiHtml,\n    'publii-image': PubliiImage,\n    'publii-gallery': PubliiGallery,\n    'publii-list': PubliiList,\n    'publii-paragraph': PubliiParagraph,\n    'publii-readmore': PubliiReadmore,\n    'publii-separator': PubliiSeparator,\n    'publii-toc': PubliiToc,\n    'publii-quote': PubliiQuote\n  },\n  computed: {\n    hasReadMore () {\n      return this.content.filter(block => block.type === 'publii-readmore').length > 0;\n    },\n    simplifiedContent () {\n      return this.content.map((block, index) => ({ \n        id: block.id, \n        type: block.type,\n        isFirstAndEmpty: index === 0 && !block.content\n      }));\n    }\n  },\n  data () {\n    return {\n      editorInstance: this,\n      uiSelectorID: false,\n      config: {\n        postID: ''\n      },\n      currentSiteData: null,\n      state: {\n        selectedBlockID: false\n      },\n      internal: {\n        lastScroll: 0,\n        firstBlockID: 0,\n        lastBlockID: 0,\n        currentBlockID: 0,\n        firstChangeDone: false,\n        editorIsLoaded: false\n      },\n      extensions: {\n        conversionHelpers: new ConversionHelpers(),\n        shortcutManager: new ShortcutManager(),\n        undoManager: new UndoManager()\n      },\n      content: [\n        {\n          id: crypto.randomUUID(),\n          type: 'publii-paragraph',\n          content: '',\n          config: {}\n        }\n      ]\n    };\n  },\n  watch: {\n    content: {\n      handler (newState) {\n        this.$bus.$emit('block-editor-content-updated');\n\n        if (!this.internal.firstChangeDone && this.internal.editorIsLoaded) {\n          this.internal.firstChangeDone = true;\n          this.$bus.$emit('block-editor-content-updated');\n        }\n\n        if (this.content.length) {\n          let lastIndex = this.content.length - 1;\n          this.internal.firstBlockID = this.content[0].id;\n          this.internal.lastBlockID = this.content[lastIndex].id;\n        }\n      },\n      deep: true\n    }\n  },\n  mounted () {\n    this.$bus.$on('block-editor-move-block-up', this.moveBlockUp);\n    this.$bus.$on('block-editor-move-block-down', this.moveBlockDown);\n    this.$bus.$on('block-editor-save-block', this.saveBlock);\n    this.$bus.$on('block-editor-delete-block', this.deleteBlock);\n    this.$bus.$on('block-editor-duplicate-block', this.duplicateBlock);\n    this.$bus.$on('block-editor-add-block', this.addNewBlock);\n    this.$bus.$on('block-editor-merge-paragraphs', this.mergeParagraphs);\n    this.$bus.$on('block-editor-shortcut-manager-add-shortcut', this.extensions.shortcutManager.add);\n    this.$bus.$on('block-editor-ui-opened-for-block', this.uiOpenedForBlock);\n    this.$bus.$on('block-editor-ui-closed-for-block', this.uiClosedForBlock);\n    this.$bus.$on('block-editor-convert-block', this.convertBlock);\n    this.$bus.$on('publii-block-editor-save', this.saveAllBlocks);\n    this.$bus.$on('publii-block-editor-load', this.loadAllBlocks);\n    this.$bus.$on('publii-block-editor-update-current-block-id', this.updateCurrentBlockID);\n    this.$bus.$on('undomanager-save-history', this.saveChangesHistory);\n    this.$bus.$on('block-editor-ui-selector-opened', this.setUISelectorID);\n    this.$bus.$on('block-editor-set-selected-block', this.setSelectedBlock);\n    this.$bus.$on('block-editor-items-reorder', this.reorderBlocks);\n\n    setTimeout(() => {\n      this.internal.editorIsLoaded = true;\n    }, 1500);\n  },\n  methods: {\n    moveBlockUp (blockID, startBlockTop) {\n      let blockIndex = this.content.findIndex(el => el.id === blockID);\n\n      if (blockIndex > 0) {\n        let tempBlock = JSON.parse(JSON.stringify(this.content[blockIndex]));\n        Vue.set(this.content, blockIndex, this.content[blockIndex - 1]);\n        Vue.set(this.content, blockIndex - 1, tempBlock);\n      }\n\n      this.scrollWindow(blockID, startBlockTop);\n    },\n    moveBlockDown (blockID, startBlockTop) {\n      let blockIndex = this.content.findIndex(el => el.id === blockID);\n\n      if (blockIndex < this.content.length - 1) {\n        let tempBlock = JSON.parse(JSON.stringify(this.content[blockIndex]));\n        Vue.set(this.content, blockIndex, this.content[blockIndex + 1]);\n        Vue.set(this.content, blockIndex + 1, tempBlock);\n      }\n\n      this.scrollWindow(blockID, startBlockTop);\n    },\n    scrollWindow (blockID, startBlockTop) {\n      if (+new Date() < this.internal.lastScroll + 100) {\n        return;\n      }\n\n      this.internal.lastScroll = +new Date();\n\n      this.$nextTick(() => {\n        let endBlockTop = this.$refs['block-wrapper-' + blockID][0].$refs['block-wrapper'].getBoundingClientRect().top;\n        window.scrollBy(0, endBlockTop - startBlockTop);\n      });\n    },\n    saveBlock (blockData) {\n      let blockIndex = this.content.findIndex(el => el.id === blockData.id);\n\n      if (blockIndex > -1) {\n        this.content[blockIndex].content = blockData.content;\n        this.content[blockIndex].config = blockData.config;\n      }\n    },\n    deleteBlock (blockID, addFocusToPreviousBlock = true) {\n      let blockIndex = this.content.findIndex(el => el.id === blockID);\n      this.content.splice(blockIndex, 1);\n      this.state.selectedBlockID = false;\n\n      if (blockIndex > 0 && addFocusToPreviousBlock) {\n        this.$refs['block-' + this.content[blockIndex - 1].id][0].focus();\n      }\n\n      if (!this.content.length) {\n        this.addNewBlock('publii-paragraph', false);\n      }\n\n      this.$refs['editor-main'].setAttribute('data-ui-opened-block', '');\n    },\n    duplicateBlock (blockID) {\n      let blockIndex = this.content.findIndex(el => el.id === blockID);\n      let blockCopy = JSON.parse(JSON.stringify(this.content[blockIndex]));\n      blockCopy.id = crypto.randomUUID() ;\n      this.content.splice(blockIndex, 0, blockCopy);\n    },\n    addNewBlock (blockType, afterBlockID = false, content = '') {\n      let blockIndex = this.content.findIndex(el => el.id === afterBlockID);\n      let blockConfig = {};\n      this.$bus.$emit('block-editor-deselect-blocks');\n\n      if (!afterBlockID) {\n        blockIndex = -1;\n      }\n\n      if (blockType.indexOf('publii-header-') === 0) {\n        let headerSize = parseInt(blockType.replace('publii-header-', ''), 10);\n        blockConfig.headingLevel = headerSize;\n        blockType = 'publii-header';\n      }\n\n      let newBlockID = crypto.randomUUID();\n      let newBlockObject = {\n        id: newBlockID,\n        type: blockType,\n        content: content,\n        config: blockConfig\n      };\n      this.content.splice(blockIndex + 1, 0, newBlockObject);\n\n      setTimeout(() => {\n        if (blockType === 'publii-readmore' || blockType === 'publii-separator') {\n          let newlyAddedBlockIndex = this.content.findIndex(el => el.id === newBlockID);\n\n          if (newlyAddedBlockIndex === this.content.length - 1) {\n            this.addNewParagraphAtEnd();\n          } else {\n            let nextIndex = newlyAddedBlockIndex + 1;\n            let nextBlockID = this.content[nextIndex].id;\n            this.$refs['block-' + nextBlockID][0].focus('end');\n            this.$bus.$emit('block-editor-block-selected', nextBlockID);\n          }\n        } else {\n          if (content === '') {\n            this.$refs['block-' + newBlockID][0].focus('end');\n          } else {\n            this.$refs['block-' + newBlockID][0].focus('start');\n          }\n        }\n      }, 50);\n    },\n    addNewParagraphAtEnd () {\n      let lastContentBlockIndex = this.content.length - 1;\n      let lastContentBlock = this.content[lastContentBlockIndex];\n\n      if (!lastContentBlock) {\n        this.addNewBlock('publii-paragraph', false);\n        return;\n      }\n\n      if (lastContentBlock.type === 'publii-paragraph' && lastContentBlock.content === '') {\n        let blockID = lastContentBlock.id;\n        this.$refs['block-' + blockID][0].focus();\n        return;\n      }\n\n      this.addNewBlock('publii-paragraph', lastContentBlock.id);\n    },\n    mergeParagraphs (blockIDToMerge) {\n      let blockIndex = this.content.findIndex(el => el.id === blockIDToMerge);\n\n      if (blockIndex === 0) {\n        return;\n      }\n\n      let previousBlockType = this.content[blockIndex - 1].type;\n      let previousBlockID = this.content[blockIndex - 1].id;\n\n      if (previousBlockType === 'publii-paragraph') {\n        this.content[blockIndex - 1].content = this.content[blockIndex - 1].content + '<span class=\"temp-paragraph-merge-caret\"></span>' + this.content[blockIndex].content;\n        this.content.splice(blockIndex, 1);\n\n        setTimeout(() => {\n          this.$refs['block-' + previousBlockID][0].content = this.content[blockIndex - 1].content;\n          this.$refs['block-' + previousBlockID][0].refresh();\n\n          setTimeout(() => {\n            let range = document.createRange();\n            let caret = this.$refs['block-' + previousBlockID][0].$refs['block'].querySelector('.temp-paragraph-merge-caret');\n            this.$refs['block-' + previousBlockID][0].focus('none');\n            let sel = document.getSelection();\n\n            setTimeout(() => {\n              range.selectNodeContents(caret);\n              range.deleteContents();\n              sel.removeAllRanges();\n              sel.addRange(range);\n              sel.baseNode.parentNode.removeChild(sel.baseNode);\n            }, 0);\n          }, 0);\n        }, 0);\n      } else {\n        this.$refs['block-' + previousBlockID][0].focus('end');\n      }\n    },\n    uiClosedForBlock (blockID) {\n      this.$refs['editor-main'].setAttribute('data-ui-opened-block', '');\n    },\n    uiOpenedForBlock (blockID) {\n      this.$refs['editor-main'].setAttribute('data-ui-opened-block', blockID);\n    },\n    loadAllBlocks () {\n      let inputField = document.querySelector('#post-editor');\n\n      if (inputField.value !== '') {\n        let loadedContent = JSON.parse(inputField.value);\n\n        if (loadedContent[0] && loadedContent[0].id && typeof loadedContent[0].id !== 'string') {\n            loadedContent = loadedContent.map(block => {\n                block.id = crypto.randomUUID();\n                return block;\n            });\n        }\n\n        Vue.set(this, 'content', loadedContent);\n      }\n    },\n    saveAllBlocks () {\n      let inputField = document.querySelector('#post-editor');\n\n      for (let block of this.content) {\n        this.$refs['block-' + block.id][0].save();\n      }\n\n      inputField.value = JSON.stringify(this.content);\n    },\n    setPostID (postID) {\n      this.config.postID = postID;\n    },\n    convertBlock (id, blockType, data) {\n      let blockToConvertIndex = this.content.findIndex(block => block.id === id);\n\n      Vue.set(this.content, blockToConvertIndex, {\n        id: id,\n        type: blockType,\n        content: data.content,\n        config: JSON.parse(JSON.stringify(data.config))\n      });\n    },\n    updateCurrentBlockID (blockID) {\n      if (this.internal.currentBlockID !== blockID) {\n        let id = this.internal.currentBlockID;\n\n        if (this.$refs['block-' + id] && this.$refs['block-' + id][0]) {\n          this.$refs['block-' + id][0].save();\n        }\n\n        this.internal.currentBlockID = blockID;\n      }\n    },\n    undo () {\n      let blockID = this.internal.currentBlockID;\n\n      if (!blockID) {\n        return;\n      }\n\n      let content = this.extensions.undoManager.undoHistory(blockID);\n\n      if (content) {\n        this.$refs['block-' + blockID][0].setContent(content);\n      }\n    },\n    saveChangesHistory (blockID, content) {\n      this.extensions.undoManager.saveHistory(blockID, content);\n    },\n    setUISelectorID (id) {\n      this.uiSelectorID = id;\n    },\n    setSelectedBlock (id) {\n      if (this.state.selectedBlockID !== false && this.state.selectedBlockID !== id) {\n        this.$bus.$emit('block-editor-deselect-block', this.state.selectedBlockID);\n      }\n\n      setTimeout(() => {\n        this.state.selectedBlockID = id;\n      }, 0);\n    },\n    reorderBlocks (newOrdering) {\n      let newContentOrder = [];\n\n      for (let id of newOrdering) {\n        let block = this.content.find(block => block.id === id);\n\n        if (block) {\n          newContentOrder.push(block);\n        }\n      }\n\n      Vue.set(this, 'content', newContentOrder);\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-move-block-up', this.moveBlockUp);\n    this.$bus.$off('block-editor-move-block-down', this.moveBlockDown);\n    this.$bus.$off('block-editor-save-block', this.saveBlock);\n    this.$bus.$off('block-editor-delete-block', this.deleteBlock);\n    this.$bus.$off('block-editor-duplicate-block', this.duplicateBlock);\n    this.$bus.$off('block-editor-add-block', this.addNewBlock);\n    this.$bus.$off('block-editor-merge-paragraphs', this.mergeParagraphs);\n    this.$bus.$off('block-editor-shortcut-manager-add-shortcut', this.extensions.shortcutManager.add);\n    this.$bus.$off('block-editor-ui-opened-for-block', this.uiOpenedForBlock);\n    this.$bus.$off('block-editor-ui-closed-for-block', this.uiClosedForBlock);\n    this.$bus.$off('block-editor-convert-block', this.convertBlock);\n    this.$bus.$off('publii-block-editor-save', this.saveAllBlocks);\n    this.$bus.$off('publii-block-editor-load', this.loadAllBlocks);\n    this.$bus.$off('publii-block-editor-update-current-block-id', this.updateCurrentBlockID);\n    this.$bus.$off('undomanager-save-history', this.saveChangesHistory);\n    this.$bus.$off('block-editor-set-selected-block', this.setSelectedBlock);\n    this.$bus.$off('block-editor-items-reorder', this.reorderBlocks);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../scss/variables.scss';\n@import '../assets/typography.scss';\n@import '../assets/prism-theme.scss';\n\n.editor {\n  min-height: 100%;\n  padding: 0 0 50px 0;\n  position: relative;\n  width: 100%;\n\n  &-inner {\n    margin: 0 auto;\n\n    &-trigger {\n      height: 100%;\n      left: 50%;\n      min-height: 100px;\n      position: relative;\n      top: 0;\n      transform: translateX(-50%);\n      width: var(--editor-width);\n      z-index: 0;\n    }\n  }\n\n  // UI animations\n  .block-editor-ui-fade-enter-active {\n    transition: opacity .2s ease;\n    transition-delay: .3s;\n  }\n  .block-editor-ui-fade-leave-active {\n    transition: opacity .2s ease;\n  }\n  .block-editor-ui-fade-enter,\n  .block-editor-ui-fade-leave-to {\n    opacity: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/BlockLinkPopup.vue",
    "content": "<template>\n  <div\n    @click.prevent.stop=\"hide()\"\n    :class=\"{ 'block-link-popup-overlay': true, 'is-visible': isVisible }\">\n    <div\n      @click.prevent.stop\n      class=\"block-link-popup\">\n      <div class=\"block-link-popup-link-type\">\n        <div\n          :class=\"{ 'block-link-popup-link-type-item': true, 'is-active': linkType === 'external' }\"\n          @click=\"setLinkType('external')\">\n          {{ $t('editor.custom') }}\n        </div>\n        <div\n          :class=\"{ 'block-link-popup-link-type-item': true, 'is-active': linkType === 'post' }\"\n          @click=\"setLinkType('post')\">\n          {{ $t('post.post') }}\n        </div>\n        <div\n          :class=\"{ 'block-link-popup-link-type-item': true, 'is-active': linkType === 'page' }\"\n          @click=\"setLinkType('page')\">\n          {{ $t('page.page') }}\n        </div>\n        <div\n          :class=\"{ 'block-link-popup-link-type-item': true, 'is-active': linkType === 'tag' }\"\n          @click=\"setLinkType('tag')\">\n          {{ $t('tag.tag') }}\n        </div>\n        <div\n          :class=\"{ 'block-link-popup-link-type-item': true, 'is-active': linkType === 'author' }\"\n          @click=\"setLinkType('author')\">\n          {{ $t('author.author') }}\n        </div>\n        <div\n          :class=\"{ 'block-link-popup-link-type-item': true, 'is-active': linkType === 'file' }\"\n          @click=\"setLinkType('file')\">\n          {{ $t('file.file') }}\n        </div>\n      </div>\n\n      <vue-select\n        v-if=\"linkType === 'post'\"\n        slot=\"field\"\n        ref=\"postPagesSelect\"\n        :options=\"postPages\"\n        v-model=\"linkSelectedPost\"\n        :custom-label=\"customPostLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        :placeholder=\"$t('post.selectPostPage')\"></vue-select>\n\n      <vue-select\n        v-if=\"linkType === 'page'\"\n        slot=\"field\"\n        ref=\"pagesSelect\"\n        :options=\"pages\"\n        v-model=\"linkSelectedPage\"\n        :custom-label=\"customPageLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        :placeholder=\"$t('page.selectPage')\"></vue-select>\n\n      <vue-select\n        v-if=\"linkType === 'tag'\"\n        slot=\"field\"\n        ref=\"tagPagesSelect\"\n        :options=\"tagPages\"\n        v-model=\"linkSelectedTag\"\n        :custom-label=\"customTagLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        :placeholder=\"$t('tag.selectTagPage')\"></vue-select>\n\n      <vue-select\n        v-if=\"linkType === 'author'\"\n        slot=\"field\"\n        ref=\"authorPagesSelect\"\n        :options=\"authorPages\"\n        v-model=\"linkSelectedAuthor\"\n        :custom-label=\"customAuthorsLabels\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        :placeholder=\"$t('author.selectAuthorPage')\"></vue-select>\n\n      <vue-select\n        v-if=\"linkType === 'file'\"\n        slot=\"field\"\n        ref=\"fileSelect\"\n        :options=\"filesList\"\n        v-model=\"linkSelectedFile\"\n        :close-on-select=\"true\"\n        :show-labels=\"false\"\n        :placeholder=\"$t('file.selectFileFromFileManager')\"></vue-select>\n\n      <input\n        v-if=\"linkType === 'external'\"\n        type=\"text\"\n        class=\"block-link-popup-link-external-input\"\n        v-model=\"link.url\"\n        :spellcheck=\"false\"\n        placeholder=\"https://example.com\"\n        @keyup.enter=\"save()\" />\n\n      <field \n        :label=\"$t('link.linkTitleAttribute')\">\n          <input\n            slot=\"field\"\n            type=\"text\"\n            class=\"block-link-popup-link-title\"\n            v-model=\"link.title\"\n            :spellcheck=\"false\" />\n      </field>\n\n      <field \n        :label=\"$t('link.linkClassAttribute')\">\n          <input\n            slot=\"field\"\n            type=\"text\"\n            class=\"block-link-popup-link-css-class\"\n            v-model=\"link.cssClass\"\n            :spellcheck=\"false\" />\n      </field>\n\n      <field \n        :label=\"$t('link.openInNewWindow')\">\n        <switcher \n          slot=\"field\"\n          v-model=\"link.targetBlank\" /> \n      </field>\n\n      <field\n        v-if=\"linkType === 'file'\" \n        :label=\"$t('link.downloadAttribute')\">\n        <switcher \n          slot=\"field\" \n          v-model=\"link.download\" /> \n      </field>\n\n      <field \n        :label=\"$t('link.linkRelAttribute')\">\n        <switcher \n           slot=\"field\" \n           label=\"nofollow\"\n           v-model=\"link.noFollow\" /> \n           \n        <switcher \n           slot=\"field\" \n           label=\"sponsored\"\n           v-model=\"link.sponsored\"/> \n\n        <switcher \n           slot=\"field\" \n           label=\"ugc\"\n           v-model=\"link.ugc\" /> \n      </field>\n\n      <div class=\"block-link-popup-buttons\">\n        <button @click.stop=\"save()\">\n          {{ $t('ui.save') }}\n        </button>\n        <button @click.stop=\"hide()\" class=\"outline\">\n          {{ $t('ui.cancel') }}\n        </button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Switcher from './elements/Switcher.vue';\nimport LinkHelpers from './mixins/LinkHelpers.vue';\nimport vSelect from 'vue-multiselect/dist/vue-multiselect.min.js';\n\nexport default {\n  name: 'block-link-popup',\n  mixins: [\n    LinkHelpers\n  ],\n  components: {\n    'switcher': Switcher,\n    'vue-select': vSelect\n  },\n  data () {\n    return {\n      isVisible: false,\n      currentBlockID: '',\n      linkType: 'external',\n      linkSelectedAuthor: '',\n      linkSelectedPost: '',\n      linkSelectedPage: '',\n      linkSelectedTag: '',\n      linkSelectedFile: '',\n      link: {\n        url: '',\n        title: '',\n        cssClass: '',\n        noFollow: false,\n        targetBlank: false,\n        sponsored: false,\n        ugc: false,\n        download: false\n      }\n    };\n  },\n  watch: {\n    linkType (newValue) {\n      if (newValue !== 'file') {\n        this.link.download = false;\n      }\n    }\n  },\n  mounted () {\n    this.$bus.$on('block-editor-show-link-popup', this.show);\n  },\n  methods: {\n    show (blockID, link) {\n      this.isVisible = true;\n      this.currentBlockID = blockID;\n      this.linkType = 'external';\n      this.linkSelectedAuthor = '';\n      this.linkSelectedPost = '';\n      this.linkSelectedPage = '';\n      this.linkSelectedTag = '';\n      this.linkSelectedFile = '';\n      this.link = {\n        url: '',\n        title: '',\n        cssClass: '',\n        noFollow: false,\n        targetBlank: false,\n        sponsored: false,\n        ugc: false,\n        download: false\n      };\n      link.cssClass = link.cssClass.replace('is-highlighted', '');\n      this.link = Object.assign(this.link, JSON.parse(JSON.stringify(link)));\n      this.parseLink();\n    },\n    hide () {\n      this.isVisible = false;\n      this.$bus.$emit('block-editor-hide-link-popup');\n\n      setTimeout(() => {\n        this.currentBlockID = '';\n      }, 500);\n    },\n    parseLink () {\n      if (this.link.url === '') {\n        this.linkType = 'external';\n        return;\n      }\n\n      if (this.link.url.indexOf('#INTERNAL_LINK#') > -1) {\n        if (this.link.url.indexOf('post') > -1) {\n          this.linkType = 'post';\n          this.linkSelectedPost = parseInt(this.link.url.split('/').pop(), 10);\n        } else if (this.link.url.indexOf('page') > -1) {\n          this.linkType = 'page';\n          this.linkSelectedPage = parseInt(this.link.url.split('/').pop(), 10);\n        } else if (this.link.url.indexOf('tag') > -1) {\n          this.linkType = 'tag';\n          this.linkSelectedTag = parseInt(this.link.url.split('/').pop(), 10);\n        } else if (this.link.url.indexOf('author') > -1) {\n          this.linkType = 'author';\n          this.linkSelectedAuthor = parseInt(this.link.url.split('/').pop(), 10);\n        } else if (this.link.url.indexOf('file') > -1) {\n          this.linkType = 'file';\n          this.linkSelectedFile = this.link.url.replace('#INTERNAL_LINK#/file/', '');\n        }\n      } else {\n        this.linkType = 'external';\n      }\n    },\n    setLinkType (type) {\n      this.linkType = type;\n\n      if (type === 'external' && this.link.url.indexOf('#INTERNAL_LINK#') === 0) {\n        this.link.url = '';\n      }\n    },\n    prepareLink () {\n      if (this.linkType === 'post') {\n        if (this.linkSelectedPost) {\n          return '#INTERNAL_LINK#/post/' + this.linkSelectedPost;\n        } else {\n          return '';\n        }\n      } else if (this.linkType === 'page') {\n        if (this.linkSelectedPage) {\n          return '#INTERNAL_LINK#/page/' + this.linkSelectedPage;\n        } else {\n          return '';\n        }\n      } else if (this.linkType === 'author') {\n        if (this.linkSelectedAuthor) {\n          return '#INTERNAL_LINK#/author/' + this.linkSelectedAuthor;\n        } else {\n          return '';\n        }\n      } else if (this.linkType === 'tag') {\n        if (this.linkSelectedTag) {\n          return '#INTERNAL_LINK#/tag/' + this.linkSelectedTag;\n        } else {\n          return '';\n        }\n      } else if (this.linkType === 'file') {\n        if (this.linkSelectedFile) {\n          return '#INTERNAL_LINK#/file/' + this.linkSelectedFile;\n        } else {\n          return '';\n        }\n      }\n\n      if (this.link.url[0] && this.link.url[0] !== '#') {\n        if (\n          this.link.url.substr(0, 8) !== 'https://' &&\n          this.link.url.substr(0, 7) !== 'http://' &&\n          this.link.url.substr(0, 6) !== 'dat://' &&\n          this.link.url.substr(0, 6) !== 'ftp://' &&\n          this.link.url.substr(0, 3) !== '://' &&\n          this.link.url.substr(0, 2) !== '//' &&\n          this.link.url.substr(0, 7) !== 'mailto:' &&\n          this.link.url.substr(0, 4) !== 'tel:'\n        ) {\n          if (this.link.url.indexOf('@') > -1) {\n            this.link.url = 'mailto:' + this.link.url;\n          } else {\n            this.link.url = 'https://' + this.link.url;\n          }\n        }\n      }\n\n      return this.link.url;\n    },\n    save () {\n      this.link.url = this.prepareLink();\n      this.$bus.$emit('block-editor-save-link-popup', this.currentBlockID, this.link);\n      this.hide();\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-show-link-popup', this.show);\n  }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../../../scss/variables.scss';\n\n.block-link-popup {\n  background: var(--popup-bg);\n  border-radius: 6px;\n  box-shadow: 0 0 32px var(--shadow);\n  padding: 4rem;\n  transform: scale(.5);\n  transition: all .24s cubic-bezier(0, 0, 0.25, 0.99);\n  user-select: none;\n  width: 720px;\n\n  &-overlay {\n    align-items: center;\n    background: var(--overlay);\n    display: flex;\n    height: 100%;\n    justify-content: center;\n    left: 0;\n    opacity: 0;\n    pointer-events: none;\n    position: fixed;\n    top: 0;\n    transition: all .3s ease-out;\n    width: 100%;\n    z-index: 999991;\n\n    &.is-visible {\n      opacity: 1;\n      pointer-events: auto;\n\n      .block-link-popup {\n        transform: scale(1);\n      }\n    }\n  }\n\n  &-link {\n    width: 100%;\n\n    &-type {\n      display: flex;\n      justify-content: space-between;\n      margin: -20px 0 3rem;\n\n      &-item {\n        border-bottom: 2px solid var(--gray-6);\n        color: var(--label-color);\n        cursor: pointer;\n        font-size: $app-font-base;\n        font-weight: var(--font-weight-semibold);\n        padding: 12px 12px 20px;\n        text-align: center;\n        transition: all .24s ease-out;\n        user-select: none;\n        width: 25%;\n\n        &:hover {\n           border-bottom-color: var(--color-primary);\n        }\n\n        &.is-active {\n          border-bottom-color: var(--color-primary);\n          font-weight: var(--font-weight-bold);\n          color: var(--color-primary);\n        }\n      }\n    }\n\n    &-external-input {\n      background: var(--input-bg);\n      border: 1px solid var(--input-border-color);\n      color: var(--text-primary-color);\n      display: block;\n      font-size: $app-font-base;\n      margin: 32px 0 24px;\n      padding: 14px;\n      width: 100%;\n\n      &::placeholder {\n        color: var(--text-light-color);\n      }\n    }\n\n    &-switcher {\n      align-items: center;\n      color: var(--label-color);\n      display: flex;\n      margin-bottom: 12px;\n\n      .switcher {\n        top: 0;\n\n          & + .switcher {\n              margin-left: 24px;\n          }\n      }\n\n      &:last-of-type {\n        margin-bottom: 12px;\n      }\n    }\n  }\n\n  .multiselect {\n    margin: 32px 0 24px;\n\n    &__tags {\n      padding-bottom: 0;\n      padding-top: 0;\n    }\n  }\n\n  &-buttons {\n    display: flex;\n    margin: 3rem -4rem -4rem;\n\n    button {\n      background: var(--button-bg);\n      border: none;\n      box-shadow: none;\n      border-bottom-left-radius: 6px;\n      border-top: 1px solid var(--button-bg);\n      color: var(--white);\n      cursor: pointer;\n      font-size: 15px;\n      font-weight: var(--font-weight-semibold);\n      line-height: 1;\n      width: 50%;\n      padding: 18px;\n      transition: all .25s ease-out;\n\n      &:hover {\n        background: var(--button-bg-hover);\n        border-color: var(--button-bg-hover);\n      }\n\n      &.outline {\n        background: var(--popup-btn-cancel-bg);\n        border: none;\n        border-top: 1px solid var(--input-border-color);\n        border-bottom-right-radius: 6px;\n        color: var(--popup-btn-cancel-color);\n\n        &:hover {\n           background: var(--popup-btn-cancel-bg-hover);\n           color: var(--popup-btn-cancel-hover-color);\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/BlockWrapper.vue",
    "content": "<template>\n  <div\n    :data-block-type=\"blockType\"\n    ref=\"block-wrapper\"\n    :data-id=\"id\"\n    :class=\"{\n      'wrapper': true,\n      'is-selected': isSelected,\n      'is-activated': isActivated,\n      'has-ui-opened': uiOpened,\n      'has-ui-block-selector-opened': newBlockUIListVisible,\n      [customCssClasses.join(' ')]: true,\n      'is-hovered': isHovered && !uiOpened && canDisplayUI\n    }\"\n    @mousemove=\"moveOverBlock\"\n    @mouseleave=\"leaveBlock\"\n    @click.stop=\"blockClick\">\n    <div\n      :class=\"{ \n        'block-selector': true, \n        'is-visible': (isHovered || newBlockUIListVisible) && !uiOpened && canDisplayUI\n      }\">\n      <button\n        class=\"block-selector-add\"\n        @click.stop=\"toggleNewBlockUI()\"\n        tabindex=\"-1\">\n        <icon name=\"add\"/>\n      </button>\n\n      <div\n        v-if=\"newBlockUIListVisible\"\n        @click.stop\n        :class=\"{ 'block-selector-list': true, 'is-visible': true }\">\n        <text-input \n            v-model=\"blockFilterPhrase\" \n            :placeholder=\"$t('editor.searchForABlock')\" \n            ref=\"block-search-input\"\n            icon=\"magnifier-small\"/>\n        <div class=\"block-selector-list-wrapper\">\n            <button\n                v-for=\"(blockItem, index) of filteredBlocks\"\n                :key=\"'filtered-blocks-' + index\"\n                :class=\"{ \n                    'block-selector-list-item': true, \n                    'is-active': newBlockUIActiveIndex === index \n                }\"\n                @click.stop=\"addNewBlock(blockItem.blockName);\">\n                <span class=\"block-selector-list-item-icon\">\n                    <icon :name=\"blockItem.icon\" />\n                </span>\n                <span class=\"block-selector-list-item-label\">\n                    {{ $t(blockItem.label) }}\n                </span>\n            </button>\n            <span \n                v-if=\"!filteredBlocks.length\"\n                class=\"block-selector-list-empty-state\">\n                {{ $t('editor.nothingFound') }}\n            </span>\n        </div>\n      </div>\n    </div>\n\n    <slot />\n\n    <div class=\"wrapper-ui\">\n      <div \n        :class=\"{ 'wrapper-ui-show-options': true }\"\n        @click.stop=\"togglePopup\">\n        <button\n          :class=\"{ 'wrapper-ui-show-options-button': true, 'is-visible': isHovered && !uiOpened && canDisplayUI }\">\n          <icon name=\"menu-dots\" />\n        </button>\n\n        <div\n          v-if=\"uiOpened\"\n          :class=\"{ 'wrapper-ui-options': true, 'is-visible': true }\">\n          <button\n            class=\"wrapper-ui-options-button-move\"\n            tabindex=\"-1\"\n            :disabled=\"$parent.internal.firstBlockID === id\"\n            @click.stop=\"moveUp\">\n            <icon name=\"up\" />\n          </button>\n          <button\n            class=\"wrapper-ui-options-button-move\"\n            tabindex=\"-1\"\n            :disabled=\"$parent.internal.lastBlockID === id\"\n            @click.stop=\"moveDown\">\n            <icon name=\"down\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport AvailableBlocks from '../available-blocks.json';\nimport Icon from '../components/elements/EditorIcon.vue';\n\nexport default {\n  name: 'BlockWrapper',\n  props: [\n    'id',\n    'blockType',\n    'editor'\n  ],\n  components: {\n    'icon': Icon\n  },\n  computed: {\n    availableBlocks () {\n        return AvailableBlocks;\n    },\n    canDisplayUI () {\n        return !this.editor.uiSelectorID || this.editor.uiSelectorID === this.id;\n    },\n    filteredBlocks () {\n      let blocks = JSON.parse(JSON.stringify(this.availableBlocks));\n\n      blocks = blocks.map(block => {\n        block.labelText = this.$t(block.label).toLocaleLowerCase();\n        return block;\n      });\n\n      if (this.editor.hasReadMore) {\n        blocks = blocks.filter(block => block.blockName !== 'publii-readmore');\n      }\n\n      if (this.blockType === 'publii-paragraph' && this.blockContentIsEmpty) {\n        blocks = blocks.filter(block => block.blockName !== 'publii-paragraph');\n      }\n\n      if (this.blockFilterPhrase.length) {\n        let phrase = this.blockFilterPhrase.toLocaleLowerCase();\n        blocks = blocks.filter(block => block.blockName.replace('publii-', '').indexOf(phrase) > -1 || block.labelText.indexOf(phrase) > -1);\n      }\n\n      return blocks;\n    }\n  },\n  data () {\n    return {\n      customCssClasses: [],\n      isHovered: false,\n      isSelected: false,\n      isActivated: false,\n      uiOpened: false,\n      moveTimeout: false,\n      // new block UI\n      blockFilterPhrase: '',\n      newBlockUIActiveIndex: 0,\n      newBlockUIListVisible: false,\n      blockContentIsEmpty: false\n    };\n  },\n  watch: {\n    uiOpened (newState, oldState) {\n      if (newState) {\n        this.$bus.$emit('block-editor-ui-opened-for-block', this.id);\n      } else {\n        this.$bus.$emit('block-editor-ui-closed-for-block', this.id);\n      }\n    },\n    newBlockUIListVisible (newState) {\n        if (newState === true) {\n            this.$bus.$emit('block-editor-deselect-other-blocks', this.id);\n\n            setTimeout(() => {\n                this.blockFilterPhrase = '';\n                this.$bus.$emit('block-editor-ui-selector-opened', this.id);\n            }, 100);\n        } else if (newState === false) {\n            this.$bus.$emit('block-editor-ui-selector-opened', false);\n        }\n    }\n  },\n  mounted () {\n    this.$bus.$on('block-editor-deselect-blocks', this.deselectBlock);\n    this.$bus.$on('block-editor-deselect-block', this.deselectSingleBlock);\n    this.$bus.$on('block-editor-deselect-other-blocks', this.deselectOtherBlocks);\n    this.$bus.$on('block-editor-close-new-block-ui', this.hideNewBlockUI);\n    this.$bus.$on('block-editor-list-activate-item', this.activateItem);\n    this.$bus.$on('block-editor-list-deactivate-item', this.deactivateItem);\n  },\n  methods: {\n    blockClick (e) {\n      this.$bus.$emit('block-editor-deselect-blocks', this.id);\n      this.setSelectionState(true);\n\n      if (e && e.detail === 2 && e.layerX <= 30 && this.$slots.default[0] && !this.$slots.default[0].componentInstance.isEmpty) {\n        this.togglePopup();\n      }\n    },\n    deselectBlock (blockID) {\n        this.newBlockUIListVisible = false;\n\n        if (blockID !== this.id) {\n            this.setSelectionState(false);\n        }\n    },\n    deselectSingleBlock (blockID) {\n        this.newBlockUIListVisible = false;\n\n        if (blockID === this.id) {\n            this.setSelectionState(false);\n        }\n    },\n    deselectOtherBlocks (blockID) {\n        if (blockID !== this.id) {\n            this.newBlockUIListVisible = false;\n            this.setSelectionState(false);\n        }\n    },\n    moveOverBlock () {\n      clearTimeout(this.moveTimeout);\n      this.isHovered = true;\n\n      this.moveTimeout = setTimeout(() => {\n        this.isHovered = false;\n      }, 2000);\n    },\n    leaveBlock () {\n      this.isHovered = false;\n    },\n    openPopup () {\n      this.uiOpened = true;\n      this.$bus.$emit('block-editor-clear-text-selection', this.id);\n      this.$bus.$emit('block-editor-set-selected-block', this.id);\n    },\n    closePopup () {\n      this.uiOpened = false;\n      this.$bus.$emit('block-editor-set-selected-block', false);\n    },\n    togglePopup () {\n      this.uiOpened = !this.uiOpened;\n\n      if (this.uiOpened) {\n        this.$bus.$emit('block-editor-clear-text-selection', this.id);\n        this.$bus.$emit('block-editor-set-selected-block', this.id);\n\n        if (this.blockType === 'publii-paragraph') {\n          this.$bus.$emit('block-editor-close-new-block-ui', this.id);\n        }\n      } else {\n        this.$bus.$emit('block-editor-set-selected-block', false);\n      }\n    },\n    setSelectionState (newState) {\n      if (this.isSelected && newState === false) {\n        this.$bus.$emit('block-editor-trigger-block-save', this.id);\n      }\n\n      this.isSelected = newState;\n\n      if (!this.isSelected) {\n        this.uiOpened = false;\n      }\n\n      if (newState) {\n        this.$bus.$emit('block-editor-block-selected', this.id);\n      }\n    },\n    addCustomCssClass (cssClass) {\n      if (this.customCssClasses.indexOf(cssClass) === -1) {\n        this.customCssClasses.push(cssClass);\n      }\n    },\n    removeCustomCssClass (cssClass) {\n      if (this.customCssClasses.indexOf(cssClass) > -1) {\n        this.customCssClasses = this.customCssClasses.filter(item => item !== cssClass);\n      }\n    },\n    moveUp () {\n      let startBlockTop = this.$refs['block-wrapper'].getBoundingClientRect().top;\n      this.$bus.$emit('block-editor-move-block-up', this.id, startBlockTop);\n\n      setTimeout(() => {\n        this.$refs['block-wrapper'].scrollIntoView({\n            behavior: 'smooth',\n            block: 'center',\n            inline: 'center'\n        });\n      }, 0);\n    },\n    moveDown () {\n      let startBlockTop = this.$refs['block-wrapper'].getBoundingClientRect().top;\n      this.$bus.$emit('block-editor-move-block-down', this.id, startBlockTop);\n\n      setTimeout(() => {\n        this.$refs['block-wrapper'].scrollIntoView({\n            behavior: 'smooth',\n            block: 'center',\n            inline: 'center'\n        });\n      }, 0);\n    },\n    saveChangesHistory () {\n        this.$slots.default[0].componentInstance.saveChangesHistory();\n    },\n    /**\n     * New block UI methods\n     */\n    toggleNewBlockUI () {\n      this.blockContentIsEmpty = this.checkIfBlockContentIsEmpty();\n      this.newBlockUIListVisible = !this.newBlockUIListVisible;\n\n      setTimeout(() => {\n        if (this.$refs['block-search-input']) {\n            this.$refs['block-search-input'].$el.querySelector('input').focus();\n        }\n      }, 100);\n    },\n    addNewBlock (blockType) {\n      this.$bus.$emit('block-editor-add-block', blockType, this.id);\n\n      if (this.blockContentIsEmpty) {\n        this.$bus.$emit('block-editor-delete-block', this.id, false);\n      }\n\n      this.newBlockUIListVisible = false;\n      this.$bus.$emit('block-editor-ui-selector-opened', false);\n    },\n    checkIfBlockContentIsEmpty () {\n\t    if (['publii-paragraph', 'publii-header', 'publii-code', 'publii-list'].indexOf(this.blockType) === -1) {\n\t      return false;\n\t    }\n\n      let isEmpty = false;\n\n      this.$slots.default.forEach(vNode => { \n        if (vNode.componentInstance.$refs['block'].innerHTML === '') {\n          isEmpty = true;\n        }\n      });\n\n      return isEmpty;\n    },\n    hideNewBlockUI () {\n      this.newBlockUIListVisible = false;\n    },\n    activateItem (id) {\n      if (id === this.id) {\n        this.isActivated = true;\n\n        setTimeout(() => {\n            this.isActivated = false;\n        }, 1200);\n      }\n    },\n    deactivateItem () {\n      this.isActivated = false;\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-deselect-blocks', this.deselectBlock);\n    this.$bus.$off('block-editor-deselect-block', this.deselectSingleBlock);\n    this.$bus.$off('block-editor-deselect-other-blocks', this.deselectOtherBlocks);\n    this.$bus.$off('block-editor-close-new-block-ui', this.hideNewBlockUI);\n    this.$bus.$off('block-editor-list-activate-item', this.activateItem);\n    this.$bus.$off('block-editor-list-deactivate-item', this.deactivateItem);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import \"../vendors/modularscale\";\n@import \"../../../scss/variables.scss\";\n@import \"../../../scss/mixins.scss\";\n\n.wrapper {\n  border: 1px solid transparent;\n  box-sizing: content-box;\n  margin: 0 auto;\n  opacity: 0.33;\n  padding: 0 68px;\n  position: relative;\n  transition: width .25s ease-out, opacity .35s ease-out, background .25s ease-out;\n  width: var(--editor-width);\n  z-index: 1;\n\n  &.has-ui-block-selector-opened {\n    z-index: 2;\n  }\n\n  &[data-block-type=\"publii-embed\"] {\n    margin: 0 auto;\n  }\n\n  &.is-hovered {\n    .wrapper-ui {\n      opacity: 1;\n      pointer-events: auto;\n    }\n  }\n\n  &.is-selected {\n    z-index: 10;\n  }\n\n  &.is-activated {\n    background: rgba(var(--color-primary-rgb), .12);\n    border-radius: calc(var(--border-radius) / 2);\n    z-index: 10;\n  }\n\n  & > div {\n    padding: baseline(4, em) 0;\n  }\n\n  &.has-ui-opened {\n    background: var(--popup-bg);\n    border-radius: var(--border-radius);\n    box-shadow: 0 0 32px var(--shadow);\n    margin-top: -44px;\n    opacity: 1;\n    padding: 0 32px;\n    user-select: none;\n    z-index: 10;\n\n    & > div {\n      display: flex;\n      flex-direction: column-reverse;\n      left: -38px;\n      opacity: 1;\n      top: 44px;\n\n      .publii-block-code-lang {\n        top: calc(#{baseline(6, em)} + 44px) !important;\n      }\n      .publii-block-code > .prism-editor__line-numbers,\n      .publii-block-html > .prism-editor__line-numbers {\n        background: var(--pre-bg-hover) !important;\n      }\n      .publii-block-code > pre,\n      .publii-block-html > pre {\n        background: var(--pre-bg-hover) !important;\n      }\n      .publii-block-gallery-uploader-loader-overlay {\n        height: 250px;\n        top: 61px;\n        width: calc(100% - 64px) !important;\n      }\n    }\n\n    .publii-block-paragraph-block-selector {\n      display: none;\n    }\n  }\n\n  &.has-block-selector-visible {\n    z-index: 10;\n  }\n\n  &.contains-wide-image {\n    width: calc(var(--editor-width) + 168px) !important;\n  }\n\n  &.contains-full-image {\n    width: calc(100% - 168px) !important;\n\n    .publii-block-image-form input {\n      margin-left: auto;\n      margin-right: auto;\n      max-width: calc(var(--editor-width) + 84px);\n    }\n  }\n\n  &-ui {\n    left: 32px;\n    opacity: 0;\n    position: absolute;\n    pointer-events: none;\n    top: -2px;\n    z-index: 1;\n\n    .wrapper-ui-show-options {\n      transition: all 0.25s ease-out;\n     \n      &-button {\n        background: none;\n        border: none;\n        cursor: pointer; \n        font-size: 19px;\n        height: 32px;\n        opacity: 0;\n        padding: 4px;\n        transition: transform 0.25s ease-out; \n        width: 32px;\n\n         // hover effect\n        &::before {\n          content: \"\";\n          background: var(--gray-6);\n          border-radius: 3px;\n          display: block;\n          left: 50%;\n          opacity: 0;\n          position: absolute;\n          height: 32px;\n          top: 50%;\n          transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);\n          transform: scale(0.5) translate(-50%, -50%);\n          transform-origin: left top;\n          width: 32px;\n          z-index: -1;\n        }\n\n        &:hover {\n          &::before {\n            opacity: 1;\n            transform: scale(1) translate(-50%, -50%);\n          }\n        }\n\n        &:focus {\n          outline: none;\n        }\n\n        & > svg {\n          color: var(--icon-tertiary-color);\n        }\n\n        &.is-visible {\n          opacity: 1;\n        }\n      }\n    }\n\n    &-options {\n      left: 0;\n      position: absolute;\n      top: 0;\n\n      &.is-visible {\n        opacity: 1;\n        pointer-events: auto;\n      }\n\n      &-row {\n        display: flex;\n      }\n\n      &-button-move {\n        align-items: center;\n        background: transparent;\n        border: none;\n        cursor: pointer;\n        display: flex;\n        height: 32px;\n        justify-content: center;\n        margin: 0;\n        outline: none;\n        padding: 0;\n        position: absolute;\n        top: 0;\n        width: 32px;\n        z-index: 0;\n\n        svg {\n          color: var(--icon-primary-color);\n          transition: var(--transition);\n        }\n\n        // hover effect\n        &::before {\n          content: \"\";\n          background: var(--gray-6);\n          border-radius: 3px;\n          display: block;\n          left: 50%;\n          opacity: 0;\n          position: absolute;\n          height: 32px;\n          top: 50%;\n          transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);\n          transform: scale(0.5) translate(-50%, -50%);\n          transform-origin: left top;\n          width: 32px;\n          z-index: -1;\n        }\n\n        &:hover,\n        &.is-active {\n          svg {\n            color: var(--icon-tertiary-color);\n          }\n\n          &::before {\n            opacity: 1;\n            transform: scale(1) translate(-50%, -50%);\n          }\n        }\n        &:disabled {\n          cursor: default;\n          opacity: 0.4;\n\n          &::before {\n            background: none;\n          }\n        }\n      }\n\n      &-button-move + .wrapper-ui-options-button-move {\n        top: 32px;\n      }\n    }\n\n    &-top-menu {\n      align-items: center;\n      border: none;\n      display: flex;\n      height: 44px;\n      justify-content: flex-end;\n      margin: -9px 0 9px 0;\n\n      &-title {\n        color: var(--gray-3);\n        display: block;\n        font-size: 11px;\n        font-weight: 700;\n        margin: 0 auto 0 0;\n        text-transform: uppercase;\n      }\n\n      &-button {\n        align-items: center;\n        background: transparent;\n        border: none;\n        cursor: pointer;\n        display: flex;\n        height: 100%;\n        min-height: 34px;\n        justify-content: center;\n        margin: 0;\n        outline: none;\n        padding: 0;\n        position: relative;\n        width: 38px;\n\n        svg {\n          color: var(--icon-tertiary-color);\n        }\n\n        // hover effect\n        &::before {\n          content: \"\";\n          background: var(--gray-6);\n          border-radius: 3px;\n          display: block;\n          left: 50%;\n          opacity: 0;\n          position: absolute;\n          height: 34px;\n          top: 50%;\n          transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);\n          transform: scale(0.5) translate(-50%, -50%);\n          transform-origin: left top;\n          width: 34px;\n          z-index: -1;\n        }\n\n        &:hover,\n        &.is-active {\n          &::before {\n            opacity: 1;\n            transform: scale(1) translate(-50%, -50%);\n          }\n        }\n      }\n    }\n}\n}\n\n.editor[data-ui-opened-block=\"\"] {\n  .wrapper {\n    opacity: 1;\n  }\n}\n\n.block-selector {\n  display: none !important;\n  left: 0;\n  position: absolute;\n  top: 0;\n  z-index: 10;\n\n  &.is-visible {\n    display: block !important;\n  }\n\n  &-list {\n    background: var(--popup-bg);\n    box-shadow: var(--box-shadow-medium);\n    border-radius: var(--border-radius);\n    font-family: var(--font-base);\n    font-size: 14px;\n    padding: 8px 8px 0;\n    position: absolute;\n    top: 60px;\n    width: 212px;\n    z-index: 100;\n    \n    input {\n        font-size: 14px;\n        padding: 10px 38px 10px 12px !important;\n    }\n    .input-wrapper svg {\n        height: 16px !important;\n        left: auto;\n        right: 12px;\n        width: 16px !important;\n    }\n\n    &-empty-state {\n      display: block;\n      padding: 4px 12px 4px;\n    }\n\n    &-wrapper {\n        margin: 4px 0;\n        max-height: 214px;\n        overflow: auto; \n    }\n\n    &-item {\n        align-items: center;\n        background: none;\n        border: none;\n        border-radius: var(--border-radius);\n        color: var(--text-primary-color);\n        cursor: pointer;\n        display: flex;   \n        font-weight: var(--font-weight-semibold);\n        margin: 5px 2px;\n        padding: 0;\n        text-align: left;\n        transition: var(--transition);\n        width: 98%;\n\n      &-icon {\n          align-items: center;\n          background-color: var(--gray-1);\n          border-radius: var(--border-radius);\n          display: inline-flex;\n          color: var(--icon-primary-color);\n          height: 36px;\n          justify-content: center;\n          margin-right: 12px;\n          transition: var(--transition);\n          width: 36px;\n      }\n\n      &:hover {\n          background-color: var(--gray-1);\n          color: var(--headings-color);\n\n        .block-selector-list-item-icon {\n            background: var(--button-secondary-bg);\n            color: var(--icon-tertiary-color);\n        }\n      }\n\n      &:focus-visible {\n          box-shadow: inset 0 0 2px 1px var(--input-border-focus);\n\n        .block-selector-list-item-icon {\n            background: none;\n        }\n      }\n    }\n  }\n\n  &-add {\n      align-items: center;\n      background: transparent;\n      border: none;\n      cursor: pointer;\n      display: flex;\n      height: 32px;\n      justify-content: center;\n      outline: none;\n      width: 32px;\n\n       // hover effect\n       &::before {\n          content: \"\";\n          background: var(--gray-6);\n          border-radius: 3px;\n          display: block;\n          left: 50%;\n          opacity: 0;\n          position: absolute;\n          height: 32px;\n          top: 50%;\n          transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);\n          transform: scale(0.5) translate(-50%, -50%);\n          transform-origin: left top;\n          width: 32px;\n          z-index: -1;\n        }\n\n      & > svg {\n        color: var(--icon-tertiary-color);\n        transition: all .25s ease-out;\n      }\n\n      &:hover {\n        &::before {\n            opacity: 1;\n            transform: scale(1) translate(-50%, -50%);\n          }\n       }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/BlocksList.vue",
    "content": "<template>\n  <aside \n    :class=\"{ 'blocks-list': true, 'is-opened': isOpened }\"\n    @click=\"deactivateItem()\">\n    <button \n      v-if=\"!isOpened\"\n      class=\"clean-invert icon small blocks-list-open\"\n      @click.prevent=\"openList\">\n      <icon name=\"open-bulk-edition\" /> \n      {{ $t('editor.viewBulkEdit') }}\n    </button>\n\n    <button \n      v-if=\"isOpened\"\n      class=\"clean-invert icon small blocks-list-close\"\n      @click.prevent=\"closeList\">\n      <icon name=\"open-bulk-edition\" /> \n      {{ $t('editor.hideBulkEdit') }}\n    </button>\n    \n    <draggable\n      tag=\"ol\"\n      group=\"block-editor-items\"\n      chosenClass=\"is-chosen\"\n      ghostClass=\"is-ghost\"\n      :class=\"{ 'blocks-list-items': true }\"\n      v-model=\"preparedContent\"\n      v-bind=\"{\n          animation: 0,\n          forceFallback: true\n      }\">\n      <li \n        v-for=\"(item, index) of preparedContent\"\n        :class=\"{\n          'blocks-list-item': true, \n          'is-active': activeItem === item.id\n        }\"\n        :key=\"'blocks-list-item-' + item.id + '-' + index\"\n        @click.stop=\"activateItem(item.id)\">\n        <div>\n          <span class=\"blocks-list-item-icon\">\n            <icon :name=\"item.icon\" />\n          </span>\n          <span>{{ item.label }}</span>\n\n          <button\n            :class=\"{ \n              'blocks-list-item-bulk-duplicate': true, \n              'is-disabled': item.icon === 'readmore'\n            }\"\n            tabindex=\"-1\"\n            :disabled=\"item.icon === 'readmore'\"\n            @click.stop=\"duplicateBlock(item.id)\">\n            <icon   \n              name=\"duplicate\" \n              customWidth=\"16\" \n              customHeight=\"16\"/>\n          </button>\n\n          <button\n            :class=\"{ \n              'blocks-list-item-bulk-delete': true,\n              'is-active': confirmDelete === item.id,\n              'has-tooltip': confirmDelete === item.id,\n              'is-disabled': index === 0 && firstBlockDeleteIsDisabled\n            }\"\n            tabindex=\"-1\"\n            :disabled=\"index === 0 && firstBlockDeleteIsDisabled\"\n            @click.stop=\"deleteBlock(item.id)\">\n            <icon \n              :name=\"confirmDelete === item.id ? 'open-trash' : 'trash'\" \n              customWidth=\"16\" \n              customHeight=\"16\"/>\n            <span \n              v-if=\"confirmDelete === item.id\"\n              class=\"ui-tooltip has-bigger-space\">\n              {{ $t('editor.clickToConfirm') }}\n            </span>\n          </button>\n        </div>\n      </li>\n    </draggable>\n  </aside>\n</template>\n\n<script>\nimport AvailableBlocks from '../available-blocks.json';\nimport Draggable from 'vuedraggable';\nimport Icon from '../components/elements/EditorIcon.vue';\n\nexport default {\n  name: 'BlocksList',\n  props: [\n    'content'\n  ],\n  components: {\n    'draggable': Draggable,\n    'icon': Icon\n  },\n  computed: {\n    availableBlocks () {\n      let blocks = {};\n\n      for (let i = 0; i < AvailableBlocks.length; i++) {\n        let keyValue = AvailableBlocks[i].blockName;\n\n        blocks[keyValue] = {\n          icon: AvailableBlocks[i].icon,\n          label: this.$t(AvailableBlocks[i].label)\n        };\n      }\n\n      return blocks;\n    },\n    firstBlockDeleteIsDisabled () {\n        if (\n            this.content && \n            this.content[0] && \n            this.content[0].type === 'publii-paragraph' && \n            this.content[0].isFirstAndEmpty && \n            this.content.length === 1\n        ) {\n            return true;\n        }\n\n        return false;\n    },\n    preparedContent: {\n      get () {\n        return this.content.map(item => ({\n          id: item.id,\n          icon: this.availableBlocks[item.type].icon,\n          label: this.availableBlocks[item.type].label\n        }));\n      },\n      set (newValue) {\n        let reorderedIDs = newValue.map(item => item.id);\n        this.$bus.$emit('block-editor-items-reorder', reorderedIDs);\n      }\n    }\n  },\n  data () {\n    return {\n      activeItem: false,\n      confirmDelete: false,\n      isOpened: false\n    };\n  },\n  mounted () {\n    this.$bus.$on('block-editor-block-selected', this.activateItemWithoutEffect);\n  },\n  methods: {\n    openList () {\n      this.isOpened = true;\n    },\n    closeList () {\n      this.isOpened = false;\n    },\n    activateItem (id) {\n      this.activeItem = id;\n      this.$bus.$emit('block-editor-list-deactivate-item');\n\n      setTimeout(() => {\n        this.$bus.$emit('block-editor-list-activate-item', id);\n\n        document.querySelector('.post-editor-form div[data-id=\"' + id + '\"]').scrollIntoView({\n            behavior: 'smooth',\n            block: 'center',\n            inline: 'center'\n        });\n      }, 0);\n    },\n    activateItemWithoutEffect (id) {\n      this.activeItem = id;\n    },\n    deactivateItem () {\n      this.activeItem = false;\n      this.confirmDelete = false;\n      this.$bus.$emit('block-editor-list-deactivate-item');\n    },\n    /*\n    moveDown (id) {\n      let startBlockTop = document.querySelector('.post-editor-form div[data-id=\"' + id + '\"]').getBoundingClientRect().top;\n      this.$bus.$emit('block-editor-move-block-down', id, startBlockTop);\n\n      setTimeout(() => {\n        document.querySelector('.post-editor-form div[data-id=\"' + id + '\"]').scrollIntoView({\n            behavior: 'smooth',\n            block: 'center',\n            inline: 'center'\n        });\n      }, 0);\n    },\n    */\n    deleteBlock (id) {\n      if (this.confirmDelete !== id) {\n        this.confirmDelete = id;\n        return;\n      }\n\n      this.$bus.$emit('block-editor-delete-block', id);\n      this.confirmDelete = false;\n\n      if (this.activeItem === id) {\n        this.deactivateItem();\n      }\n    },\n    duplicateBlock (id) {\n      this.$bus.$emit('block-editor-duplicate-block', id);\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-block-selected', this.activateItemWithoutEffect);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n\n.blocks-list {\n  bottom: .4rem;\n  height: 4.6rem;\n  left: 1.8rem;\n  position: fixed;\n  z-index: 1;\n\n  &.is-opened {\n    background: var(--option-sidebar-bg);\n    bottom: 0;\n    border-right: 1px solid var(--input-border-color);\n    height: calc(100vh - var(--topbar-height));\n    left: 0;\n    overflow: auto;\n    position: fixed;\n    top: var(--topbar-height);\n    width: 300px;\n    z-index: 1000000;\n\n    &::before {\n      background: linear-gradient(to top, var(--option-sidebar-bg) 0%,var(--option-sidebar-bg) 75%,transparent 100%);\n      content: \"\";\n      bottom: 0;\n      left: -1px;\n      height: 6rem;\n      position: fixed;\n      width: inherit;\n      z-index: 1;\n    }\n  }\n\n  &-open,\n  &-close {\n    align-items: center;\n    background: transparent;\n    border: none;\n    border-radius: 0 var(--border-radius) 0 0;\n    color: var(--link-primary-color-hover);\n    cursor: pointer;\n    display: flex;\n    font-size: 1.4rem;\n    font-weight: var(--font-weight-semibold);\n    height: 4.6rem;\n    line-height: 4.5rem;\n    padding: 0 1.3rem 0 3.8rem;\n    transition: var(--transition);\n    user-select: none;\n    white-space: nowrap;\n    z-index: 2;\n\n    & > svg {\n      left: 0.9rem;\n      position: absolute;\n      top: 50%;\n      transform: translateY(-50%);\n    }\n\n    &:hover {\n      color: var(--link-primary-color);\n    }\n  }\n\n  &-close {\n    bottom: .4rem;\n    left: 1.8rem;\n    position: fixed;\n    text-align: center; \n  }\n\n  &-items {\n    list-style-type: none;\n    margin: 2rem 2rem 6rem;\n    padding: 0;\n  }\n\n  &-item {\n    border-radius: calc(var(--border-radius) / 2);\n    cursor: pointer;\n    font-size: 14px;\n    font-weight: var(--font-weight-semibold);\n    margin: 0;\n    padding: 3px 0;\n\n    &.is-active {\n      position: relative;\n      z-index: 1;\n\n      & > div {\n        background-color: var(--button-secondary-bg);\n        box-shadow: 0 0 0 1px var(--color-primary);\n      }\n    }\n\n    &.is-ghost {\n      cursor: move;\n     \n      & > div {\n        background-color: transparent !important;\n        border: 1px dashed var(--input-border-focus);  \n        box-shadow: none; \n        \n        * {\n          opacity: 0;\n        }\n      }\n    }\n\n    & > div {\n      align-items: center;\n      background-color: var(--gray-1);\n      display: grid;\n      border-radius: var(--border-radius);\n      grid-template-columns: auto 1fr auto auto 6px;\n      transition: var(--transition);\n\n      &:hover {\n        background-color: var(--button-secondary-bg);\n        color: var(--headings-color);\n\n        .blocks-list-item-icon {\n            color: var(--icon-tertiary-color);\n        }\n      }\n    }\n\n    &-icon {\n      align-items: center;\n      border-radius: var(--border-radius);\n      display: inline-flex;\n      color: var(--icon-primary-color);\n      height: 38px;\n      justify-content: center;\n      margin-right: 1px;\n      transition: var(--transition);\n      width: 38px;\n    }\n\n    &-bulk-delete, \n    &-bulk-duplicate {\n      background: transparent;\n      border: none;\n      border-radius: 50%;\n      cursor: pointer;\n      display: inline-block;\n      height: 2.8rem;\n      padding: 0;\n      text-align: center;\n      transition: all .3s ease-out;\n      width: 2.3rem;\n\n      &:active,\n      &:focus,\n      &:hover {\n        color: var(--headings-color)\n      }\n\n      &:hover {\n        & > svg {\n            color: currentColor;\n            transform: scale(1);\n        }\n      }\n\n      svg {\n        color: var(--icon-secondary-color);\n        pointer-events: none;\n        transform: scale(.9);\n        transition: var(--transition);\n        vertical-align: middle;\n      }\n\n      &.is-disabled {\n        opacity: .25;\n        pointer-events: none;\n      }\n    }\n    &-bulk-delete {\n      &:hover {\n        & > svg {\n          color: var(--warning);\n        }\n      }\n      &.has-tooltip:hover .ui-tooltip { \n        transform: scale(1) translateX(-72%); \n\n        &::after {\n          left: 68%;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-code/block.vue",
    "content": "<template>\n  <div :class=\"{ 'is-empty': isEmpty }\">\n    <prism-editor\n      :class=\"{ 'publii-block-code': true }\"\n      ref=\"block\"\n      @focus=\"updateCurrentBlockID\"\n      @keyup=\"handleKeyboard($event); debouncedSave()\"\n      :code=\"content\"\n      :emitEvents=\"true\"\n      v-model=\"content\"\n      :lineNumbers=\"true\"\n      :language=\"config.language\">\n    </prism-editor>\n\n    <top-menu\n      ref=\"top-menu\"\n      :conversions=\"conversions\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport AvailableConversions from './conversions.js';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nexport default {\n  name: 'Code',\n  mixins: [\n    Block,\n    ContentEditableImprovements\n  ],\n  components: {\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      config: {\n        language: this.getLastSelectedLanguage(),\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: '',\n      conversions: AvailableConversions,\n      lastSelectedLanguage: null,\n      topMenuConfig: [\n        {\n          type: 'select',\n          label: this.$t('langs.language'),\n          configKey: 'language',\n          clearable: true,\n          searchable: true,\n          options: []\n        }\n      ]\n    };\n  },\n  watch: {\n    'config.language': function (newValue) {\n      localStorage.setItem('block-editor-last-selected-language', newValue);\n    }\n  },\n  computed: {\n    availableLanguages () {\n      return [\n        'apacheconf',\n        'aspnet',\n        'bash',\n        'basic',\n        'batch',\n        'bbcode',\n        'c',\n        'cpp',\n        'cfscript',\n        'csharp',\n        'clike',\n        'css',\n        'dart',\n        'docker',\n        'elixir',\n        'elm',\n        'gdscript',\n        'git',\n        'glsl',\n        'go',\n        'graphql',\n        'haml',\n        'handlebars',\n        'haskell',\n        'html',\n        'http',\n        'ini',\n        'java',\n        'javascript',\n        'json',\n        'jsonp',\n        'jsx',\n        'kotlin',\n        'latex',\n        'less',\n        'lisp',\n        'lua',\n        'makefile',\n        'markdown',\n        'matlab',\n        'nasm',\n        'nginx',\n        'objectivec',\n        'pascal',\n        'perl',\n        'php',\n        'powershell',\n        'pug',\n        'python',\n        'r',\n        'regex',\n        'ruby',\n        'rust',\n        'sass',\n        'scss',\n        'scala',\n        'sql',\n        'swift',\n        'twig',\n        'typescript',\n        'vbnet',\n        'visual-basic',\n        'yaml',\n        'xml'\n      ]\n    }\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  mounted () {\n    this.content = this.inputContent;\n    this.topMenuConfig[0].options = this.availableLanguages;\n  },\n  methods: {\n    focus () {\n      this.$refs['block'].$el.querySelector('pre').focus();\n    },\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === true) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Tab' && e.shiftKey === false) {\n        e.preventDefault();\n        // eslint-disable-next-line\n        document.execCommand('insertHTML', false, \"  \");\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['block'].code === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n    },\n    save () {\n      this.content = this.$refs['block'].code;\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    },\n    getLastSelectedLanguage () {\n      let value = localStorage.getItem('block-editor-last-selected-language');\n\n      if (value !== null) {\n        value = value.replace(/[^a-z0-9-]/gmi, '');\n      } else {\n        value = 'html';\n      }\n\n      return value;\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/vendor/modularscale';\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n\n.publii-block-code {\n    border-radius: var(--border-radius);\n    background: var(--pre-bg);\n    border-radius: var(--border-radius);\n    box-shadow: 2px 4px 26px var(--shadow);\n    outline: none;\n    width: 100%;\n\n    &:empty {\n        &:before {\n            content: attr(data-translation);\n            color: var(--gray-4);\n        }\n    }\n\n    & > .prism-editor__line-numbers {\n        background: var(--pre-bg) !important;\n        display: block;\n    }\n\n    & > pre {\n        background: var(--pre-bg) !important;\n        display: block;\n\n        code {\n            background: transparent !important;\n            padding: 0 !important;\n        }\n    }\n\n    &-lang {\n        position: absolute;\n        right: 40px;\n        top: baseline(6,em);\n\n        .multiselect__content {\n            margin: 0 !important;\n            padding: 0 !important;\n        }\n\n        .multiselect__element {\n            padding: 0 !important;\n        }\n\n        .multiselect__option:after {\n            display: none;\n        }\n    }\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-code/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.code.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-code/conversions.js",
    "content": "const availableConversions = [\n  {\n    'icon': 'paragraph',\n    'name': 'editor.conversions.toParagraph',\n    'type': 'publii-paragraph',\n    'convert': function (config, content, editorInstance) {\n      let newContent = editorInstance.extensions.conversionHelpers.stripTags(content).replace(/\\n/gmi, '<br>');\n      let newConfig = {\n        textAlign: 'left',\n        advanced: {\n          style: '',\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'html',\n    'name': 'editor.conversions.toHTML',\n    'type': 'publii-html',\n    'convert': function (config, content, editorInstance, rawBlock) {\n      let newContent = content;\n      let newConfig = {\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  }\n];\n\nmodule.exports = availableConversions;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-code/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let languageToUse = blockData.config.language;\n\n  if (languageToUse === 'xml') {\n    languageToUse = 'markup';\n  }\n\n  let languageClass = ' language-' + languageToUse;\n  let cssClasses = ' class=\"line-numbers ' + blockData.config.advanced.cssClasses + languageClass + '\"';\n  let html = `<pre${id}${cssClasses}><code>${blockData.content.replace(/</gmi, '&lt;').replace(/>/gmi, '&gt;')}</code></pre>`;\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-embed/block.vue",
    "content": "<template>\n  <div :class=\"{ 'publii-block-embed-wrapper': true, 'is-empty': isEmpty }\">\n    <div\n      :class=\"{ 'publii-block-embed': true, 'is-visible': view === 'code' }\"\n      ref=\"block\">\n      <textarea\n        @keydown=\"handleKeyboard\"\n        @keyup=\"handleCaret($event); debouncedSave()\"\n        ref=\"code\"\n        :placeholder=\"$t('editor.enterUrlOrEmbedCode')\"\n        v-model=\"content\"></textarea>\n    </div>\n    <div\n      v-if=\"view === 'preview'\"\n      v-html=\"modifiedContent\"\n      :class=\"{ 'publii-block-embed-preview': true }\">\n    </div>\n\n    <top-menu\n      ref=\"top-menu\"\n      :config=\"[]\" />\n  </div>\n</template>\n\n<script>\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport EmbedHelper from './embed.js';\nimport HasPreview from './../../mixins/HasPreview.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nexport default {\n  name: 'Embed',\n  mixins: [\n    Block,\n    ContentEditableImprovements,\n    HasPreview\n  ],\n  components: {\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      focusable: ['code'],\n      config: {\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: ''\n    };\n  },\n  computed: {\n    modifiedContent () {\n      if (EmbedHelper.isEmbedable(this.content)) {\n        return EmbedHelper.embed(this.content, this.$bus, this.id);\n      }\n\n      return this.content;\n    }\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  mounted () {\n    this.content = this.inputContent;\n    this.view = this.content === '' ? 'code' : 'preview';\n  },\n  methods: {\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === false) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Tab' && e.shiftKey === false) {\n        e.preventDefault();\n        // eslint-disable-next-line\n        document.execCommand('insertHTML', false, \"  \");\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.content === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n    },\n    save () {\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" >\n@import '../../../../../scss/vendor/modularscale';\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n\n.publii-block-embed {\n    background: var(--gray-1);\n    border-radius: var(--border-radius);\n    color: var(--gray-2);\n    display: none;\n    font-size: ms(-1);\n    padding: baseline(6,em);\n    outline: none;\n    width: 100%;\n\n    textarea {\n        background: var(--white);\n        border: 1px solid var(--gray-2);\n        border-radius: var(--border-radius);\n        font-family: var(--font-monospace);\n        font-size: ms(-1);\n        min-height: 180px;\n        padding: 20px;\n        resize: vertical;\n        width: 100%;\n    }\n\n    &.is-visible {\n        display: block;\n    }\n\n    &-preview {\n        background: var(--gray-1);\n        margin: 0;\n        padding: 0 0 56.25%;\n        position: relative;\n\n        iframe {\n            height: 100%;\n            pointer-events: none;\n            position: absolute;\n            width: 100%;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-embed/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.embed.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-embed/embed.js",
    "content": "let embedHelper = {\n  isEmbedable (code) {\n    if (embedHelper.getService(code)) {\n      return true;\n    }\n\n    if (embedHelper.isUrl(code)) {\n      return true;\n    }\n\n    return false;\n  },\n  embed (code) {\n    let service = embedHelper.getService(code);\n    let iframeCode = embedHelper.createIframe(service, code);\n    return iframeCode;\n  },\n  isUrl (code) {\n    if (code.substr(0, 8) === 'https://' || code.substr(0, 7) === 'http://') {\n      return true;\n    }\n\n    return false;\n  },\n  getService (code) {\n    if (code.substr(0, 8) === 'https://' && code.indexOf('youtube.com') > -1) {\n      return 'youtube';\n    }\n\n    if (code.substr(0, 8) === 'https://' && code.indexOf('vimeo.com') > -1) {\n      return 'vimeo';\n    }\n\n    return false;\n  },\n  createIframe (service, code) {\n    switch (service) {\n      case 'youtube':\n        return embedHelper.youtubeIframe(code);\n      case 'vimeo':\n        return embedHelper.vimeoIframe(code);\n      default:\n        return embedHelper.rawIframe(code);\n    }\n  },\n  youtubeIframe (code) {\n    let url = code;\n    let queryParams = new URLSearchParams(url.split('?')[1]);\n\n    if (queryParams.has('v')) {\n      url = 'https://www.youtube.com/embed/' + queryParams.get('v') + '?feature=oembed';\n    }\n\n    return '<iframe src=\"' + url + '\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>';\n  },\n  vimeoIframe (code) {\n    let url = code.replace('https://vimeo.com/', '');\n    url = 'https://player.vimeo.com/video/' + url;\n\n    return '<iframe src=\"' + url + '\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe>';\n  },\n  rawIframe (code) {\n    return '<iframe width=\"100%\" height=\"500\" src=\"' + code + '\" frameborder=\"0\"></iframe>';\n  }\n};\n\nexport default embedHelper;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-embed/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = blockData.config.advanced.cssClasses ? ' class=\"' + blockData.config.advanced.cssClasses + '\"' : '';\n  let html = ``;\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-gallery/block.vue",
    "content": "<template>\n  <div\n    :class=\"{ 'publii-block-gallery-wrapper': true, 'is-empty': isEmpty }\"\n    @dragover.stop.prevent=\"dragOver\"\n    @dragleave.stop.prevent=\"dragLeave\"\n    @drop.stop.prevent=\"drop\"\n    @click=\"resetDeleteConfirmation\">\n    <draggable\n      v-if=\"content.images.length > 0 && view === 'preview'\"\n      ref=\"block\"\n      v-model=\"content.images\"\n      handle=\"img\"\n      :data-cols=\"config.columns\"\n      :data-count=\"content.images.length\"\n      @start=\"draggingInProgress = true\"\n      @end=\"draggingInProgress = false\"\n      :class=\"{ 'publii-block-gallery': true, 'is-wide': config.imageAlign === 'wide', 'is-full': config.imageAlign === 'full' }\">\n      <div\n        v-for=\"(image, index) of content.images\"\n        :key=\"'gallery-item-' + index\"\n        class=\"publii-block-gallery-item\">\n        <img\n          :src=\"image.src\"\n          :height=\"image.height\"\n          :width=\"image.width\" />\n\n        <button\n          v-if=\"confirmDelete !== index\"\n          class=\"publii-block-gallery-item-delete\"\n          @click.stop.prevent=\"removeImage(index)\">\n          <icon name=\"trash\" />\n        </button>\n\n        <button\n          v-if=\"confirmDelete === index\"\n          class=\"publii-block-gallery-item-delete is-active has-tooltip\"\n          @click.stop.prevent=\"removeImage(index)\">\n          <icon name=\"open-trash\" />\n          <span class=\"ui-tooltip has-bigger-space\">\n            {{ $t('editor.clickToConfirm') }}\n          </span>\n        </button>\n      </div>\n    </draggable>\n\n    <draggable\n      v-if=\"content.images.length > 0 && view === 'edit'\"\n      v-model=\"content.images\"\n      :data-cols=\"config.columns\"\n      :data-count=\"content.images.length\"\n      @start=\"draggingInProgress = true\"\n      @end=\"draggingInProgress = false\"\n      class=\"publii-block-gallery-list\">\n      <div\n        v-for=\"(image, index) of content.images\"\n        :key=\"'gallery-item-' + index\"\n        class=\"publii-block-gallery-list-item\">\n        <div class=\"publii-block-gallery-list-item-image\">\n          <img\n            :src=\"image.thumbnailSrc\"\n            :height=\"image.height\"\n            :width=\"image.width\" />\n        </div>\n\n        <div class=\"publii-block-gallery-list-item-config\">\n          <input type=\"text\" v-model=\"image.alt\" :placeholder=\"$t('editor.enterAltText')\"/>\n          <input type=\"text\" v-model=\"image.caption\" :placeholder=\"$t('editor.enterCaption')\"/>\n        </div>\n      </div>\n    </draggable>\n\n    <div\n      v-if=\"(content.images.length === 0 || view === 'edit')\"\n      :class=\"{ 'publii-block-gallery-form': true, 'is-visible': content.images.length === 0 }\"\n      ref=\"block\">\n      <div\n        :class=\"{ 'publii-block-gallery-uploader': true, 'is-hovered': isHovered }\">\n        <div class=\"publii-block-gallery-uploader-inner\">\n          <icon\n            v-if=\"!imageUploadInProgress\"\n            name=\"blank-gallery\"\n            height=\"62\"\n            width=\"75\" />\n          <span v-if=\"!imageUploadInProgress\">\n            {{ $t('editor.dropToUploadYourPhotosOr') }}\n          </span>\n          <button\n            v-if=\"!imageUploadInProgress\"\n            @click=\"filePickerCallback\">\n            {{ $t('editor.selectFiles') }}\n          </button>\n        </div>\n      </div>\n    </div>\n\n    <div\n      v-if=\"imageUploadInProgress\"\n      class=\"publii-block-gallery-uploader-loader-overlay\">\n      <span class=\"publii-block-gallery-uploader-loader\"></span>\n    </div>\n\n    <top-menu\n      ref=\"top-menu\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport Draggable from 'vuedraggable';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport EditorIcon from './../../elements/EditorIcon.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\nimport Utils from './../../utils/Utils.js';\n\nexport default {\n  name: 'PGallery',\n  mixins: [\n    Block,\n    ContentEditableImprovements\n  ],\n  components: {\n    'icon': EditorIcon,\n    'top-menu': TopMenuUI,\n    'draggable': Draggable\n  },\n  watch: {\n    '$parent.uiOpened': function (newValue) {\n      if (!this.content.images.length) {\n        return;\n      }\n\n      if (newValue) {\n        this.view = 'edit';\n      } else {\n        this.view = 'preview';\n      }\n    }\n  },\n  data () {\n    return {\n      confirmDelete: false,\n      draggingInProgress: false,\n      isHovered: false,\n      imageUploadInProgress: false,\n      imagesQueue: [],\n      imageUploader: null,\n      view: 'preview',\n      config: {\n        imageAlign: 'center',\n        columns: 3,\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: {\n        images: []\n      },\n      topMenuConfig: [\n        {\n          type: 'select',\n          label: this.$t('image.columns'),\n          configKey: 'columns',\n          clearable: false,\n          searchable: false,\n          cssClasses: 'is-narrow',\n          options: [1, 2, 3, 4, 5, 6, 7, 8]\n        },\n        {\n          activeState: function () { return this.config.imageAlign === 'center'; },\n          onClick: function () { this.alignImage('center'); },\n          icon: 'center',\n          tooltip: this.$t('image.centeredImage')\n        },\n        {\n          activeState: function () { return this.config.imageAlign === 'wide'; },\n          onClick: function () { this.alignImage('wide'); },\n          icon: 'wide',\n          tooltip: this.$t('image.wideImage')\n        },\n        {\n          activeState: function () { return this.config.imageAlign === 'full'; },\n          onClick: function () { this.alignImage('full'); },\n          icon: 'full',\n          tooltip: this.$t('image.fullWidthImage')\n        }\n      ]\n    };\n  },\n  computed: {\n    isInsidePublii () {\n      return !!window.process;\n    }\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  mounted () {\n    this.content = Utils.deepMerge(this.content, this.inputContent);\n    this.initFakeFilePicker();\n    this.setParentCssClasses(this.config.imageAlign);\n  },\n  methods: {\n    dragOver (e) {\n      this.isHovered = true;\n    },\n    dragLeave (e) {\n      this.isHovered = false;\n    },\n    async drop (e) {\n      let files = e.dataTransfer.files;\n\n      if (!files[0]) {\n        this.imageUploadInProgress = false;\n      } else {\n        this.imagesQueue = [];\n\n        for (let i = 0; i < files.length; i++) {\n            this.imagesQueue.push(await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(files[i])));\n        }\n\n        this.imageUploadInProgress = true;\n        this.$parent.$el.setAttribute('style', 'height: ' + this.$parent.$el.clientHeight + 'px; overflow: hidden;');\n        this.uploadImage();\n      }\n\n      this.isHovered = false;\n    },\n    initFakeFilePicker () {\n      this.imageUploader = document.getElementById('post-editor-fake-multiple-images-uploader');\n      this.imageUploader.addEventListener('change', () => {\n        if (!this.imageUploader.value) {\n          return;\n        }\n\n        setTimeout(async () => {\n          if (!this.fileSelectionCallback) {\n            return;\n          }\n\n          if (this.imageUploader.files) {\n            this.imagesQueue = [];\n\n            for (let i = 0; i < this.imageUploader.files.length; i++) {\n              this.imagesQueue.push(await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(this.imageUploader.files[i])));\n            }\n          } else {\n            return;\n          }\n\n          this.imageUploadInProgress = true;\n          this.$parent.$el.setAttribute('style', 'height: ' + this.$parent.$el.clientHeight + 'px; overflow: hidden;');\n          this.uploadImage();\n        }, 50);\n      });\n    },\n    uploadImage () {\n      let filePath = this.imagesQueue.shift();\n\n      // eslint-disable-next-line\n      mainProcessAPI.send('app-image-upload', {\n        id: this.editor.config.postID,\n        site: window.app.getSiteName(),\n        path: filePath,\n        imageType: 'galleryImages'\n      });\n\n      // eslint-disable-next-line\n      mainProcessAPI.receiveOnce('app-image-uploaded', (data) => {\n        let thumbnailSrc = data.thumbnailDimensions ? data.thumbnailPath[0] : data.thumbnailPath;\n        let sizeWidth = data.baseImage.size[0] || '';\n        let sizeHeight = data.baseImage.size[1] || '';\n\n        this.content.images.push({\n          src: data.baseImage.url,\n          thumbnailSrc: thumbnailSrc,\n          height: data.thumbnailDimensions ? data.thumbnailDimensions.height : sizeHeight,\n          width: data.thumbnailDimensions ? data.thumbnailDimensions.width : sizeWidth,\n          dimensions: data.baseImage.size.join('x'),\n          alt: '',\n          caption: ''\n        });\n\n        if (this.imagesQueue.length) {\n          this.uploadImage();\n        } else {\n          this.$parent.$el.removeAttribute('style');\n          this.fileSelectionCallback = false;\n          this.imageUploadInProgress = false;\n          this.imageUploader.value = '';\n          this.$parent.openPopup();\n        }\n      });\n    },\n    filePickerCallback () {\n      this.fileSelectionCallback = true;\n      document.getElementById('post-editor-fake-multiple-images-uploader').click();\n    },\n    alignImage (newValue) {\n      this.config.imageAlign = newValue;\n      this.setParentCssClasses(newValue);\n    },\n    setParentCssClasses (imageMode) {\n      if (imageMode === 'full') {\n        this.$parent.addCustomCssClass('contains-full-image');\n      } else {\n        this.$parent.removeCustomCssClass('contains-full-image');\n      }\n\n      if (imageMode === 'wide') {\n        this.$parent.addCustomCssClass('contains-wide-image');\n      } else {\n        this.$parent.removeCustomCssClass('contains-wide-image');\n      }\n    },\n    removeImage (index) {\n      if (this.confirmDelete !== index) {\n        this.confirmDelete = index;\n      } else {\n        this.content.images.splice(index, 1);\n      }\n    },\n    save () {\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: JSON.parse(JSON.stringify(this.content))\n      });\n    },\n    resetDeleteConfirmation () {\n      this.confirmDelete = false;\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-save-link-popup', this.setLink);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/vendor/modularscale';\n\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n\n.publii-block-gallery {\n  display: flex;\n  flex-wrap: wrap;\n  margin: -1rem;\n  outline: none;\n  position: relative;\n\n  &-empty-state {\n    color: var(--gray-3);\n    font-family: var(--font-base);\n    font-size: 14px;\n    text-align: center;\n  }\n\n  &-item {\n    cursor: move;\n    padding: 1rem;\n    position: relative;\n    width: calc(100% / 3);\n\n    & > img {\n      display: block;\n      height: 30vh;\n      max-width: 100%;\n      object-fit: cover;\n      width: 100%;\n    }\n\n    &-delete {\n      align-items: center;\n      background: var(--warning);\n      border: none;\n      border-radius: var(--border-radius);\n      cursor: pointer;\n      display: flex;\n      height: 34px;\n      justify-content: center;\n      left: 25px;\n      opacity: 0;\n      pointer-events: none;\n      position: absolute;\n      top: 25px;\n      transition: var(--transition);\n      width: 34px;\n      z-index: 2;\n\n      svg {\n        color: var(--white);\n      }\n\n      &:active,\n      &:focus,\n      &:hover {\n        transform: scale(1.1);\n      }\n    }\n\n    &:hover {\n      .publii-block-gallery-item-delete {\n        opacity: 1;\n        pointer-events: auto;\n      }\n    }\n  }\n\n  &-uploader {\n    border: 2px dashed var(--input-border-color);\n    border-radius: var(--border-radius);\n    height: 250px;\n    margin: 0 0 16px 0;\n    padding: 6px;\n    position: relative;\n    width: 100%;\n\n    &.is-hovered {\n      border-color: var(--color-primary);\n    }\n\n    &-loader {\n      animation: loader 1s linear infinite;\n      border: 3px solid var(--color-primary);\n      border-left-color: transparent;\n      border-radius: 50%;\n      display: block;\n      height: 32px;\n      left: 50%;\n      position: absolute;\n      top: 50%;\n      transform: translateX(-50%) translateY(-50%);\n      width: 32px!important;\n\n      &-overlay {\n        background: var(--bg-primary);\n        border: 2px dashed var(--input-border-color);\n        border-radius: var(--border-radius);\n        bottom: 0;\n        padding: 6px;\n        position: absolute;\n        left: auto;\n        right: auto;\n        top: 0;\n        width: var(--editor-width);\n        z-index: 1;\n\n        &::after {\n          content: \"\";\n          background: var(--gray-1);\n          display: block;\n          height: 100%;\n          width: 100%;\n        }\n      }\n    }\n\n   &-inner {\n      align-items: center;\n      display: flex;\n      flex-wrap: wrap;\n      font-family: var(--font-base);\n      justify-content: center;\n      height: 234px;\n      padding: 2rem;\n      width: 100%;\n\n      svg {\n        fill: var(--icon-quaternary-color);\n      }\n\n      span {\n        display: block;\n        font-size: $app-font-base;\n        text-align: center;\n        width: 100%;\n      }\n\n      button {\n        background: var(--button-secondary-bg);\n        border: 1px solid var(--button-secondary-bg);\n        border-radius: var(--border-radius);\n        color: var(--button-secondary-color);\n        cursor: pointer;\n        font-weight: var(--font-weight-semibold);\n        font-size: 15px;\n        padding: .5rem 2rem;\n        text-align: center;\n        outline: none;\n\n        &:active,\n        &:focus,\n        &:hover {\n          background: var(--button-secondary-bg-hover);\n          border-color: var(--button-secondary-bg-hover);\n          color: var(--button-secondary-color-hover);\n        }\n      }\n    }\n  }\n\n  &-list {\n    &-item {\n      align-items: center;\n      display: flex;\n      margin: 2rem 0 2.5rem;\n\n      &-image {\n        height: 112px;\n        margin: 0 20px 0 0;\n        overflow: hidden;\n        position: relative;\n        width: 120px;\n\n        img {\n          height: auto;\n          left: 50%;\n          width: 100%;\n          position: absolute;\n          top: 50%;\n          transform: translateX(-50%) translateY(-50%);\n        }\n      }\n\n      &-config {\n        width: calc(100% - 140px);\n\n        input {\n          display: block;\n          width: 100%;       \n\n          & + input {\n             margin-top: 16px;\n          }\n        }\n      }\n    }\n  }\n}\n\n@for $i from 1 through 8 {\n  .publii-block-gallery[data-cols=\"#{$i}\"] .publii-block-gallery-item {\n    flex-grow: 1;\n    width: calc(100% / #{$i});\n  }\n}\n\n@keyframes loader {\n  from {\n    transform: translateX(-50%) translateY(-50%) rotate(0deg);\n  }\n\n  to {\n    transform: translateX(-50%) translateY(-50%) rotate(360deg);\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-gallery/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.gallery.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-gallery/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = ['gallery-wrapper', blockData.config.advanced.cssClasses, 'gallery-wrapper--' + blockData.config.imageAlign].filter(item => item && item.trim() !== '' && item !== 'gallery-wrapper--center');\n  cssClasses = cssClasses.length ? ' class=\"' + cssClasses.join(' ') + '\"' : '';\n  let images = ``;\n\n  for (let i = 0; i < blockData.content.images.length; i++) {\n    let img = blockData.content.images[i];\n    let caption = '';\n\n    if (img.caption.trim() !== '') {\n      caption = `<figcaption>${img.caption}</figcaption>`;\n    }\n\n    images += `<figure class=\"gallery__item\">\n      <a href=\"${img.src}\" data-size=\"${img.dimensions}\">\n        <img src=\"${img.thumbnailSrc}\" height=\"${img.height}\" width=\"${img.width}\" alt=\"${img.alt.replace(/\\\"/gmi, '\\'')}\" />\n      </a>\n      ${caption}\n    </figure>`;\n  }\n\n  let html = `\n  <div ${id}${cssClasses}>\n    <div class=\"gallery\" data-columns=\"${blockData.config.columns}\">\n      ${images}\n    </div>\n  </div>`;\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-header/block.vue",
    "content": "<template>\n  <div>\n    <component\n      :is=\"'h' + config.headingLevel\"\n      contenteditable=\"true\"\n      @keyup=\"getFocusFromTab($event); handleCaret($event); debouncedSave()\"\n      @keydown=\"handleKeyboard\"\n      @focus=\"updateCurrentBlockID\"\n      @paste=\"pastePlainText\"\n      ref=\"block\"\n      :style=\"'text-align: ' + config.textAlign + ';'\"\n      :class=\"{ 'publii-block-header': true, 'is-link': config.link.url !== '' }\"\n      :title=\"config.link.url !== '' ? config.link.url : ''\"\n      v-initial-html=\"content\"\n      :data-translation=\"$t('editor.addHeading')\" />\n\n    <top-menu\n      ref=\"top-menu\"\n      :conversions=\"conversions\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport AvailableConversions from './conversions.js';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport LinkConfig from './../../mixins/LinkConfig.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nif (!mainProcessAPI) {\n  mainProcessAPI = {\n    slug: function (text) {\n      return text.toLowerCase().replace(/[^a-zA-Z0-9\\s]/gmi, '').replace(/\\s/gmi, '-').trim();\n    }\n  };\n}\n\nexport default {\n  name: 'Header',\n  mixins: [\n    Block,\n    ContentEditableImprovements,\n    LinkConfig\n  ],\n  components: {\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      config: {\n        headingLevel: 2,\n        textAlign: 'left',\n        link: {\n          url: '',\n          noFollow: false,\n          targetBlank: false,\n          sponsored: false,\n          ugc: false\n        },\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          customId: this.getAdvancedConfigDefaultValue('customId'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: '',\n      conversions: AvailableConversions,\n      topMenuConfig: [\n        {\n          activeState: function () { return this.config.headingLevel === 1; },\n          onClick: function () { this.setHeadingLevel(1); },\n          icon: 'h1',\n          tooltip: this.$t('editor.heading1')\n        },\n        {\n          activeState: function () { return this.config.headingLevel === 2; },\n          onClick: function () { this.setHeadingLevel(2); },\n          icon: 'h2',\n          tooltip: this.$t('editor.heading2')\n        },\n        {\n          activeState: function () { return this.config.headingLevel === 3; },\n          onClick: function () { this.setHeadingLevel(3); },\n          icon: 'h3',\n          tooltip: this.$t('editor.heading3')\n        },\n        {\n          activeState: function () { return this.config.headingLevel === 4; },\n          onClick: function () { this.setHeadingLevel(4); },\n          icon: 'h4',\n          tooltip: this.$t('editor.heading4')\n        },\n        {\n          activeState: function () { return this.config.headingLevel === 5; },\n          onClick: function () { this.setHeadingLevel(5); },\n          icon: 'h5',\n          tooltip: this.$t('editor.heading5')\n        },\n        {\n          activeState: function () { return this.config.headingLevel === 6; },\n          onClick: function () { this.setHeadingLevel(6); },\n          icon: 'h6',\n          tooltip: this.$t('editor.heading6')\n        },\n        {\n          activeState: function () { return this.config.textAlign === 'left'; },\n          onClick: function () { this.alignText('left'); },\n          icon: 'align-left',\n          tooltip: this.$t('editor.alignTextLeft')\n        },\n        {\n          activeState: function () { return this.config.textAlign === 'center'; },\n          onClick: function () { this.alignText('center'); },\n          icon: 'align-center',\n          tooltip: this.$t('editor.alignTextCenter')\n        },\n        {\n          activeState: function () { return this.config.textAlign === 'right'; },\n          onClick: function () { this.alignText('right'); },\n          icon: 'align-right',\n          tooltip: this.$t('editor.alignTextRight')\n        },\n        {\n          activeState: () => this.config.link.url !== '',\n          onClick: this.showLinkPopupWithoutHighlight,\n          icon: 'link',\n          tooltip: this.$t('link.addLink')\n        },\n        {\n          activeState: () => false,\n          onClick: this.removeLink,\n          isVisible: () => this.config.link.url !== '',\n          icon: 'unlink',\n          tooltip: this.$t('link.removeLink')\n        }\n      ]\n    };\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  beforeMount () {\n    this.content = this.inputContent;\n  },\n  mounted () {\n    this.$bus.$on('block-editor-save-link-popup', this.setLink);\n  },\n  methods: {\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === false) {\n        document.execCommand('insertHTML', false, '<line-separator />');\n\n        if (this.$refs['block'].innerHTML.substr(-33) === '<line-separator></line-separator>') {\n          this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n          this.$refs['block'].innerHTML = this.$refs['block'].innerHTML.replace('<line-separator></line-separator>', '');\n        } else {\n          let separatedContent = this.$refs['block'].innerHTML.split('<line-separator></line-separator>');\n          let firstPart = separatedContent[0];\n          let secondPart = separatedContent[1];\n\n          if (secondPart.substr(0, 4) === '<br>') {\n            secondPart = secondPart.substr(4);\n          }\n\n          this.$refs['block'].innerHTML = firstPart;\n          this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id, secondPart);\n        }\n\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['block'].innerHTML === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n    },\n    alignText (position) {\n      this.config.textAlign = position;\n    },\n    setHeadingLevel (level) {\n      this.save();\n      this.config.headingLevel = level;\n      this.save();\n    },\n    async save () {\n      if (!this.$refs['block'].innerHTML) {\n        return;\n      }\n\n      this.content = this.$refs['block'].innerHTML.replace('<line-separator></line-separator>', '');\n\n      if (!this.config.advanced.customId) {\n        this.config.advanced.id = await mainProcessAPI.invoke('app-main-process-create-slug', this.content);\n      }\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-save-link-popup', this.setLink);\n  }\n}\n</script>\n\n<style scoped lang=\"scss\">\n.publii-block-header {\n  outline: none;\n  width: 100%;\n\n  &:empty {\n    &:before {\n      content: attr(data-translation);\n      color: var(--gray-4);\n    }\n  }\n\n  &.is-link {\n    cursor: pointer;\n    text-decoration: underline;\n    text-decoration-color: rgba(var(--yellow), 1);  \n    text-decoration-thickness: 3px;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-header/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"customId\",\n        \"type\": \"checkbox\",\n        \"label\": \"editor.blocks.header.customIdLabel\",\n        \"tooltip\": \"editor.blocks.header.customIdTooltip\",\n        \"defaultValue\": false\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.header.idTooltip\",\n        \"defaultValue\": \"\",\n        \"disabled\": [\n            {\n                \"field\": \"customId\",\n                \"value\": false\n            }\n        ]\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-header/conversions.js",
    "content": "const availableConversions = [\n  {\n    'icon': 'paragraph',\n    'name': 'editor.conversions.toParagraph',\n    'type': 'publii-paragraph',\n    'convert': function (config, content, editorInstance) {\n      // eslint-disable-next-line\n      let newContent = content.replace(/\\n/gmi, '<br>');\n      let newConfig = {\n        textAlign: config.textAlign,\n        advanced: {\n          style: '',\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'html',\n    'name': 'editor.conversions.toHTML',\n    'type': 'publii-html',\n    'convert': function (config, content, editorInstance, rawBlock) {\n      let newContent = rawBlock.outerHTML.replace(/<h([0-9]{1,1}).*?>/gmi, '<h$1>');\n      let newConfig = {\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  }\n];\n\nmodule.exports = availableConversions;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-header/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = [blockData.config.advanced.cssClasses, blockData.config.advanced.style, 'align-' + blockData.config.textAlign].filter(item => item && item.trim() !== '' && item !== 'align-left');\n  cssClasses = cssClasses.length ? ' class=\"' + cssClasses.join(' ') + '\"' : '';\n  let headingLevel = blockData.config.headingLevel;\n  let html = ``;\n\n  if (blockData.config.link.url !== '') {\n    let targetBlank = '';\n    let relAttr = [];\n\n    if (blockData.config.link.noFollow) {\n      relAttr.push('nofollow noopener');\n    }\n\n    if (blockData.config.link.sponsored) {\n      relAttr.push('sponsored');\n    }\n\n    if (blockData.config.link.ugc) {\n      relAttr.push('ugc');\n    }\n\n    if (relAttr.length) {\n      relAttr = ' rel=\"' + relAttr.join(' ') + '\"';\n    }\n\n    if (blockData.config.link.targetBlank) {\n      targetBlank = ' target=\"_blank\"'\n    }\n\n    html = `\n    <h${headingLevel}${id}${cssClasses}>\n      <a href=\"${blockData.config.link.url}\"${relAttr}${targetBlank}>\n      ${blockData.content}\n      </a>\n    </h${headingLevel}>`;\n  } else {\n    html = `\n    <h${headingLevel}${id}${cssClasses}>\n      ${blockData.content}\n    </h${headingLevel}>`;\n  }\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-html/aspect-ratio.js",
    "content": "function findGreatestCommonDivider (a, b) {\n  return b ? findGreatestCommonDivider(b, a % b) : a;\n}\n\nfunction calculateAspectRatio (width, height) {\n  let divider = findGreatestCommonDivider(width, height);\n  return `${width / divider}-${height / divider}`;\n}\n\nfunction addAspectRatio (code) {\n  return code.replace(/<iframe([\\s\\S]*?)>[\\s\\S]*?<\\/[\\s\\S]*?iframe>/gmi, function (matches, attrs) {\n    let width = false;\n    let height = false;\n    let src = false;\n    let wrapperClass = '';\n    let outputAttrs = [];\n\n    attrs = attrs.replace(/\\s{2,}/gmi, ' ').split(' ');\n\n    for (let attr of attrs) {\n      if (attr.indexOf('width') > -1) {\n        attr = attr.split('=');\n\n        if (attr[1]) {\n          width = parseInt(attr[1].replace(/\"/gmi, ''), 10);\n        }\n      }\n\n      if (attr.indexOf('height') > -1) {\n        attr = attr.split('=');\n\n        if (attr[1]) {\n          height = parseInt(attr[1].replace(/\"/gmi, ''), 10);\n        }\n      }\n\n      if (attr.indexOf('src') > -1) {\n        attr = attr.split('=');\n\n        if (attr[1]) {\n          src = attr[1].replace(/\"/gmi, '');\n        }\n      }\n    }\n\n    if (width !== false && !isNaN(width) && height !== false && !isNaN(height)) {\n      let aspectRatio = calculateAspectRatio(width, height);\n      wrapperClass = 'aspect-ratio-' + aspectRatio;\n    } else {\n      wrapperClass = 'aspect-ratio-4-3';\n    }\n\n    if (\n      src && (\n        src.indexOf('http://') === 0 ||\n        src.indexOf('https://') === 0 ||\n        src.indexOf('//') === 0 ||\n        src.indexOf('dat://') === 0 ||\n        src.indexOf('ipfs://') === 0 || \n        src.indexOf('dweb://') === 0\n      )\n    ) {\n      outputAttrs.push('src=\"' + src + '\"')\n    }\n\n    outputAttrs.push('sandbox=\"allow-scripts\"');\n\n    return `<div class=\"embed-wrapper ${wrapperClass}\"><iframe ${outputAttrs.join(' ')}></iframe></div>`;\n  });\n}\n\nmodule.exports = addAspectRatio;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-html/block.vue",
    "content": "<template>\n  <div>\n    <prism-editor\n      :class=\"{ 'publii-block-html': true, 'is-visible': true }\"\n      ref=\"block\"\n      @keyup=\"handleKeyboard($event); debouncedSave()\"\n      @focus=\"updateCurrentBlockID\"\n      :code=\"content\"\n      :emitEvents=\"true\"\n      v-model=\"content\"\n      :lineNumbers=\"true\"\n      language=\"html\">\n    </prism-editor>\n\n    <top-menu\n      ref=\"top-menu\"\n      :conversions=\"conversions\"\n      :config=\"[]\" />\n  </div>\n</template>\n\n<script>\nimport AvailableConversions from './conversions.js';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport contentFilter from './content-filter.js';\nimport addAspectRatio from './aspect-ratio.js';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nexport default {\n  name: 'Html',\n  mixins: [\n    Block,\n    ContentEditableImprovements\n  ],\n  components: {\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      config: {\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: '',\n      conversions: AvailableConversions\n    };\n  },\n  computed: {\n    modifiedContent () {\n      let codeWithAspectRatio = addAspectRatio(this.$refs['block'].code);\n      return contentFilter(codeWithAspectRatio);\n    }\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  mounted () {\n    this.content = this.inputContent;\n  },\n  methods: {\n    focus () {\n      this.$refs['block'].$el.querySelector('pre').focus();\n    },\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === true) {\n        e.preventDefault();\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Tab' && e.shiftKey === false) {\n        e.preventDefault();\n        // eslint-disable-next-line\n        document.execCommand('insertHTML', false, \"  \");\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['block'].$el.querySelector('pre').innerHTML === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n    },\n    save () {\n      this.content = this.$refs['block'].code;\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/vendor/modularscale';\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n\n.publii-block-html {\n  border-radius: var(--border-radius);\n  background: var(--gray-4);\n  box-shadow: 2px 4px 26px var(--shadow);\n  outline: none;\n  width: 100%;\n\n  & > pre,\n  & > .prism-editor__line-numbers {\n    display: none;\n  }\n\n  &.is-visible {\n    & > pre,\n    & > .prism-editor__line-numbers {\n      background: var(--pre-bg) !important;\n      display: block;\n    }\n\n    & > pre {\n      background: var(--pre-bg) !important;\n    }\n\n    code {\n      background: transparent !important;\n      padding: 0!important;\n    }\n  }\n\n  .prism-editor__line-numbers {\n    background: var(--input-bg) !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-html/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.html.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-html/content-filter.js",
    "content": "function contentFilter (code, mode = 'editor') {\n  if (mode === 'editor') {\n    code = code.replace(/&gt;/gmi, '>');\n    code = code.replace(/&lt;/gmi, '<');\n    code = code.replace(/<script/gmi, '<publii-script');\n    code = code.replace(/<\\/script/gmi, '</publii-script');\n    code = code.replace(/<webview[\\s\\S]*?>[\\s\\S]*?<\\/[\\s\\S]*?webview>/gmi, '');\n  } else if (mode === 'render') {\n    code = code.replace(/&gt;/gmi, '>');\n    code = code.replace(/&lt;/gmi, '<');\n    code = code.replace(/<publii-script/gmi, '<script');\n    code = code.replace(/<\\/publii-script/gmi, '</script');\n    code = code.replace(/<webview[\\s\\S]*?>[\\s\\S]*?<\\/[\\s\\S]*?webview>/gmi, '');\n  }\n\n  return code;\n}\n\nmodule.exports = contentFilter;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-html/conversions.js",
    "content": "const availableConversions = [\n  {\n    'icon': 'paragraph',\n    'name': 'editor.conversions.toParagraph',\n    'type': 'publii-paragraph',\n    'convert': function (config, content, editorInstance) {\n      let newContent = editorInstance.extensions.conversionHelpers.stripTags(content).replace(/\\n/gmi, '<br>');\n      let newConfig = {\n        textAlign: 'left',\n        advanced: {\n          style: '',\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  }\n];\n\nmodule.exports = availableConversions;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-html/render.js",
    "content": "const contentFilter = require('./content-filter.js');\n\nfunction render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = blockData.config.advanced.cssClasses ? ' class=\"' + blockData.config.advanced.cssClasses + '\"' : '';\n  let html = contentFilter(blockData.content, 'render');\n  return `<div${id}${cssClasses}>${html}</div>`;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-image/block.vue",
    "content": "<template>\n  <div \n    :class=\"{ 'publii-block-image-wrapper': true, 'is-empty': isEmpty }\"\n    @click=\"resetDeleteConfirmation\">\n    <figure\n      v-if=\"view === 'preview' || content.image !== ''\"\n      ref=\"block\"\n      :class=\"{ 'publii-block-image': true, 'is-wide': config.imageAlign === 'wide', 'is-full': config.imageAlign === 'full' }\">\n      <img\n        v-if=\"!!content.image\"\n        :src=\"content.image\"\n        :height=\"content.imageHeight\"\n        :width=\"content.imageWidth\" />\n\n      <button\n        v-if=\"!!content.image && !confirmDelete\"\n        class=\"publii-block-image-delete\"\n        @click.stop.prevent=\"clearImage()\">\n        <icon name=\"trash\" />\n      </button>\n\n      <button\n        v-if=\"!!content.image && confirmDelete\"\n        class=\"publii-block-image-delete is-active has-tooltip\"\n        @click.stop.prevent=\"clearImage()\">\n        <icon name=\"open-trash\" />\n        <span class=\"ui-tooltip has-bigger-space\">\n          {{ $t('editor.clickToConfirm') }}\n        </span>\n      </button>\n\n      <figcaption v-if=\"content.caption !== '' && view === 'preview'\">\n        {{ content.caption }}\n      </figcaption>\n    </figure>\n\n    <div\n      v-if=\"(content.image === '') || $parent.uiOpened\"\n      :class=\"{ 'publii-block-image-form': true, 'is-visible': true }\"\n      ref=\"block\">\n      <div\n        v-if=\"content.image === ''\"\n        :class=\"{ 'publii-block-image-uploader': true, 'is-hovered': isHovered }\"\n        @drag.stop.prevent\n        @dragstart.stop.prevent\n        @dragend.stop.prevent\n        @dragover.stop.prevent=\"dragOver\"\n        @dragenter.stop.prevent\n        @dragleave.stop.prevent=\"dragLeave\"\n        @drop.stop.prevent=\"drop\">\n        <div class=\"publii-block-image-uploader-inner\">\n          <icon\n            v-if=\"!imageUploadInProgress\"\n            name=\"blank-image\"\n            height=\"62\"\n            width=\"75\" />\n          <span v-if=\"!imageUploadInProgress\">\n            {{ $t('editor.dropToUploadYourPhotoOr') }}\n          </span>\n          <button\n            v-if=\"!imageUploadInProgress\"\n            @click=\"filePickerCallback\">\n            {{ $t('file.selectFile') }}\n          </button>\n          <span\n            v-if=\"imageUploadInProgress\"\n            class=\"publii-block-image-uploader-loader\"></span>\n        </div>\n      </div>\n\n      <input\n        v-if=\"$parent.uiOpened\"\n        type=\"text\"\n        @focus=\"updateCurrentBlockID\"\n        @keydown=\"handleCaptionKeyboard\"\n        @keyup=\"handleCaretCaption\"\n        @click.stop\n        v-model=\"content.caption\"\n        :placeholder=\"$t('image.enterCaption')\"\n        ref=\"contentCaption\" />\n      <input\n        v-if=\"$parent.uiOpened\"\n        type=\"text\"\n        @focus=\"updateCurrentBlockID\"\n        @keydown=\"handleAltKeyboard\"\n        @keyup=\"handleCaretAlt\"\n        @click.stop\n        v-model=\"content.alt\"\n        :placeholder=\"$t('image.enterAltText')\"\n        ref=\"contentAlt\" />\n    </div>\n\n    <top-menu\n      ref=\"top-menu\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport EditorIcon from './../../elements/EditorIcon.vue';\nimport HasPreview from './../../mixins/HasPreview.vue';\nimport LinkConfig from './../../mixins/LinkConfig.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\nimport Utils from './../../utils/Utils.js';\n\nexport default {\n  name: 'PImage',\n  mixins: [\n    Block,\n    ContentEditableImprovements,\n    HasPreview,\n    LinkConfig\n  ],\n  components: {\n    'icon': EditorIcon,\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      caretIsAtStartCaption: false,\n      caretIsAtEndCaption: false,\n      caretIsAtStartAlt: false,\n      caretIsAtEndAlt: false,\n      confirmDelete: false,\n      isHovered: false,\n      imageUploadInProgress: false,\n      config: {\n        imageAlign: 'center',\n        link: {\n          url: '',\n          noFollow: false,\n          targetBlank: false,\n          sponsored: false,\n          ugc: false\n        },\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: {\n        image: '',\n        imageHeight: '',\n        imageWidth: '',\n        alt: '',\n        caption: ''\n      },\n      topMenuConfig: [\n        {\n          activeState: function () { return this.config.imageAlign === 'center'; },\n          onClick: function () { this.alignImage('center'); },\n          icon: 'center',\n          tooltip: this.$t('image.centeredImage')\n        },\n        {\n          activeState: function () { return this.config.imageAlign === 'wide'; },\n          onClick: function () { this.alignImage('wide'); },\n          icon: 'wide',\n          tooltip: this.$t('image.wideImage')\n        },\n        {\n          activeState: function () { return this.config.imageAlign === 'full'; },\n          onClick: function () { this.alignImage('full'); },\n          icon: 'full',\n          tooltip: this.$t('image.fullWidthImage')\n        },\n        {\n          activeState: () => this.config.link.url !== '',\n          onClick: this.showLinkPopupWithoutHighlight,\n          icon: 'link',\n          tooltip: this.$t('link.addLink')\n        },\n        {\n          activeState: () => false,\n          onClick: this.removeLink,\n          isVisible: () => this.config.link.url !== '',\n          icon: 'unlink',\n          tooltip: this.$t('link.removeLink')\n        }\n      ]\n    };\n  },\n  computed: {\n    isInsidePublii () {\n      return !!window.process;\n    }\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  mounted () {\n    this.content = Utils.deepMerge(this.content, this.inputContent);\n    this.view = (this.content.image === '') ? 'code' : 'preview';\n    this.initFakeFilePicker();\n    this.setParentCssClasses(this.config.imageAlign);\n    this.$bus.$on('block-editor-save-link-popup', this.setLink);\n  },\n  methods: {\n    dragOver (e) {\n      this.isHovered = true;\n    },\n    dragLeave (e) {\n      this.isHovered = false;\n    },\n    async drop (e) {\n      let files = e.dataTransfer.files;\n      let siteName = window.app.getSiteName();\n      this.imageUploadInProgress = true;\n\n      if (!files[0]) {\n        this.imageUploadInProgress = false;\n      } else {\n        mainProcessAPI.send('app-image-upload', {\n          id: this.editor.config.postID,\n          site: siteName,\n          path: await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(files[0])),\n          imageType: 'contentImages'\n        });\n\n        mainProcessAPI.receiveOnce('app-image-uploaded', (data) => {\n          if (data.baseImage && data.baseImage.size && data.baseImage.size.length >= 2) {\n            this.content.imageWidth = data.baseImage.size[0];\n            this.content.imageHeight = data.baseImage.size[1];\n            this.content.image = data.baseImage.url;\n          } else {\n            this.content.image = data.url;\n          }\n\n          this.imageUploadInProgress = false;\n          this.$parent.openPopup();\n        });\n      }\n\n      this.isHovered = false;\n    },\n    initFakeFilePicker () {\n      let imageUploader = document.getElementById('post-editor-fake-image-uploader');\n\n      imageUploader.addEventListener('change', () => {\n        if (!imageUploader.value) {\n          return;\n        }\n\n        setTimeout(async () => {\n          if (!this.fileSelectionCallback) {\n            return;\n          }\n\n          let filePath = false;\n\n          if (imageUploader.files) {\n            filePath = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(imageUploader.files[0]));\n          }\n\n          if (!filePath) {\n            return;\n          }\n\n          this.imageUploadInProgress = true;\n          // eslint-disable-next-line\n          mainProcessAPI.send('app-image-upload', {\n            id: this.editor.config.postID,\n            site: window.app.getSiteName(),\n            path: filePath,\n            imageType: 'contentImages'\n          });\n\n          // eslint-disable-next-line\n          mainProcessAPI.receiveOnce('app-image-uploaded', (data) => {\n            this.content.imageWidth = data.baseImage.size[0];\n            this.content.imageHeight = data.baseImage.size[1];\n            this.content.image = data.baseImage.url;\n            this.fileSelectionCallback = false;\n            this.imageUploadInProgress = false;\n            this.$parent.openPopup();\n          });\n\n          imageUploader.value = '';\n        }, 50);\n      });\n    },\n    filePickerCallback () {\n      this.fileSelectionCallback = true;\n      document.getElementById('post-editor-fake-image-uploader').click();\n    },\n    clearImage () {\n      if (!this.confirmDelete) {\n        this.confirmDelete = true;\n      } else {\n        this.content.image = '';\n        this.isHovered = false;\n      }\n    },\n    setView (newView) {\n      if (\n        this.view === 'code' &&\n        newView === 'preview'\n      ) {\n        this.save();\n      }\n\n      if (\n        !this.content.image &&\n        newView === 'preview'\n      ) {\n        this.view = 'code';\n      } else {\n        setTimeout(() => {\n          this.view = newView;\n        }, 0);\n      }\n    },\n    alignImage (newValue) {\n      this.config.imageAlign = newValue;\n      this.setParentCssClasses(newValue);\n    },\n    setParentCssClasses (imageMode) {\n      if (imageMode === 'full') {\n        this.$parent.addCustomCssClass('contains-full-image');\n      } else {\n        this.$parent.removeCustomCssClass('contains-full-image');\n      }\n\n      if (imageMode === 'wide') {\n        this.$parent.addCustomCssClass('contains-wide-image');\n      } else {\n        this.$parent.removeCustomCssClass('contains-wide-image');\n      }\n    },\n    focus () {\n      this.setView('code');\n    },\n    handleCaptionKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === false) {\n        this.$refs['contentAlt'].focus();\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['contentCaption'].value === '' && this.$refs['contentAlt'].value === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n    },\n    handleAltKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === false) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['contentCaption'].value === '') {\n        this.$refs['contentCaption'].focus();\n        e.returnValue = false;\n      }\n    },\n    handleCaretCaption (e) {\n      if (e.code === 'ArrowUp' && this.getCursorPosition('contentCaption') === 0) {\n        if (!this.caretIsAtStartCaption) {\n          this.caretIsAtStartCaption = true;\n          return;\n        }\n\n        let previousBlockID = this.findPreviousBlockID();\n\n        if (previousBlockID) {\n          this.editor.$refs['block-wrapper-' + previousBlockID][0].blockClick();\n          this.editor.$refs['block-' + previousBlockID][0].focus();\n        }\n      }\n\n      if (e.code === 'ArrowDown' && this.getCursorPosition('contentCaption') >= this.$refs['contentCaption'].value.length - 2) {\n        if (!this.caretIsAtEndCaption) {\n          this.caretIsAtEndCaption = true;\n          return;\n        }\n\n        this.$refs['contentAlt'].focus();\n        e.returnValue = false;\n      }\n\n      if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {\n        this.caretIsAtStartCaption = false;\n        this.caretIsAtEndCaption = false;\n      }\n    },\n    handleCaretAlt (e) {\n      if (e.code === 'ArrowUp' && this.getCursorPosition('contentAlt') === 0) {\n        if (!this.caretIsAtStartAlt) {\n          this.caretIsAtStartAlt = true;\n          return;\n        }\n\n        this.$refs['contentCaption'].focus();\n        e.returnValue = false;\n      }\n\n      if (e.code === 'ArrowDown' && this.getCursorPosition('contentAlt') >= this.$refs['contentAlt'].value.length - 2) {\n        if (!this.caretIsAtEndAlt) {\n          this.caretIsAtEndAlt = true;\n          return;\n        }\n\n        let nextBlockID = this.findNextBlockID();\n\n        if (nextBlockID) {\n          this.editor.$refs['block-wrapper-' + nextBlockID][0].blockClick();\n          this.editor.$refs['block-' + nextBlockID][0].focus('none');\n        }\n      }\n\n      if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {\n        this.caretIsAtStartAlt = false;\n        this.caretIsAtEndAlt = false;\n      }\n    },\n    save () {\n      if (this.$refs['contentAlt']) {\n        this.content.alt = this.$refs['contentAlt'].value;\n      }\n\n      if (this.$refs['contentCaption']) {\n        this.content.caption = this.$refs['contentCaption'].value;\n      }\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: JSON.parse(JSON.stringify(this.content))\n      });\n    },\n    resetDeleteConfirmation () {\n      this.confirmDelete = false;\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-save-link-popup', this.setLink);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/vendor/modularscale';\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n\n.publii-block-image {\n  outline: none;\n  position: relative;\n\n  &:hover {\n    .publii-block-image-delete {\n      opacity: 1;\n      pointer-events: auto;\n    }\n  }\n\n  & > img {\n    display: block;\n    height: auto;\n    margin: 0 auto;\n    max-width: 100%;\n    transition: all .25s ease-out;\n  }\n\n  & > figcaption {\n    display: block;\n    padding: baseline(3,em) 0 0;\n  }\n\n  &-empty-state {\n    color: var(--gray-3);\n    font-family: var(--font-base);\n    font-size: 14px;\n    text-align: center;\n  }\n\n  &-delete {\n    align-items: center;\n    background: var(--warning);\n    border: none;\n    border-radius: var(--border-radius);\n    cursor: pointer;\n    display: flex;\n    height: 34px;\n    justify-content: center;\n    left: 15px;\n    opacity: 0;\n    pointer-events: none;\n    position: absolute;\n    top: 15px;\n    transition: var(--transition);\n    will-change: transform;\n    width: 34px;\n    z-index: 2;\n\n    svg {\n       color: var(--white);\n    }\n\n    &:active,\n    &:focus,\n    &:hover {\n       transform: scale(1.1);\n    }\n  }\n\n  & + &-form {\n    padding: baseline(5,em) 0;\n  }\n\n  &-form {\n    display: none;\n\n    &.is-visible {\n      display: block;\n      order: -1;\n    }\n\n    input,\n    textarea {  \n      display: block;\n      width: 100%;\n    }\n\n\n    input + input {\n      margin-top: 16px;\n    }\n  }\n\n  &-uploader {       \n    border: 2px dashed var(--input-border-color);\n    border-radius: var(--border-radius);\n    height: 250px;\n    margin: 0 0 16px 0;\n    padding: 6px;\n    position: relative;\n    width: 100%;\n\n    &.is-hovered {\n      border-color: var(--color-primary);\n    }\n\n    &-loader {\n      animation: loader 1s linear infinite;\n      border: 3px solid var(--color-primary);\n      border-left-color: transparent;\n      border-radius: 50%;\n      display: block;\n      height: 32px;\n      left: 50%;\n      position: absolute;\n      top: 50%;\n      transform: translateX(-50%) translateY(-50%);\n      width: 32px!important;\n    }\n\n    &-inner {\n      align-items: center;\n      display: flex;\n      flex-wrap: wrap;\n      font-family: var(--font-base);\n      justify-content: center;\n      height: 234px;\n      padding: 2rem;\n      width: 100%;\n\n      svg {\n        fill: var(--icon-quaternary-color);\n      }\n\n      span {\n        display: block;\n        font-size: $app-font-base;\n        text-align: center;\n        width: 100%;\n      }\n\n      button {\n        background: var(--button-secondary-bg);\n        border: 1px solid var(--button-secondary-bg);\n        border-radius: var(--border-radius);\n        color: var(--button-secondary-color);\n        cursor: pointer;\n        font-weight: var(--font-weight-semibold);\n        font-size: 15px;\n        padding: .5rem 2rem;\n        text-align: center;\n        outline: none;\n\n        &:active,\n        &:focus,\n        &:hover {\n          background: var(--button-secondary-bg-hover);\n          border-color: var(--button-secondary-bg-hover);\n          color: var(--button-secondary-color-hover);\n        }\n      }\n    }\n  }\n}\n\n@keyframes loader {\n  from {\n    transform: translateX(-50%) translateY(-50%) rotate(0deg);\n  }\n\n  to {\n    transform: translateX(-50%) translateY(-50%) rotate(360deg);\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-image/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.image.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-image/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let caption = `<figcaption>${blockData.content.caption}</figcaption>`;\n  let cssClasses = [blockData.config.advanced.cssClasses, 'post__image', 'post__image--' + blockData.config.imageAlign].filter(item => item && item.trim() !== '');\n  cssClasses = cssClasses.length ? ' class=\"' + cssClasses.join(' ') + '\"' : '';\n  let html = ``;\n\n  if (blockData.content.caption.trim() === '') {\n    caption = '';\n  }\n\n  if (blockData.config.link.url !== '') {\n    let targetBlank = '';\n    let relAttr = [];\n\n    if (blockData.config.link.noFollow) {\n      relAttr.push('nofollow noopener');\n    }\n\n    if (blockData.config.link.sponsored) {\n      relAttr.push('sponsored');\n    }\n\n    if (blockData.config.link.ugc) {\n      relAttr.push('ugc');\n    }\n\n    if (relAttr.length) {\n      relAttr = ' rel=\"' + relAttr.join(' ') + '\"';\n    }\n\n    if (blockData.config.link.targetBlank) {\n      targetBlank = ' target=\"_blank\"'\n    }\n\n    html = `\n    <figure${id}${cssClasses}>\n      <a href=\"${blockData.config.link.url}\"${relAttr}${targetBlank}>\n        <img src=\"${blockData.content.image}\" height=\"${blockData.content.imageHeight}\" width=\"${blockData.content.imageWidth}\" alt=\"${blockData.content.alt}\" />\n      </a>\n      ${caption}\n    </figure>`;\n  } else {\n    html = `\n    <figure${id}${cssClasses}>\n      <img src=\"${blockData.content.image}\" height=\"${blockData.content.imageHeight}\" width=\"${blockData.content.imageWidth}\" alt=\"${blockData.content.alt}\" />\n      ${caption}\n    </figure>`;\n  }\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-list/block.vue",
    "content": "<template>\n  <div :class=\"{ 'is-empty': isEmpty }\">\n    <component\n      :is=\"config.listType\"\n      contenteditable=\"true\"\n      @keyup=\"getFocusFromTab($event); handleCaret($event); handleKeyUp($event); debouncedSave()\"\n      @focus=\"updateCurrentBlockID\"\n      @paste=\"pastePlainText\"\n      @keydown=\"handleKeyboard\"\n      @mouseup=\"handleMouseUp\"\n      v-initial-html=\"content\"\n      ref=\"block\"\n      class=\"publii-block-list\" />\n\n    <inline-menu ref=\"inline-menu\" />\n\n    <top-menu\n      ref=\"top-menu\"\n      :conversions=\"conversions\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport AvailableConversions from './conversions.js';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport InlineMenu from './../../mixins/InlineMenu.vue';\nimport InlineMenuUI from './../../helpers/InlineMenuUI.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nexport default {\n  name: 'List',\n  mixins: [\n    Block,\n    InlineMenu\n  ],\n  components: {\n    'inline-menu': InlineMenuUI,\n    'top-menu': TopMenuUI\n  },\n  computed: {\n    isEmpty () {\n      return this.content === '<li></li>';\n    }\n  },\n  data () {\n    return {\n      config: {\n        listType: 'ul',\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: '<li></li>',\n      conversions: AvailableConversions,\n      topMenuConfig: [\n        {\n          activeState: function () { return this.config.listType === 'ul'; },\n          onClick: function () { this.setListType('ul'); },\n          icon: 'unordered-list',\n          tooltip: this.$t('editor.useUnorderedList')\n        },\n        {\n          activeState: function () { return this.config.listType === 'ol'; },\n          onClick: function () { this.setListType('ol'); },\n          icon: 'ordered-list',\n          tooltip: this.$t('editor.useOrderedList')\n        },\n        {\n          activeState: () => false,\n          onClick: function () { this.clearListHtml(); },\n          icon: 'eraser',\n          tooltip: this.$t('editor.clearFormatting')\n        }\n      ]\n    };\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  beforeMount () {\n    this.content = this.inputContent;\n\n    if (!this.inputContent) {\n      this.content = '<li></li>';\n    }\n  },\n  methods: {\n    handleKeyUp (e) {\n      this.textIsHighlighted = false;\n\n      if (e.code === 'Backspace') {\n        e.preventDefault();\n        let range = document.getSelection().getRangeAt(0);\n        range.deleteContents();\n      }\n    },\n    handleKeyboard (e) {\n      if (\n        e.code === 'Backspace' &&\n        (\n          this.$refs['block'].innerHTML === '<li></li>' ||\n          this.$refs['block'].innerHTML === '<li><br></li>'\n        )\n      ) {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n\n      if (\n        e.code === 'Enter' && !e.isComposing &&\n        (\n          (\n            this.$refs['block'].innerHTML.substr(-13) === '<li><br></li>' ||\n            this.$refs['block'].innerHTML.substr(-9) === '<li></li>'\n          ) && this.$refs['block'].querySelectorAll('li').length > 1\n        )\n      ) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        this.$refs['block'].innerHTML = this.$refs['block'].innerHTML.substr(0, this.$refs['block'].innerHTML.length - 13);\n        e.returnValue = false;\n      }\n    },\n    setListType (type) {\n      this.save();\n      this.config.listType = type;\n    },\n    save () {\n      this.content = this.$refs['block'] ? this.$refs['block'].innerHTML : '';\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    },\n    clearListHtml () {\n      let html = this.$refs['block'].innerHTML;\n      let tempNode = document.createElement('div');\n      tempNode.innerHTML = html;\n      let allElements = tempNode.querySelectorAll('*');\n\n      for (let i = 0; i < allElements.length; i++) {\n        if (['UL', 'OL', 'LI'].indexOf(allElements[i].tagName) > -1) {\n          continue;\n        }\n\n        let parent = allElements[i].parentNode;\n\n        while (allElements[i].firstChild) {\n          parent.insertBefore(allElements[i].firstChild, allElements[i]);\n        }\n\n        parent.removeChild(allElements[i]);\n      }\n\n      this.$refs['block'].innerHTML = tempNode.innerHTML;\n      tempNode.remove();\n    },\n    saveChangesHistory () {\n      this.$bus.$emit('undomanager-save-history', this.id, this.$refs['block'].innerHTML);\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.publii-block-list {\n  outline: none;\n  width: 100%;\n\n  li {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-list/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.list.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-list/conversions.js",
    "content": "const availableConversions = [\n  {\n    'icon': 'paragraph',\n    'name': 'editor.conversions.toParagraph',\n    'type': 'publii-paragraph',\n    'convert': function (config, content, editorInstance) {\n      // eslint-disable-next-line\n      let newContent = content.replace(/<ul.*?>/gmi, '')\n                                // eslint-disable-next-line\n                                .replace(/<ol.*?>/gmi, '')\n                                // eslint-disable-next-line\n                                .replace(/<\\/li><li>/gmi, \"<br>\")\n                                // eslint-disable-next-line\n                                .replace(/\\n/gmi, '<br>')\n                                // eslint-disable-next-line\n                                .replace(/<li.*?>/gmi, '')\n                                // eslint-disable-next-line\n                                .replace(/<\\/li.*?>/gmi, '');\n      let newConfig = {\n        textAlign: 'left',\n        advanced: {\n          style: '',\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'html',\n    'name': 'editor.conversions.toHTML',\n    'type': 'publii-html',\n    'convert': function (config, content, editorInstance, rawBlock) {\n      let newContent = rawBlock.outerHTML\n        // eslint-disable-next-line\n        .replace(/<ul.*?>/gmi, '<ul>')\n        // eslint-disable-next-line\n        .replace(/<ol.*?>/gmi, '<ol>')\n        // eslint-disable-next-line\n        .replace(/<\\/li><li>/gmi, \"</li>\\n<li>\")\n        // eslint-disable-next-line\n        .replace(/ul><li>/gmi, \"ul>\\n<li>\")\n        // eslint-disable-next-line\n        .replace(/ol><li>/gmi, \"ol>\\n<li>\")\n        // eslint-disable-next-line\n        .replace(/<\\/li><\\/ul>/gmi, \"</li>\\n</ul>\");\n      let newConfig = {\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  }\n];\n\nmodule.exports = availableConversions;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-list/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = blockData.config.advanced.cssClasses ? ' class=\"' + blockData.config.advanced.cssClasses + '\"' : '';\n  let listType = blockData.config.listType;\n  let html = `\n  <${listType}${id}${cssClasses}>\n    ${blockData.content}\n  </${listType}>`;\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-paragraph/block.vue",
    "content": "<template>\n  <div :class=\"{ 'is-empty': isEmpty }\">\n    <p\n      :class=\"{ 'publii-block-paragraph': true, [config.advanced.style]: true }\"\n      :style=\"'text-align: ' + config.textAlign + ';'\"\n      ref=\"block\"\n      slot=\"block\"\n      @focus=\"updateCurrentBlockID\"\n      @blur=\"handleEditBlur\"\n      @keyup=\"getFocusFromTab($event); handleKeyUp($event); handleCaret($event); debouncedSave()\"\n      @paste=\"pastePlainText\"\n      @keydown=\"handleKeyboard\"\n      @mouseup=\"handleMouseUp\"\n      contenteditable=\"true\"\n      v-initial-html=\"content\"\n      :data-translation=\"$t('editor.startWritingOrPressTabToChooseBlock')\">\n    </p>\n\n    <inline-menu ref=\"inline-menu\" />\n\n    <top-menu\n      ref=\"top-menu\"\n      :conversions=\"conversions\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport AvailableConversions from './conversions.js';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport EditorIcon from './../../elements/EditorIcon.vue';\nimport InlineMenu from './../../mixins/InlineMenu.vue';\nimport InlineMenuUI from './../../helpers/InlineMenuUI.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nexport default {\n  name: 'Paragraph',\n  mixins: [\n    Block,\n    ContentEditableImprovements,\n    InlineMenu\n  ],\n  components: {\n    'icon': EditorIcon,\n    'inline-menu': InlineMenuUI,\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      config: {\n        textAlign: 'left',\n        advanced: {\n          style: this.getAdvancedConfigDefaultValue('style'),\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: '',\n      conversions: AvailableConversions,\n      topMenuConfig: [\n        {\n          activeState: function () { return this.config.textAlign === 'left'; },\n          onClick: function () { this.alignText('left'); },\n          icon: 'align-left',\n          tooltip: this.$t('editor.alignTextLeft')\n        },\n        {\n          activeState: function () { return this.config.textAlign === 'center'; },\n          onClick: function () { this.alignText('center'); },\n          icon: 'align-center',\n          tooltip: this.$t('editor.alignTextCenter')\n        },\n        {\n          activeState: function () { return this.config.textAlign === 'right'; },\n          onClick: function () { this.alignText('right'); },\n          icon: 'align-right',\n          tooltip: this.$t('editor.alignTextRight')\n        },\n        {\n          activeState: () => false,\n          onClick: function () { this.clearContentHtml('block'); },\n          icon: 'eraser',\n          tooltip: this.$t('editor.clearFormatting')\n        }\n      ]\n    };\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  beforeMount () {\n    this.content = this.inputContent;\n  },\n  methods: {\n    refresh () {\n      this.$refs['block'].innerHTML = this.content;\n    },\n    handleEditBlur (e) {\n      if (e.relatedTarget && e.relatedTarget.classList.contains('wrapper-ui-inline-menu-button')) {\n        return;\n      }\n\n      this.save();\n    },\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === false) {\n        let newElementName = this.$parent.$parent.extensions.shortcutManager.checkContentForShortcuts(this.$refs['block'].innerText);\n\n        if (newElementName === 'publii-readmore' && this.editor.hasReadMore) {\n          if (window.app) {\n            this.$bus.$emit('alert-display', {\n              message: 'You can add only one read more per post.'\n            });\n          } else {\n            alert('You can add only one read more per post.');\n          }\n          e.returnValue = false;\n          return;\n        }\n\n        if (newElementName === 'publii-paragraph') {\n          document.execCommand('insertHTML', false, '<line-separator />');\n\n          if (this.$refs['block'].innerHTML.substr(-33) === '<line-separator></line-separator>') {\n            this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n            this.$refs['block'].innerHTML = this.$refs['block'].innerHTML.replace('<line-separator></line-separator>', '');\n          } else {\n            let separatedContent = this.$refs['block'].innerHTML.split('<line-separator></line-separator>');\n            let firstPart = separatedContent[0];\n            let secondPart = separatedContent[1];\n\n            if (secondPart.substr(0, 4) === '<br>') {\n              secondPart = secondPart.substr(4);\n            }\n\n            this.$refs['block'].innerHTML = firstPart;\n            this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id, secondPart);\n          }\n\n          this.save();\n        } else {\n          this.$bus.$emit('block-editor-add-block', newElementName, this.id);\n          this.$bus.$emit('block-editor-delete-block', this.id);\n        }\n\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['block'].innerHTML === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n        return;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['block'].innerHTML !== '' && this.cursorIsAtTheBeginning()) {\n        this.mergeParagraphs();\n        e.returnValue = false;\n        return;\n      }\n\n      if (e.code === 'Tab' && this.$refs['block'].innerHTML === '' && this.$parent.newBlockUIListVisible === false) {\n        this.$parent.toggleNewBlockUI();\n        e.returnValue = false;\n        return;\n      }\n    },\n    handleKeyUp (e) {\n      this.textIsHighlighted = false;\n\n      if (e.code === 'Backspace') {\n        e.preventDefault();\n        let range = document.getSelection().getRangeAt(0);\n        range.deleteContents();\n      }\n\n      if (e.code === 'Space') {\n        this.saveChangesHistory();\n      }\n    },\n    alignText (position) {\n      this.config.textAlign = position;\n    },\n    mergeParagraphs () {\n      this.save();\n\n      setTimeout(() => {\n        this.$bus.$emit('block-editor-merge-paragraphs', this.id);\n      }, 0);\n    },\n    save () {\n      this.content = this.$refs['block'] ? this.$refs['block'].innerHTML : '';\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    },\n    cursorIsAtTheBeginning () {\n      let selectedObject = document.getSelection();\n      return selectedObject.rangeCount === 1 && selectedObject.baseOffset === 0;\n    },\n    saveChangesHistory () {\n      this.$bus.$emit('undomanager-save-history', this.id, this.$refs['block'].innerHTML);\n    },\n    setContent (newContent) {\n      this.content = newContent;\n      this.$refs['block'].innerHTML = newContent;\n\n      setTimeout(() => {\n        this.setCursorAtEndOfElement();\n      }, 0);\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/variables.scss';\n\n.publii-block-paragraph {\n  outline: none;\n  width: 100%;\n\n  code,\n  mark {\n    display: inline;\n  }\n\n  &:empty {\n    &:before {\n      content: attr(data-translation);\n      color: var(--gray-4);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-paragraph/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"style\",\n        \"type\": \"select\",\n        \"label\": \"editor.blocks.paragraph.styleLabel\",\n        \"tooltip\": \"editor.blocks.paragraph.styleTooltip\",\n        \"values\": [\n            {\n                \"value\": \"msg msg--info\",\n                \"label\": \"editor.blocks.paragraph.styles.info\"\n            },\n            {\n                \"value\": \"msg msg--highlight\",\n                \"label\": \"editor.blocks.paragraph.styles.highlight\"\n            },\n            {\n                \"value\": \"msg msg--success\",\n                \"label\": \"editor.blocks.paragraph.styles.success\"\n            },\n            {\n                \"value\": \"msg msg--warning\",\n                \"label\": \"editor.blocks.paragraph.styles.warning\"\n            }\n        ],\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.paragraph.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-paragraph/conversions.js",
    "content": "const availableConversions = [\n  {\n    'icon': 'headings',\n    'name': 'editor.conversions.toHeader',\n    'type': 'publii-header',\n    'convert': function (config, content, editorInstance) {\n      // eslint-disable-next-line\n      let newContent = editorInstance.extensions.conversionHelpers.stripTags(content.replace(/<br>/gmi, \"\\n\")).replace(/\\n/gmi, '<br>');\n      let newConfig = {\n        headingLevel: 2,\n        textAlign: config.textAlign,\n        link: {\n          url: '',\n          noFollow: false,\n          targetBlank: false,\n          sponsored: false,\n          ugc: false\n        },\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          customId: false,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'quote',\n    'name': 'editor.conversions.toQuote',\n    'type': 'publii-quote',\n    'convert': function (config, content, editorInstance) {\n      let newConfig = {\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: {\n          text: content,\n          author: ''\n        },\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'unordered-list',\n    'name': 'editor.conversions.toList',\n    'type': 'publii-list',\n    'convert': function (config, content, editorInstance) {\n      let newContent = '<li>' + content.split('<br>').join('</li><li>') + '</li>';\n      let newConfig = {\n        listType: 'ul',\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'code',\n    'name': 'editor.conversions.toCode',\n    'type': 'publii-code',\n    'convert': function (config, content, editorInstance) {\n      // eslint-disable-next-line\n      let newContent = editorInstance.extensions.conversionHelpers.stripTags(content.replace(/<br>/gmi, \"\\n\"));\n      let newConfig = {\n        language: 'html',\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'html',\n    'name': 'editor.conversions.toHTML',\n    'type': 'publii-html',\n    'convert': function (config, content, editorInstance, rawBlock) {\n      let newContent = rawBlock.outerHTML.replace(/<p.*?>/gmi, '<p>');\n      let newConfig = {\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  }\n];\n\nmodule.exports = availableConversions;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-paragraph/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = [blockData.config.advanced.cssClasses, blockData.config.advanced.style, 'align-' + blockData.config.textAlign].filter(item => item && item.trim() !== '' && item !== 'align-left');\n  cssClasses = cssClasses.length ? ' class=\"' + cssClasses.join(' ') + '\"' : '';\n\n  let html = `\n  <p${id}${cssClasses}>\n    ${blockData.content}\n  </p>`;\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-quote/block.vue",
    "content": "<template>\n  <div :class=\"{ 'is-empty': isEmpty }\">\n    <div class=\"publii-block-quote-wrapper\">\n      <div\n        :class=\"{ 'publii-block-quote-form': true, 'is-visible': view === 'code' }\"\n        ref=\"block\">\n        <div\n          class=\"publii-block-quote-text\"\n          @focus=\"updateCurrentBlockID\"\n          @keyup=\"getFocusFromTab($event); handleCaretText($event); handleKeyUp($event); debouncedSave()\"\n          @keydown=\"handleTextKeyboard\"\n          @paste=\"pastePlainText\"\n          @mouseup=\"handleMouseUp\"\n          ref=\"contentText\"\n          contenteditable=\"true\"\n          v-initial-html=\"content.text\"></div>\n        <input\n          type=\"text\"\n          @focus=\"updateCurrentBlockID\"\n          @keyup=\"handleCaretAuthor($event); debouncedSave()\"\n          @keydown=\"handleAuthorKeyboard\"\n          v-model=\"content.author\"\n          :placeholder=\"$t('editor.quoteAuthor')\"\n          ref=\"contentAuthor\" />\n      </div>\n      <figure\n        v-if=\"view === 'preview'\"\n        class=\"publii-block-quote\"\n        ref=\"block\"\n        :data-translation=\"$t('editor.quoteText')\">\n          <blockquote v-html=\"content.text\" />\n          <figcaption v-html=\"content.author\" />\n      </figure>\n    </div>\n\n    <inline-menu ref=\"inline-menu\" />\n\n    <top-menu\n      ref=\"top-menu\"\n      :conversions=\"conversions\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport AvailableConversions from './conversions.js';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport ContentEditableImprovements from './../../helpers/ContentEditableImprovements.vue';\nimport HasPreview from './../../mixins/HasPreview.vue';\nimport InlineMenu from './../../mixins/InlineMenu.vue';\nimport InlineMenuUI from './../../helpers/InlineMenuUI.vue';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\nimport Utils from './../../utils/Utils.js';\n\nexport default {\n  name: 'Quote',\n  mixins: [\n    Block,\n    ContentEditableImprovements,\n    HasPreview,\n    InlineMenu\n  ],\n  components: {\n    'inline-menu': InlineMenuUI,\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      focusable: ['contentText', 'contentAuthor'],\n      caretIsAtStartText: false,\n      caretIsAtEndText: false,\n      caretIsAtStartAuthor: false,\n      caretIsAtEndAuthor: false,\n      config: {\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: {\n        text: '',\n        author: ''\n      },\n      conversions: AvailableConversions,\n      inlineMenuContainer: 'contentText',\n      topMenuConfig: [\n        {\n          activeState: () => false,\n          onClick: function () { this.clearContentHtml('contentText'); },\n          icon: 'eraser',\n          tooltip: this.$t('editor.clearFormatting')\n        }\n      ]\n    };\n  },\n  watch: {\n    'view': function (newValue, oldValue) {\n      if (oldValue === 'code' && newValue === 'preview') {\n        this.setCursorAtEndOfElement('contentText', true);\n      }\n    }\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  beforeMount () {\n    this.content = Utils.deepMerge(this.content, this.inputContent);\n  },\n  mounted () {\n    this.view = (!this.content.text && !this.content.author) ? 'code' : 'preview';\n  },\n  methods: {\n    handleKeyUp (e) {\n      this.textIsHighlighted = false;\n\n      if (e.code === 'Backspace') {\n        e.preventDefault();\n        let range = document.getSelection().getRangeAt(0);\n        range.deleteContents();\n      }\n    },\n    handleTextKeyboard (e) {\n      if (e.code === 'Backspace' && this.$refs['contentText'].value === '' && this.$refs['contentAuthor'].value === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n    },\n    handleAuthorKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing && e.shiftKey === false) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['contentAuthor'].value === '') {\n        this.$refs['contentText'].focus();\n        e.returnValue = false;\n      }\n    },\n    handleCaretText (e) {\n      if (e.code === 'ArrowUp' && this.getCursorPosition('contentText') === 0) {\n        if (!this.caretIsAtStartText) {\n          this.caretIsAtStartText = true;\n          return;\n        }\n\n        let previousBlockID = this.findPreviousBlockID();\n\n        if (previousBlockID) {\n          this.editor.$refs['block-wrapper-' + previousBlockID][0].blockClick();\n          this.editor.$refs['block-' + previousBlockID][0].focus();\n        }\n      }\n\n      if (e.code === 'ArrowDown' && this.getCursorPosition('contentText') >= this.$refs['contentText'].innerHTML.length - 2) {\n        if (!this.caretIsAtEndText) {\n          this.caretIsAtEndText = true;\n          return;\n        }\n\n        this.$refs['contentAuthor'].focus();\n        e.returnValue = false;\n      }\n\n      if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {\n        this.caretIsAtStartText = false;\n        this.caretIsAtEndText = false;\n      }\n    },\n    handleCaretAuthor (e) {\n      if (e.code === 'ArrowUp' && this.getCursorPosition('contentAuthor') === 0) {\n        if (!this.caretIsAtStartAuthor) {\n          this.caretIsAtStartAuthor = true;\n          return;\n        }\n\n        this.$refs['contentText'].focus();\n        e.returnValue = false;\n      }\n\n      if (e.code === 'ArrowDown' && this.getCursorPosition('contentAuthor') >= this.$refs['contentAuthor'].value.length - 2) {\n        if (!this.caretIsAtEndAuthor) {\n          this.caretIsAtEndAuthor = true;\n          return;\n        }\n\n        let nextBlockID = this.findNextBlockID();\n\n        if (nextBlockID) {\n          this.editor.$refs['block-wrapper-' + nextBlockID][0].blockClick();\n          this.editor.$refs['block-' + nextBlockID][0].focus('none');\n        }\n      }\n\n      if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {\n        this.caretIsAtStartAuthor = false;\n        this.caretIsAtEndAuthor = false;\n      }\n    },\n    setView (newView) {\n      if (\n        this.view === 'code' &&\n        newView === 'preview'\n      ) {\n        this.save();\n      }\n\n      if (\n        !this.content.text &&\n        newView === 'preview'\n      ) {\n        this.view = 'code';\n      } else {\n        setTimeout(() => {\n          this.view = newView;\n        }, 0);\n      }\n    },\n    save () {\n      this.content.text = this.$refs['contentText'].innerHTML;\n      this.content.author = this.$refs['contentAuthor'].value;\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: JSON.parse(JSON.stringify(this.content))\n      });\n    },\n    saveChangesHistory () {\n      this.$bus.$emit('undomanager-save-history', this.id, this.$refs['block'].innerHTML);\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n@import '../../../../../scss/vendor/modularscale';\n\n\n.publii-block-quote {\n    outline: none;\n\n  p {\n    outline: none;\n  }\n\n  figcaption {\n    font-size: ms(-2) !important;\n    border-left: 2px solid var(--gray-3);\n    padding: baseline(2,em) 0 0 baseline(5,em);\n    text-align: left !important;\n  }\n\n  &-form {\n    display: none;\n    padding: 0;\n\n    &.is-visible {\n      display: block;\n    }\n\n    input {    \n      display: block;   \n      width: 100%;\n    }\n  }\n\n  &-text {\n    background: var(--input-bg);\n    border: 1px solid var(--input-border-color);\n    border-radius: 3px;\n    font-size: inherit;\n    line-height: inherit;\n    margin-bottom: 16px;\n    outline: none;\n    padding: 20px;\n    width: 100%;\n\n    &:empty {\n      &:before {\n        content: attr(data-translation);\n        color: var(--gray-4);\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-quote/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.quote.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-quote/conversions.js",
    "content": "const availableConversions = [\n  {\n    'icon': 'paragraph',\n    'name': 'editor.conversions.toParagraph',\n    'type': 'publii-paragraph',\n    'convert': function (config, content, editorInstance) {\n      let newConfig = {\n        textAlign: 'left',\n        advanced: {\n          style: '',\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: content.text,\n        config: newConfig\n      };\n    }\n  },\n  {\n    'icon': 'html',\n    'name': 'editor.conversions.toHTML',\n    'type': 'publii-html',\n    'convert': function (config, content, editorInstance, rawBlock) {\n      let newContent = content;\n      let newConfig = {\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  }\n];\n\nmodule.exports = availableConversions;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-quote/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = ['blockquote', blockData.config.advanced.cssClasses].filter(item => item && item.trim() !== '');\n  cssClasses = cssClasses.length ? ' class=\"' + cssClasses.join(' ') + '\"' : '';\n  let html = '';\n\n  if (blockData.content.author.trim() !== '') {\n    html = `\n    <figure${id}${cssClasses}>\n      <blockquote>${blockData.content.text}</blockquote>\n      <figcaption>${blockData.content.author}</figcaption>\n    </figure>`;\n  } else {\n    html = `\n    <blockquote${id}${cssClasses}>\n      ${blockData.content.text}\n    </blockquote>`;\n  }\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-readmore/block.vue",
    "content": "<template>\n  <div :class=\"{ 'is-empty': isEmpty }\">\n    <div\n      class=\"publii-block-readmore\"\n      contenteditable=\"true\"\n      @keyup=\"getFocusFromTab($event); handleCaret($event)\"\n      @paste=\"pastePlainText\"\n      @click=\"updateCurrentBlockID\"\n      ref=\"block\"\n      :data-translation=\"$t('editor.readMore')\">\n      <hr />\n    </div>\n\n    <top-menu\n      ref=\"top-menu\"\n      :config=\"[]\" />\n  </div>\n</template>\n\n<script>\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nexport default {\n  name: 'ReadMore',\n  mixins: [\n    Block\n  ],\n  components: {\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      config: {\n        advanced: {}\n      },\n      content: false\n    };\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  mounted () {\n    this.content = this.inputContent;\n    this.$refs['block'].addEventListener('keydown', this.handleKeyboard);\n  },\n  methods: {\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n      }\n\n      if (e.code === 'Backspace') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code !== 'Tab') {\n        e.returnValue = false;\n      }\n    },\n    save () {\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    }\n  },\n  beforeDestroy () {\n    this.$refs['block'].removeEventListener('keydown', this.handleKeyboard);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/vendor/modularscale';\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n\n.publii-block-readmore {\n  align-items: center;\n  caret-color: transparent;\n  display: flex;\n  justify-content: center;\n  outline: none;\n  width: 100%;\n\n  hr {\n    border: none;\n    height: auto;\n    line-height: 100%;\n    position: relative;\n  }\n\n  &::before {\n    border-bottom: 1px solid var(--text-primary-color);\n    content: \" \";\n    display: block;\n    position: relative;\n    transform: translateY(-50%);\n    width: 100%;\n    white-space: pre;\n  }\n\n  &::after {\n    background: var(--input-bg);\n    content: attr(data-translation);\n    display: inline-block;\n    font-family: var(--font-base);\n    font-size: ms(-3);\n    font-weight: var(--font-weight-semibold);\n    padding: 6px 16px;\n    position: absolute;\n    white-space: pre;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-readmore/config-form.json",
    "content": "[]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-readmore/render.js",
    "content": "function render () {\n  return `<hr id=\"read-more\">`;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-separator/block.vue",
    "content": "<template>\n  <div :class=\"{ 'is-empty': isEmpty }\">\n    <div\n      class=\"publii-block-separator\"\n      contenteditable=\"true\"\n      @keyup=\"getFocusFromTab($event); handleCaret($event)\"\n      @paste=\"pastePlainText\"\n      ref=\"block\">\n      <hr :class=\"config.type\" />\n    </div>\n\n    <top-menu\n      ref=\"top-menu\"\n      :conversions=\"conversions\"\n      :config=\"topMenuConfig\"\n      :advancedConfig=\"configForm\" />\n  </div>\n</template>\n\n<script>\nimport AvailableConversions from './conversions.js';\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\n\nexport default {\n  name: 'Separator',\n  mixins: [\n    Block\n  ],\n  components: {\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      config: {\n        type: 'dots',\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: false,\n      topMenuConfig: [\n        {\n          activeState: function () { return this.config.type === 'long-line'; },\n          onClick: function () { this.setType('long-line'); },\n          icon: 'long-line',\n          tooltip: this.$t('editor.wideLine')\n        },\n        {\n          activeState: function () { return this.config.type === 'dots'; },\n          onClick: function () { this.setType('dots'); },\n          icon: 'dotted-line',\n          tooltip: this.$t('editor.dots')\n        },\n        {\n          activeState: function () { return this.config.type === 'dot'; },\n          onClick: function () { this.setType('dot'); },\n          icon: 'dot',\n          tooltip: this.$t('editor.dot')\n        }\n      ],\n      conversions: AvailableConversions\n    };\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  mounted () {\n    this.$refs['block'].addEventListener('keydown', this.handleKeyboard);\n  },\n  methods: {\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code !== 'Tab') {\n        e.returnValue = false;\n      }\n    },\n    setType (type) {\n      this.config.type = type;\n    },\n    save () {\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: this.content\n      });\n    }\n  },\n  beforeDestroy () {\n    this.$refs['block'].removeEventListener('keydown', this.handleKeyboard);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../../scss/vendor/modularscale';\n@import '../../../../../scss/variables.scss';\n@import '../../../../../scss/mixins.scss';\n\n.publii-block-separator {\n  caret-color: transparent;\n  margin: baseline(2,em) 0;\n  outline: none;\n  width: 100%;\n\n  hr {\n    border: none;\n    height: auto;\n    line-height: 100%;\n    position: relative;\n  }\n\n  hr.long-line {\n\n      &::after {\n          border-bottom: 1px solid var(--input-border-color);\n          content: \" \";\n          display: block;\n          margin-top: -1px;\n          position: relative;\n          transform: translateY(-50%);\n          width: 100%;\n          white-space: pre;\n      }\n  }\n\n  hr.medium {\n    width: 50%;\n  }\n\n  hr.short {\n    width: 25%;\n  }\n\n  hr.dots,\n  hr.dot {\n    margin: 0 auto;\n\n    &:before {\n      content: \"* * *\";\n      display: block;\n      text-align: center;\n      transform: scale(1.5);\n    }\n  }\n\n  hr.dot {\n    &:before {\n      content: \"*\";\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-separator/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.separator.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-separator/conversions.js",
    "content": "const availableConversions = [\n  {\n    'icon': 'html',\n    'name': 'editor.conversions.toHTML',\n    'type': 'publii-html',\n    'convert': function (config, content, editorInstance, rawBlock) {\n      let newContent = rawBlock.innerHTML;\n      let newConfig = {\n        advanced: {\n          cssClasses: config.advanced.cssClasses,\n          id: config.advanced.id\n        }\n      };\n\n      return {\n        content: newContent,\n        config: newConfig\n      };\n    }\n  }\n];\n\nmodule.exports = availableConversions;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-separator/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = ['separator', blockData.config.advanced.cssClasses, 'separator--' + blockData.config.type].filter(item => item && item.trim() !== '');\n  cssClasses = cssClasses.length ? ' class=\"' + cssClasses.join(' ') + '\"' : '';\n  let html = `<hr${id}${cssClasses} />`;\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-toc/block.vue",
    "content": "<template>\n  <div :class=\"{ 'is-empty': isEmpty }\">\n    <div\n      class=\"publii-block-toc-wrapper\"\n      ref=\"block\">\n      <h3\n        class=\"publii-block-toc-title\"\n        ref=\"title\"\n        @focus=\"updateCurrentBlockID\"\n        @keydown=\"handleKeyboard\"\n        @keyup=\"handleCaret($event, 'title'); debouncedSave()\"\n        contenteditable=\"true\"\n        v-initial-html=\"content.title\"\n        :data-translation=\"$t('editor.addLabel')\"></h3>\n      <ol\n        class=\"publii-block-toc\"\n        ref=\"content\"\n        v-html=\"content.toc\"\n        :data-translation=\"$t('editor.tocAutoGenerationInfo')\">\n      </ol>\n    </div>\n\n    <top-menu\n      ref=\"top-menu\"\n      :config=\"[]\" />\n  </div>\n</template>\n\n<script>\nimport Block from './../../Block.vue';\nimport ConfigForm from './config-form.json';\nimport TopMenuUI from './../../helpers/TopMenuUI.vue';\nimport Utils from './../../utils/Utils.js';\n\nexport default {\n  name: 'ToC',\n  mixins: [\n    Block\n  ],\n  components: {\n    'top-menu': TopMenuUI\n  },\n  data () {\n    return {\n      focusable: ['title'],\n      config: {\n        advanced: {\n          cssClasses: this.getAdvancedConfigDefaultValue('cssClasses'),\n          id: this.getAdvancedConfigDefaultValue('id')\n        }\n      },\n      content: {\n        title: '',\n        toc: ''\n      }\n    };\n  },\n  beforeCreate () {\n    this.configForm = ConfigForm;\n  },\n  beforeMount () {\n    this.content = Utils.deepMerge(this.content, this.inputContent);\n  },\n  mounted () {\n    this.updateToc();\n    this.$bus.$on('block-editor-content-updated', this.updateToc);\n    this.$bus.$on('block-editor-block-selected', this.selectBlock);\n  },\n  methods: {\n    handleKeyboard (e) {\n      if (e.code === 'Enter' && !e.isComposing) {\n        this.$bus.$emit('block-editor-add-block', 'publii-paragraph', this.id);\n        e.returnValue = false;\n      }\n\n      if (e.code === 'Backspace' && this.$refs['title'].innerHTML === '') {\n        this.$bus.$emit('block-editor-delete-block', this.id);\n        e.returnValue = false;\n      }\n    },\n    updateToc () {\n      let headers = this.$parent.$parent.content.filter(block => block.type === 'publii-header');\n\n      if (headers.length === 0) {\n        this.content.toc = '';\n        return;\n      }\n\n      let html = '';\n      let prevLevel = 2;\n      let processedHeader;\n      let nextLevel;\n\n      if (!headers.length) {\n        return '';\n      }\n\n      for (let i = 0; i < headers.length; i++) {\n        processedHeader = headers[i];\n        let headingLevel = processedHeader.config.headingLevel || 2;\n\n        if (headers[i + 1]) {\n          nextLevel = headers[i + 1].config.headingLevel;\n        }\n\n        if (prevLevel === headingLevel) {\n          html += '<li>';\n        } else {\n          for (let j = prevLevel; j < headingLevel; j++) {\n            html += '<ol><li>';\n          }\n        }\n\n        let headingID = processedHeader.config.advanced ? processedHeader.config.advanced.id : '';\n        html += '<a href=\"#' + headingID + '\">' + processedHeader.content + '</a>';\n\n        if (!nextLevel || nextLevel === headingLevel) {\n          html += '</li>';\n\n          if (!nextLevel) {\n            html += '</ol>';\n          }\n        } else {\n          for (let j = headingLevel; j > nextLevel; j--) {\n            html += '</li></ol><li>';\n          }\n        }\n\n        prevLevel = headingLevel;\n      }\n\n      this.content.toc = html.replace(/<li>[\\s]*?<\\/li>/gmi, '');\n    },\n    selectBlock (id) {\n      if (this.id === id) {\n        this.focus('end');\n      }\n    },\n    save () {\n      this.content = {\n        toc: this.$refs['content'].innerHTML,\n        title: this.$refs['title'].innerHTML\n      };\n\n      this.$bus.$emit('block-editor-save-block', {\n        id: this.id,\n        config: JSON.parse(JSON.stringify(this.config)),\n        content: JSON.parse(JSON.stringify(this.content))\n      });\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-content-updated', this.updateToc);\n    this.$bus.$off('block-editor-block-selected', this.selectBlock);\n  }\n}\n</script>\n\n<style lang=\"scss\">\n\n.publii-block-toc {\n  caret-color: transparent;\n  display: block;\n  margin: 10px 0;\n  padding: 15px 0 15px 20px;\n  outline: none;\n  width: 100%;\n\n  &-wrapper {\n    outline: none;\n    width: 100%;\n\n    .publii-block-toc-title {\n      outline: none;\n      margin: 0 0 20px !important;\n      width: 100%;\n\n      &:empty {\n        &:before {\n          content: attr(data-translation);\n          color: var(--gray-4);\n        }\n      }\n    }\n\n    a {\n      pointer-events: none;\n    }\n  }\n\n  &:empty {\n    &:before {\n      content: attr(data-translation);\n      color: var(--gray-4);\n      display: block;\n      margin-left: -2em;\n      width: 100%;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-toc/config-form.json",
    "content": "[\n    {\n        \"name\": \"cssClasses\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.cssClassesLabel\",\n        \"tooltip\": \"editor.blocks.cssClassesTooltip\",\n        \"defaultValue\": \"\"\n    },\n    {\n        \"name\": \"id\",\n        \"type\": \"text\",\n        \"label\": \"editor.blocks.idLabel\",\n        \"tooltip\": \"editor.blocks.toc.idTooltip\",\n        \"defaultValue\": \"\"\n    }\n]"
  },
  {
    "path": "app/src/components/block-editor/components/default-blocks/publii-toc/render.js",
    "content": "function render (blockData) {\n  let id = blockData.config.advanced.id ? ' id=\"' + blockData.config.advanced.id + '\"' : '';\n  let cssClasses = ['post__toc', blockData.config.advanced.cssClasses].filter(item => item && item.trim() !== '');\n  cssClasses = cssClasses.length ? ' class=\"' + cssClasses.join(' ') + '\"' : '';\n  let tocHeading = '';\n\n  if (blockData.content.title.trim() !== '') {\n    tocHeading = `<h3>${blockData.content.title}</h3>`;\n  }\n\n  let html = `\n  <div${id}${cssClasses}>\n    ${tocHeading}\n    <ul>\n      ${blockData.content.toc.replace(/\\<ol/gmi, '<ul').replace(/\\<\\/ol\\>/gmi, '</ul>')}\n    </ul>\n  </div>\n  `;\n\n  return html;\n};\n\nmodule.exports = render;\n"
  },
  {
    "path": "app/src/components/block-editor/components/elements/EditorIcon.vue",
    "content": "<template>\n  <svg\n    :width=\"width\"\n    :height=\"height\">\n    <use :xlink:href=\"iconPath\" />\n  </svg>\n</template>\n\n<script>\nexport default {\n  name: 'editor-icon',\n  props: {\n    'name': {\n      default: '',\n      type: String\n    },\n    'customWidth': {\n      default: '',\n      type: [String, Number]\n    },\n    'customHeight': {\n      default: '',\n      type: [String, Number]\n    }\n  },\n  computed: {\n    width () {\n      if (this.customWidth !== '') {\n        return this.customWidth;\n      }\n\n      switch (this.name) {\n        case 'add': return 18;\n        case 'headings': return 18;\n        case 'image': return 18;\n        case 'gallery': return 26;\n        case 'unordered-list': return 18;\n        case 'quote': return 16;\n        case 'code': return 18;\n        case 'table': return 18;\n        case 'html': return 18;\n        case 'separator': return 18;\n        case 'readmore': return 18;\n        case 'video': return 18;\n        case 'toc': return 18;\n        case 'menu': return 15;\n        case 'tab': return 9;\n        case 'bold': return 18;\n        case 'italic': return 18;\n        case 'underline': return 18;\n        case 'strikethrough': return 18;\n        case 'link': return 18;\n        case 'unlink': return 18;\n        case 'marker': return 18;\n        case 'align-left': return 18;\n        case 'align-right': return 18;\n        case 'align-center': return 18;\n        case 'paragraph': return 18;\n        case 'h1': return 20;\n        case 'h2': return 20;\n        case 'h3': return 20;\n        case 'h4': return 20;\n        case 'h5': return 20;\n        case 'h6': return 20;\n        case 'ordered-list': return 18;\n        case 'nesting': return 18;\n        case 'flattening': return 18;\n        case 'long-line': return 18;\n        case 'dotted-line': return 18;\n        case 'dot': return 18;\n        case 'preview': return 19;\n        case 'left': return 18;\n        case 'center': return 18;\n        case 'right': return 18;\n        case 'wide': return 18;\n        case 'full': return 18;\n        case 'enter': return 11;\n        case 'down': return 18;\n        case 'up': return 18;\n        case 'trash': return 18;\n        case 'open-trash': return 18;\n        case 'gear': return 18;\n        case 'duplicate': return 18;\n        case 'eraser': return 18;\n        case 'open-bulk-edition': return 18;\n      }\n\n      return '16';\n    },\n    height () {\n      if (this.customHeight !== '') {\n        return this.customHeight;\n      }\n\n      switch (this.name) {\n        case 'add': return 18;\n        case 'headings': return 18;\n        case 'image': return 18;\n        case 'gallery': return 18;\n        case 'unordered-list': return 18;\n        case 'quote': return 16;\n        case 'code': return 18;\n        case 'table': return 14;\n        case 'html': return 18;\n        case 'separator': return 18;\n        case 'readmore': return 18;\n        case 'video': return 18;\n        case 'toc': return 18;\n        case 'menu': return 3;\n        case 'tab': return 5;\n        case 'bold': return 18;\n        case 'italic': return 18;\n        case 'underline': return 18;\n        case 'strikethrough': return 18;\n        case 'link': return 18;\n        case 'unlink': return 18;\n        case 'marker': return 18;\n        case 'align-left': return 18;\n        case 'align-right': return 18;\n        case 'align-center': return 18;\n        case 'paragraph': return 18;\n        case 'h1': return 20;\n        case 'h2': return 20;\n        case 'h3': return 20;\n        case 'h4': return 20;\n        case 'h5': return 20;\n        case 'h6': return 20;\n        case 'ordered-list': return 18;\n        case 'nesting': return 18;\n        case 'flattening': return 18;\n        case 'long-line': return 18;\n        case 'dotted-line': return 18;\n        case 'dot': return 18;\n        case 'preview': return 12;\n        case 'left': return 18;\n        case 'center': return 18;\n        case 'right': return 18;\n        case 'wide': return 18;\n        case 'full': return 18;\n        case 'enter': return 13;\n        case 'down': return 18;\n        case 'up': return 18;\n        case 'trash': return 18;\n        case 'open-trash': return 18;\n        case 'gear': return 18;\n        case 'duplicate': return 18;\n        case 'eraser': return 18;\n        case 'open-bulk-edition': return 18;\n      }\n\n      return '16';\n    },\n    iconPath () {\n        return `../src/assets/svg/svg-map-block-editor.svg#${this.name}`;\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "app/src/components/block-editor/components/elements/Switcher.vue",
    "content": "<template>\n  <span :class=\"{ \n    'switcher-wrapper': true,\n    'has-label': !!label\n  }\">\n    <span\n      :class=\"cssClasses\"\n      @click=\"toggle\"></span>\n    {{ label }}\n  </span>\n</template>\n\n<script>\nexport default {\n  name: 'switcher',\n  props: {\n    value: {\n      type: [Boolean, Number]\n    },\n    isMini: {\n      default: false,\n      type: Boolean\n    },\n    checked: {\n      default: false,\n      type: Boolean\n    },\n    onToggle: {\n      default: () => false,\n      type: Function\n    },\n    label: {\n        default: '',\n        type: String\n    },\n    disabled: {\n      default: false,\n      type: Boolean\n    },\n    lowerZindex: {\n      default: false,\n      type: Boolean\n    }\n  },\n  data () {\n    return {\n      isChecked: this.checked\n    };\n  },\n  computed: {\n    cssClasses () {\n      return {\n        'switcher': true,\n        'is-checked': this.isChecked,\n        'lower-zindex': this.lowerZindex,\n        'is-disabled': this.disabled,\n        'is-mini': this.isMini\n      };\n    }\n  },\n  watch: {\n    value: function (newValue, oldValue) {\n      this.isChecked = !!newValue;\n    }\n  },\n  mounted () {\n    if (this.value) {\n      this.isChecked = !!this.value;\n    }\n  },\n  methods: {\n    toggle () {\n      this.isChecked = !this.isChecked;\n      this.$emit('input', this.isChecked);\n      this.onToggle(this.isChecked);\n    },\n    getValue () {\n      return !!this.isChecked;\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../../../scss/variables.scss';\n\n.switcher {\n  background: var(--input-border-dark);\n  border-radius: 20px;\n  cursor: pointer;\n  display: inline-block;\n  height: 18px;\n  margin-right: .5rem;\n  position: relative;\n  top: 3px;\n  transition: all .28s ease;\n  width: 32px;\n  z-index: 1;\n\n  &-wrapper {\n    display: inline-flex;\n\n    &.has-label {\n      padding-right: 1rem;\n    }\n  }\n\n  &.lower-zindex {\n    z-index: 0;\n  }\n\n  &:after {\n    position: absolute;\n    left: 2px;\n    top: 2px;\n    display: block;\n    width: 14px;\n    height: 14px;\n    border-radius: 50%;\n    background: var(--input-bg-lightest);\n    content: '';\n    transition: all .28s ease;\n  }\n\n  &:active:after {\n    transform: scale(0.8);\n  }\n\n  &.is-checked {\n    background: var(--input-border-focus);\n\n    &:after {\n      left: 16px;\n      background: var(--input-bg-lightest);\n    }\n  }\n\n  &.is-disabled {\n    opacity: .5;\n    pointer-events: none;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/extensions/ConversionHelpers.js",
    "content": "export default class ConversionHelpers {\n  stripTags (input, saveLineBreaks = true) {\n    let div = document.createElement('div');\n    div.innerHTML = input.replace(/<br>/gmi, '[[[BR]]]').replace(/<br \\/>/gmi, '[[[BR]]]').replace(/<br\\/>/gmi, '[[[BR]]]');\n    // eslint-disable-next-line\n    let output = div.innerText.replace(/\\[\\[\\[BR\\]\\]\\]/gmi, \"\\n\");\n\n    return output;\n  }\n}\n"
  },
  {
    "path": "app/src/components/block-editor/components/extensions/ShortcutManager.js",
    "content": "export default class ShortcutManager {\n  constructor () {\n    this.shortcuts = {};\n    this.initDefaultShortcuts();\n    this.initMarkdownDefaultShortcuts();\n  }\n\n  initDefaultShortcuts () {\n    this.shortcuts['/separator'] = 'publii-separator';\n    this.shortcuts['/hr'] = 'publii-separator';\n    this.shortcuts['/header'] = 'publii-header';\n    this.shortcuts['/h1'] = 'publii-header-1';\n    this.shortcuts['/h2'] = 'publii-header-2';\n    this.shortcuts['/h3'] = 'publii-header-3';\n    this.shortcuts['/h4'] = 'publii-header-4';\n    this.shortcuts['/h5'] = 'publii-header-5';\n    this.shortcuts['/h6'] = 'publii-header-6';\n    this.shortcuts['/list'] = 'publii-list';\n    this.shortcuts['/quote'] = 'publii-quote';\n    this.shortcuts['/blockquote'] = 'publii-quote';\n    this.shortcuts['/code'] = 'publii-code';\n    this.shortcuts['/readmore'] = 'publii-readmore';\n    this.shortcuts['/more'] = 'publii-readmore';\n    this.shortcuts['/html'] = 'publii-html';\n    this.shortcuts['/toc'] = 'publii-toc';\n    // this.shortcuts['/embed'] = 'publii-embed';\n    this.shortcuts['/image'] = 'publii-image';\n    this.shortcuts['/img'] = 'publii-image';\n    this.shortcuts['/gallery'] = 'publii-gallery';\n  }\n\n  initMarkdownDefaultShortcuts () {\n    this.shortcuts['---'] = 'publii-separator';\n    this.shortcuts['***'] = 'publii-readmore';\n    this.shortcuts['#'] = 'publii-header-1';\n    this.shortcuts['##'] = 'publii-header-2';\n    this.shortcuts['###'] = 'publii-header-3';\n    this.shortcuts['####'] = 'publii-header-4';\n    this.shortcuts['#####'] = 'publii-header-5';\n    this.shortcuts['######'] = 'publii-header-6';\n    this.shortcuts['*'] = 'publii-list';\n    this.shortcuts['>'] = 'publii-quote';\n    this.shortcuts['```'] = 'publii-code';\n  }\n\n  checkContentForShortcuts (text) {\n    if (text !== '' && text.length < 24 && this.shortcuts[text.trim()]) {\n      return this.shortcuts[text.trim()];\n    }\n\n    return 'publii-paragraph';\n  }\n\n  add (shortcut, componentName) {\n    if (!this.shortcuts[shortcut]) {\n      this.shortcuts[shortcut] = componentName;\n    } else {\n      console.warn('The following shortcut is already defined: ' + shortcut + ' for the following block: ' + this.shortcuts[shortcut]);\n    }\n  }\n};\n"
  },
  {
    "path": "app/src/components/block-editor/components/extensions/UndoManager.js",
    "content": "export default class UndoManager {\n  constructor () {\n    this.history = [];\n    this.historyMaxLength = 500;\n  }\n\n  saveHistory (blockID, blockContent) {\n    let historyLength = this.history.unshift({\n      id: blockID,\n      content: JSON.parse(JSON.stringify(blockContent))\n    });\n\n    if (historyLength > this.historyMaxLength) {\n      this.history = this.history.slice(0, this.historyMaxLength - 1);\n    }\n  }\n\n  undoHistory (blockID) {\n    for (let i = 0; i < this.history.length; i++) {\n      if (this.history[i].id === blockID) {\n        let content = JSON.parse(JSON.stringify(this.history[i].content));\n        this.history.splice(i, 1);\n        return content;\n      }\n    }\n  }\n\n  redoHistory (blockID) {\n\n  }\n};\n"
  },
  {
    "path": "app/src/components/block-editor/components/helpers/ContentEditableImprovements.vue",
    "content": "<script>\nexport default {\n  name: 'ContentEditableImprovements'\n}\n</script>\n\n<style lang=\"scss\">\n*[contenteditable=\"true\"] {\n  display: inline-block;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/helpers/InlineMenuUI.vue",
    "content": "<template>\n  <div\n    v-if=\"showInlineMenu\"\n    class=\"wrapper-ui-inline-menu\"\n    :style=\"'left: ' + left + '; top: ' + top + ';'\"\n    :key=\"'inline-menu-' + $parent.id\">\n    <div class=\"wrapper-ui-inline-menu-buttons\">\n      <button\n        :class=\"{ 'wrapper-ui-inline-menu-button': true, 'is-active': $parent.selectedText.containedTags.strong }\"\n        @click.stop=\"$parent.doInlineOperation('strong');\">\n        <icon name=\"bold\" />\n      </button>\n      <button\n        :class=\"{ 'wrapper-ui-inline-menu-button': true, 'is-active': $parent.selectedText.containedTags.em }\"\n        @click.stop=\"$parent.doInlineOperation('em');\">\n        <icon name=\"italic\" />\n      </button>\n      <button\n        :class=\"{ 'wrapper-ui-inline-menu-button': true, 'is-active': $parent.selectedText.containedTags.u }\"\n        @click.stop=\"$parent.doInlineOperation('u');\">\n        <icon name=\"underline\" />\n      </button>\n      <button\n        :class=\"{ 'wrapper-ui-inline-menu-button': true, 'is-active': $parent.selectedText.containedTags.s }\"\n        @click.stop=\"$parent.doInlineOperation('s');\">\n        <icon name=\"strikethrough\" />\n      </button>\n      <button\n        :class=\"{ 'wrapper-ui-inline-menu-button': true, 'is-active': $parent.selectedText.containedTags.a }\"\n        @click.stop=\"$parent.showLinkPopup();\">\n        <icon name=\"link\" />\n      </button>\n      <button\n        v-if=\"$parent.selectedText.containedTags.a\"\n        :class=\"{ 'wrapper-ui-inline-menu-button': true }\"\n        @click.stop=\"$parent.doInlineOperation('unlink');\">\n        <icon name=\"unlink\" />\n      </button>\n      <button\n        :class=\"{ 'wrapper-ui-inline-menu-button': true, 'is-active': $parent.selectedText.containedTags.code }\"\n        @click.stop=\"$parent.doInlineOperation('code');\">\n        <icon name=\"code\" />\n      </button>\n      <button\n        :class=\"{ 'wrapper-ui-inline-menu-button': true, 'is-active': $parent.selectedText.containedTags.mark }\"\n        @click.stop=\"$parent.doInlineOperation('mark');\">\n        <icon name=\"marker\" />\n      </button>\n      <button\n        v-if=\"$parent.$parent.blockType === 'publii-list'\"\n        :class=\"{ 'wrapper-ui-inline-menu-button': true }\"\n        :disabled=\"!$parent.selectedText.allowedOperations.indent\"\n        @click.stop=\"$parent.doInlineOperation('indent');\">\n        <icon name=\"nesting\" />\n      </button>\n      <button\n        v-if=\"$parent.$parent.blockType === 'publii-list'\"\n        :class=\"{ 'wrapper-ui-inline-menu-button': true }\"\n        :disabled=\"!$parent.selectedText.allowedOperations.outdent\"\n        @click.stop=\"$parent.doInlineOperation('outdent');\">\n        <icon name=\"flattening\" />\n      </button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport EditorIcon from './../elements/EditorIcon.vue';\n\nexport default {\n  name: 'inline-menu-ui',\n  components: {\n    'icon': EditorIcon\n  },\n  computed: {\n    showInlineMenu () {\n      return this.$parent.$parent.isSelected && this.$parent.textIsHighlighted && !this.$parent.$parent.uiOpened;\n    }\n  },\n  data () {\n    return {\n      left: '',\n      top: ''\n    };\n  },\n  methods: {\n    setPosition (left, top) {\n      this.left = left;\n      this.top = top;\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../scss/variables.scss';\n\n.is-highlighted {\n  background: var(--color-primary);\n}\n\n.wrapper-ui-inline-menu {\n  animation: inlineMenuIn .15s ease backwards;\n  align-items: center;\n  background: var(--popup-bg);\n  border: none;\n  border-radius: var(--border-radius);\n  box-shadow: 0 5px 10px -5px var(--shadow), 4px -11px 26px -12px var(--shadow), 0 24px 50px 2px var(--shadow);\n  left: 50%;\n  margin-top: 16px;\n  min-height: 44px;\n  padding: 0 4px;\n  position: absolute;\n  top: 0%;\n  transform: scale(1) translateX(-50%) translateY(64px);\n  transform-origin: center left;\n  width: auto;\n  z-index: 10;\n\n  @keyframes inlineMenuIn {\n    0% {\n        opacity: 0;\n        transform: scale(.9) translateX(-50%) translateY(78px);\n    }\n\n    100% {\n        opacity: 1;\n    }\n  }\n\n  &::after {\n    border: 9px solid var(--popup-bg);\n    border-left-color: transparent;\n    border-right-color: transparent;\n    border-top-color: transparent;\n    content: \"\";\n    height: 18px;\n    left: 50%;\n    position: absolute;\n    top: -18px;\n    transform: translateX(-50%);\n    width: 18px;\n    z-index: 1;\n  }\n\n  &-button {\n    align-items: center;\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    display: flex;\n    height: 100%;\n    min-height: 34px;\n    justify-content: center;\n    margin: 0;\n    outline: none;\n    padding: 0;\n    position: relative;\n    width: 38px;\n\n    &[disabled] {\n      opacity: .3;\n      pointer-events: none;\n    }\n\n    svg {\n      color: var(--icon-tertiary-color);\n    }\n\n    // hover effect\n    &::before {\n       content: \"\";\n       background: var(--gray-6);\n       border-radius: 3px;\n       display: block;\n       left: 50%;\n       opacity: 0;\n       position: absolute;\n       height: 34px;\n       top: 50%;\n       transition: all .15s cubic-bezier(.4,0,.2,1);\n       transform: scale(.5) translate(-50%, -50%);\n       transform-origin: left top;\n       width: 34px;\n       z-index: -1;\n    }\n\n    &:hover,\n    &.is-active {\n\n      &::before {\n         opacity: 1;\n         transform: scale(1) translate(-50%, -50%);\n      }\n    }\n  }\n\n  &-buttons {\n    cursor: pointer;\n    display: flex;\n    height: 43px;\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/helpers/TopMenuUI.vue",
    "content": "<template>\n  <div\n    class=\"wrapper-ui-top-menu\"\n    v-if=\"isVisible\"\n    @click=\"resetDeleteConfirmation\">\n    <span\n      v-if=\"conversions.length\"\n      class=\"wrapper-ui-top-menu-title wrapper-ui-top-menu-conversions\">\n      {{ $t('editor.convertTo') }}\n      <span\n        v-for=\"(conversion, index) of conversions\"\n        :key=\"'conversion-' + index\"\n        class=\"wrapper-ui-top-menu-conversion has-tooltip\"\n        @click=\"makeConversion(conversion.type, conversion.convert); resetDeleteConfirmation();\">\n        <icon :name=\"conversion.icon\" />\n        <span class=\"ui-tooltip\">\n          {{ $t(conversion.name) }}\n        </span>\n      </span>\n    </span>\n    <div\n      @click.stop\n      class=\"wrapper-ui-top-menu-options\">\n      <template v-for=\"(uiElement, index) of filteredConfig\">\n        <template v-if=\"!uiElement.type || uiElement.type === 'button'\">\n          <button\n            :key=\"'top-menu-element-' + index\"\n            :class=\"{ 'wrapper-ui-top-menu-button': true, 'is-active': uiElement.activeState.bind($parent)(), 'has-tooltip': true }\"\n            tabindex=\"-1\"\n            @click=\"uiElement.onClick.bind($parent)(); resetDeleteConfirmation();\">\n            <icon :name=\"uiElement.icon\" />\n            <span class=\"ui-tooltip has-bigger-space\">\n              {{ uiElement.tooltip }}\n            </span>\n          </button>\n        </template>\n        <template v-else-if=\"uiElement.type === 'select'\">\n          <label :key=\"'top-menu-element-label-' + index\">\n            {{ uiElement.label }}\n          </label>\n          <vue-select\n            :key=\"'top-menu-element-' + index\"\n            :class=\"uiElement.cssClasses\"\n            :options=\"uiElement.options\"\n            :clearable=\"uiElement.clearable\"\n            :searchable=\"uiElement.searchable\"\n            v-model=\"$parent.config[uiElement.configKey]\" />\n        </template>\n      </template>\n      <button\n        v-if=\"$parent.$parent.blockType !== 'publii-readmore'\"\n        :class=\"{ 'wrapper-ui-top-menu-button': true, 'is-active': settingsAreChanged, 'has-tooltip': true }\"\n        tabindex=\"-1\"\n        @click.stop=\"showAdvancedConfig(); resetDeleteConfirmation();\">\n        <icon name=\"gear\" />\n        <span class=\"ui-tooltip has-bigger-space\">\n          {{ $t('settings.advancedOptions') }}\n        </span>\n      </button>\n      <button\n        v-if=\"!confirmDelete\"\n        class=\"wrapper-ui-top-menu-button has-tooltip\"\n        tabindex=\"-1\"\n        @click.stop=\"deleteBlock\">\n        <icon name=\"trash\" />\n        <span class=\"ui-tooltip has-bigger-space\">\n          {{ $t('editor.deleteBlock') }}\n        </span>\n      </button>\n      <button\n        v-if=\"confirmDelete\"\n        class=\"wrapper-ui-top-menu-button top-menu-button-trash is-active has-tooltip\"\n        tabindex=\"-1\"\n        @click.stop=\"deleteBlock\">\n        <icon name=\"open-trash\" />\n        <span class=\"ui-tooltip has-bigger-space\">\n          {{ $t('editor.clickToConfirm') }}\n        </span>\n      </button>\n    </div>\n  </div>\n</template>\n\n<script>\nimport EditorIcon from './../elements/EditorIcon.vue';\nimport vSelect from 'vue-multiselect/dist/vue-multiselect.min.js';\n\nexport default {\n  name: 'top-menu-ui',\n  props: {\n    'config': {\n      type: Array,\n      default: () => ([])\n    },\n    'advancedConfig': {\n      type: [Array, Boolean],\n      default: false\n    },\n    'conversions': {\n      type: Array,\n      default: () => ([])\n    }\n  },\n  components: {\n    'icon': EditorIcon,\n    'vue-select': vSelect\n  },\n  computed: {\n    filteredConfig () {\n      return this.config.filter(uiElement => typeof uiElement.isVisible === 'undefined' || uiElement.isVisible());\n    },\n    isVisible () {\n      return this.$parent.$parent.uiOpened;\n    },\n    settingsAreChanged () {\n      if (!this.advancedConfig) {\n        return false;\n      }\n\n      let settingsKeys = Object.keys(this.$parent.config.advanced);\n\n      for (let i = 0; i < settingsKeys.length; i++) {\n        let key = settingsKeys[i];\n\n        if (this.advancedConfig.length && this.settingIsDisabled(key)) {\n          return false;\n        }\n\n        if (this.$parent.config.advanced[key] !== this.getSettingDefaultValue(key)) {\n          return true;\n        }\n      }\n\n      return false;\n    }\n  },\n  watch: {\n    isVisible (newValue) {\n      if (newValue) {\n        this.confirmDelete = false;\n      }\n    }\n  },\n  data () {\n    return {\n      confirmDelete: false\n    };\n  },\n  methods: {\n    makeConversion (outputType, convertCallback) {\n      let transformedData = convertCallback(this.$parent.config, this.$parent.content, this.$parent.editor, this.$parent.$refs['block']);\n      this.$bus.$emit('block-editor-convert-block', this.$parent.id, outputType, transformedData);\n    },\n    deleteBlock () {\n      if (!this.confirmDelete) {\n        this.confirmDelete = true;\n      } else {\n        this.$bus.$emit('block-editor-delete-block', this.$parent.id);\n      }\n    },\n    showAdvancedConfig () {\n      this.$bus.$emit('block-editor-trigger-advanced-config', this.$parent.id);\n    },\n    resetDeleteConfirmation () {\n      this.confirmDelete = false;\n    },\n    settingIsDisabled (fieldName) {\n      let fieldDisabledRules = this.getSetting(fieldName).disabled;\n\n      if (typeof fieldDisabledRules === 'undefined') {\n        return false;\n      }\n\n      for (let rule of fieldDisabledRules) {\n        if (this.$parent.config.advanced[rule.field] === rule.value) {\n          return true;\n        }\n      }\n\n      return false;\n    },\n    getSetting (fieldName) {\n      let index = this.advancedConfig.findIndex(field => field.name === fieldName);\n      return this.advancedConfig[index];\n    },\n    getSettingDefaultValue (fieldName) {\n      let index = this.advancedConfig.findIndex(field => field.name === fieldName);\n      return this.advancedConfig[index].defaultValue;\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n@import '../../../../scss/variables.scss';\n\n.wrapper-ui-top-menu {\n  font-family: var(--font-base);\n  \n  svg {\n    color: var(--icon-tertiary-color);\n  }\n\n  &-conversions {\n    align-items: center;\n    display: flex;\n  }\n\n  &-conversion {\n    display: inline-flex;\n    justify-content: center;\n    padding: 0;\n    position: relative;\n    width: 38px;\n\n    // hover effect\n    &::before {\n       content: \"\";\n       background: var(--gray-6);\n       border-radius: 3px;\n       display: block;\n       left: 50%;\n       opacity: 0;\n       position: absolute;\n       height: 34px;\n       top: 50%;\n       transition: all .15s cubic-bezier(.4,0,.2,1);\n       transform: scale(.5) translate(-50%, -50%);\n       transform-origin: left top;\n       width: 34px;\n       z-index: -1;\n    }\n\n    &:hover {\n      cursor: pointer;\n\n      &::before {\n         opacity: 1;\n         transform: scale(1) translate(-50%, -50%);\n      }\n\n      & > svg {\n         color: var(--icon-tertiary-color);\n      }\n    }\n  }\n\n  &-options {\n    align-items: center;\n    display: flex;\n\n    label {\n      font-size: 15px;\n      padding-right: 10px;\n    }\n  }\n\n  .top-menu-button-trash {\n      &::before {\n         background: var(--warning);\n      }\n         svg {\n            color: var(--white);\n      }\n  }\n\n  .multiselect {\n    font-size: 13px;\n    min-height: auto;\n    margin-left: auto;\n    margin-right: 6px;\n    position: relative;\n    top: 0;\n    width: auto;\n\n    &__tags {\n      background: var(--bg-secondary);\n      border: 2px solid var(--input-border-color);\n      color: var(--text-primary-color);\n      height: 34px;\n      min-height: 100%;\n      padding: 4px 40px 5px 14px;\n      min-width: 34px;\n    }\n\n    &__placeholder {\n      color: var(--text-light-color);\n      display: block;\n      font-size: $app-font-base;\n      margin-bottom: 0;\n      padding-top: 1px;\n    }\n\n    &__single {\n      background: transparent;\n      color: var(--text-primary-color);\n      min-height: 20px;\n      line-height: 20px;\n      margin-bottom: 0;\n      padding: 1px 0 0 0;\n    }\n\n    &__select {\n      height: 28px;\n      top: 3px;\n      width: 34px;\n\n      &::before {\n          border-color: var(--gray-3) transparent transparent;\n      }\n    }\n\n    &__content {\n      margin-left: 0!important;\n    }\n\n    &__element {\n      padding-left: 0!important;\n    }\n\n    &__content-wrapper {\n      background: var(--bg-secondary);\n      border: 2px solid var(--input-border-color);\n      border-top: none;\n      color: var(--text-primary-color);\n      margin-top: -1px;\n\n      & > ul {\n        padding: 0 !important;\n      }\n    }\n\n    &__option {\n        font-size: 14px;\n        padding: 8px 15px;\n        min-height: 30px;\n\n      &--highlight {\n        background: var(--input-bg-light);\n        color: var(--text-primary-color);\n\n        &:after {\n          display: none;\n        }\n      }\n\n      &.multiselect__option--selected {\n        background: var(--gray-1);\n        color: var(--text-primary-color);\n\n        &:after {\n          display: none;\n        }\n      }\n    }\n\n    &__input {\n      background: none !important;\n      color: var(--text-primary-color);\n      font-size: 14px;\n      height: 21px;\n\n      &::placeholder {\n        color: var(--gray-4);\n      }\n    }\n\n    &.is-narrow {\n      .multiselect__select {\n        display: none;\n      }\n\n      .multiselect__tags {\n        padding: 4px 0 6px 0;\n        text-align: center;\n      }\n\n      .multiselect__option {\n        min-height: 30px;\n        padding: 8px 0;\n        text-align: center;\n      }\n    }\n  }\n}\n\n.ui-tooltip {\n  background: var(--input-bg-light);\n  border-radius: var(--border-radius);\n  box-shadow: 0 2px 6px rgba(0, 0, 0, .16);\n  color: var(--text-primary-color);\n  display: flex;\n  flex-wrap: wrap;\n  font-family: var(--font-base);\n  font-size: 13px;\n  font-weight: normal;\n  justify-content: center;\n  height: auto;\n  left: 50%;\n  min-width: 64px;\n  opacity: 0;\n  padding: 5px 8px;\n  pointer-events: none;\n  position: absolute;\n  text-transform: none;\n  top: 34px;\n  white-space: nowrap;\n  z-index: 10;\n\n  &.has-bigger-space {\n    top: 42px;\n  }\n\n  &:after {\n    border: 6px solid var(--input-bg-light);\n    border-left-color: transparent;\n    border-right-color: transparent;\n    border-top-color: transparent;\n    content: \"\";\n    filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, .08));\n    height: 14px;\n    left: 50%;\n    position: absolute;\n    top: -10px;\n    transform: scale(.5) translateX(-50%);\n    transform-origin: center center;\n    width: 14px;\n  }\n}\n\n.has-tooltip {\n  position: relative;\n\n  &:hover {\n    .ui-tooltip {\n      opacity: 1;\n      transform: scale(1) translateX(-50%);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/block-editor/components/mixins/AdvancedConfig.vue",
    "content": "<script>\nexport default {\n  name: 'AdvancedConfig',\n  mounted () {\n    this.$bus.$on('block-editor-save-advanced-config', this.saveAdvancedConfig);\n    this.$bus.$on('block-editor-trigger-advanced-config', this.showAdvancedConfig);\n  },\n  methods: {\n    getAdvancedConfigDefaultValue (fieldName) {\n      let index = this.configForm.findIndex(field => field.name === fieldName);\n      return this.configForm[index].defaultValue;\n    },\n    saveAdvancedConfig (blockID, savedValues) {\n      if (blockID !== this.id) {\n        return;\n      }\n\n      let keys = Object.keys(savedValues);\n\n      for (let key of keys) {\n        this.config.advanced[key] = savedValues[key];\n      }\n\n      this.save();\n    },\n    showAdvancedConfig (blockID) {\n      if (blockID !== this.id) {\n        return;\n      }\n\n      this.$bus.$emit('block-editor-show-advanced-config', this.id, this.config.advanced, this.configForm);\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-save-advanced-config', this.saveAdvancedConfig);\n    this.$bus.$off('block-editor-trigger-advanced-config', this.showAdvancedConfig);\n  }\n};\n</script>\n"
  },
  {
    "path": "app/src/components/block-editor/components/mixins/HasPreview.vue",
    "content": "<script>\nexport default {\n  name: 'has-preview',\n  data () {\n    return {\n      view: 'code'\n    };\n  },\n  mounted () {\n    this.$bus.$on('block-editor-deselect-blocks', this.deselectBlock);\n    this.$bus.$on('block-editor-block-selected', this.selectBlock);\n  },\n  methods: {\n    setView (newView) {\n      if (\n        this.view === 'code' &&\n        newView === 'preview'\n      ) {\n        this.save();\n      }\n\n      if (\n        this.content === '' &&\n        newView === 'preview'\n      ) {\n        this.view = 'code';\n      } else {\n        setTimeout(() => {\n          this.view = newView;\n        }, 0);\n      }\n    },\n    selectBlock (id) {\n      if (this.id === id) {\n        this.setView('code');\n\n        setTimeout(() => {\n          this.focus('end');\n        }, 0);\n      }\n    },\n    deselectBlock (id) {\n      if (this.id !== id) {\n        this.setView('preview');\n      }\n    },\n    showPreview () {\n      this.setView('preview');\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-deselect-blocks', this.deselectBlock);\n    this.$bus.$off('block-editor-block-selected', this.selectBlock);\n  }\n}\n</script>\n"
  },
  {
    "path": "app/src/components/block-editor/components/mixins/InlineMenu.vue",
    "content": "<script>\nimport EditorIcon from './../elements/EditorIcon.vue';\nimport LinkConfig from './../mixins/LinkConfig.vue';\nimport SelectedText from './../utils/SelectedText.js';\nimport Vue from 'vue';\n\nexport default {\n  name: 'InlineMenu',\n  mixins: [\n    LinkConfig\n  ],\n  components: {\n    'icon': EditorIcon\n  },\n  data () {\n    return {\n      inlineMenuContainer: 'block',\n      selectedText: {\n        containedTags: {},\n        allowedOperations: {}\n      }\n    };\n  },\n  methods: {\n    handleMouseUp (e) {\n      setTimeout(() => {\n        let sel = document.getSelection();\n\n        if (sel.isCollapsed) {\n          this.textIsHighlighted = false;\n        } else {\n          this.textIsHighlighted = !sel.isCollapsed || (sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset);\n        }\n\n        if (this.textIsHighlighted) {\n          setTimeout(() => {\n            this.showInlineMenu();\n          }, 0);\n        }\n      }, 0);\n    },\n    showInlineMenu () {\n      let sel = document.getSelection();\n      this.selectedText = new SelectedText(this.$refs[this.inlineMenuContainer], this.$parent.blockType);\n      this.selectedText.analyzeSelectedText();\n      let oRange = sel.getRangeAt(0);\n      let oRect = oRange.getBoundingClientRect();\n      let wrapperRect = this.$refs[this.inlineMenuContainer].getBoundingClientRect();\n      let inlineMenuOffsets = this.getInlineMenuOffsets();\n      let inlineMenuLeft = (((oRect.left - wrapperRect.left) + (oRect.width / 2)) + inlineMenuOffsets.x) + 'px';\n      let inlineMenuTop = ((oRect.top - wrapperRect.top) + oRect.height + inlineMenuOffsets.y) + 'px';\n      this.$refs['inline-menu'].setPosition(inlineMenuLeft, inlineMenuTop);\n    },\n    updateInlineMenuPosition () {\n      let sel = document.getSelection();\n\n      if (sel.toString() === '') {\n        this.closeInlineMenu();\n        return;\n      }\n\n      let oRange = sel.getRangeAt(0);\n      let oRect = oRange.getBoundingClientRect();\n      let wrapperRect = this.$refs[this.inlineMenuContainer].getBoundingClientRect();\n      let inlineMenuOffsets = this.getInlineMenuOffsets();\n      let inlineMenuLeft = (((oRect.left - wrapperRect.left) + (oRect.width / 2)) + inlineMenuOffsets.x) + 'px';\n      let inlineMenuTop = ((oRect.top - wrapperRect.top) + oRect.height + inlineMenuOffsets.y) + 'px';\n      this.$refs['inline-menu'].setPosition(inlineMenuLeft, inlineMenuTop);\n    },\n    closeInlineMenu () {\n      this.textIsHighlighted = false;\n    },\n    getInlineMenuOffsets () {\n      let x = 30;\n      let y = -50;\n\n      if (this.$parent.blockType === 'publii-quote') {\n        y = -40;\n      }\n\n      if (this.$parent.blockType === 'publii-list') {\n        x = 65;\n      }\n\n      return { x, y };\n    },\n    doInlineOperation (operationType) {\n      switch (operationType) {\n        case 'strong': this.execCommand('strong'); break;\n        case 'em': this.execCommand('em'); break;\n        case 's': this.execCommand('s'); break;\n        case 'u': this.execCommand('u'); break;\n        case 'code': this.execCommand('code'); break;\n        case 'mark': this.execCommand('mark'); break;\n        case 'unlink': this.removeLink(); break;\n        case 'indent': this.indentList(); break;\n        case 'outdent': this.outdentList(); break;\n      }\n\n      this.selectedText = new SelectedText(this.$refs[this.inlineMenuContainer], this.$parent.blockType);\n      this.selectedText.analyzeSelectedText();\n\n      Vue.nextTick(() => {\n        this.$parent.saveChangesHistory();\n      });\n    },\n    execCommand (tagToUse) {\n      if (this.selectedText.containedTags[tagToUse]) {\n        this.selectedText.removeStyle(tagToUse);\n        setTimeout(() => {\n          Vue.set(this.selectedText.containedTags, tagToUse, false);\n        }, 0);\n      } else {\n        let range = window.getSelection().getRangeAt(0);\n        let newTag = document.createElement(tagToUse);\n        newTag.appendChild(range.extractContents());\n        range.insertNode(newTag);\n      }\n    },\n    indentList () {\n      document.execCommand('indent', false, null);\n      setTimeout(() => {\n        this.updateInlineMenuPosition();\n      }, 100);\n    },\n    outdentList () {\n      document.execCommand('outdent', false, null);\n      setTimeout(() => {\n        this.updateInlineMenuPosition();\n      }, 100);\n    },\n    removeLink () {\n      let selection = document.getSelection();\n      let linkToRemove = this.findFirstLinkInSelection(selection);\n\n      if (linkToRemove) {\n        let selection = window.getSelection();\n        selection.removeAllRanges();\n        let range = document.createRange();\n        range.selectNodeContents(linkToRemove);\n        selection.addRange(range);\n        let firstChild = linkToRemove.firstChild;\n        let lastChild = linkToRemove.firstChild;\n\n        while (linkToRemove.firstChild) {\n          lastChild = linkToRemove.firstChild;\n          linkToRemove.parentNode.insertBefore(linkToRemove.firstChild, linkToRemove);\n        }\n\n        linkToRemove.parentNode.removeChild(linkToRemove);\n        range.setStartBefore(firstChild);\n        range.setEndAfter(lastChild);\n      }\n    },\n    findFirstLinkInSelection (selection) {\n      let anchorNode = selection.anchorNode;\n      let focusNode = selection.focusNode;\n      let range = selection.getRangeAt(0);\n\n      if (\n        anchorNode.previousElementSibling &&\n        anchorNode.previousElementSibling.tagName === 'A' &&\n        range.intersectsNode(anchorNode.previousElementSibling)\n      ) {\n        return anchorNode.previousElementSibling;\n      }\n\n      if (\n        anchorNode.nextElementSibling &&\n        anchorNode.nextElementSibling.tagName === 'A' &&\n        range.intersectsNode(anchorNode.nextElementSibling)\n      ) {\n        return anchorNode.nextElementSibling;\n      }\n\n      if (anchorNode.parentNode.tagName === 'A') {\n        return anchorNode.parentNode;\n      }\n\n      if (anchorNode.nodeType === 1 && anchorNode.closest('a')) {\n        return anchorNode.closest('a');\n      }\n\n      if (\n        focusNode.previousElementSibling &&\n        focusNode.previousElementSibling.tagName === 'A' &&\n        range.intersectsNode(focusNode.previousElementSibling)\n      ) {\n        return focusNode.previousElementSibling;\n      }\n\n      if (\n        focusNode.nextElementSibling &&\n        focusNode.nextElementSibling.tagName === 'A' &&\n        range.intersectsNode(focusNode.nextElementSibling)\n      ) {\n        return focusNode.nextElementSibling;\n      }\n\n      if (focusNode.parentNode.tagName === 'A') {\n        return focusNode.parentNode;\n      }\n\n      if (focusNode.nodeType === 1 && focusNode.closest('a')) {\n        return focusNode.closest('a');\n      }\n\n      return null;\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "app/src/components/block-editor/components/mixins/LinkConfig.vue",
    "content": "<script>\nexport default {\n  name: 'LinkConfig',\n  data () {\n    return {\n      linkCreationMode: 'new'\n    };\n  },\n  mounted () {\n    this.$bus.$on('block-editor-save-link-popup', this.saveLinkConfig);\n    this.$bus.$on('block-editor-hide-link-popup', this.removeHighlight);\n  },\n  methods: {\n    saveLinkConfig (blockID, linkConfig) {\n      if (\n        blockID !== this.id ||\n        this.$parent.blockType === 'publii-header' ||\n        this.$parent.blockType === 'publii-image'\n      ) {\n        return;\n      }\n\n      this.config.link = JSON.parse(JSON.stringify(linkConfig));\n\n      if (this.config.link.url !== '') {\n        if (this.linkCreationMode === 'new') {\n          this.createNewLinkFromSelection();\n        } else if (this.linkCreationMode === 'edit') {\n          this.editSelectedLink();\n        }\n      }\n\n      this.save();\n    },\n    showLinkPopupWithoutHighlight () {\n      this.$bus.$emit('block-editor-show-link-popup', this.id, this.config.link);\n    },\n    showLinkPopup () {\n      this.addHighlight();\n\n      if (this.config.link) {\n        this.$bus.$emit('block-editor-show-link-popup', this.id, this.config.link);\n      } else {\n        let linkConfig = {\n          url: '',\n          title: '',\n          cssClass: '',\n          noFollow: false,\n          targetBlank: false,\n          sponsored: false,\n          ugc: false,\n          download: false\n        };\n\n        this.$bus.$emit('block-editor-show-link-popup', this.id, linkConfig);\n      }\n    },\n    setLink (blockID, linkConfig) {\n      if (blockID !== this.id) {\n        return;\n      }\n\n      this.config.link.url = linkConfig.url;\n      this.config.link.title = linkConfig.title;\n      this.config.link.cssClass = linkConfig.cssClass;\n      this.config.link.noFollow = linkConfig.noFollow;\n      this.config.link.targetBlank = linkConfig.targetBlank;\n      this.config.link.sponsored = linkConfig.sponsored;\n      this.config.link.ugc = linkConfig.ugc;\n      this.config.link.download = linkConfig.download;\n      this.save();\n    },\n    removeLink () {\n      this.config.link = {\n        url: '',\n        title: '',\n        cssClass: '',\n        noFollow: false,\n        targetBlank: false,\n        sponsored: false,\n        ugc: false,\n        download: false\n      };\n\n      this.save();\n    },\n    addHighlight () {\n      let selection = document.getSelection();\n      let linkInSelection = this.findFirstLinkInSelection(selection);\n\n      if (linkInSelection) {\n        this.linkCreationMode = 'edit';\n        linkInSelection.classList.add('is-highlighted');\n        this.selectElement(linkInSelection);\n        linkInSelection.setAttribute('data-link-popup-id', this.id);\n\n        this.config.link = {\n          url: linkInSelection.getAttribute('href'),\n          title: linkInSelection.getAttribute('title') ? linkInSelection.getAttribute('title') : '',\n          cssClass: linkInSelection.getAttribute('class') ? linkInSelection.getAttribute('class') : '',\n          targetBlank: linkInSelection.getAttribute('target') === '_blank',\n          noFollow: linkInSelection.getAttribute('rel') && linkInSelection.getAttribute('rel').indexOf('nofollow noopener') > -1,\n          sponsored: linkInSelection.getAttribute('rel') && linkInSelection.getAttribute('rel').indexOf('sponsored') > -1,\n          ugc: linkInSelection.getAttribute('rel') && linkInSelection.getAttribute('rel').indexOf('ugc') > -1,\n          download: linkInSelection.getAttribute('download') !== null\n        };\n      } else {\n        let wrapper = document.createElement('span');\n        wrapper.setAttribute('class', 'is-highlighted');\n        let range = selection.getRangeAt(0).cloneRange();\n        range.surroundContents(wrapper);\n        selection.removeAllRanges();\n        selection.addRange(range);\n        this.linkCreationMode = 'new';\n\n        this.config.link = {\n          url: '',\n          title: '',\n          cssClass: '',\n          noFollow: false,\n          targetBlank: false,\n          sponsored: false,\n          ugc: false,\n          download: false\n        };\n      }\n\n      setTimeout(() => {\n        this.updateInlineMenuPosition();\n      }, 0);\n    },\n    removeHighlight () {\n      setTimeout(() => {\n        let highlightedText = document.querySelector('.is-highlighted');\n        let selection = window.getSelection();\n        let range = document.createRange();\n\n        if (highlightedText) {\n          range.setStartBefore(highlightedText);\n          range.setEndAfter(highlightedText);\n\n          setTimeout(() => {\n            if (highlightedText.tagName === 'A') {\n              highlightedText.classList.remove('is-highlighted');\n              selection.removeAllRanges();\n              selection.addRange(range);\n              return;\n            }\n\n            let extractedContent = range.extractContents();\n            let extractedContentChildren = extractedContent.children;\n\n            if (extractedContentChildren[0]) {\n              let nodesToInsert = extractedContentChildren[0].childNodes;\n              let firstNode = nodesToInsert[0];\n              let lastNode = nodesToInsert[nodesToInsert.length - 1];\n\n              for (let i = nodesToInsert.length - 1; i >= 0; i--) {\n                range.insertNode(nodesToInsert[i]);\n              }\n\n              setTimeout(() => {\n                range.setStartBefore(firstNode);\n                range.setEndAfter(lastNode);\n                selection.removeAllRanges();\n                selection.addRange(range);\n              }, 0);\n            }\n          }, 0);\n        }\n      }, 100);\n    },\n    createNewLinkFromSelection () {\n      let highlightedText = document.querySelector('.is-highlighted');\n      let linkElement = document.createElement('a');\n      let temporaryID = +new Date();\n      let relAttr = [];\n      linkElement.setAttribute('href', this.config.link.url);\n      linkElement.setAttribute('data-temp-id', temporaryID);\n\n      if (this.config.link.title) {\n        linkElement.setAttribute('title', this.config.link.title);\n      }\n\n      if (this.config.link.cssClass) {\n        linkElement.setAttribute('class', this.config.link.cssClass);\n      }\n\n      if (this.config.link.targetBlank) {\n        linkElement.setAttribute('target', '_blank');\n      }\n\n      if (this.config.link.noFollow) {\n        relAttr.push('nofollow noopener');\n      }\n\n      if (this.config.link.sponsored) {\n        relAttr.push('sponsored');\n      }\n\n      if (this.config.link.ugc) {\n        relAttr.push('ugc');\n      }\n\n      if (relAttr.length) {\n        linkElement.setAttribute('rel', relAttr.join(' '));\n      }\n\n      if (this.config.link.download) {\n        linkElement.setAttribute('download', 'download');\n      }\n\n      linkElement.innerHTML = highlightedText.innerHTML;\n      highlightedText.parentNode.insertBefore(linkElement, highlightedText);\n      highlightedText.parentNode.removeChild(highlightedText);\n\n      setTimeout(() => {\n        let element = document.querySelector('a[data-temp-id=\"' + temporaryID + '\"]');\n        element.removeAttribute('data-temp-id');\n        this.selectElement(element);\n      }, 0);\n\n      this.selectedText.containedTags.a = true;\n    },\n    editSelectedLink () {\n      if (this.config.link.url === '') {\n        this.removeLink();\n        return;\n      }\n\n      let selectedLink = document.querySelector('a[data-link-popup-id=\"' + this.id + '\"]');\n      selectedLink.setAttribute('href', this.config.link.url);\n      let relAttr = [];\n\n      if (this.config.link.targetBlank) {\n        selectedLink.setAttribute('target', '_blank');\n      } else {\n        selectedLink.removeAttribute('target');\n      }\n\n      if (this.config.link.noFollow) {\n        relAttr.push('nofollow noopener');\n      }\n\n      if (this.config.link.sponsored) {\n        relAttr.push('sponsored');\n      }\n\n      if (this.config.link.ugc) {\n        relAttr.push('ugc');\n      }\n\n      if (relAttr.length) {\n        selectedLink.setAttribute('rel', relAttr.join(' '));\n      } else {\n        selectedLink.removeAttribute('rel');\n      }\n\n      if (this.config.link.download) {\n        selectedLink.setAttribute('download', 'download');\n      } else {\n        selectedLink.removeAttribute('download');\n      }\n\n      if (this.config.link.title) {\n        selectedLink.setAttribute('title', this.config.link.title);\n      } else {\n        selectedLink.removeAttribute('title');\n      }\n\n      if (this.config.link.cssClass) {\n        selectedLink.setAttribute('class', this.config.link.cssClass);\n      } else {\n        selectedLink.removeAttribute('class');\n      }\n\n      setTimeout(() => {\n        let selectedLink = document.querySelector('a[data-link-popup-id=\"' + this.id + '\"]');\n        selectedLink.classList.remove('is-highlighted');\n        this.selectElement(selectedLink);\n\n        setTimeout(() => {\n          let selectedLink = document.querySelector('a[data-link-popup-id=\"' + this.id + '\"]');\n          selectedLink.removeAttribute('data-link-popup-id');\n        }, 0);\n      }, 0);\n    },\n    selectElement (element) {\n      let selection = window.getSelection();\n      selection.removeAllRanges();\n      let range = document.createRange();\n      range.selectNodeContents(element);\n      selection.addRange(range);\n\n      setTimeout(() => {\n        this.updateInlineMenuPosition();\n      }, 0);\n    }\n  },\n  beforeDestroy () {\n    this.$bus.$off('block-editor-save-link-popup', this.saveLinkConfig);\n    this.$bus.$off('block-editor-hide-link-popup', this.removeHighlight);\n  }\n};\n</script>\n"
  },
  {
    "path": "app/src/components/block-editor/components/mixins/LinkHelpers.vue",
    "content": "<script>\nexport default {\n  name: 'link-helpers',\n  computed: {\n    tagPages () {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.tags.length) {\n        return [0];\n      }\n\n      return this.$parent.currentSiteData.tags.filter(tag => tag.additionalData.indexOf('\"isHidden\":true') === -1).map(tag => tag.id);\n    },\n    authorPages () {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.authors.length) {\n        return [''];\n      }\n\n      return this.$parent.currentSiteData.authors.map(author => author.username).sort((a, b) => {\n        if (a.toLowerCase() < b.toLowerCase()) {\n          return -1;\n        }\n\n        if (a.toLowerCase() > b.toLowerCase()) {\n          return 1;\n        }\n\n        return 0;\n      });\n    },\n    postPages () {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.posts.length) {\n        return [0];\n      }\n\n      return this.$parent.currentSiteData.posts.filter(post => post.status.indexOf('published') > -1).map(post => post.id);\n    },\n    pages () {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.pages.length) {\n        return [0];\n      }\n\n      return this.$parent.currentSiteData.pages.filter(page => page.status.indexOf('published') > -1).map(page => page.id);\n    },\n    filesList () {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.files.length) {\n        return [''];\n      }\n\n      return this.$parent.currentSiteData.files;\n    }\n  },\n  methods: {\n    linkIsInvalid (link) {\n      if (\n        link.indexOf('http://') === -1 &&\n        link.indexOf('https://') === -1 &&\n        link.indexOf('://') === -1 &&\n        link.indexOf('dat://') === -1 &&\n        link.indexOf('ipfs://') === -1 &&\n        link.indexOf('dweb://') === -1 &&\n        link.indexOf('//') !== 0 &&\n        link.indexOf('#') !== 0\n      ) {\n        return true;\n      }\n\n      return false;\n    },\n    customTagLabels (value) {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.tags.length) {\n        return '';\n      }\n\n      return this.$parent.currentSiteData.tags.filter(tag => tag.id === value).map(tag => tag.name)[0];\n    },\n    customAuthorsLabels (value) {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.authors.length) {\n        return '';\n      }\n\n      return this.$parent.currentSiteData.authors.filter(author => author.username === value).map(author => author.name)[0];\n    },\n    customPostLabels (value) {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.posts.length) {\n        return '';\n      }\n\n      return this.$parent.currentSiteData.posts.filter(post => post.id === value).map(post => post.title)[0];\n    },\n    customPageLabels (value) {\n      if (!this.$parent.currentSiteData || !this.$parent.currentSiteData.pages.length) {\n        return '';\n      }\n\n      return this.$parent.currentSiteData.pages.filter(page => page.id === value).map(page => page.title)[0];\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "app/src/components/block-editor/components/utils/SelectedText.js",
    "content": "import Vue from 'vue';\n\nexport default class SelectedText {\n  constructor (inlineMenuContainer, blockType) {\n    this.blockType = blockType;\n    this.inlineMenuContainer = inlineMenuContainer;\n\n    this.containedTags = {\n      strong: false,\n      em: false,\n      u: false,\n      s: false,\n      code: false,\n      mark: false,\n      a: false\n    };\n\n    this.allowedOperations = {\n      indent: true,\n      outdent: true,\n      clearFormatting: false\n    };\n\n    this.tagsToCheck = [\n      'strong',\n      'em',\n      'u',\n      's',\n      'code',\n      'mark',\n      'a'\n    ];\n  }\n\n  isInvalidTextSelection () {\n    let selection = document.getSelection();\n    return !selection || !selection.anchorNode || !selection.focusNode;\n  }\n\n  analyzeSelectedText () {\n    if (this.isInvalidTextSelection()) {\n      return;\n    }\n\n    let range = document.getSelection().getRangeAt(0);\n    let commonAncestor = range.commonAncestorContainer;\n    let tempElement = document.createElement('div');\n    tempElement.appendChild(range.cloneContents());\n    let htmlToCheck = tempElement.innerHTML;\n\n    if (commonAncestor.nodeType === 3) {\n      commonAncestor = commonAncestor.parentNode;\n    }\n\n    for (let i = 0; i < this.tagsToCheck.length; i++) {\n      let tag = this.tagsToCheck[i];\n\n      if (htmlToCheck.indexOf('<' + tag + ' ') > -1 || htmlToCheck.indexOf('</' + tag + '>') > -1) {\n        Vue.set(this.containedTags, this.tagsToCheck[i], true);\n      } else {\n        if (commonAncestor.tagName === tag.toUpperCase() || commonAncestor.closest(tag) !== null) {\n          Vue.set(this.containedTags, this.tagsToCheck[i], true);\n        } else {\n          Vue.set(this.containedTags, this.tagsToCheck[i], false);\n        }\n      }\n    }\n\n    tempElement.remove();\n\n    if (this.blockType === 'publii-list') {\n      Vue.set(this.allowedOperations, 'indent', this.checkIfElementCanBeNested());\n      Vue.set(this.allowedOperations, 'outdent', this.checkIfElementCanBeFlattened());\n    }\n  }\n\n  removeStyle (tag) {\n    let range = document.getSelection().getRangeAt(0);\n    let elementToWrap = null;\n\n    if (range.startContainer.nodeType === 1 && range.startContainer.tagName === tag.toUpperCase()) {\n      elementToWrap = range.startContainer;\n    } else if (range.startContainer.nodeType === 3 && range.startContainer.parentNode.tagName === tag.toUpperCase()) {\n      elementToWrap = range.startContainer.parentNode;\n    } else if (range.startContainer.nodeType === 1 && range.startContainer.querySelector(tag)) {\n      elementToWrap = range.startContainer.querySelector(tag);\n    } else if (range.startContainer.nodeType === 3 && range.startContainer.parentNode.querySelector(tag)) {\n      elementToWrap = range.startContainer.parentNode.querySelector(tag);\n    } else if (range.startContainer.nodeType === 1 && range.startContainer.closest(tag)) {\n      elementToWrap = range.startContainer.closest(tag);\n    } else if (range.startContainer.nodeType === 3 && range.startContainer.parentNode.closest(tag)) {\n      elementToWrap = range.startContainer.parentNode.closest(tag);\n    } else if (range.endContainer.nodeType === 1 && range.endContainer.tagName === tag.toUpperCase()) {\n      elementToWrap = range.endContainer;\n    } else if (range.endContainer.nodeType === 3 && range.endContainer.parentNode.tagName === tag.toUpperCase()) {\n      elementToWrap = range.endContainer.parentNode;\n    } else if (range.startContainer.nodeType === 1 && range.endContainer.querySelector(tag)) {\n      elementToWrap = range.endContainer.querySelector(tag);\n    } else if (range.startContainer.nodeType === 3 && range.endContainer.parentNode.querySelector(tag)) {\n      elementToWrap = range.endContainer.parentNode.querySelector(tag);\n    } else if (range.endContainer.nodeType === 1 && range.endContainer.closest(tag)) {\n      elementToWrap = range.endContainer.closest(tag);\n    } else if (range.endContainer.nodeType === 3 && range.endContainer.parentNode.closest(tag)) {\n      elementToWrap = range.endContainer.parentNode.closest(tag);\n    }\n\n    if (elementToWrap) {\n      range.setStartBefore(elementToWrap);\n      range.setEndAfter(elementToWrap);\n\n      setTimeout(() => {\n        let extractedContent = range.extractContents();\n        let extractedContentChildren = extractedContent.children;\n        let nodesToInsert = extractedContentChildren[0].childNodes;\n        let firstNode = nodesToInsert[0];\n        let lastNode = nodesToInsert[nodesToInsert.length - 1];\n\n        for (let i = nodesToInsert.length - 1; i >= 0; i--) {\n          range.insertNode(nodesToInsert[i]);\n        }\n\n        setTimeout(() => {\n          range.setStartBefore(firstNode);\n          range.setEndAfter(lastNode);\n        }, 0);\n      }, 0);\n    }\n  }\n\n  checkIfElementCanBeNested () {\n    let baseItem = document.getSelection().baseNode;\n    let parentList;\n    let listItem;\n\n    if (document.getSelection().baseNode.nodeType === 3) {\n      baseItem = document.getSelection().baseNode.parentNode;\n    }\n\n    parentList = baseItem.closest('ul,ol');\n\n    if (document.getSelection().baseNode.tagName === 'LI') {\n      listItem = baseItem;\n    } else {\n      listItem = baseItem.closest('li');\n    }\n\n    if (parentList.children.length <= 1 || parentList.children[0] === listItem) {\n      return false;\n    }\n\n    return true;\n  }\n\n  checkIfElementCanBeFlattened () {\n    let baseItem = document.getSelection().baseNode;\n    let parentList;\n\n    if (document.getSelection().baseNode.nodeType === 3) {\n      baseItem = document.getSelection().baseNode.parentNode;\n    }\n\n    parentList = baseItem.closest('ul,ol');\n\n    if (parentList !== null && parentList !== parentList.closest('.publii-block-list')) {\n      return true;\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "app/src/components/block-editor/components/utils/Utils.js",
    "content": "export default class Utils {\n  /*\n   * Deep merge for objects as Object.assign not merge objects properly\n   */\n  static deepMerge (target, source) {\n    if (typeof target !== 'object') {\n      target = {};\n    }\n\n    for (let property in source) {\n      if (source.hasOwnProperty(property)) {\n        let sourceProperty = source[property];\n\n        if (typeof sourceProperty === 'object' && !Array.isArray(sourceProperty) && !(sourceProperty instanceof Date)) {\n          target[property] = Utils.deepMerge(target[property], sourceProperty);\n          continue;\n        } else if (sourceProperty instanceof Date) {\n          target[property] = new Date(sourceProperty.getTime());\n          continue;\n        }\n\n        target[property] = sourceProperty;\n      }\n    }\n\n    for (let a = 2, l = arguments.length; a < l; a++) {\n      Utils.deepMerge(target, arguments[a]);\n    }\n\n    return target;\n  }\n\n  /*\n   * Run function if it is not invoked since X ms.\n   */\n  static debounce (func, wait, immediate) {\n    var timeout;\n\n    return function () {\n      var context = this;\n      var args = arguments;\n      var later = function () {\n        timeout = null;\n\n        if (!immediate) {\n          func.apply(context, args);\n        }\n      };\n\n      var callNow = immediate && !timeout;\n      clearTimeout(timeout);\n\n      timeout = setTimeout(later, wait);\n\n      if (callNow) {\n        func.apply(context, args);\n      }\n    };\n  }\n}\n"
  },
  {
    "path": "app/src/components/block-editor/vendors/_modularscale.scss",
    "content": "// Defaults and variables\n@import 'modularscale/vars';\n\n// Core functions\n@import 'modularscale/settings';\n@import 'modularscale/pow';\n@import 'modularscale/strip-units';\n@import 'modularscale/sort';\n@import 'modularscale/target';\n@import 'modularscale/function';\n@import 'modularscale/round-px';\n\n// Mixins\n@import 'modularscale/respond';\n\n// Syntax sugar\n@import 'modularscale/sugar';"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_function.scss",
    "content": "@function ms-function($v: 0, $base: false, $ratio: false, $thread: false, $settings: $modularscale) {\n\n  // Parse settings\n  $ms-settings: ms-settings($base,$ratio,$thread,$settings);\n  $base: nth($ms-settings, 1);\n  $ratio: nth($ms-settings, 2);\n\n  // Render target values from settings.\n  @if unit($ratio) != '' {\n    $ratio: ms-target($ratio,$base)\n  }\n\n  // Fast calc if not multi stranded\n  @if(length($base) == 1) {\n    @return ms-pow($ratio, $v) * $base;\n  }\n\n  // Create new base array\n  $ms-bases: nth($base,1);\n\n  // Normalize base values\n  @for $i from 2 through length($base) {\n    // initial base value\n    $ms-base: nth($base,$i);\n    // If the base is bigger than the main base\n    @if($ms-base > nth($base,1)) {\n      // divide the value until it aligns with main base.\n      @while($ms-base > nth($base,1)) {\n        $ms-base: $ms-base / $ratio;\n      }\n      $ms-base: $ms-base * $ratio;\n    }\n    // If the base is smaller than the main base.\n    @else if ($ms-base < nth($base,1)) {\n      // pump up the value until it aligns with main base.\n      @while $ms-base < nth($base,1) {\n        $ms-base: $ms-base * $ratio;\n      }\n    }\n    // Push into new array\n    $ms-bases: append($ms-bases,$ms-base);\n  }\n\n  // Sort array from smallest to largest.\n  $ms-bases: ms-sort($ms-bases);\n\n  // Find step to use in calculation\n  $vtep: floor($v / length($ms-bases));\n  // Find base to use in calculation\n  $ms-base: round(($v / length($ms-bases) - $vtep) * length($ms-bases)) + 1;\n\n  @return ms-pow($ratio, $vtep) * nth($ms-bases,$ms-base);\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_pow.scss",
    "content": "@use \"sass:math\";\n\n// Sass does not have native pow() support so this needs to be added.\n// Compass and other libs implement this more extensively.\n// In order to keep this simple, use those when they are avalible.\n// Issue for pow() support in Sass: https://github.com/sass/sass/issues/684\n\n@function ms-pow($b,$e) {\n\n  // Return 1 if exponent is 0\n  @if $e == 0 {\n    @return 1;\n  }\n\n  // If pow() exists (compass or mathsass) use that.\n  @if function-exists('pow') {\n    @return pow($b,$e);\n  }\n\n  // This does not support non-integer exponents,\n  // Check and return an error if a non-integer exponent is passed.\n  @if (floor($e) != $e) {\n    @error 'Non-integer values are not supported in modularscale by default. Try using mathsass in your project to add non-integer scale support. https://github.com/terkel/mathsass'\n  }\n\n  // Seed the return.\n  $ms-return: $b;\n\n  // Multiply or divide by the specified number of times.\n  @if $e > 0 {\n    @for $i from 1 to $e {\n      $ms-return: $ms-return * $b;\n    }\n  }\n  @if $e < 0 {\n    @for $i from $e through 0 {\n      $ms-return: math.div($ms-return, $b);\n    }\n  }\n  @return $ms-return;\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_respond.scss",
    "content": "// Generate calc() function\n// based on Mike Riethmuller's Precise control over responsive typography\n// http://madebymike.com.au/writing/precise-control-responsive-typography/\n@function ms-fluid($val1: 1em, $val2: 1em, $break1: 0, $break2: 0) {\n  $diff: ms-unitless($val2) - ms-unitless($val1);\n\n  // v1 + (v2 - v1) * ( (100vw - b1) / b2 - b1 )\n  @return calc( #{$val1} + #{ms-unitless($val2) - ms-unitless($val1)} * ( ( 100vw - #{$break1}) / #{ms-unitless($break2) - ms-unitless($break1)} ) );\n}\n\n// Main responsive mixin\n@mixin ms-respond($prop, $val, $map: $modularscale, $ms-important: false) {\n  $base: $ms-base;\n  $ratio: $ms-ratio;\n\n  $first-write: true;\n  $last-break: null;\n\n  $important: '';\n\n  @if $ms-important == true {\n    $important: ' !important';\n  }\n\n  // loop through all settings with a breakpoint type value\n  @each $v, $s in $map {\n    @if type-of($v) == number {\n      @if unit($v) != '' {\n\n        // Write out the first value without a media query.\n        @if $first-write {\n          #{$prop}: unquote(\"#{ms-function($val, $thread: $v, $settings: $map)}#{$important}\");\n\n          // Not the first write anymore, reset to false to move on.\n          $first-write: false;\n          $last-break: $v;\n        }\n\n        // Write intermediate breakpoints.\n        @else {\n          @media (min-width: $last-break) and (max-width: $v) {\n            $val1: ms-function($val, $thread: $last-break, $settings: $map);\n            $val2: ms-function($val, $thread: $v, $settings: $map);\n            #{$prop}: unquote(\"#{ms-fluid($val1,$val2,$last-break,$v)}#{$important}\");\n          }\n          $last-break: $v;\n        }\n      }\n    }\n  }\n\n  // Write the last breakpoint.\n  @if $last-break {\n    @media (min-width: $last-break) {\n      #{$prop}: unquote(\"#{ms-function($val, $thread: $last-break, $settings: $map)}#{$important}\");\n    }\n  }\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_round-px.scss",
    "content": "@function ms-round-px($r) {\n    @if unit($r) == 'px' {\n        @return round($r);\n    }\n    @warn \"ms-round-px is no longer used by modular scale and will be removed in the 3.1.0 release.\";\n    @return $r;\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_settings.scss",
    "content": "// Parse settings starting with defaults.\n// Settings should cascade down like you would expect in CSS.\n// More specific overrides previous settings.\n\n@function ms-settings($b: false, $r: false, $t: false, $m: $modularscale) {\n  $base: $ms-base;\n  $ratio: $ms-ratio;\n  $thread: map-get($m, $t);\n\n  // Override with user settings\n  @if map-get($m, base) {\n    $base: map-get($m, base);\n  }\n  @if map-get($m, ratio) {\n    $ratio: map-get($m, ratio);\n  }\n\n  // Override with thread settings\n  @if $thread {\n    @if map-get($thread, base) {\n      $base: map-get($thread, base);\n    }\n    @if map-get($thread, ratio) {\n      $ratio: map-get($thread, ratio);\n    }\n  }\n\n  // Override with inline settings\n  @if $b {\n    $base: $b;\n  }\n  @if $r {\n    $ratio: $r;\n  }\n\n  @return $base $ratio;\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_sort.scss",
    "content": "// Basic list sorting\n// Would like to replace with http://sassmeister.com/gist/30e4863bd03ce0e1617c\n// Unfortunately libsass has a bug with passing arguments into the min() funciton.\n\n@function ms-sort($l) {\n\n  // loop until the list is confirmed to be sorted\n  $sorted: false;\n  @while $sorted == false {\n\n    // Start with the assumption that the lists are sorted.\n    $sorted: true;\n\n    // Loop through the list, checking each value with the one next to it.\n    // Swap the values if they need to be swapped.\n    // Not super fast but simple and modular scale doesn't lean hard on sorting.\n    @for $i from 2 through length($l) {\n      $n1: nth($l,$i - 1);\n      $n2: nth($l,$i);\n\n      // If the first value is greater than the 2nd, swap them.\n      @if $n1 > $n2 {\n        $l: set-nth($l, $i, $n1);\n        $l: set-nth($l, $i - 1, $n2);\n\n        // The list isn't sorted and needs to be looped through again.\n        $sorted: false;\n      }\n    }\n  }\n\n  // Return the sorted list.\n  @return $l;\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_strip-units.scss",
    "content": "// Stripping units is not a best practice\n// This function should not be used elsewhere\n// It is used here because calc() doesn't do unit logic\n// AND target ratios use units as a hack to get a number.\n@function ms-unitless($val) {\n  @return ($val / ($val - $val + 1));\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_sugar.scss",
    "content": "// To attempt to avoid conflicts with other libraries\n// all funcitons are namespaced with `ms-`.\n// However, to increase usability, a shorthand function is included here.\n\n@function ms($v: 0, $base: false, $ratio: false, $thread: false, $settings: $modularscale) {\n  @return ms-function($v, $base, $ratio, $thread, $settings);\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_target.scss",
    "content": "// Convert number string to number\n@function ms-to-num($n) {\n  $l: str-length($n);\n  $r: 0;\n  $m: str-index($n,'.');\n  @if $m == null {\n    $m: $l + 1;\n  }\n  // Loop through digits and convert to numbers\n  @for $i from 1 through $l {\n    $v: str-slice($n,$i,$i);\n    @if $v == '1' { $v: 1; }\n    @else if $v == '2' { $v: 2; }\n    @else if $v == '3' { $v: 3; }\n    @else if $v == '4' { $v: 4; }\n    @else if $v == '5' { $v: 5; }\n    @else if $v == '6' { $v: 6; }\n    @else if $v == '7' { $v: 7; }\n    @else if $v == '8' { $v: 8; }\n    @else if $v == '9' { $v: 9; }\n    @else if $v == '0' { $v: 0; }\n    @else { $v: null; }\n    @if $v != null {\n      $m: $m - 1;\n      $r: $r + ms-pow(10,$m - 1) * $v;\n    } @else {\n      $l: $l - 1;\n    }\n  }\n  @return $r;\n}\n\n// Find a ratio based on a target value\n@function ms-target($t,$b) {\n  // Convert to string\n  $t: $t + '';\n  // Remove base units to calulate ratio\n  $b: ms-unitless(nth($b,1));\n  // Find where 'at' is in the string\n  $at: str-index($t,'at');\n\n  // Slice the value and target out\n  // and convert strings to numbers\n  $v: ms-to-num(str-slice($t,0,$at - 1));\n  $t: ms-to-num(str-slice($t,$at + 2));\n\n  // Solve the modular scale function for the ratio.\n  @return ms-pow(($v/$b),(1/$t));\n}"
  },
  {
    "path": "app/src/components/block-editor/vendors/modularscale/_vars.scss",
    "content": "// Ratios\n$double-octave    : 4                 ;\n$pi               : 3.14159265359     ;\n$major-twelfth    : 3                 ;\n$major-eleventh   : 2.666666667       ;\n$major-tenth      : 2.5               ;\n$octave           : 2                 ;\n$major-seventh    : 1.875             ;\n$minor-seventh    : 1.777777778       ;\n$major-sixth      : 1.666666667       ;\n$phi              : 1.618034          ;\n$golden           : $phi              ;\n$minor-sixth      : 1.6               ;\n$fifth            : 1.5               ;\n$augmented-fourth : 1.41421           ;\n$fourth           : 1.333333333       ;\n$major-third      : 1.25              ;\n$minor-third      : 1.2               ;\n$major-second     : 1.125             ;\n$minor-second     : 1.066666667       ;\n\n// Base config\n$ms-base          : 1em       !default;\n$ms-ratio         : $fifth    !default;\n$modularscale     : ()        !default;"
  },
  {
    "path": "app/src/components/configs/defaultDeploymentSettings.js",
    "content": "export default {\n    protocol: '',\n    port: '',\n    server: '',\n    username: '',\n    password: '',\n    askforpassword: '',\n    passphrase: '',\n    path: '',\n    sftpkey: '',\n    git: {\n        url: '',\n        branch: '',\n        user: '',\n        password: '',\n        commitAuthor: '',\n        commitEmail: '',\n        commitMessage: 'Publii: update content'\n    },\n    github: {\n        server: 'api.github.com',\n        user: '',\n        repo: '',\n        branch: '',\n        token: '',\n        parallelOperations: 1,\n        apiRateLimiting: 1\n    },\n    gitlab: {\n        server: 'https://gitlab.com/',\n        rejectUnauthorized: true,\n        repo: '',\n        branch: '',\n        token: ''\n    },\n    google: {\n        bucket: '',\n        key: '',\n        prefix: ''\n    },\n    manual: {\n        output: 'catalog',\n        outputDirectory: ''\n    },\n    netlify: {\n        id: '',\n        token: ''\n    },\n    s3: {\n        customProvider: false,\n        provider: 'aws',\n        endpoint: '',\n        id: '',\n        key: '',\n        bucket: '',\n        region: '',\n        prefix: '',\n        acl: 'public-read'\n    }\n};\n"
  },
  {
    "path": "app/src/components/configs/postEditor.config.js",
    "content": "export default {\n    selector: '#post-editor',\n    file_picker_types: 'image',\n    contextmenu: false,\n    plugins: \"advlist autolink autosave codesample link image lists hr pagebreak searchreplace media table paste autoresize emoticons textpattern toc\",\n    toolbar1: \"bold italic underline strikethrough forecolor publiilink unlink emoticons blockquote alignleft aligncenter alignright bullist numlist image gallery media table toc\",\n    toolbar2: \"styleselect formatselect codesample searchreplace hr readmore undo redo restoredraft removeformat sourcecode\",\n    toolbar3: \"\",\n    icons: \"publii\",\n    block_formats: 'Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Address=address;Pre=pre;Code=code;Blockquote=blockquote',\n    extended_valid_elements: \"a[*],altGlyph[*],altGlyphDef[*],altGlyphItem[*],animate[*],animateColor[*],animateMotion[*],animateTransform[*],circle[*],clipPath[*],color-profile[*],cursor[*],defs[*],desc[*],discard[*],ellipse[*],feBlend[*],feColorMatrix[*],feComponentTransfer[*],feComposite[*],feConvolveMatrix[*],feDiffuseLighting[*],feDisplacementMap[*],feDistantLight[*],feDropShadow[*],feFlood[*],feFuncA[*],feFuncB[*],feFuncG[*],feFuncR[*],feGaussianBlur[*],feImage[*],feMerge[*],feMergeNode[*],feMorphology[*],feOffset[*],fePointLight[*],feSpecularLighting[*],feSpotLight[*],feTile[*],feTurbulence[*],filter[*],font[*],font-face[*],font-face-format[*],font-face-name[*],font-face-src[*],font-face-uri[*],foreignObject[*],g[*],glyph[*],glyphRef[*],hatch[*],hatchpath[*],hkern[*],iframe[*],image[*],line[*],linearGradient[*],marker[*],mask[*],mesh[*],meshgradient[*],meshpatch[*],meshrow[*],metadata[*],missing-glyph[*],mpath[*],path[*],pattern[*],polygon[*],polyline[*],radialGradient[*],rect[*],set[*],solidcolor[*],stop[*],style[*],svg[*],switch[*],symbol[*],text[*],textPath[*],title[*],tref[*],tspan[*],unknown[*],use[*],view[*],vkern[*],publii-amp,publii-non-amp,script[*],i[*],video[*],audio[*],source[*],stream[*],input[*]\",\n    valid_children: '+a[div|p|figure|pre|h1|h2|h3|h4|h5|h6|header|footer|article|aside|section|table|blockquote|video]',\n    formats: {\n        alignleft: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left' },\n        aligncenter: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center' },\n        alignright: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right' },\n        alignjustify: { selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-justify' }\n    },\n    preview_styles: false,\n    resize: false,\n    menubar: false,\n    forced_root_block : \"\",\n    force_br_newlines : false,\n    force_p_newlines : true,\n    paste_as_text: false,\n    keep_styles: false,\n    image_class_list: [\n        {title: 'None', value: 'post__image'},\n        {title: 'Full image', value: 'post__image post__image--full'},\n        {title: 'Wide image', value: 'post__image post__image--wide'},\n        {title: 'Left-aligned image', value: 'post__image post__image--left'},\n        {title: 'Right-aligned image', value: 'post__image post__image--right'},\n        {title: 'Centered image', value: 'post__image post__image--center'}\n    ],\n    codesample_languages: [\n        { text: 'Apache Configuration', value: 'apacheconf' },\n        { text: 'ASP.NET', value: 'aspnet' },\n        { text: 'Bash', value: 'bash' },\n        { text: 'BASIC', value: 'basic' },\n        { text: 'Batch', value: 'batch' },\n        { text: 'BBcode', value: 'bbcode' },\n        { text: 'C', value: 'c' },\n        { text: 'C++', value: 'cpp' },\n        { text: 'ColdFusion Script', value: 'cfscript' },\n        { text: 'C#', value: 'csharp' },\n        { text: 'C-like', value: 'clike' },\n        { text: 'CSS', value: 'css' },\n        { text: 'Dart', value: 'dart' },\n        { text: 'Docker', value: 'docker' },\n        { text: 'Elixir', value: 'elixir' },\n        { text: 'Elm', value: 'elm' },\n        { text: 'GDScript', value: 'gdscript' },\n        { text: 'Git', value: 'git' },\n        { text: 'GLSL', value: 'glsl' },\n        { text: 'Go', value: 'go' },\n        { text: 'GraphQL', value: 'graphql' },\n        { text: 'HAML', value: 'haml' },\n        { text: 'Handlebars', value: 'handlebars' },\n        { text: 'Haskell', value: 'haskell' },\n        { text: 'HTML', value: 'html' },\n        { text: 'HTTP', value: 'http' },\n        { text: 'INI', value: 'ini' },\n        { text: 'Java', value: 'java' },\n        { text: 'JavaScript', value: 'javascript' },\n        { text: 'JSON', value: 'json' },\n        { text: 'JSONP', value: 'jsonp' },\n        { text: 'JSX', value: 'jsx' },\n        { text: 'Kotlin', value: 'kotlin' },\n        { text: 'LaTeX', value: 'latex' },\n        { text: 'LESS', value: 'less' },\n        { text: 'Lisp', value: 'lisp' },\n        { text: 'Lua', value: 'lua' },\n        { text: 'Makefile', value: 'makefile' },\n        { text: 'Markdown', value: 'markdown' },\n        { text: 'MATLAB', value: 'matlab' },\n        { text: 'NASM', value: 'nasm' },\n        { text: 'Nginx', value: 'nginx' },\n        { text: 'Objective-C', value: 'objectivec' },\n        { text: 'Pascal', value: 'pascal' },\n        { text: 'Perl', value: 'perl' },\n        { text: 'PHP', value: 'php' },\n        { text: 'PowerShell', value: 'powershell' },\n        { text: 'Pug', value: 'pug' },\n        { text: 'Python', value: 'python' },\n        { text: 'R', value: 'r' },\n        { text: 'Regex', value: 'regex' },\n        { text: 'Ruby', value: 'ruby' },\n        { text: 'Rust', value: 'rust' },\n        { text: 'Sass', value: 'sass' },\n        { text: 'SCSS', value: 'scss' },\n        { text: 'Scala', value: 'scala' },\n        { text: 'SQL', value: 'sql' },\n        { text: 'Swift', value: 'swift' },\n        { text: 'Twig', value: 'twig' },\n        { text: 'TypeScript', value: 'typescript' },\n        { text: 'VB.NET', value: 'vbnet' },\n        { text: 'Visual Basic', value: 'visual-basic' },\n        { text: 'YAML', value: 'yaml' },\n        { text: 'XML', value: 'markup' }\n    ],\n    element_format : 'html',\n    fix_list_elements : true,\n    image_caption: true,\n    autosave_ask_before_unload: false,\n    autosave_interval: \"10s\",\n    autosave_restore_when_empty: false,\n    autosave_retention: \"30m\",\n    entity_encoding: \"raw\",\n    allow_script_urls: true,\n    convert_urls: false,\n    textpattern_patterns: [\n        {start: '*', end: '*', format: 'italic'},\n        {start: '**', end: '**', format: 'bold'},\n        {start: '##', format: 'h2'},\n        {start: '###', format: 'h3'},\n        {start: '####', format: 'h4'},\n        {start: '#####', format: 'h5'},\n        {start: '######', format: 'h6'},\n        {start: '1. ', cmd: 'InsertOrderedList'},\n        {start: '* ', cmd: 'InsertUnorderedList'},\n        {start: '- ', cmd: 'InsertUnorderedList'}\n   ],\n   toc_depth: 6,\n   toc_header: \"h3\",\n   toc_class: \"post__toc\",\n   rel_list: [\n    {title: 'noreferrer', value: 'noreferrer'},\n    {title: 'nofollow', value: 'nofollow'},\n    {title: 'noopener', value: 'noopener'},\n    {title: 'sponsored', value: 'sponsored'},\n    {title: 'ugc', value: 'ugc'},\n   ],\n   link_context_toolbar: false,\n   link_quicklink: false,\n   codesample_global_prismjs: true\n};\n"
  },
  {
    "path": "app/src/components/configs/preloaderImages.js",
    "content": "/*\n * This file contains inline preloaders for the use in the post editor\n *\n * in the future it will be replaced with the SVG or CSS preloaders\n */\n\nexport default {\n    gray: 'data:image/gif;base64,R0lGODlhGAAYAPe+APj5+vf4+fb3+PT19vX29+jp7Nrc4Nze4ubn6vDx89/h5O3u8ODi5fLz9O7v8cXHzfP09ezu8PHy89ja3q6xudfZ3eLk58/R1urr7uvt79bY3O/x8qGlru3v8eTl6OHj5rm8w+7w8s7Q1eXm6aapst7g483P1NHU2MLFy8nL0dTW2vLz9ezt78HEysjK0KSosdDS16uutr/CyK2wuOfo65+jrLq9xMvN07S3v5mcpp6iq8rM0t3f477Bx6irtMDDyeLj5urs7rG0vPP19qKmr8zO0+zt8Lu+xLe6wdLV2bO2vpygqZqdp+bo6vP09u/w8uHi5dnb3+Xn6amstOfp69vd4ZyfqfHz9Onq7e/x86CkrfX2+Onr7b3Axqqttba4wLS3vtze4eXn6pueqMzO1K+yuqmstfX3+Lq8w+bo6+3v8NPV2sfJz/Dy8/Hy9Ovs7vT297y/xd3e4sTHzLm7wpibpeTm6Keqs9DS1t3f4srN0tvc4OTm6dHS152hqtrb3/f5+vb4+eXm6J6hq83P1evs7/Hz9ZSYo97f452gqbW4wMDDyMjL0OPl5+Pl6NbY3eDh5La5wNTW28HDybW3vqistcDCyM/S16KlrsfKz8bIzsTGzPb3+cPFy8XIzff4+vT19+jq7PL09ePk59XX29nb3tLU2bK1vaWpsry/xqOnsLK1vNrc39PV2d7g5LCzu7u9xMbJzsrM0bi7wuLj59vd4Le5wc/S1uDh5aaqsr7Ax6SosMvN0rGzu5eapcDCycPGy6qttpmcp8bIzbS2vtbX29TX262wt+Di5ry+xdHT2e3u8ejq7cnL0NXX2ra4v7/Cx8nM0dTV2qyvt7i6wtHT2LW3v5WZpKCjrcPGzNrb4Obn6c3Q1Keqspibpt/h5a+yuerr7dDR1qGlrbG1vaWosbi6wdHU162xuaGkrd/g5NPW2q2xuNDT172/xrG1vOnq7LW4v6istNbX3K6yuaKlr6ers6uttpyfquLl56epssnK0NfY3La5wbCzutLT2SH/C05FVFNDQVBFMi4wAwEAAAAh/i1NYWRlIGJ5IEtyYXNpbWlyYSBOZWpjaGV2YSAod3d3LmxvYWRpbmZvLm5ldCkAIfkEBAoA/wAsAAAAABgAGAAACP8AAQgcKJDThia4FIgJwYmgQ4cDPuxIhqNXmVfxgB2A8NAhjUy2lAh5RWFGDC8+zNj4EKBjAB66kHwhdmrVqV7HUJJ4EWydgIcluoCYheSXKVoFCuAygYOEKiIvkjgs0OKIjVSk2jwMsSMXBy25FAwEZUJXlx55WnbkZGpXDR1gGggc0UJGjwlqOwLgxMbPEh0TAASQ1OJHNDd6CS6IMYYJGk4NbnRCUSuxwwc5cuhjwcLFg2EFLBNkJcwXPlwFXGjat0D0wBGoOOwKE0qWix0dXAs8oyZChAELyKTghUG3wABqRSm7UYSBcQEECPwMUKuICVJOXAcYwP2MQC4XRFyHyJdX7wAJDSA0BLDlD4wTkrCUdzgkxAYJHAd2MNZKRYUmoDzESRsRqOFAFj8RFEoxpGgwgStYJACBEw04gAEWQWTQQYAOBcBFKRWUYsABJTBggQcjIFAAFkZkp9cGDBgwYgkKfDCKByouwGFiAjgwAjIKMACEBzSwsMJ8iQVAQAMJ4OedXgEBACH5BAUKAFoALAAAAAAYABgAAAj/AAEIHCgwQJtQoyxQycKJoEOHoDzgyZYKhDlqqVIocPLQIRZuMuLYAIHEmpJTr4T88PCpY4APnmT0iHPExhE6OIS8ojBjVYWGDi08aPFDRqwKIzCEG3UCFoVpMWZocPgmxSYUc6JceZjlArgpUygAGUhAGpsHbIAE6AgggIZpPkjAWiGwQAoXbOS0ZNu2CIkXqAwA+KRtRwoYdPkKdEBOFREZnESZunHjm2KC0TiMQ7fAgTgTJoJcHniAQ41yFoKIEwHjyWiBdojoGGfgzQk8STa8BuAh3ZIarJ6oONEqwm4DVphg+zCklDQVUnZvypGjm5FPH0hpOABq9JM71+rQh2kYYUKFCQVGgzpHwttUAAIYlDJwwDjfAGszVHMzMMGBKge4EoQAD31CwAAEAOUQCzzwUAID2yywAhwEwCFKGw1AMIAAezm0AAMKQDGKHQiEgkEGHWwgQQMDrMXWFU1YMMoICBQQTgYRhJAABC7eJ8EbBdBgIgshXEHAbgIJMAAEThDQ40MBAQAh+QQFCgBXACwAAAAAGAAYAAAI/wABCBwoMICbIFJGcJEQiKBDhwRozNsxp4WMRZsuWBjw0GEGFZkeoPgho0ucIyBAZGrSEUCAEYRcsHnQCcXILjZAIPmCZk9DhwgI7UjhQkQYKiwySHmEwhYOJTgMOFzQp8gNMiVWPJSwJpIQIYr4DDzzR4QJESMCtAxgAEwZCi2cCHxzAsaFD2pbAggEg8IMCogAAIKU5EQFuXoFPqETI8aDQAP+qFAhNvFAEV6mxHOQYIIGDR0sD1TgxccMPh0mVIjiRrRABPdImMnjwECUPa1di5H3wp4cNwcMVHniGkCeF0TkOYLD4IAcDMUZceBArwOgNDxKNCIgegG7GjV6cI0CsEGBAgYsLHPS5EeHligCOTWBAkRQAr2cLgwaswSMoYErCOKBIDQ8MZ5DcDxgRQ5M1KOAQwkggAANBRhhCAGcBBAAIHB8cUgdieDRURsFFIBFIRGE0AYEAwgAABA6DHKBix0NYAQGGXQQggQNDEBAIIHccgAgiQUwRBsbZMHjAGfkVZxLApwhgJMdBQQAIfkEBQoAawAsAAAAABgAGAAACP8A1wgcOFDUglAFWDQIAKChw4cPt3CppYyMLBcubqiQAgqixw5RLhS5kcKFpgedWrQwEYqgy1DGYIgwUYQXr2bDUPyQoatFCYYQuRRrdeIWKSgYOiwLVSUFtC5HuvCA+KQUKRXOLDjxeGVCDxsgdKVxKIBBBQ0VsLh0mSfVLCSZhjR0YKDUhCZrXQYgZeuLLSgNxRgw4KqjR4gJfhFToicAAWQHDjA7fNjUKSHJnjRQUKJEAsoeP6x6hSNNAgYKoIgCDbHAKQq9GEgA8kEr64dUXs0ogwuCh1EeGtx2iOtYjFcIztDwMMLB8IYmpnh55pwFAgQYBAx3gMOHD2AMVxSIKIAlQV6XO1CRMBOmYYAFWIJEGHJeoKlcql7AWt0QlJEMaoQwAFAPLRPLLhwQEcwHEDnRgQMbSDCAdmdIwcoDMehQgxYvrHMYKFlI0MAABASgBirC5DDGEjrkkkR9AkAAAYkBRMCBLzkwoQMYuBBIGSdbaBfBLlaQgMYEKzz30ABhMMCCdocFBAAh+QQFCgBYACwAAAAAGAAYAAAI/wABCBwoMMCQJ2+COBAVgKBDhwIifCil4gSMC6a0FSDw0GECBhNISTvRToSJGztkSXvTUaCRAxMqaFDRKkmfkylcsElhoaHDBTyqGJhwQEqEDU+CfIPB5sGmBx8cXmHA48ABKqAeipLDBkULT/AGBmiioISrCC0BBAAy54cMbgMESrAARUGQtAWj9NjrQeCbUaO2bcEr0A2bOHFgBBBQYISdBYQHVjhiI1uCATQQIFgRWSAfGyDchYIQqkCorJ0x2EACYpQTDFwwcOzMhY41ahYIsMiQAU5nABZwKKFWIECICB1E/T4h5FSqDQCuhAjRxifeJ7BevUrRkEACCQ1mp4UNcIEdhVfqBkJoAGGA9YcBNFCYNuOHE7EDBhAIMODJwxBFTDNFDKv0RVAAWwgAQDt3bLKHHXzkEc07JPgwxQwVdNRQBiRck4MV6RDBAQeqvEACBRoo2FI13tSRAxNL6FADB0SgAktPeF2hwSzdYFODFi+wI0MtynUmgBEfsGKABQuo+FBAACH5BAUKAJwALAAAAAAYABgAAAj/AAEIHCgQEBw3DtQkGACIoEOHgTakYXDAwIQJfyC9OfPQ4YomCnhUjFJBg4okJ6Is6CgwgSAoCkrIqbIniskTMET0QRDgYRsEHoAwaIRhwxU3auxUuGCiCKERDgcUQCBIEIstD4d8EHFjB6FCAwMsKECDRgKWAACNIJPChSQCAodgwFLgCdqCJdjopSGwTYZCRgTcFbhCxIMHGgIEyNIhgqHBA8NsQqHnioAsIULAhQyACoofc4KckSChjWDOGVDIaCFGQIMGEHpyLvSjiyUpAQbolg1ZSpc4i7gAOENAAO/Bj2wcmdMGQIBAAOAMgJwABQgQF2QHAPLlwWmWAdZEiUICwsLASzoOWbnwHaKBSDi+ZJou8MCgOjkGaXLwcAMMMEIogQYCDl2QCBNj+DEDIzwgkIYCItBBQRlC4LDHQwLggckSftTAwQuVxOCFFzHMQIEiBkD3ECAKgKGFh0S8QIIPU8RAwSR8NITWClH0QIEPd5gxAyUPIOIEZwAIoIYjcuRhhwPtERQQACH5BAUKAE0ALAAAAAAYABgAAAj/AAEIHDjwDAQJCRoQAESwYcMAK1jQ8ACEgQJkYjoIcNgQ1IICCDyM+qCgxAEDBhhs4CjQiREsIEd4sMDApIFSFUpxCeAQVIcMQbBgcNDACYQEWFxN0ECqWKiGArI4UBOhDaeeCCqoaGWsA0EnEjaEGMISACAskk7A+LNFICcIDSQMKCsQUL4LIi5wEXhmgF+edAE4IWWiSC2eAggQ2BhYIIMiN/6JqsuwscBwvFKQWTAgQgQ1ZywD6LDDhaxQYXZxQDVC9IJ9mlwUwIXPlzBWogsMe+CCBQt9OXI8EF0LRacbDTihYTImxoLGbqL9aCGJ5wQdS/ywuVo2wIQeMlq0hQbQAIyOGrtMcXcYIE+PLrpMgBqoIJcWDrl2PHHYhlQqG0e0UEBDSbxAhCok4GACAwUUQIspvyAxCwhdlOCQAGsE8wIJPnhxTC+nrHIKMV8goQsPgDUEyAc2mNFhDDNQ4I8QStiSCQ2BQRAGMPH4U0YvOByxAxRzWSaAA2IogEsaG6zXUEAAIfkEBQoAWQAsAAAAABgAGAAACP8AAQgcODAAAScQBgggyLAhAAJXQrDAEIpGgTcSAjgkGABCghARMoQrgGDEKAtNrmwEEGBAAwkbOmSgiMDOKCgKGCxwGGDLAAgN2oiCQwDOigXbGJTgwcOIQwEEBhDQyHBLEFcHqhxIMNBNtQwsqW6McMBAKQYLAWjwRuIcqJUDqUyoMCECAE6z6ly78wSuQFAHNJD6EMBItxw5NvkdKEWFtFJDPmBjYsXAYoERWp1Q8YRVjSVaPFwGsCEJnhMZDIzTQcTO6CcwRIh7Y6FcDQ4HRmMwYUKcgwXoxnGINvrbjRumRHGSQUQVOQeLV8BIsUObRgOoXpAoIpanHDYuZBWDELgCFgkf0yp05wiEzQM20ggMBEJhyhRwF7gyvBJlDopNKbzBkAYzxDANBbCcMEo4GIxQARsy/NDCA7Q0xEkFq8xAwStC4EDHETYcEUcPMnhCGE8e/PDKK6coYQ0SINgQhwzcYOGXE+qkkAo15oCQSjZ4ePDWZZxsQIUFo4TSxnoDBQQAIfkEBQoAVwAsAAAAABgAGAAACP8AAQgcSDCAgIMBCCpcKDDAlgENJGTZ0GbIlYsYM14BdOBWoEAEIEoI0SEDhgUDGAIQcGGQDiArB0BoEyJCISwFCrRhiCdRnUNf4AAKEIATAUNGCtBAgCCBQgX1mOSw8gDOQk5PaAjyIGjFQENglowZdIGTSgAJBAGB0sQsgChadPjx5PYsCwYKFGy42KNGDXYLzg4k0KgEjzSAOtDjwIGRYIIY5BxgAMeRPCIv8jwe+KSKgQNu5Nh7IU/MZoFu9kQx4CCPGRL3EJwG4CZKhQkd7Mzw4UXB7AUaNExI4IDSFC8iZttRoeLPgEAPYsSg82SzkwonkkACBAARhRkUYGiMHH/lwwUYJ94IdDKJQhkwBshjHCHChIg/WwbyUSRESL81Oym0Qglk3FBEH4ERZAAOSuBgCwqPSJEBC1SEIYILKexAiGwKBbIHCF8gAYINXbSAAgqdPMCGC4SMIB8ATbABAghHxNGFDD+goKI0GTw2gAUXbLKIDC3MscM8NBAwWyBtcDGCFEG4Id8VAQEAIfkEBQoAbAAsAAAAABgAGAAACP8AAQgcKFAAC1xhBhBcyFDgigloSFjZFQGAgC2cGhIMgAuMDiY5fHGIEIDAAAgQBLBZyZJlklw6lozJIQyVmpIDGkjIAqrhmhdaauiI8YCVlDMWB0jY4KCDk4UfghHhsCvWMoYBBoRQk8FIT4GiYL1QlctUy7NshkQIgmVBAIFhzJBAtQMt2gRYChRYASAAMB8+cDjQSFAABgQIWABw8MzLFBOEFzoY4YHGGQSvYhzDFZlgAw+jPEDAVWbGKyqdBzqx8AGIBAa9KJwqkBosAwUMEjTB8WoVrdoAEpQooaDBk2RCTpkCzuzAAWQEAuxQQuxXgtSgXBkwIEYgFFtfbJGIsnu2yYRSBgYDGJIJyaxUPMivxFJBQwUGAgam0QXCRo8JVzC0mjMqkFLKEwvx0MURXUCTQhWhLNMBBlCQcssJrRTDBVYltKCLDD+gMEwzvPBShAkiwGAMM/KFYkILLXTygCYupHBDERdE0UFnoEihwg0uuCALGcrUwsUWwAXQAAsFhLKAKPIFBAAh+QQFCgBOACwAAAAAGAAYAAAI/wABCBwoUMACC3tYfTAigKBDh6JqyWD3QksNbN3o8LvykOAnC7BQEeFQQ8cSJjnqeKvWsaAGCiReqOLAgUg6KzmukcgA4FPHCjOm+CDxLloePnb2bLrTDoCALQEcelgVY8q0Ig4ePhkQgMAArgMhTJoxDRy/qC0DDIDQAMJAda8osLuAtiUAAg0kJCAAIECKV69gPbE78FObECE4bkh1SsgJwgRFdYgQIgAVakpwWIA8EE6GDCwIWKBmDQQXzgIJYOCCYcgoEEhsYEANAFSoAqEghHIHwgYf2isQIKAxIEE2G0cq0F5gZ0QBAQFgxInDxg3nLdtGjXoj0EOP71HqtogMogCKBQkCB3CT8WMOEJ8dP0VwVUJBk7rwPLVAwUaOqIegUHHAATwwwBFBHzywyQNswIBLEE9ssIAUB0xgQBU8LPBQALSkwIYLKdxgQh9JtKKCBhVMcIARdr0hjSw7iChCOydIQ8oEDCQAGQEFaGPKBTCcoEIpH0TQEGoBiOJAEG88MQR8DwUEACH5BAUKAJkALAAAAAAYABgAAAj/AAEIHCiQkwM7eeQ4UiOAoEOHThA9oDTDzB0fFHpEMfSQ4Cc+kyjEmOKDxAsiHGpoAaPgU8dABhRRmBHDi5cYlV6k9LMEE56GDvfgEFKGAh0RCtIg4MFohp8xTBJdcIgAjRIhYGBseKjG06AcdQYdGDiAzRcckQwE6giA0wUrh3RcGmgBBJJIa9ayBSDgwRcgAQQGuAACBIoEe8nCARAocJs5R2w8SkwwgAACZwBwWRSnixTKAwMMGB1AiqUuPwqBFgyhQQMBYlrIQJFhdds2EiScCTLnBwoqtgmECJFFwBU9KDqFsW0oQocsAQJoePBAxArQnIwUytBGYBo24Eu4iEz8pAAWDEMEEpDkIgUZKeM7JqBBo8CCwAILEdpxQ8SH9A5twYIggiBQwAAOjUBIESZcUAEfarhxxQYYNMIAEB4g0J1DASDQhwgwnKCCBlHsUYUcJSgAhSCIsbVAFCckMWIFURhwAA8KNHFdYme8AckfE0xgIwNpbMCJbZ8MkIAaDrgBByBsBQQAOw==',\n    blue: 'data:image/gif;base64,R0lGODlhGAAYAKUAAESm9KTS/HS69NTq/Lze/IzG/Fyu9Oz2/Kza/HzC/MTm/JTO/GS29OTy/Pz+/FSu9KzW/Hy+9Nzy/MTi/JTK/GSy9PT2/LTa/ITC/Mzm/JzO/Gy29Eyq9KTW/HS+9Nzu/Lzi/IzK/Fyy9PT6/LTe/ITG/Mzq/JzS/Gy69EKl9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJgSLTIHB2C5JDqHBtLIQVUyARzAc5g4jKZVJieb3VK8X4fFMbqOOQ+Os2sZ1TOYhza1fD/0Qw8NB4QHCXtOWH9/exeDBw0VW0OKDwZygoMNEZNEAJYPIgAhDaUkiJ0pHCKsDxcSpQKpniIVFQ8ZDR8DcrNDtQwVH8MTvkQiDMkDAx8gxr8bKAwKyxmoswwoKBsQAxkmIs8AKAIoFSUZ6SHPDwICHn/pGSAPvgACER4echoKExMBrj1hkCBBBAZCHhD4B0JDryfjMGAw+FAACAIYO6B4yOcBhhIgExhwEgHjhZMdCiTwkCAEhRAFCmAI9wQFgpMIIHQIEEDDIIKXIUrQ3PIgBIScPE9o0ECBwj5f7UJoOHGCwkGOQ4IAACH5BAkFACgALAAAAAAYABgAhUSm9KTS/HS69NTq/IzG/Lze/Fyu9Oz2/Kza/HzC/JTO/GS29Mzm/OTy/MTm/Pz+/FSu9KzW/Hy+9Nzy/JTK/MTi/GSy9PT2/LTa/ITC/JzO/Gy29Eyq9KTW/HS+9Nzu/IzK/Lzi/Fyy9PT6/LTe/ITG/JzS/Gy69EKl9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb+QJRwiDJQKo3RqMHQiIjQIQRzOChHj+yDAYgSE5+qVfnAPiSAtBfUEFsv1WwjTYdKGvhqJQNJQyQCdAAcXUIQAxN4HwmFUWkckIURHx8NAxZeRJCbKIeUHx6ZRIOQfSUDHwMRolClfREDsSesoxC2EBUMAwwctES3EAzCIb6/BscODA4kxVIGIiIFuQWNvs/QARUhFRDNAAsLFhAJFQUFJc0c4Au25gUYvbTfGycLXRQkJBga1V4iAic2POkUAQMGBCDiObLgwcOJE41OHIwQQcOGagAgCEjQUEA3Ih4odghgQkEGhx4yZEggQYIHA14WBBhpQoMCCiAIlCix0sMcRy8QMpgIYJMCTgIZJJxQKIrDBgkgcJ5cwHRIEAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJgyhAifw6ExWYiI0OED0mgoLYfRyDEiPKPCyKCqVGq3DoelBC59GhLy8mBBp0lRz2f/BiUeAAAPEQRpE4EAUhkDewMRiVEbExyIQwEDmBkMYESIgSkPGSaYApxQABypABgZrR2mp5SUAQqtG7CdqakEE72QuEIcD6kgvRfARMIPDwQgBAjIQ8oPFwQEF7/Ay8saJBckD9EABuSDF+cJ0Rwi7MIIFwgQHMAAIhXsiQUQ+yHZYCIMGFQIl4JDBwgdApSYB6behoAMfjEIQPEEhYidHmxAgWLDBoZDUAQ4oWEBhRARUDBA4aGlAI4EoTCgUPJkgRIlMESI4OFlGswoHCJQGHoTA4YEPDHi4sAgAs4SKUWAJBIEACH5BAkFACgALAAAAAAYABgAhUSm9KTS/HS69NTq/Lze/IzG/Fyu9Oz2/Kza/HzC/MTm/JTO/GS29OTy/Pz+/FSu9KzW/Hy+9Nzy/MTi/JTK/PT2/LTa/ITC/Mzm/JzO/Gy29Eyq9KTW/HS+9Nzu/Lzi/IzK/Fyy9PT6/LTe/ITG/Mzq/JzS/Gy69EKl9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb+QJRwiHqALIPGwfOhhIjQ4YPjaSiVh4pIZDFEh52Bp2o9mA9b0YH0JQ3E40Y8u3XYF9AO5i22RB4AABsdBHV2F1ITewMYHQBfJw12DhiPKBkYmRNPX0IPDSIZgUUTEwoYJ51EDAyBjwkfpQGqUK6BGROxDLS1ggAWBMEbvESCGwAjBCMQxMUbzxbRs81Cz88cFggQls3PDxsU2hAP1AAP54MQ6h3UG+ffDwEcASbDvOYGBoAoJAH0Bdw6PQgR4ly1DCYyZEhgL4o5BiEYGODGQOECCiRaOWPFykBDIScuUgBRgIQAjidOaNDAilyUECBIlkyQIEIHAQJUMvgIZVAWSRIXItTskDJEwE4bGJyIIPTkxC9BAAAh+QQJBQAoACwAAAAAGAAYAIVEpvSk0vx0uvTU6vy83vyMxvxcrvTs9vys2vx8wvzE5vyUzvxktvT8/vxUrvSs1vx8vvTk8vzE4vyUyvxksvT09vy02vyEwvzM5vyczvxstvRMqvSk1vx0vvTc7vy84vyMyvxcsvT0+vy03vyExvzM6vyc0vxsuvRCpfUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/kCUcIhykCwYj2dAABmI0KHDNBgoPZHsIWJxRIcCSalqVWoP28s3ocBgyEtsBF0RiSbQk6TtRnQcAAAbHRYHIoYiakIOFhKOHwIAXxoedochQhMEHxIEmF+LHg2jBCgbIwSpGqBEIXajIRAjFgQLrFABow0ZExa+DLetDREEEAEIFg+SwVKBAA/QJsxEzgAcDxwZ00PVGQEBJsvTgoIk4Ble2xvrACcmGRkC24LsG/ALExvTGw7sKBD4QCQQB4pfP32mQICYUKADwiiCDDiYKC4EiAIgLiQwQDBiiIn9oDAoQILEBQgdNIRYyYDCyhAGHrY6mQBCgg4CTmjQ0HKlGEwoG07YRHmiKIOjgJhtCMHgRE6VSaMEAQAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJh6YDqEjGlyKTyI0OGDAppkMoPsZwt5RoWoy2R8xQ62n8Yn8fUQ3lVFxnpON+4haPjyJgQEDwAAHB4QEg0HiWxCHBAIFyQXKABfGwOIBw0GQiWOF11fUgOJFiRFARCpDKFEIgcWIwciKB0BHXmsRAEjvAsFAcAiuUQPvCMZFCfKlMNDHw4ODQsaGhTNRCDQIwvcBddDBNAOBRQUIczXE9AHCSEUBRzfKRwiGBQMBQUhq9eCgvMFSpTAEK+ZP2YoBmKYNGwQh3/zIpSIkIBhKA4YB7WKQNEDikBQADzAmBGKiAQePAhAsUHEg5cwRz788kDAShQoGDAQIcJAGUx0UQAwYLmBQYUKPH3ObPhAhE6eI4EOCQIAIfkECQUAKQAsAAAAABgAGAAABv7AlHCYekQ0F9CE0Ck9iNAhJ3QhESYTRSYzyASeUeGmc7kQCMrJ1jRoe8IoCKJsPmPZg4++AGV0IIAXCygPAAAcGwF5ehIRUhoBAR0dGwBhDBkfDQ0fYAknkSdgYUUKmw0XKRwaGicaFaREIh8HDQcPKAsLGhixUBoHwRQJFBQLIr5ED8EHEyUUISGWyUMDBxYSIQUhvdRDBCMjFiUFBY7eQiDhIxglJRjT3hnhDQIRJREc6BwHDg4TIvAlYIAOgz8HGjgkiJDAQ7xYADIcNJCCQUMPDB6GOXTBAQEhHAR4ECBAhEYihgxFGIULBYoNDDhoPJTyJK4NMBmIePCAAySHnj5ThnnAoGgFETt5+gx6cggAEUeRGuAJ1BA1ADynKpUZJggAIfkECQUAKQAsAAAAABgAGAAABv7AlHCYengoEBLhcko8iNAhpwRBXC4EAmgyUWieUSEj0IFALkoCN8NWeMKoUyBQvV637MxgUIJWNBpzHRQbHAAAHBsnensfb0IcFAsnGicMAGEMIAMfHwNgRwsaFGBhRROdHxApHCEUryKmRCKcDRIPDAUhIRGyUAsNwRQeuiGlvikGwQ0ECSUlGJjIQxkHBx8Y0L3TQyTWBwkRJY/cKRcW1hHhHtLc1SMfKB4RHhzlHAcjIxMiHh4Csbhh0DdiAQcBAlCgaCeLwwcH+mKJULhBBMMwFxxoJABpA4oNDCzK2qDRwQEDUhiorCDCEJRDAEJoxAAFV0gRBh5w2GnI0CChABrCcBDB8kFOnj4P+QJg9IFTnTyVTkPklINOmGGCAAAh+QQJBQAnACwAAAAAGAAYAIVEpvSk0vx0uvTU6vyMxvy83vxcrvTs9vys2vx8wvyUzvxktvTM5vzE5vz8/vxUrvSs1vx8vvTk8vyUyvzE4vxksvT09vy02vyEwvyczvxstvRMqvSk1vx0vvTc7vyMyvy84vxcsvT0+vy03vyExvyc0vxsuvRCpfUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/sCTcHjaCD4lCAKiiDyI0OEmEQhwIJCLtlBQPKPC0KRUsiq3IAqlIABrFIpMpgq5XrjpBiMBDX0mE3IkCxsAABsLGWoMjB1SJB9/HxVgJyEFjAMNXwIEngRflRuYAwMBRSSpoJVEIQwDHgMPIRgJJI6sRBOxHh8mCRhOuUQPHsYXHQkREQDDRA0SEgwRHREmzkQX0RId3dfYQtrRJgICJs3gFAcSHhUm7xvgABIHBwUPJhoaocMY9QcKAGhYQBBdrg0DRIg4YMASwQoPDFa6oFDEBSEAKlQIESJiJQAUHSwMtYFjCAMPCkEx4cGBSBEYoJTsmDJiIUMaXLqcAGbDHYOfKTfcNDTAgYWYH30KFWrIEIYCIZwdWnpIIpQgACH5BAkFACkALAAAAAAYABgAAAb+wJRwmOJsEpTTSRMSPIjQIUcQoiw0p0AAAkEUnlGhqFSoXjWBDvdyQaDC40LZqsFuERcSgRCBGjAYJSUhESIcAAAcDCFseyBvQhweCYEYImEpIhAgExMEYAwRCQkRHJiREJ0TGkUeHhERYKcpBiAKGRMPDwKuDLNEBRnCJQwCKE6/Qw8ZJgMQDCjRAMlDBAMDEygbKL7UQh3XAwwbDBXeQhAfHwMVDOXnKSTqJiIiFYbeAB8NHwQc9fioJWhAsMA/EQYeTPvFIcMBgk90SUwG4cBDCEIASHxwCBOAihYbyOKgi4PJhUMARBgwwoLFBFBMcjw57cSEAw5G6LRAIUwfIpqIPjjImfMATI+Jkuob6sACCQPJEEkdcGCVLCJBAAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJjiVDylQiiEQXGI0CFnkyglQxTKQnPCPKPChyeBwVy13ICaATZ4IglyshTaagKdDsQDfaA8bwkoIgCFHCIleBAQFyhDACgCAoAPYCkGJ4wICJUpIhsooV+WHAEXFwQhKQAbDCgbo5YpDxckBAQPDwy7IrJQJbcECQYMFQyxvg8EExMnIs8VAL5QEMwEzyKd00MnGRMKIgbZ20QaGee5ueRDFybe6Q/S5AAZAwMQHLkcyLIR9gMlAOx7wEGeLwATPnwYUGnfPoOyAkhQCEGIwIIFZQEI0KAjw0cFCwEgSASAhwwHOn6IAEVkoQkjMiCAQKDBgZsdVUURycCBIE8HI4JawJng4IWfI4COuHlBmywR5hqMsNBgAgWnQ4IAACH5BAkFACkALAAAAAAYABgAAAb+wJRwmOKIUBFMCeNhcIjQIYAhiCQSpVIhRKFEnlHhAyXwWBPKLWXBZoQfG7LHfMWoFxrNCQXlbBgoZCgPHACGIhF4JwEBbkJTgHFgUQYLAR0QAWAPFQyek2EACxCYBSkAIhUiTmFQD5gXCIQitA+tUQkXuh4PBrSgtykPuiQaD8cPAMFQAQQEEMi2y0QLziDIwMsaEyATHITZwR0TExcc54XTjxMZEx0A6MrqHhn1GPCG8sscBAP1tvgMTdMwoOAJKfk0hDt14sMHf9JOGdLg4EABUAA8KGjgcICHKCEciBxxYAKpfgc4OjQVBcMIkQ5GyDxAs4HNARFuiSDwUuYSCAs0U0KbVmlCA5ofCIQwECYIACH5BAkFACkALAAAAAAYABgAAAb+wJRwmAIYGIJIIoKqcIjQIUCEEng8ykSpVPA8o0IOA0W2KjGYUqgQEoEfDMamfFVyQxTKYgPliCpxGwwPAIVGHngLCxoMUiKPDE5gKQ8FiycaXw8PjyJfkxwhGgEBCUUPBiIGn5OUGh0dAQ8cm5utUB4dEBACtJsAt0QPuwghtLTAwUMaCBcBHNCsyiEX1dHSwRQXBBfRycpCJwQECIaF4Ckc4yAa5t/BKBPypoa0yhwXCvIPRQAeBwPegVmQoaCGIRAcKMyATQgADQMKTuAnpIRChQ0KsPI3YYBHEwKgLFBoYcSIBhNiXRjwoaVHDGAwHHBgcsSBAw1ySnDpoZUZgQsjLNy8mbPBB1nKRFCY8AHnAAIFKEIJAgAh+QQJBQAnACwAAAAAGAAYAIVEpvSk0vx0uvTU6vyMxvy83vxcrvTs9vys2vx8wvyUzvxktvTE5vz8/vxUrvSs1vx8vvTk8vyUyvxksvT09vy02vyEwvyczvxstvTM5vxMqvSk1vx0vvTc7vyMyvzE4vxcsvT0+vy03vyExvyc0vxsuvTM6vxCpfUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/sCTcHgCOECLkhID0hCfQ+NkgVEKOBxIAuOECjUg5IKqxGoTFpBXcwxPkyXB2TIaLZ5GhyHcBACKDhh0IwQeakJ5DnpdUBoWhR4EXRpsin9eiCMeEhIQRZQOlJhEGhIKpxoAlKKjRCUXFyQlq6mtpLABI36qtk8SASQXqn69RCMbDwG7l8UnBA8PG7u1zRIIyQIIHSF3xQAIFRUSFw3lD80lBeEcIOUNFA69AA8FIiLxBeUhGcyYEgUAJQgxcCBEgxAfGOFRwODDh3tDLIQIQSFEhxEKAZSokOFDQwy+QhwYeaBDARIkHmQYMCBDRwteEpA8EKFmhw4sWzIQMMpAF4UING3ebBkgni0DHgoMuJnhAQGjT4IAACH5BAkFACkALAAAAAAYABgAAAb+wJRwmAJwHiJRpSJ6AIjQofHxMCQZWBRKxIlKqdSkCLvRCgQPr/EITlYYqLLHExFB1xw251nkiAR0HglpUnl5fFEcAhEJGAldRUaSXlKCGBgoQgCbe5REHJclJV2bm55QDAUhBQwaBSKIp0IcJSEhEQcODg2QskIYFBQhug4HvkQRCwsUI7oNx0MJGgsaB80HsbIlGicUEw4WIwzQACcBASULI+sB0AwQ51vhBweEpwAaEB0daSQHFgcmZItSAAEECCWEGGhA7wCBXncKkLhwAUKvBPQaNBiAIRYAFBAIULyQiUgIjRo/DEAwLQCBCSAIECDhwUuCDw0+6BwwIIMgzwkwLwjw9ACCTpU8MyhQAIKCPaIFJ5jIQKADhqdDggAAIfkECQUAJwAsAAAAABgAGACFRKb0pNL8dLr01Or8jMb8vN78XK707Pb8rNr8fML8lM78zOb8ZLb05PL8xOb8/P78VK70rNb8fL703PL8lMr8xOL8ZLL09Pb8tNr8hML8nM78TKr0pNb8dL703O78jMr8vOL8XLL0bLr09Pr8tN78hMb8nNL8QqX1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABv7Ak3B4AgA2kKQSQGwOjRsk0hCqWiBMpxAaVUKsjDBEy40ik2CGSDRuGqHvLVK9FrRPocTbqD1tLCICAiIbQgUPFVh9T4EdHQx4D5IHd4sbAhKZGwqSDwtZi0J5CQkhC50CoUQbCRkZAg0XIxeFqkMSJRkJI7wetkQdJSUEvCO+v0ICHwQEHiMHDaC2CR8fJRUH2RbIABTeEhTZBybIFgrnIiEN0B6Vfd0mGhqFGA32BdJaCQHxEkIQHuxNwFDLCYAMHALwKyhhQgMPHiokKFiEgYkIGDlsI9IM4oABFThQ+KAAg0kMCCKI0NJhgYcBC2JWqFCgJsoIkPpACLAA5hcCBzMLkMBAgKLOEhwKVACBQYMEd0KCAAAh+QQJBQApACwAAAAAGAAYAAAG/sCUcJgCADjIB0cJIDqJRmTy8TBQm8+hMTqtir6crCa05Ry7okqF8XCWHI7S1ig8PtIMxiYsFB3gDhtZdSJ5KChYBIAHgoNFDIcoIikiIyNwEY5DHBsoAgIAC5YWE1ialAIeHiITliOZp3WqESgSFiMHfLEpAgkJHgcWBx+7QygJGAkHywPFQijJGB/LH6axHiUFCQQN3RXFAAUFJR4h3Q0axSIh7AwG5wNtpwAlFBQhYRASHx8k1lkRFiyg4EHIgwH8PkDQ5QRAhBMaNFDQFWGAxQEgIlgDwIBCgAAQvxEpcTFDBhABxlHoAAFCB5AosngwaXICCAIESFxA0PIEHANHDzQomEAU54WjEErI0/QAQ4ALOSEs8MBwSBAAIfkECQUAKQAsAAAAABgAGAAABv7AlHA4BAA4SI6RyByKCBGjMfl4KJvD0sihkCqpVQ524Sg7UF5OtWoQiYmY0basIU4fbZF7+DhsRxYYAFgAD3oVIoMpJHIjBwxYRQYVDAwiKQYHFoARkXaVDBsAFAd+IIqeQg8bKCgPIKUHHqlEHKwoFR+lDW+0QgwCrQ3DA75EDB4eAsMNGcZDDBEeEQPEqL4oCdIXH92XxgAJGAkoId0fC88GJeIiDwMfAxkPvuEFJSViHQP8CNdYAkIUKIBCVQZ5GTr0YgLAA4UQIfIN8XAwwwQCHq4BqFBAw4KH34ZgUGDx4oUFJTAUOBHghEcKGwCCmHCRAIkLECB0aOmxghunBxQIgCBw4QKCnC0xLIz0IMICCDgDhHCFJQgAO1pLS0ZrNFJPdlJ0cDZyWm5DeXhwUWY3aUpSN09qSFBJbEdWOUxOMnY1b0VjM1J1clNvcHBZczc5N1RhS3lLTmw='\n};\n"
  },
  {
    "path": "app/src/components/configs/s3ACLs.js",
    "content": "export default {\n    \"public-read\": 'Public read',\n    \"private\": 'Private',\n    \"bucket-owner-read\": 'Bucket owner read',\n    \"bucket-owner-full-control\": 'Bucket owner full control',\n    \"authenticated-read\": \"Authenticated read\"\n};\n"
  },
  {
    "path": "app/src/components/configs/s3Regions.js",
    "content": "export default {\n    \"\": 'Select region',\n    \"us-east-1\": 'US East (N. Virginia) (us-east-1)',\n    \"us-east-2\": 'US East (Ohio) (us-east-2)',\n    \"us-west-1\": 'US West (N. California) (us-west-1)',\n    \"us-west-2\": 'US West (Oregon) (us-west-2)',\n    \"ca-central-1\": 'Canada (Central) (ca-central-1)',\n    \"ca-west-1\": 'Canada West (Calgary) (ca-west-1)',\n    \"eu-central-1\": 'Europe (Frankfurt) (eu-central-1)',\n    \"eu-central-2\": 'Europe (Zurich) (eu-central-2)',\n    \"eu-south-1\": 'Europe (Milan) (eu-south-1)',\n    \"eu-south-2\": 'Europe (Spain) (eu-south-2)',\n    \"eu-west-1\": 'Europe (Ireland) (eu-west-1)',\n    \"eu-west-2\": 'Europe (London) (eu-west-2)',\n    \"eu-west-3\": 'Europe (Paris) (eu-west-3)',\n    \"eu-north-1\": 'Europe (Stockholm) (eu-north-1)',\n    \"af-south-1\": 'Africa (Cape Town) (af-south-1)',\n    \"ap-east-1\": 'Asia Pacific (Hong Kong) (ap-east-1)',\n    \"ap-east-2\": 'Asia Pacific (Taipei) (ap-east-2)',\n    \"ap-northeast-1\": 'Asia Pacific (Tokyo) (ap-northeast-1)',\n    \"ap-northeast-2\": 'Asia Pacific (Seoul) (ap-northeast-2)',\n    \"ap-northeast-3\": 'Asia Pacific (Osaka) (ap-northeast-3)',\n    \"ap-southeast-1\": 'Asia Pacific (Singapore) (ap-southeast-1)',\n    \"ap-southeast-2\": 'Asia Pacific (Sydney) (ap-southeast-2)',\n    \"ap-southeast-3\": 'Asia Pacific (Jakarta) (ap-southeast-3)',\n    \"ap-southeast-4\": 'Asia Pacific (Melbourne) (ap-southeast-4)',\n    \"ap-southeast-5\": 'Asia Pacific (Malaysia) (ap-southeast-5)',\n    \"ap-south-1\": 'Asia Pacific (Mumbai) (ap-south-1)',\n    \"ap-south-2\": 'Asia Pacific (Hyderabad) (ap-south-2)',\n    \"sa-east-1\": 'South America (São Paulo) (sa-east-1)',\n    \"cn-north-1\": 'China (Beijing) (cn-north-1)',\n    \"cn-northwest-1\": 'China (Ningxia) (cn-northwest-1)',\n    \"me-south-1\": 'Middle East (Bahrain) (me-south-1)',\n    \"me-central-1\": \"Middle East (UAE) (me-central-1)\",\n    \"mx-central-1\": 'Mexico (Central) (mx-central-1)',\n    \"il-central-1\": 'Israel (Tel Aviv) (il-central-1)',\n    \"us-gov-east-1\": 'AWS GovCloud (US-East) (us-gov-east-1)',\n    \"us-gov-west-1\": 'AWS GovCloud (US) (us-gov-west-1)'\n};\n"
  },
  {
    "path": "app/src/components/configs/sidebar-icons.js",
    "content": "export default {\n    DEFAULT: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon default\"><path d=\"M12 13v8\"/><path d=\"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242\"/><path d=\"m8 17 4-4 4 4\"/></svg>',\n    NO_CONFIG: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon no-config\"><path d=\"m2 2 20 20\"/><path d=\"M8.35 2.69A10 10 0 0 1 21.3 15.65\"/><path d=\"M19.08 19.08A10 10 0 1 1 4.92 4.92\"/></svg>',\n    PREPARING: '<span class=\"sidebar-sync-preparing\"><span></span></span>', \n    PREPARED: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon prepared\"><path d=\"m10.852 19.772-.383.924\"/><path d=\"m13.148 14.228.383-.923\"/><path d=\"M13.148 19.772a3 3 0 1 0-2.296-5.544l-.383-.923\"/><path d=\"m13.53 20.696-.382-.924a3 3 0 1 1-2.296-5.544\"/><path d=\"m14.772 15.852.923-.383\"/><path d=\"m14.772 18.148.923.383\"/><path d=\"M4.2 15.1a7 7 0 1 1 9.93-9.858A7 7 0 0 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.2\"/><path d=\"m9.228 15.852-.923-.383\"/><path d=\"m9.228 18.148-.923.383\"/></svg>',\n    NOT_PREPARED: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon nor-prepared\"><path d=\"M12 12v4\"/><path d=\"M12 20h.01\"/><path d=\"M17 18h.5a1 1 0 0 0 0-9h-1.79A7 7 0 1 0 7 17.708\"/></svg>',\n    SYNCING: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon syncing\"><path d=\"M12 13v8\"/><path d=\"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242\"/><path d=\"m8 17 4-4 4 4\"/></svg>',\n    SYNCED: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon synced\"><path d=\"m17 15-5.5 5.5L9 18\"/><path d=\"M5 17.743A7 7 0 1 1 15.71 10h1.79a4.5 4.5 0 0 1 1.5 8.742\"/></svg>',\n    NOT_SYNCED: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon default\"><path d=\"M12 13v8\"/><path d=\"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242\"/><path d=\"m8 17 4-4 4 4\"/></svg>',\n    PROVIDE_ACCESS: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-interjection-icon provide-access\"><path d=\"M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4\"/><path d=\"M14 13.12c0 2.38 0 6.38-1 8.88\"/><path d=\"M17.29 21.02c.12-.6.43-2.3.5-3.02\"/><path d=\"M2 12a10 10 0 0 1 18-6\"/><path d=\"M2 16h.01\"/><path d=\"M21.8 16c.2-2 .131-5.354 0-6\"/><path d=\"M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2\"/><path d=\"M8.65 22c.21-.66.45-1.32.57-2\"/><path d=\"M9 6.8a6 6 0 0 1 9 5.2v2\"/></svg>',\n    SYNC: '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"sidebar-sync-icon sync\"><path d=\"M12 13v8\"/><path d=\"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242\"/><path d=\"m8 17 4-4 4 4\"/></svg>'\n};\n"
  },
  {
    "path": "app/src/components/mixins/BackToTools.js",
    "content": "export default {\n    methods: {\n        goBack: function() {\n            let siteName = this.$route.params.name;\n            this.$router.push('/site/' + siteName + '/tools/');\n        }\n    }\n};\n"
  },
  {
    "path": "app/src/components/mixins/CollectionCheckboxes.js",
    "content": "export default {\n    computed: {\n        anyCheckboxIsSelected () {\n            return !!this.selectedItems.length;\n        }\n    },\n    methods: {\n        toggleAllCheckboxes (useArrayIndexAsID = false) {\n            if(this.selectedItems.length > 0 && this.selectedItems.length >= this.items.length) {\n                this.selectedItems = [];\n            } else {\n                this.selectedItems = [];\n\n                if (!useArrayIndexAsID) {\n                    for (let item of this.items) {\n                        this.selectedItems.push(item.id);\n                    }\n                } else {\n                    for (let i = 0; i < this.items.length; i++) {\n                        this.selectedItems.push(i);\n                    }\n                }\n            }\n        },\n        isChecked (id) {\n            return this.selectedItems.indexOf(id) > -1;\n        },\n        toggleSelection (id) {\n            let index = this.selectedItems.indexOf(id);\n\n            if(index > -1) {\n                this.selectedItems.splice(index, 1);\n            } else {\n                this.selectedItems.push(id);\n            }\n        },\n        getSelectedItems (itemsCanBeFiltered = true) {\n            let visibleIDs;\n            let selectedItems;\n\n            if(itemsCanBeFiltered) {\n                visibleIDs = this.items.map(item => item.id);\n                selectedItems = this.selectedItems.filter(id => visibleIDs.indexOf(id) > -1);\n            } else {\n                selectedItems = this.selectedItems;\n            }\n\n            return selectedItems;\n        }\n    }\n};\n"
  },
  {
    "path": "app/src/components/mixins/GoToLastOpenedWebsite.vue",
    "content": "<script>\nexport default {\n    name: 'go-to-last-opened-website',\n    methods: {\n        goBack () {\n            let lastOpened = localStorage.getItem('publii-last-opened-website');\n            let sites = Object.keys(this.$store.state.sites);\n\n            if (sites.indexOf(lastOpened) > -1) {\n                this.$router.push('/site/' + lastOpened + '/posts/');\n            } else {\n                if (sites.length > 0) {\n                    this.$router.push('/site/' + sites[0] + '/posts/');\n                } else {\n                    this.$router.push('/site/!/posts/');\n                }\n            }\n        }\n    }\n}\n</script>\n"
  },
  {
    "path": "app/src/components/mixins/PostEditorsCommon.vue",
    "content": "<script>\nexport default {\n    name: 'post-editors-common',\n    mounted () {\n        this.sidebarVisible = localStorage.getItem('publii-' + this.$options.editorType + '-editor-sidebar') === 'opened';\n        this.$bus.$on('update-post-slug', this.updateSlug);\n    },\n    methods: {\n        slugUpdated () {\n            this.postSlugEdited = true;\n        },\n        async updateSlug (forceUpdate = false) {\n            if ((this.isEdit || this.postSlugEdited) && !forceUpdate) {\n                return;\n            }\n\n            let slugValue = await mainProcessAPI.invoke('app-main-process-create-slug', this.postData.title);\n            this.postData.slug = slugValue;\n        },\n        toggleSidebar () {\n            this.sidebarVisible = !this.sidebarVisible;\n\n            if (this.sidebarVisible) {\n                this.sidebarVisible = true;\n                localStorage.setItem('publii-' + this.$options.editorType + '-editor-sidebar', 'opened');\n            } else {\n                this.sidebarVisible = false;\n                localStorage.setItem('publii-' + this.$options.editorType + '-editor-sidebar', 'closed');\n            }\n        },\n        closeEditor () {\n            let siteName = this.$route.params.name;\n            let itemType = this.itemType === 'page' ? 'pages' : 'posts'; \n\n            if (this.postData.isTrashed) {\n                this.$router.push('/site/' + siteName + '/' + itemType + '/trashed');\n            } else {\n                this.$router.push('/site/' + siteName + '/' + itemType + '/');\n            }\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('update-post-slug', this.updateSlug);\n    }\n}\n</script>\n"
  },
  {
    "path": "app/src/components/post-editor/AuthorPopup.vue",
    "content": "<template>\n    <div\n        v-if=\"isVisible\"\n        class=\"overlay\">\n        <div class=\"popup\">\n            <p class=\"message\">\n                <template v-if=\"itemType === 'post'\">\n                    {{ $t('author.changePostAuthor') }}\n                </template>\n                <template v-else-if=\"itemType === 'page'\">\n                    {{ $t('author.changePageAuthor') }}\n                </template>\n            </p>\n\n            <dropdown\n                id=\"author-select\"\n                :items=\"authors\"\n                v-model=\"selectedAuthor\" />\n\n            <div class=\"buttons\">\n                <p-button\n                    type=\"medium no-border-radius half-width\"\n                    @click.native=\"changeAuthor\">\n                    {{ $t('ui.ok') }}\n                </p-button>\n\n                <p-button\n                    type=\"medium no-border-radius half-width cancel-popup\"\n                    @click.native=\"cancel\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'author-popup',\n    props: [\n        'itemType'\n    ],\n    data () {\n        return {\n            isVisible: false,\n            selectedAuthor: 0\n        };\n    },\n    computed: {\n        authors () {\n            let authors = [];\n            let sortedAuthors = this.$store.state.currentSite.authors.slice().sort((a, b) => {\n                if (a.name.toLowerCase() > b.name.toLowerCase()) {\n                    return 1;\n                }\n\n                if (a.name.toLowerCase() < b.name.toLowerCase()) {\n                    return -1;\n                }\n\n                return 0;\n            });\n\n            for (let author of sortedAuthors) {\n                authors.push({\n                    value: author.id,\n                    label: author.name\n                });\n            }\n\n            return authors;\n        }\n    },\n    mounted: function() {\n        this.$bus.$on('author-popup-display', (authorID) => {\n            this.selectedAuthor = authorID;\n\n            setTimeout(() => {\n                this.isVisible = true;\n            }, 0);\n        });\n    },\n    methods: {\n        changeAuthor: function() {\n            this.$bus.$emit('author-changed', this.selectedAuthor);\n            this.isVisible = false;\n        },\n        cancel: function() {\n            this.isVisible = false;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('author-popup-display');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100005;\n}\n\n.popup {\n    max-width: 48rem;\n    min-width: 48rem;\n    padding: 4rem;\n}\n\n.message {\n    font-size: 1.6rem;\n    padding: 0 0 4rem 0;\n}\n\n.buttons {\n    display: flex;\n    margin: 4rem -4rem -4rem -4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n}\n\n#author-select {\n    margin-top: -1rem;\n}\n\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/CodeMirror/codemirror-4.inline-attachment.js",
    "content": "/*\n * CodeMirror version for inlineAttachment for Publii\n *\n * Call inlineAttachment.attach(editor) to attach to a codemirror instance\n *\n * It is a modified version of the codemirror-4.inline-attachment.js file\n * Original Author: Roy van Kaathoven\n * Contact: ik@royvankaathoven.nl\n * \n * License: MIT \n * \n * Copyright (c) 2013 Roy van Kaathoven\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n(function() {\n    'use strict';\n  \n    var codeMirrorEditor = function(instance) {\n  \n      if (!instance.getWrapperElement) {\n        throw \"Invalid CodeMirror object given\";\n      }\n  \n      this.codeMirror = instance;\n    };\n  \n    codeMirrorEditor.prototype.getValue = function() {\n      return this.codeMirror.getValue();\n    };\n  \n    codeMirrorEditor.prototype.insertValue = function(val) {\n      this.codeMirror.replaceSelection(val);\n    };\n  \n    codeMirrorEditor.prototype.setValue = function(val) {\n      var cursor = this.codeMirror.getCursor();\n      this.codeMirror.setValue(val);\n      this.codeMirror.setCursor(cursor);\n    };\n  \n    codeMirrorEditor.attach = function(codeMirror, options) {\n      options = options || {};\n  \n      var editor = new codeMirrorEditor(codeMirror),\n        inlineattach = new inlineAttachment(options, editor),\n        el = codeMirror.getWrapperElement();\n  \n      codeMirror.setOption('onDragEvent', function(data, e) {\n        if (e.type === \"drop\") {\n          e.stopPropagation();\n          e.preventDefault();\n          return inlineattach.onDrop(e);\n        }\n      });\n    };\n  \n    var codeMirrorEditor4 = function(instance) {\n      codeMirrorEditor.call(this, instance);\n    };\n  \n    codeMirrorEditor4.attach = function(codeMirror, options) {\n      options = options || {};\n  \n      var editor = new codeMirrorEditor(codeMirror),\n        inlineattach = new inlineAttachment(options, editor),\n        el = codeMirror.getWrapperElement();\n  \n      codeMirror.on('drop', function(data, e) {\n        if (inlineattach.onDrop(e)) {\n          e.stopPropagation();\n          e.preventDefault();\n          return true;\n        } else {\n          return false;\n        }\n      });\n    };\n  \n    inlineAttachment.editors.codemirror4 = codeMirrorEditor4;\n  })();\n"
  },
  {
    "path": "app/src/components/post-editor/CodeMirror/inline-attachment.js",
    "content": "/*\n * Inline Text Attachment for Publii in CodeMirror\n *\n * It is a modified version of the inline-attachment.js file\n * Original Author: Roy van Kaathoven\n * Contact: ik@royvankaathoven.nl\n * \n * License: MIT \n * \n * Copyright (c) 2013 Roy van Kaathoven\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n(function(document, window) {\n  'use strict';\n\n  var inlineAttachment = function(options, instance) {\n    this.settings = inlineAttachment.util.merge(options, inlineAttachment.defaults);\n    this.editor = instance;\n    this.filenameTag = '{filename}';\n    this.lastValue = null;\n  };\n\n  inlineAttachment.editors = {};\n\n  inlineAttachment.util = {\n    merge: function() {\n      var result = {};\n      for (var i = arguments.length - 1; i >= 0; i--) {\n        var obj = arguments[i];\n        for (var k in obj) {\n          if (obj.hasOwnProperty(k)) {\n            result[k] = obj[k];\n          }\n        }\n      }\n      return result;\n    },\n    appendInItsOwnLine: function(previous, appended) {\n      return (previous + \"\\n\\n[[D]]\" + appended)\n        .replace(/(\\n{2,})\\[\\[D\\]\\]/, \"\\n\\n\")\n        .replace(/^(\\n*)/, \"\");\n    },\n    insertTextAtCursor: function(el, text) {\n      var scrollPos = el.scrollTop,\n        strPos = 0,\n        browser = false,\n        range;\n\n      if ((el.selectionStart || el.selectionStart === '0')) {\n        browser = \"ff\";\n      } else if (document.selection) {\n        browser = \"ie\";\n      }\n\n      if (browser === \"ie\") {\n        el.focus();\n        range = document.selection.createRange();\n        range.moveStart('character', -el.value.length);\n        strPos = range.text.length;\n      } else if (browser === \"ff\") {\n        strPos = el.selectionStart;\n      }\n\n      var front = (el.value).substring(0, strPos);\n      var back = (el.value).substring(strPos, el.value.length);\n      el.value = front + text + back;\n      strPos = strPos + text.length;\n      if (browser === \"ie\") {\n        el.focus();\n        range = document.selection.createRange();\n        range.moveStart('character', -el.value.length);\n        range.moveStart('character', strPos);\n        range.moveEnd('character', 0);\n        range.select();\n      } else if (browser === \"ff\") {\n        el.selectionStart = strPos;\n        el.selectionEnd = strPos;\n        el.focus();\n      }\n      el.scrollTop = scrollPos;\n    }\n  };\n\n  inlineAttachment.defaults = {\n    uploadFieldName: 'file',\n    defaultExtension: 'png',\n    jsonFieldName: 'filename',\n    allowedTypes: [\n      'image/jpeg',\n      'image/png',\n      'image/jpg',\n      'image/gif',\n      'image/svg+xml',\n      'image/webp'\n    ],\n    progressText: '![Uploading file...]()',\n    urlText: \"![Image description]({filename})\",\n    errorText: \"Error uploading file\",\n    extraParams: {},\n    extraHeaders: {},\n    beforeFileUpload: function() {\n      return true;\n    },\n    onFileReceived: function() {},\n    onFileUploadResponse: function() {\n      return true;\n    },\n    onFileUploaded: function() {}\n  };\n\n\n  inlineAttachment.prototype.uploadFile = async function(file) {\n    let postID = parseInt(document.querySelector('.post-editor-markdown').getAttribute('data-post-id'), 10);\n\n    mainProcessAPI.send('app-image-upload', {\n        'id': postID,\n        'site': window.app.getSiteName(),\n        'path': await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(file))\n    });\n\n    mainProcessAPI.receiveOnce('app-image-uploaded', (data) => {            \n        var newValue = '';\n        \n        if (data.baseImage.size) {\n            newValue = this.settings.urlText.replace(this.filenameTag, data.baseImage.url + ' =' + data.baseImage.size[0] + 'x' + data.baseImage.size[1]);\n        } else {\n            newValue = this.settings.urlText.replace(this.filenameTag, data.baseImage.url);\n        }\n\n        var text = this.editor.getValue().replace(this.lastValue, newValue);\n        this.editor.setValue(text);\n    });\n  };\n\n  inlineAttachment.prototype.isFileAllowed = function(file) {\n    if (file.kind === 'string') { return false; }\n    if (this.settings.allowedTypes.indexOf('*') === 0){\n      return true;\n    } else {\n      return this.settings.allowedTypes.indexOf(file.type) >= 0;\n    }\n  };\n\n  inlineAttachment.prototype.onFileUploadResponse = function(xhr) {\n    if (this.settings.onFileUploadResponse.call(this, xhr) !== false) {\n      var result = JSON.parse(xhr.responseText),\n        filename = result[this.settings.jsonFieldName];\n\n      if (result && filename) {\n        var newValue;\n        if (typeof this.settings.urlText === 'function') {\n          newValue = this.settings.urlText.call(this, filename, result);\n        } else {\n          newValue = this.settings.urlText.replace(this.filenameTag, filename);\n        }\n        var text = this.editor.getValue().replace(this.lastValue, newValue);\n        this.editor.setValue(text);\n        this.settings.onFileUploaded.call(this, filename);\n      }\n    }\n  };\n\n  inlineAttachment.prototype.onFileInserted = function(file) {\n    if (this.settings.onFileReceived.call(this, file) !== false) {\n      this.lastValue = this.settings.progressText;\n      this.editor.insertValue(this.lastValue);\n    }\n  };\n\n  inlineAttachment.prototype.onDrop = function(e) {\n    var result = false;\n    for (var i = 0; i < e.dataTransfer.files.length; i++) {\n      var file = e.dataTransfer.files[i];\n      if (this.isFileAllowed(file)) {\n        result = true;\n        this.onFileInserted(file);\n        this.uploadFile(file);\n      }\n    }\n\n    return result;\n  };\n\n  window.inlineAttachment = inlineAttachment;\n})(document, window);\n"
  },
  {
    "path": "app/src/components/post-editor/DatePopup.vue",
    "content": "<template>\n    <div\n        v-if=\"isVisible\"\n        class=\"overlay\">\n        <div class=\"popup\">\n            <p class=\"message\">\n                <template v-if=\"itemType === 'post'\">\n                    {{ $t('date.changePostPublicationDate') }}:\n                </template>\n                <template v-else-if=\"itemType === 'page'\">\n                    {{ $t('date.changePagePublicationDate') }}:  \n                </template>\n            </p>\n\n            <div class=\"item-date-picker\">\n                <input\n                    type=\"date\"\n                    v-model=\"itemDateTime.date\" />\n                <input \n                    type=\"time\"\n                    v-model=\"itemDateTime.time\" />\n            </div>\n\n            <div class=\"buttons\">\n                <p-button\n                    type=\"medium no-border-radius half-width\"\n                    @click.native=\"changeDate\">\n                    {{ $t('ui.ok') }}\n                </p-button>\n\n                <p-button\n                    type=\"medium no-border-radius half-width cancel-popup\"\n                    @click.native=\"cancel\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'date-popup',\n    props: [\n        'itemType'\n    ],\n    data () {\n        return {\n            isVisible: false,\n            timestamp: 0,\n            itemDateTime: {\n                date: '',\n                time: ''\n            }\n        };\n    },\n    mounted: function() {\n        this.$bus.$on('date-popup-display', timestamp => {\n            if (timestamp) {\n                this.timestamp = timestamp;\n            } else {\n                this.timestamp = +new Date();\n            }\n\n            this.calculateTimeFromTimestamp();\n\n            setTimeout(() => {\n                this.isVisible = true;\n            }, 0);\n        });\n    },\n    methods: {\n        changeDate () {\n            this.calculateTimestampFromTime();\n            this.$bus.$emit('date-changed', this.timestamp);\n            this.isVisible = false;\n        },\n        cancel () {\n            this.isVisible = false;\n        },\n        calculateTimeFromTimestamp () {\n            let date = new Date(this.timestamp);\n            let month = date.getMonth() + 1;\n            let day = date.getDate();\n            let year = date.getFullYear();\n            let hours = date.getHours();\n            let minutes = date.getMinutes();\n\n            this.itemDateTime.date = [year, month, day].map(n => n < 10 ? '0' + n : n).join('-');\n            this.itemDateTime.time = [hours, minutes].map(n => n < 10 ? '0' + n : n).join(':');\n        },\n        calculateTimestampFromTime () {\n            this.timestamp = new Date(this.itemDateTime.date + ' ' + this.itemDateTime.time).getTime();\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('date-popup-display');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100005;\n}\n\n.popup {\n    max-width: 60rem;\n    min-width: 60rem;\n    padding: 4rem;\n}\n\n.message {\n    font-size: 1.6rem;\n    padding: 0 0 4rem 0;\n}\n\n.buttons {\n    display: flex;\n    margin: 4rem -4rem -4rem -4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n}\n\n.item-date-picker {\n    display: flex;\n    justify-content: space-between;\n    margin-top: -1rem;\n    text-align: center;\n    white-space: nowrap;\n\n    input[type=\"date\"],\n    input[type=\"time\"] {\n        background: none;\n        border-radius: var(--border-radius);\n        border: none;\n        box-shadow: inset 0 0 0 1px var(--input-border-color);\n        color: var(--text-primary-color); \n        color-scheme: var(--input-data-time-popup);\n        font-size: 2.1rem;\n        font-weight: var(--font-weight-normal);\n        min-height: 46px;\n        min-width: 48%;\n        padding: .45rem 1.2rem;\n\n        &:focus {\n          box-shadow: inset 0 0 2px 1px var(--input-border-focus);\n        }\n\n        &::-webkit-datetime-edit-fields-wrapper {\n            background: transparent; \n        }\n\n        &::-webkit-datetime-edit-text {\n            color: var(--text-primary-color); \n            padding: 0 .2em;\n        }\n\n        &::-webkit-calendar-picker-indicator {\n            cursor: pointer;\n        }\n\n        &::-webkit-datetime-edit-month-field,\n        &::-webkit-datetime-edit-day-field,\n        &::-webkit-datetime-edit-year-field,\n        &::-webkit-datetime-edit-hour-field,\n        &::-webkit-datetime-edit-minute-field { \n            cursor: text; \n        }\n\n        &::-webkit-calendar-picker-indicator {\n            background-size: 22px;\n            background-position-y: 50%;\n              \n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/EasyMde.vue",
    "content": "<template>\n  <div class=\"vue-easymde\">\n    <textarea \n      ref=\"editor-textarea\"\n      :name=\"name\"></textarea>\n  </div>\n</template>\n\n<script>\nimport EasyMDE from 'easymde';\n\nexport default {\n  name: 'vue-easymde',\n  props: {\n    configs: {\n      type: Object,\n      default() {\n        return {};\n      }\n    },\n    name: String,\n    value: String\n  },\n  mounted() {\n    let configs = Object.assign({\n      element: this.$refs['editor-textarea'],\n      initialValue: this.value,\n      renderingConfig: {},\n    }, this.configs);\n\n    if (configs.initialValue) {\n      this.$emit('input', configs.initialValue);\n    }\n\n    this.easymde = new EasyMDE(configs);\n    this.initEvents();\n  },\n  watch: {\n    value(val) {\n      if (val === this.easymde.value()) {\n        return;\n      }\n\n      this.easymde.value(val);\n    },\n  },\n  activated() {\n    let editor = this.easymde;\n\n    if (!editor) {\n      return;\n    }\n\n    let isActive = editor.isSideBySideActive() || editor.isPreviewActive();\n\n    if (isActive) {\n      editor.toggleFullScreen();\n    }\n  },\n  methods: {\n    initEvents() {\n      this.easymde.codemirror.on('change', () => {\n        this.$emit('input', this.easymde.value());\n      });\n    }\n  },\n  destroyed() {\n    this.easymde = null;\n  }\n};\n</script>\n"
  },
  {
    "path": "app/src/components/post-editor/Editor.vue",
    "content": "<template>\n    <textarea id=\"post-editor\"\n              class=\"post-editor-form-content post-editor-field-content is-hidden\"\n              :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n              data-name=\"main-content\"></textarea>\n</template>\n\n<script>\nimport EditorBridge from './EditorBridge.js';\n\nexport default {\n    name: 'editor',\n    data () {\n        return {\n            editorInstance: false\n        };\n    },\n    methods: {\n        init () {\n            if (!this.editorInstance) {\n                this.editorInstance = new EditorBridge(this.$parent.postID, this.$parent.itemType);\n            } else {\n                this.editorInstance.reloadEditor();\n            }\n        },\n        focus () {\n            this.editorInstance.focus();\n        }\n    },\n    beforeDestroy () {\n        tinymce.remove();\n    }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/EditorBridge.js",
    "content": "import EditorConfig from './../configs/postEditor.config.js';\nimport Utils from './../../helpers/utils';\n\nclass EditorBridge {\n    constructor(itemID, itemType = 'post') {\n        this.itemID = itemID;\n        this.itemType = itemType;\n        this.tinyMCECSSFiles = this.getTinyMCECSSFiles();\n        this.customThemeEditorConfig = this.getCustomThemeEditorConfig();\n        this.tinymceEditor = false;\n        this.callbackForTinyMCE = false;\n        this.postEditorInnerDragging = false;\n        this.contentImageUploading = false;\n        this.init();\n    }\n\n    updateItemID (newItemID) {\n        this.itemID = newItemID;\n        let contentToUpdate = this.tinymceEditor.getContent().replace(/media\\/posts\\/temp/gmi, 'media/posts/' + this.itemID + '/');\n        this.tinymceEditor.setContent(contentToUpdate);\n    }\n\n    init() {\n        let customFormats = this.loadCustomFormatsFromTheme();\n        let editorConfig = Object.assign({}, EditorConfig, {\n            setup: this.setupEditor.bind(this, customFormats),\n            file_picker_callback: this.filePickerCallback.bind(this),\n            content_css: this.tinyMCECSSFiles,\n            style_formats: customFormats,\n            statusbar: true,\n            browser_spellcheck: window.app.spellcheckerIsEnabled()\n        });\n\n        if (window.app.wysiwygAdditionalValidElements() !== '') {\n            let additionalValidElements = window.app.wysiwygAdditionalValidElements();\n            editorConfig.extended_valid_elements = editorConfig.extended_valid_elements + ',' + additionalValidElements;\n        }\n\n        if (window.app.wysiwygCustomElements() !== '') {\n            let customElements = window.app.wysiwygCustomElements();\n            editorConfig.custom_elements = customElements;\n        }\n\n        // Remove style selector when there is no custom styles from the theme\n        if(customFormats.length === 0) {\n            editorConfig.toolbar2 = editorConfig.toolbar2.replace('styleselect', '');\n        }\n\n        editorConfig = Utils.deepMerge(editorConfig, window.app.tinymceCustomConfig());\n\n        if(this.customThemeEditorConfig) {\n            editorConfig = Utils.deepMerge(editorConfig, this.customThemeEditorConfig);\n        }\n\n        if (window.app.getWysiwygTranslation()) {\n            tinymce.addI18n('custom', window.app.getWysiwygTranslation());\n            editorConfig.language = 'custom';\n        }\n\n        tinymce.init(editorConfig);\n    }\n\n    focus () {\n        this.tinymceEditor.focus();\n    }\n\n    setupEditor(customFormats, editor) {\n        let self = this;\n        this.tinymceEditor = editor;\n        this.addEditorButtons();\n\n        editor.on('init', async () => {\n            $('.tox-tinymce').append($('<div class=\"tinymce-overlay\"><div><svg class=\"upload-icon\" width=\"24\" height=\"24\" viewbox=\"0 0 24 24\"> <path d=\"M11,19h2v2h-2V19z M12,4l-7,6.6L6.5,12L11,7.7V16h2V7.7l4.5,4.3l1.5-1.4L12,4z\"/></svg>Drag image here</div></div>'));\n            $('.tox-tinymce').addClass('is-loaded');\n            this.initEditorDragNDropImages(editor);\n\n            // Scroll the editor to bottom in order to avoid issues\n            // with the text under gradient\n            let iframe = document.getElementById('post-editor_ifr');\n\n            if (document.getElementById('app').classList.contains('use-wide-scrollbars')) {\n                iframe.contentWindow.document.documentElement.classList.add('use-wide-scrollbars');\n            }\n\n            iframe.contentWindow.window.document.body.addEventListener(\"keydown\", function(e) {\n                let selectedNode = $(editor.selection.getNode());\n                let selectedNodeHeight = selectedNode.outerHeight();\n\n                if(selectedNodeHeight > iframe.contentWindow.window.outerHeight * .75) {\n                    selectedNodeHeight = 0;\n                }\n\n                let cursorPos = selectedNode.position().top + selectedNodeHeight;\n                let iframeContentHeight = iframe.contentWindow.window.document.body.scrollHeight;\n\n                if(cursorPos > iframeContentHeight - 150) {\n                    iframe.contentWindow.scrollTo(0, iframeContentHeight);\n                }\n            }, false);\n\n            // Handle Enter key in figcaption to exit figure\n            iframe.contentWindow.window.document.body.addEventListener(\"keydown\", function (e) {\n                if (e.keyCode === 13 && !e.shiftKey) { // on enter, but when shift is not pressed\n                    let node = editor.selection.getNode();\n\n                    if (node.tagName === 'FIGCAPTION' || node.parentNode.tagName === 'FIGCAPTION') {\n                        let figcaption = node.tagName === 'FIGCAPTION' ? node : node.parentNode;\n                        let figure = figcaption.closest('figure');\n\n                        if (figure) {\n                            e.preventDefault();\n                            e.stopPropagation();\n\n                            // check if next element is paragraph - then focus on in, instead of creating a new paragraph\n                            let next = figure.nextElementSibling;\n                            \n                            if (next && next.tagName === 'P') {\n                                let range = editor.dom.createRng();\n                                range.setStart(next, 0);\n                                range.collapse(true);\n                                editor.selection.setRng(range);\n                            } else {\n                                editor.selection.select(figure); \n                                editor.selection.collapse(false);\n                                editor.execCommand('mceInsertContent', false, '<p></p>');\n                            }\n\n                            return false;\n                        }\n                    }\n                }\n            }, true);\n\n            // DblClick on IMG opens the dialog\n            iframe.contentWindow.window.document.body.addEventListener(\"dblclick\", function (e) {\n                if (e.target.tagName === 'IMG') {\n                    const figure = e.target.closest('figure');\n                    if (figure) {\n                        // Ensure selection/handles work as expected\n                        if (figure.getAttribute('contenteditable') === 'false') {\n                            figure.removeAttribute('contenteditable');\n                            setTimeout(() => figure.setAttribute('contenteditable', 'false'), 100);\n                        }\n                        // Copy post__image* classes from FIGURE to IMG for better preselect\n                        const figcaption = figure.querySelector('figcaption');\n                        if (figcaption) {\n                            const figureClasses = Array.from(figure.classList).filter(cls => cls.startsWith('post__image'));\n                            figureClasses.forEach(cls => {\n                                if (!e.target.classList.contains(cls)) {\n                                    e.target.classList.add(cls);\n                                }\n                            });\n                        }\n                    }\n                    tinymce.activeEditor.execCommand('mceImage');\n                }\n            }, false);\n\n            // ---------------------- Image dialog handling (scoped) ----------------------\n\n            const LAYOUT_CLASSES = [\n                'post__image--full',\n                'post__image--wide',\n                'post__image--center',\n                'post__image--left',\n                'post__image--right'\n            ];\n\n            // State keyed by a stable token assigned on open\n            const preDialogCustomImgOnly = new Map(); // token -> string[] (custom classes originally on IMG)\n            const preDialogImgLayout = new Map(); // token -> { removed: string[], injected: string|null }\n            const customImageClasses = new Map(); // token -> string[] (persist custom classes for Save)\n\n            let activeToken = null;\n            let dialogWasConfirmed = false;\n\n            const makeToken = () => 'publii-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);\n\n            const findNodesByToken = (doc, token) => {\n                const figure = doc.querySelector(`figure[data-publii-token=\"${token}\"]`);\n                const img = figure ? figure.querySelector('img') : null;\n                return { figure, img, figcaption: figure ? figure.querySelector('figcaption') : null };\n            };\n\n            const getLayoutFrom = (el) => {\n                if (!el) return null;\n                const cls = Array.from(el.classList);\n                return cls.find(c => LAYOUT_CLASSES.includes(c)) || null;\n            };\n\n            const stripLayouts = (el) => {\n                if (!el) return;\n                LAYOUT_CLASSES.forEach(c => el.classList.remove(c));\n            };\n\n            editor.on('BeforeExecCommand', function (e) {\n                if (e.command !== 'mceImage') return;\n\n                const selNode = editor.selection.getNode();\n                if (!selNode || selNode.tagName !== 'IMG') return;\n\n                const img = selNode;\n                const figure = img.closest('figure');\n\n                // Assign token ONLY to existing figure (never to IMG)\n                activeToken = makeToken();\n                if (figure) figure.setAttribute('data-publii-token', activeToken);\n                dialogWasConfirmed = false;\n\n                // Snapshot current IMG classes (keep original layout info always)\n                const imgClasses = Array.from(img.classList);\n                const imgLayoutNow = imgClasses.filter(c => LAYOUT_CLASSES.includes(c));\n                const imgCustomNow = imgClasses.filter(c => !c.startsWith('post__image'));\n\n                // Store custom IMG classes and temporarily remove them for cleaner dialog UI\n                if (imgCustomNow.length) {\n                    preDialogCustomImgOnly.set(activeToken, imgCustomNow.slice());\n                    customImageClasses.set(activeToken, imgCustomNow.slice());\n                    imgCustomNow.forEach(c => img.classList.remove(c));\n                } else {\n                    customImageClasses.set(activeToken, []);\n                }\n\n                // Merge custom classes from FIGURE (keep them for Save)\n                if (figure) {\n                    const figCustom = Array.from(figure.classList).filter(c => !c.startsWith('post__image'));\n                    if (figCustom.length) {\n                        const prev = customImageClasses.get(activeToken) || [];\n                        customImageClasses.set(activeToken, [...new Set([...prev, ...figCustom])]);\n                    }\n                }\n\n                // Preselect layout:\n                // - if FIGURE exists -> prefer FIGURE's layout and avoid duplicates on IMG\n                // - if FIGURE does NOT exist -> DO NOT strip existing IMG layout (keep preselect intact)\n                const layoutToUse = getLayoutFrom(figure) || (imgLayoutNow.length ? imgLayoutNow[0] : null);\n\n                let injected = null;\n                if (figure) {\n                    // Figure present: remove all IMG layouts to avoid dupes, inject figure's layout for dialog preselect\n                    imgLayoutNow.forEach(c => img.classList.remove(c));\n                    if (layoutToUse && !img.classList.contains(layoutToUse)) {\n                        img.classList.add(layoutToUse);\n                        injected = layoutToUse;\n                    }\n                } else {\n                    // No figure yet: leave IMG layouts as-is; no injection\n                    injected = null;\n                }\n\n                preDialogImgLayout.set(activeToken, {\n                    removed: imgLayoutNow,  // original layout classes on IMG (may be empty if none)\n                    injected                // what we temporarily added to IMG (or null)\n                });\n            });\n\n            // Mark that dialog confirmed a change (we'll apply it in CloseWindow to avoid races)\n            editor.on('ExecCommand', function (e) {\n                if (e.command === 'mceImage') {\n                    dialogWasConfirmed = true;\n                }\n            });\n\n            // Close dialog — single place to handle both Save and Cancel deterministically\n            editor.on('CloseWindow', function () {\n                setTimeout(() => {\n                    const doc = iframe.contentWindow.window.document;\n\n                    // Resolve nodes by token; if not found (node replaced), fallback to selection\n                    const resolveByTokenOrSelection = () => {\n                        let { figure, img } = findNodesByToken(doc, activeToken);\n\n                        if (!img) {\n                            let selNode = editor.selection.getNode();\n                            if (selNode && selNode.nodeType === 1) {\n                                if (selNode.tagName === 'IMG') {\n                                    img = selNode;\n                                    figure = img.closest('figure');\n                                } else if (selNode.closest) {\n                                    const f = selNode.closest('figure');\n                                    if (f) {\n                                        figure = f;\n                                        img = f.querySelector('img');\n                                    }\n                                }\n                            }\n                            if (!figure && img && img.closest) figure = img.closest('figure');\n                        }\n                        return { figure, img };\n                    };\n\n                    try {\n                        const { figure, img } = resolveByTokenOrSelection();\n                        const hasFigure = !!figure;\n                        const snap = preDialogImgLayout.get(activeToken) || { removed: [], injected: null };\n                        const origCustomImg = preDialogCustomImgOnly.get(activeToken) || [];\n                        const storedCustom = customImageClasses.get(activeToken) || [];\n\n                        // FIX: treat first insert as confirmed if figure exists and IMG holds layout/post__image\n                        if (!dialogWasConfirmed && hasFigure && img) {\n                        // if IMG has layout/post__image but FIGURE doesn't → user intended to save\n                        const imgHasLayout = LAYOUT_CLASSES.some(c => img.classList.contains(c));\n                        const figHasLayout = LAYOUT_CLASSES.some(c => figure.classList.contains(c));\n                        const imgHasPost   = img.classList.contains('post__image');\n                        const figHasPost   = figure.classList.contains('post__image');\n\n                        if ((imgHasLayout && !figHasLayout) || (imgHasPost && !figHasPost)) {\n                            dialogWasConfirmed = true;\n                        }\n                        }\n\n                        // ---- CANCEL ----\n                        if (!dialogWasConfirmed) {\n                            if (img) {\n                                // Remove injected layout and restore original IMG layout (only if we actually removed/injected)\n                                if (snap.injected) img.classList.remove(snap.injected);\n                                if (snap.removed && snap.removed.length) {\n                                    snap.removed.forEach(c => {\n                                        if (!img.classList.contains(c)) img.classList.add(c);\n                                    });\n                                }\n                                // Restore custom classes on IMG\n                                if (origCustomImg.length) {\n                                    origCustomImg.forEach(c => {\n                                        if (!img.classList.contains(c)) img.classList.add(c);\n                                    });\n                                }\n                            }\n                            // Do not touch FIGURE on Cancel\n                        } else {\n                            // ---- SAVE ----\n                            if (hasFigure && img) {\n                                // Classes after dialog\n                                const imgClsNow = Array.from(img.classList);\n                                const figClsNow = Array.from(figure.classList);\n\n                                // Layout chosen in dialog; null means \"None\"\n                                let newLayout = imgClsNow.find(c => LAYOUT_CLASSES.includes(c)) || null;\n\n                                // Detect explicit \"None\" when we had injected a layout into IMG\n                                // (BeforeExecCommand sets snap.injected for figure edits)\n                                const userSelectedNone = (newLayout === null) && (snap && snap.injected);\n\n                                // Fallback only if no explicit \"None\" and we previously removed layout from IMG\n                                if (!newLayout && !userSelectedNone && snap && snap.removed && snap.removed.length) {\n                                newLayout = snap.removed[0]; // restore previous layout (e.g., --wide)\n                                }\n\n                                // Strip all layout classes first\n                                stripLayouts(img);\n                                stripLayouts(figure);\n\n                                // Merge non-layout classes for FIGURE\n                                const nonLayoutImg = imgClsNow.filter(c => !LAYOUT_CLASSES.includes(c));\n                                const nonLayoutFigure = figClsNow.filter(c => !LAYOUT_CLASSES.includes(c));\n\n                                // Include stored custom classes captured before dialog\n                                const merged = new Set([\n                                    ...nonLayoutFigure,\n                                    ...nonLayoutImg,\n                                    ...storedCustom\n                                ]);\n\n                                if (!merged.has('post__image')) merged.add('post__image');\n\n                                figure.className = Array.from(merged).join(' ');\n\n                                // Apply exactly one layout (if any) on FIGURE\n                                if (newLayout) figure.classList.add(newLayout);\n\n                                // IMG must be clean in caption mode\n                                img.removeAttribute('class');\n                            } else if (img && !hasFigure) {\n                                // Plain IMG (no caption) on Save: restore custom classes we removed for dialog\n                                if (origCustomImg.length) {\n                                    origCustomImg.forEach(c => {\n                                        if (!img.classList.contains(c)) img.classList.add(c);\n                                    });\n                                }\n                                // Layout left as TinyMCE set it on IMG (we don't interfere here)\n                            }\n                        }\n\n                        // Cleanup tokens and state (remove from FIGURE if present; we never add token to IMG)\n                        if (figure && figure.removeAttribute) figure.removeAttribute('data-publii-token');\n\n                        preDialogImgLayout.delete(activeToken);\n                        preDialogCustomImgOnly.delete(activeToken);\n                        customImageClasses.delete(activeToken);\n                        activeToken = null;\n                        dialogWasConfirmed = false;\n\n                        // ---------- SAFE, LOCAL post-cleanup ----------\n                        try {\n                            // anchor: edited figure OR <p><img> OR the <img> itself\n                            const anchorEl =\n                                (hasFigure && figure) ||\n                                (img && img.closest && img.closest('p')) ||\n                                img;\n\n                            const nextEl = anchorEl && anchorEl.nextElementSibling;\n\n                            if (nextEl && nextEl.tagName === 'P') {\n                                // Keep paragraph if it contains any media\n                                const hasMedia = nextEl.querySelector('img,figure,video,iframe,svg,picture,source,canvas,audio');\n\n                                // Visible text check (strip non-breaking spaces)\n                                const text = nextEl.textContent.replace(/\\u00a0/g, '').trim();\n\n                                // Remove only TinyMCE fillers from HTML and re-check emptiness\n                                const cleanedHTML = nextEl.innerHTML\n                                    .replace(/&nbsp;/gi, '')\n                                    .replace(/<br[^>]*data-mce-bogus=\"1\"[^>]*>/gi, '')\n                                    .trim();\n\n                                // Remove paragraph ONLY if it has no media AND no visible text AND no real HTML\n                                if (!hasMedia && text === '' && cleanedHTML === '') {\n                                    nextEl.remove();\n                                }\n                            }\n                        } catch (_) { }\n                        // ---------------------------------------------\n\n                    } catch (error) {\n                        console.warn('Error during image dialog close handling:', error);\n                        // Best-effort cleanup\n                        preDialogImgLayout.delete(activeToken);\n                        preDialogCustomImgOnly.delete(activeToken);\n                        customImageClasses.delete(activeToken);\n                        activeToken = null;\n                        dialogWasConfirmed = false;\n                    }\n                }, 80); // small delay to ensure dialog DOM changes are committed\n            });\n\n            // ---------------------- END image dialog handling ----------------------\n\n            // Support for dark mode\n            let htmlElement = iframe.contentWindow.window.document.querySelector('html');\n            htmlElement.setAttribute('data-theme', await window.app.getCurrentAppTheme());\n            htmlElement.setAttribute('style', window.app.overridedCssVariables());\n\n            // Add inline editors\n            this.addInlineEditor(customFormats);\n            this.addLinkEditor(iframe);\n\n            this.tinymceEditor.once('keyup', e => {\n                window.app.reportPossibleDataLoss();\n            });\n\n            this.tinymceEditor.on('keyup', e => {\n                if(e.keyCode !== 13 && e.keyCode !== 40) {\n                    return;\n                }\n\n                let node = this.tinymceEditor.selection.getNode();\n\n                if(\n                    e.keyCode === 40 &&\n                    node.tagName === 'PRE' &&\n                    node.nextSibling === null\n                ) {\n                    this.tinymceEditor.execCommand('mceInsertContent', false, '<p></p>');\n                    return;\n                }\n\n                if(\n                    e.keyCode === 13 &&\n                    node.tagName === 'P' &&\n                    node.getAttribute('class')\n                ) {\n                    node.removeAttribute('class');\n                    return;\n                }\n\n                if(\n                    e.keyCode === 13 &&\n                    node.tagName === 'P' &&\n                    node.parentNode.tagName === 'BLOCKQUOTE' &&\n                    node.previousSibling &&\n                    node.previousSibling.tagName === 'P' &&\n                    node.previousSibling.childNodes &&\n                    node.previousSibling.childNodes[0] &&\n                    node.previousSibling.childNodes[0].tagName === 'BR' &&\n                    node.previousSibling.childNodes[0].getAttribute('data-mce-bogus') === '1' &&\n                    node.nextSibling === null\n                ) {\n                    // get the element's parent node\n                    let parent = node.parentNode;\n\n                    if(parent.nextSibling) {\n                        parent.parentNode.insertBefore(node, parent.nextSibling);\n                        parent.removeChild(parent.lastChild);\n                    } else {\n                        parent.parentNode.appendChild(node);\n                        parent.removeChild(parent.lastChild);\n                    }\n\n                    setTimeout(() => {\n                        this.tinymceEditor.selection.select(parent.nextSibling, true);\n                    }, 0);\n                }\n            });\n\n            iframe.contentWindow.window.document.body.addEventListener(\"click\", (e) => {\n                let clickedElement = e.path ? e.path[0] : e.srcElement;\n                let showPopup = false;\n\n                if(localStorage.getItem('publii-writers-panel') === null) {\n                    localStorage.setItem('publii-writers-panel', 'opened');\n                    window.app.writersPanelOpen();\n                }\n\n                if(clickedElement.tagName === 'FIGCAPTION') {\n                    return;\n                }\n\n                if(clickedElement.tagName === 'SCRIPT') {\n                    let content = this.tinymceEditor.getContent({\n                        source_view: true\n                    });\n\n                    window.app.sourceCodeEditorShow(content, this.tinymceEditor);\n                    return;\n                }\n\n                if(clickedElement.tagName === 'FIGURE') {\n                    showPopup = true;\n                } else if(e.path && e.path[1] && e.path[1].tagName === 'FIGURE') {\n                    clickedElement = e.path[1];\n                    showPopup = true;\n                } else if(e.srcElement && e.srcElement.parentNode && e.srcElement.parentNode === 'FIGURE') {\n                    clickedElement = e.srcElement.parentNode;\n                    showPopup = true;\n                }\n\n                if(clickedElement.tagName === 'A' || clickedElement.parentNode.tagName === 'A') {\n                    let selection = iframe.contentWindow.window.getSelection();\n                    selection.removeAllRanges();\n                    let range = iframe.contentWindow.window.document.createRange();\n\n                    if (clickedElement.tagName === 'A') {\n                        range.selectNode(clickedElement);\n                    } else if (clickedElement.parentNode && clickedElement.parentNode.tagName === 'A') {\n                        range.selectNode(clickedElement.parentNode);\n                    }\n\n                    selection.addRange(range);\n\n                    if (this.checkInlineLinkTrigger(clickedElement)) {\n                        window.app.updateLinkEditor({\n                            sel: selection,\n                            text: clickedElement\n                        });\n                    }\n                } else {\n                    window.app.updateLinkEditor({\n                        sel: false,\n                        text: false\n                    });\n                }\n\n                if(\n                    clickedElement.tagName === 'DIV' &&\n                    clickedElement.getAttribute('class') &&\n                    clickedElement.getAttribute('class').indexOf('gallery') !== -1\n                ) {\n                    window.app.updateGalleryPopup({\n                        postID: this.itemID,\n                        galleryElement: clickedElement\n                    });\n\n                    window.app.galleryPopupUpdated(this.galleryPopupUpdated.bind(this));\n                }\n            });\n\n            let linkToolbar = $('#link-toolbar');\n            let inlineToolbar = $('#inline-toolbar');\n            let lastScroll = -1;\n            let hideToolbars = function (e) {\n                if (linkToolbar.css('display') !== 'block' && inlineToolbar.css('display') !== 'block') {\n                    return;\n                }\n\n                let iframeScrollOffset = iframe.contentWindow.document.body.parentNode.scrollTop;\n\n                if (lastScroll !== -1 && Math.abs(iframeScrollOffset - lastScroll) > 20) {\n                    lastScroll = -1;\n                    linkToolbar.css('display', 'none');\n                    inlineToolbar.css('display', 'none');\n                } else if (lastScroll === -1) {\n                    lastScroll = iframeScrollOffset;\n                }\n            };\n\n            iframe.contentWindow.window.addEventListener(\"scroll\", hideToolbars);\n\n            $('#post-editor-fake-image-uploader').on('change', () => {\n                if (!$('#post-editor-fake-image-uploader')[0].value) {\n                    return;\n                }\n\n                setTimeout(async () => {\n                    if(this.callbackForTinyMCE) {\n                        let filePath = false;\n\n                        if($('#post-editor-fake-image-uploader')[0].files) {\n                            filePath = await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile($('#post-editor-fake-image-uploader')[0].files[0]));\n                        }\n\n                        if(!filePath) {\n                            return;\n                        }\n\n                        mainProcessAPI.send('app-image-upload', {\n                            id: this.itemID,\n                            site: window.app.getSiteName(),\n                            path: filePath,\n                            imageType: 'contentImages'\n                        });\n\n                        mainProcessAPI.receiveOnce('app-image-uploaded', (data) => {\n                            let imagePath = data.baseImage.url;\n                            imagePath = imagePath.replace('file://', 'file:///');\n\n                            this.callbackForTinyMCE(imagePath, {\n                                alt: '',\n                                dimensions: {\n                                    height: data.baseImage.size[1],\n                                    width: data.baseImage.size[0]\n                                }\n                            });\n                        });\n\n                        $('#post-editor-fake-image-uploader')[0].value = '';\n                    }\n                }, 50);\n            });\n\n            // Writers Panel\n            let updateWritersPanel = function () {\n                 window.app.writersPanelRefresh();\n            };\n            let throttledUpdate = Utils.debouncedFunction(updateWritersPanel, 1000);\n            editor.on('setcontent beforeaddundo undo redo keyup', throttledUpdate);\n            updateWritersPanel();\n\n            iframe.contentWindow.window.document.addEventListener('copy', () => {\n                self.hideToolbarsOnCopyOrScroll();\n            });\n\n            iframe.contentWindow.window.document.addEventListener('scroll', () => {\n                self.hideToolbarsOnCopyOrScroll();\n            });\n\n            // Clean up content before saving\n            editor.on('GetContent', function (e) {\n                if (e.format === 'html') {\n                    // Remove contenteditable from output\n                    e.content = e.content.replace(/\\s*contenteditable=\"(true|false)\"/gi, '');\n\n                    // Remove empty paragraphs after figures\n                    e.content = e.content.replace(/<\\/figure>\\s*<p>\\s*(&nbsp;|\\u00a0)?\\s*<\\/p>/gi, '</figure>');\n\n                    // Clean up double figures\n                    e.content = e.content.replace(/<figure[^>]*>\\s*<figure[^>]*>/gi, '<figure class=\"post__image\">');\n                    e.content = e.content.replace(/<\\/figure>\\s*<\\/figure>/gi, '</figure>');\n                }\n            });\n        });\n\n        editor.ui.registry.addButton('gallery', {\n            icon: 'gallery',\n            tooltip: window.app.translate('editor.insertGallery'),\n            onAction: function () {\n                editor.insertContent('<div class=\"gallery\" data-is-empty=\"true\" contenteditable=\"false\" data-translation=\"' + window.app.translate('image.addImages') + '\"></div>');\n            }\n        });\n    }\n\n    extensionsPath () {\n        return [\n            'file:///',\n            window.app.getSiteDir(),\n            '/input/themes/',\n            window.app.getSiteTheme(),\n            '/'\n        ].join('');\n    }\n\n    galleryPopupUpdated (response) {\n        this.hideToolbarsOnCopyOrScroll();\n\n        if(response) {\n            response.gallery.innerHTML = response.html;\n            response.gallery.setAttribute('data-is-empty', response.html === '&nbsp;');\n        }\n    }\n\n    getTinyMCECSSFiles () {\n        let pathToEditorCSS = this.extensionsPath() + 'assets/css/editor.css';\n        let customEditorCSS = pathToEditorCSS;\n\n        return [\n            'css/editor.css?v=0710',\n            customEditorCSS\n        ].join(',');\n    }\n\n    getCustomThemeEditorConfig () {\n        // Add custom editor config\n        let customEditorConfig = false;\n\n        if (window.app.hasPostEditorConfigOverride()) {\n            let configOverridePath = this.extensionsPath() + 'tinymce.override.json';\n\n            jQuery.ajax({\n                url: configOverridePath,\n                dataType: 'json',\n                async: false,\n                success: function(json) {\n                    customEditorConfig = json;\n                }\n            });\n        }\n\n        return customEditorConfig;\n    }\n\n    loadCustomFormatsFromTheme() {\n        let output = [];\n        let customElements = [];\n        let inlineElements = [\n            'a', 'b', 'abbr', 'acronym', 'cite', 'dfn', 'kbd',\n            'samp', 'time', 'var', 'bdo', 'br', 'big', 'code',\n            'i', 'em', 'small','strong','span', 'tt', 'img',\n            'map', 'object', 'q', 'script', 'sub', 'sup', 'button',\n            'input', 'label', 'select', 'textarea'\n        ];\n\n        // Detect mode\n        if (window.app.getThemeCustomElementsMode() === 'advanced') {\n            output = JSON.parse(JSON.stringify(window.app.getThemeCustomElements()));\n            return output;\n        }\n\n        // Load custom elements\n        if (window.app.getThemeCustomElements()) {\n            customElements = window.app.getThemeCustomElements();\n        }\n\n        if(customElements && customElements.length) {\n            for(let i = 0; i < customElements.length; i++) {\n                if(!customElements[i]) {\n                    continue;\n                }\n\n                if(!customElements[i].tag && !customElements[i].selector) {\n                    continue;\n                }\n\n                if(customElements[i].postEditor === false) {\n                    continue;\n                }\n\n                let style = {\n                    title: customElements[i].label,\n                    classes: customElements[i].cssClasses\n                };\n\n                if(customElements[i].selector) {\n                    style.selector = customElements[i].selector;\n                } else {\n                    if (inlineElements.indexOf(customElements[i].tag)) {\n                        style.inline = customElements[i].tag;\n                    } else {\n                        style.block = customElements[i].tag;\n                    }\n                }\n\n                output.push(style);\n            }\n        }\n\n        return output;\n    }\n\n    filePickerCallback(callback, value, meta) {\n        // Provide image and alt text for the image dialog\n        if (meta.filetype == 'image') {\n            this.callbackForTinyMCE = callback;\n            $('#post-editor-fake-image-uploader').trigger('click');\n        } else {\n            this.callbackForTinyMCE = false;\n        }\n    }\n\n    addEditorButtons() {\n        this.tinymceEditor.ui.registry.addButton(\"publiilink\", {\n            icon: 'link',\n            tooltip: window.app.translate('link.insertEditLink'),\n            onAction: () => {\n                let selectedNode = tinymce.activeEditor.selection.getNode();\n\n                if (selectedNode.tagName === 'IMG' && selectedNode.parentNode && selectedNode.parentNode.tagName === 'A') {\n                    window.app.initLinkPopup({\n                        postID: this.itemID,\n                        selection: selectedNode.parentNode.outerHTML\n                    });\n                } else {\n                    window.app.initLinkPopup({\n                        postID: this.itemID,\n                        selection: tinymce.activeEditor.selection.getContent()\n                    });\n                }\n            }\n        });\n\n        this.tinymceEditor.ui.registry.addButton(\"sourcecode\", {\n            icon: 'sourcecode',\n            tooltip: window.app.translate('editor.sourceCode'),\n            text: \"HTML\",\n            onAction: () => {\n                let content = this.tinymceEditor.getContent({\n                    source_view: true\n                });\n\n                window.app.sourceCodeEditorShow(content, this.tinymceEditor);\n            }\n        });\n\n        this.tinymceEditor.ui.registry.addButton('readmore', {\n            icon: 'readmore',\n            text: window.app.translate('editor.readMore'),\n            onAction: () => {\n                this.tinymceEditor.insertContent('<hr id=\"read-more\" data-translation=\"' + window.app.translate('editor.readMore') + '\">' + \"\\n\");\n            }\n        });\n    }\n\n    addInlineEditor(customFormats) {\n        let iframe = document.getElementById('post-editor_ifr');\n        let win = iframe.contentWindow.window;\n        let doc = win.document;\n\n        window.app.initInlineEditor('init-inline-editor', customFormats);\n\n        $(doc.querySelector('html')).on('mouseup', (e) => {\n            let sel = win.getSelection();\n            let text = sel.toString();\n\n            if (this.checkInlineTrigger(e.target)) {\n                window.app.updateInlineEditor({\n                    sel,\n                    text\n                });\n            }\n        });\n    }\n\n    checkInlineTrigger (target) {\n        let excludedTags = ['FIGURE', 'FIGCAPTION', 'IMG', 'PRE'];\n\n        if (excludedTags.indexOf(target.tagName) > -1) {\n            return false;\n        }\n\n        if (target.tagName === 'DIV' && target.classList.contains('gallery')) {\n            return false;\n        }\n\n        for ( ; target && target !== document; target = target.parentNode) {\n            if (target.matches && target.matches('.post__toc')) {\n                return false;\n            }\n\n            if (target.matches && target.matches('pre')) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    checkInlineLinkTrigger (target) {\n        for ( ; target && target !== document; target = target.parentNode) {\n            if (target.matches && target.matches('.post__toc')) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    addLinkEditor(iframe) {\n        window.app.initLinkEditor(iframe);\n    }\n\n    hideToolbarsOnCopyOrScroll() {\n        $('#link-toolbar').css('display', 'none');\n        $('#inline-toolbar').css('display', 'none');\n    }\n\n    initEditorDragNDropImages(editor) {\n        let editorArea = $('.tox-tinymce');\n        let postEditor = $('.post-editor');\n        let hoverState = false;\n        let tinymceOverlay = $('.tinymce-overlay');\n\n        postEditor.on('dragover', () => {\n            if(!this.postEditorInnerDragging && !$('.popup.gallery-popup').length) {\n                hoverState = true;\n                editorArea.addClass('is-hovered');\n            }\n        });\n\n        tinymceOverlay.on('dragover', e => {\n            if(!this.postEditorInnerDragging && !$('.popup.gallery-popup').length) {\n                hoverState = true;\n                editorArea.addClass('is-hovered');\n            }\n        });\n\n        postEditor.on('dragleave', () => {\n            hoverState = false;\n\n            setTimeout(() => {\n                if(!hoverState) {\n                    editorArea.removeClass('is-hovered');\n                }\n            }, 250);\n        });\n\n        document.getElementById('post-editor_ifr').contentWindow.addEventListener(\"dragover\", e => {\n            if(!this.postEditorInnerDragging) {\n                e.preventDefault();\n                e.stopPropagation();\n                editorArea.addClass('is-hovered');\n            }\n        }, false);\n\n        document.getElementById('post-editor_ifr').contentWindow.addEventListener('mousedown', () => {\n            this.postEditorInnerDragging = true;\n        });\n\n        document.getElementById('post-editor_ifr').contentWindow.addEventListener('mouseup', () => {\n            this.postEditorInnerDragging = false;\n        });\n\n        document.getElementById('post-editor_ifr').contentWindow.addEventListener('mouseout', () => {\n            this.postEditorInnerDragging = false;\n        });\n\n        editorArea.on('dragover', this.fileDragOver.bind(this));\n        editorArea.on('drop', this.editorFileSelect.bind(this));\n    }\n\n    fileDragOver (e) {\n        if(!this.postEditorInnerDragging) {\n            e.originalEvent.stopPropagation();\n            e.originalEvent.preventDefault();\n            e.originalEvent.dataTransfer.dropEffect = 'copy';\n        }\n    }\n\n    async editorFileSelect (e) {\n        e.originalEvent.stopPropagation();\n        e.originalEvent.preventDefault();\n\n        let files = e.originalEvent.dataTransfer.files;\n        let siteName = window.app.getSiteName();\n\n        if(this.postEditorInnerDragging) {\n            return;\n        }\n\n        if(!files[0]) {\n            $('.tox-tinymce').removeClass('is-hovered');\n            $('.tox-tinymce').removeClass('is-loading-image');\n            $('.tinymce-overlay').text('Drag your image here');\n\n            this.contentImageUploading = false;\n            return;\n        }\n\n        $('.tox-tinymce').addClass('is-loading-image');\n        $('.tinymce-overlay').html('<div><div class=\"loader\"><span></span></div> ' + 'Upload in progress</div>');\n\n        mainProcessAPI.send('app-image-upload', {\n            \"id\": this.itemID,\n            \"site\": siteName,\n            \"path\": await mainProcessAPI.normalizePath(await mainProcessAPI.getPathForFile(files[0]))\n        });\n\n        this.contentImageUploading = true;\n\n        mainProcessAPI.receiveOnce('app-image-uploaded', (data) => {\n            if(data.baseImage && data.baseImage.size && data.baseImage.size[0] && data.baseImage.size[1]) {\n                tinymce.activeEditor.insertContent('<p><img alt=\"\" class=\"post__image\" height=\"' + data.baseImage.size[1] + '\" width=\"' + data.baseImage.size[0] + '\" src=\"' + data.baseImage.url + '\"/></p>');\n            } else {\n                tinymce.activeEditor.insertContent('<p><img alt=\"\" src=\"' + data.url + '\" class=\"post__image\" /></p>');\n            }\n\n            $('.tox-tinymce').removeClass('is-hovered');\n            $('.tox-tinymce').removeClass('is-loading-image');\n            $('.tinymce-overlay').html('<div><svg class=\"upload-icon\" width=\"24\" height=\"24\" viewbox=\"0 0 24 24\"> <path d=\"M11,19h2v2h-2V19z M12,4l-7,6.6L6.5,12L11,7.7V16h2V7.7l4.5,4.3l1.5-1.4L12,4z\"/></svg>Drag image here</div>');\n\n            this.contentImageUploading = false;\n        });\n    }\n\n    reloadEditor () {\n        this.tinymceEditor.once('keyup', e => {\n            window.app.reportPossibleDataLoss();\n        });\n    }\n}\n\nexport default EditorBridge;\n"
  },
  {
    "path": "app/src/components/post-editor/GalleryPopup.vue",
    "content": "<template>\n    <div\n        v-if=\"isVisible\"\n        class=\"overlay\">\n        <div class=\"popup gallery-popup\">\n            <h1>{{ $t('image.insertEditGallery') }}</h1>\n\n            <div class=\"gallery-popup-images\">\n                <draggable\n                    v-if=\"!isUploading\"\n                    tag=\"ul\"\n                    group=\"gallery-items\"\n                    chosenClass=\"is-chosen\"\n                    ghostClass=\"is-ghost\"\n                    handle=\"img\"\n                    class=\"gallery-popup-images-list\"\n                    v-model=\"images\"\n                    :data-translation=\"$t('image.yourGalleryIsEmpty')\">\n                    <li\n                        v-for=\"(image, index) of images\"\n                        :data-id=\"index\"\n                        :key=\"'images-list-' + index\"\n                        class=\"gallery-popup-images-list-item\">\n                        <img\n                            :src=\"image.thumbnailPath\"\n                            alt=\"\"\n                            :data-full-image=\"image.fullImagePath\"\n                            :data-size=\"image.dimensions\" />\n\n                        <div>\n                            <input\n                                type=\"text\"\n                                class=\"gallery-popup-images-list-item-alt\"\n                                v-model=\"image.alt\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :placeholder=\"$t('image.imageAlternativeText')\" />\n                            <input\n                                type=\"text\"\n                                class=\"gallery-popup-images-list-item-caption\"\n                                v-model=\"image.caption\"\n                                :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                :placeholder=\"$t('image.imageCaption')\" />\n                        </div>\n\n                        <div class=\"gallery-popup-images-list-operations\">\n                            <a\n                                href=\"#up\"\n                                @click.prevent=\"moveImage(index, 'up')\">\n                                &lsaquo;\n                            </a>\n\n                            <a\n                                href=\"#remove\"\n                                @click.prevent=\"removeImage(index)\">\n                                &times;\n                            </a>\n\n                            <a\n                                href=\"#down\"\n                                @click.prevent=\"moveImage(index, 'down')\">\n                                &lsaquo;\n                            </a>\n                        </div>\n                    </li>\n                </draggable>\n\n                <div\n                    v-if=\"isUploading\"\n                    class=\"loading-state\">\n                    <icon\n                        name=\"gallery\"\n                        customHeight=\"62\"\n                        customWidth=\"75\" />\n\n                    <progress-bar\n                        color=\"blue\"\n                        :progress=\"progress\"\n                        :stopped=\"false\"\n                        :message=\"uploadMessage\" />\n                </div>\n            </div>\n\n            <div class=\"gallery-popup-config\">\n                <label>\n                    {{ $t('image.layout') }}:\n                    <select\n                        v-model=\"columns\"\n                        class=\"gallery-popup-config-cols\">\n                        <option :value=\"1\">{{ $t('image.oneColumn') }}</option>\n                        <option :value=\"2\">{{ $t('image.twoColumns') }}</option>\n                        <option :value=\"3\">{{ $t('image.threeColumns') }}</option>\n                        <option :value=\"4\">{{ $t('image.fourColumns') }}</option>\n                        <option :value=\"5\">{{ $t('image.fiveColumns') }}</option>\n                        <option :value=\"6\">{{ $t('image.sixColumns') }}</option>\n                        <option :value=\"7\">{{ $t('image.sevenColumns') }}</option>\n                        <option :value=\"8\">{{ $t('image.eightColumns') }}</option>\n                    </select>\n                </label>\n\n                <label>\n                    {{ $t('image.align') }}:\n                    <select\n                        v-model=\"layout\"\n                        class=\"gallery-popup-config-cols\">\n                        <option value=\"\">{{ $t('image.none') }}</option>\n                        <option value=\"gallery-wrapper--wide\">{{ $t('image.wide') }}</option>\n                        <option value=\"gallery-wrapper--full\">{{ $t('image.fullWidth') }}</option>\n                    </select>\n                </label>\n\n                <p-button\n                    @click.native=\"addImages\"\n                    slot=\"buttons\"\n                    type=\"primary icon\"\n                    icon=\"add-site-mono\">\n                    <template v-if=\"!isUploading\">{{ $t('image.addImages') }}</template>\n                    <template v-if=\"isUploading\">{{ $t('ui.loading') }}</template>\n                </p-button>\n            </div>\n\n            <div class=\"buttons\">\n                <p-button\n                    type=\"medium no-border-radius half-width\"\n                    @click.native=\"save\">\n                    {{ $t('ui.ok') }}\n                </p-button>\n\n                <p-button\n                    type=\"medium no-border-radius half-width cancel-popup\"\n                    @click.native=\"cancel\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\nimport Draggable from 'vuedraggable';\n\nexport default {\n    name: 'gallery-popup',\n    components: {\n        'draggable': Draggable\n    },\n    data () {\n        return {\n            postID: 0,\n            galleryElement: null,\n            isVisible: false,\n            isUploading: false,\n            images: [],\n            columns: 3,\n            layout: '',\n            uploadProgress: 0,\n            uploadMessage: '',\n            imagesToUpload: 0\n        };\n    },\n    computed: {\n        progress () {\n            return (this.uploadProgress / this.imagesToUpload) * 100;\n        }\n    },\n    mounted () {\n        this.$bus.$on('update-gallery-popup', async (config) => {\n            this.postID = config.postID;\n            this.galleryElement = config.galleryElement;\n            this.isVisible = true;\n            this.isUploading = false;\n            this.images = [];\n            this.columns = 3;\n            this.imagesToUpload = 0;\n            this.parseInputElement();\n\n            if (!this.images.length) {\n                await this.addImages();\n            }\n        });\n    },\n    methods: {\n        async addImages () {\n            await mainProcessAPI.invoke('app-main-process-select-files', false, [\n                {\n                    name: 'Images',\n                    extensions: ['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'tiff']\n                }\n            ]);\n\n            mainProcessAPI.stopReceiveAll('app-files-selected');\n            mainProcessAPI.receiveOnce('app-files-selected', (data) => {\n                if (data.paths !== undefined && data.paths.filePaths.length) {\n                    this.isUploading = true;\n                    this.imagesToUpload = data.paths.filePaths.length;\n                    this.uploadProgress = 0;\n                    this.uploadMessage = '';\n                    this.loadImages(data.paths.filePaths);\n                }\n            });\n        },\n        loadImages(imagesPaths) {\n            let nextImagePath = imagesPaths.shift();\n\n            mainProcessAPI.send('app-image-upload', {\n                id: this.postID,\n                site: this.$store.state.currentSite.config.name,\n                path: nextImagePath,\n                imageType: 'galleryImages'\n            });\n\n            mainProcessAPI.receiveOnce('app-image-uploaded', (data) => {\n                this.uploadProgress = this.uploadProgress + 1;\n                this.uploadMessage = `${this.$t('image.uploading')} ${this.uploadProgress} ${this.$t('ui.of')} ${this.imagesToUpload} ${this.$t('image.pictures')}`;\n\n                this.images.push({\n                    fullImagePath: data.baseImage.url,\n                    thumbnailPath: data.thumbnailPath,\n                    thumbnailHeight: data.thumbnailDimensions ? data.thumbnailDimensions.height : '',\n                    thumbnailWidth: data.thumbnailDimensions ? data.thumbnailDimensions.width : '',\n                    dimensions: data.baseImage.size.join('x'),\n                    alt: '',\n                    caption: ''\n                });\n\n                if(imagesPaths.length) {\n                    this.loadImages(imagesPaths);\n                } else {\n                    this.isUploading = false;\n                }\n            });\n        },\n\n        removeImage(index) {\n            this.images.splice(index, 1);\n        },\n\n        parseInputElement () {\n            let galleryHandler = $(this.galleryElement);\n            let images = $(galleryHandler).find('.gallery__item');\n            this.columns = galleryHandler.attr('data-columns') || 3;\n            this.layout = '';\n\n            if (galleryHandler.hasClass('gallery-wrapper--wide')) {\n                this.layout = 'gallery-wrapper--wide';\n            } else if (galleryHandler.hasClass('gallery-wrapper--full')) {\n                this.layout = 'gallery-wrapper--full';\n            }\n\n            if (!images.length) {\n                return;\n            }\n\n            for (let image of images) {\n                image = $(image);\n\n                this.images.push({\n                    fullImagePath: image.find('a').attr('href'),\n                    thumbnailPath: image.find('img').attr('src'),\n                    thumbnailHeight: image.find('img').attr('height') ? image.find('img').attr('height') : '',\n                    thumbnailWidth: image.find('img').attr('width') ? image.find('img').attr('width') : '',\n                    alt: image.find('img').attr('alt'),\n                    caption: image.find('figcaption').length ? image.find('figcaption').html() : '',\n                    dimensions: image.find('a').attr('data-size')\n                });\n            }\n        },\n\n        save () {\n            this.isVisible = false;\n            this.$bus.$emit('gallery-popup-updated', this.generateOutput());\n        },\n\n        cancel () {\n            this.isVisible = false;\n            this.$bus.$emit('gallery-popup-updated', false);\n        },\n\n        moveImage(index, direction) {\n            index = parseInt(index, 10);\n            let tempMoved = JSON.parse(JSON.stringify(this.images[index]));\n            let tempReplaced;\n\n            if(direction === 'up') {\n                tempReplaced = JSON.parse(JSON.stringify(this.images[index - 1]));\n                Vue.set(this.images, index - 1, tempMoved);\n            } else {\n                tempReplaced = JSON.parse(JSON.stringify(this.images[index + 1]));\n                Vue.set(this.images, index + 1, tempMoved);\n            }\n\n            Vue.set(this.images, index, tempReplaced);\n        },\n\n        generateOutput() {\n            let output = '';\n\n            for(let i = 0; i < this.images.length; i++) {\n                let img = this.images[i];\n                let description = ``;\n\n                if(img.caption !== '') {\n                    description = `<figcaption>${img.caption}</figcaption>`;\n                }\n\n                let imgAlt = img.alt;\n\n                if (imgAlt) {\n                    imgAlt = imgAlt.replace(/\\\"/gmi, '\\'');\n                }\n\n                let link = `<a href=\"${img.fullImagePath}\" data-size=\"${img.dimensions}\"><img src=\"${img.thumbnailPath}\" alt=\"${imgAlt}\" /></a>`;\n\n                if(img.thumbnailWidth === '') {\n                    link = `<a href=\"${img.fullImagePath}\" data-size=\"${img.dimensions}\"><img src=\"${img.thumbnailPath}\" alt=\"${imgAlt}\" /></a>`;\n                } else {\n                    link = `<a href=\"${img.fullImagePath}\" data-size=\"${img.dimensions}\"><img src=\"${img.thumbnailPath}\" alt=\"${imgAlt}\" height=\"${img.thumbnailHeight}\" width=\"${img.thumbnailWidth}\" /></a>`;\n                }\n\n                let item = `<figure class=\"gallery__item\">${link}${description}</figure>`;\n                output += item;\n            }\n\n            if(!this.images.length) {\n                output = '&nbsp;';\n            }\n\n            $(this.galleryElement).attr('data-columns', this.columns);\n            $(this.galleryElement).removeClass('gallery-wrapper--wide').removeClass('gallery-wrapper--full');\n\n            if (this.layout !== '') {\n                $(this.galleryElement).addClass(this.layout);\n            }\n\n            return {\n                gallery: this.galleryElement,\n                html: output\n            };\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('update-gallery-popup');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100005;\n}\n\nh1 {\n    text-align: center;\n}\n\n.gallery-popup {\n    max-width: 70rem;\n    min-width: 70rem;\n    padding: 0 0 4rem 0;\n\n    h1 {\n        margin: 4rem 0 2rem 0;\n    }\n\n    &-buttons {\n        margin: 0;\n    }\n\n    &-config {\n        display: flex;\n        padding: 1.5rem 3rem;\n        position: relative;\n\n        &:after {\n            background: linear-gradient(transparent, var(--popup-bg));\n            bottom: 100%;\n            content: \"\";\n            height: 40px;\n            left: 0;\n            pointer-events: none;\n            position: absolute;\n            right: 0;\n            z-index: 1;\n        }\n\n        & > * {\n            width: auto;\n        }\n\n        .button {\n            margin-left: auto;\n        }\n\n        label {\n            align-items: center;\n            display: flex;\n            margin-right: 30px;\n        }\n\n        select {\n            -webkit-appearance: none;\n            max-width: 100%;\n            min-width: 100px;\n            min-height: 46px;          \n            margin-left: 10px;\n            padding: 0 12px 0 18px;\n            position: relative;\n            width: 140px;\n\n            &:not([multiple]) {\n                background: url('data:image/svg+xml;utf8,<svg fill=\"%238e929d\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 6\"><polygon points=\"10 0 5 0 0 0 5 6 10 0\"/></svg>') no-repeat calc(100% - 2rem) 50%;\n                background-color: var(--input-bg);\n                background-size: 10px;\n                padding-right: 3rem;\n            }\n        }\n    }\n\n    .loading-state {\n        padding: 2rem 4rem;\n\n        svg {\n            fill: var(--icon-quaternary-color);\n            margin: 0 0 4rem;\n        }\n    }\n\n    &-images-list {\n        list-style-type: none;\n        min-height: 400px;\n        margin: 0;\n        max-height: 60vh;\n        overflow: scroll;\n        padding: 0 3rem 20px;\n\n        &:empty {\n            min-height: 400px;\n            position: relative;\n\n            &::before {\n                background-color: var(--icon-quaternary-color);\n                bottom: 0;\n                content: \"\";\n                left: 0;\n                mask: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 148'%3e%3cpath d='M157.887,44.459L52.274,10.298c-4.204-1.36-8.714.946-10.074,5.15l-25.236,78.02c-1.36,4.204.946,8.714,5.15,10.074l105.612,34.161c4.204,1.36,8.714-.946,10.074-5.15l25.236-78.02c1.36-4.204-.946-8.714-5.15-10.074ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3cg%3e%3cpath d='M26.525,101.386l23.854-17.207c1.309-.944,3.057-1.009,4.433-.165l13.435,8.247c1.432.879,3.261.769,4.577-.274l46.564-36.911c1.516-1.202,3.674-1.146,5.127.131l28.984,25.498-.033,34.303c-.004,4.415-3.585,7.992-8,7.992H34.5c-4.424,0-8.008-3.591-8-8.015l.025-13.599Z' /%3e%3cpath d='M145.5,25H34.5c-4.418,0-8,3.582-8,8v82c0,4.418,3.582,8,8,8h111c4.418,0,8-3.582,8-8V33c0-4.418-3.582-8-8-8ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3c/g%3e%3cg%3e%3cpath d='M13.5,27c-.829,0-1.5-.671-1.5-1.5v-3.105c0-.126.003-.252.01-.376.007-.129.016-.251.028-.373.013-.13.029-.251.047-.372.021-.129.043-.251.068-.372.026-.123.054-.243.085-.363.031-.117.064-.233.1-.346.035-.112.075-.227.115-.338.045-.121.091-.237.141-.351.045-.104.095-.215.148-.324.054-.11.111-.22.17-.327.055-.099.116-.204.179-.306.06-.097.126-.198.194-.297.071-.103.14-.2.211-.292.068-.089.142-.182.217-.272.078-.093.156-.181.234-.267.076-.084.16-.169.243-.252.087-.087.179-.173.271-.255.091-.081.18-.157.271-.23.086-.071.179-.142.272-.212.105-.077.202-.145.302-.21.104-.069.204-.132.306-.193.104-.062.209-.123.317-.179.108-.057.213-.11.319-.16.108-.051.219-.101.33-.147.113-.047.226-.091.339-.132.116-.041.229-.079.343-.114.112-.036.237-.069.361-.101.115-.029.234-.056.354-.081.124-.025.247-.047.37-.066.123-.018.241-.033.361-.046.135-.015.262-.023.391-.03.133-.006.259-.01.386-.01h4.012c.829,0,1.5.671,1.5,1.5s-.671,1.5-1.5,1.5h-4.012c-.079,0-.158.002-.236.006-.068.004-.143.009-.217.017l-.238.03c-.068.01-.142.023-.216.038s-.149.032-.222.05c-.068.018-.135.035-.2.055l-.226.076c-.066.024-.136.052-.204.08-.063.026-.13.057-.196.088l-.201.101c-.059.032-.121.067-.182.104l-.185.117c-.056.037-.115.078-.173.12l-.337.275c-.045.041-.094.085-.141.131-.05.05-.101.101-.149.154l-.41.514c-.036.052-.074.11-.11.169-.041.065-.075.126-.109.188-.036.065-.067.125-.097.187-.031.063-.061.127-.088.192-.029.068-.055.132-.079.196-.023.064-.048.133-.07.203l-.063.218c-.018.066-.034.136-.048.206-.015.071-.029.143-.04.215-.01.067-.02.143-.028.218-.007.07-.013.148-.017.225-.003.068-.006.145-.006.223v3.105c0,.829-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M24.5,133h-4.012c-.127,0-.253-.004-.379-.01-.127-.006-.253-.017-.378-.028-.13-.013-.261-.029-.389-.05-.114-.017-.243-.039-.37-.065-.104-.021-.221-.048-.337-.076-.127-.032-.241-.064-.351-.098-.132-.04-.252-.081-.369-.123-.106-.038-.229-.085-.348-.136-.104-.045-.21-.092-.314-.141-.106-.05-.211-.104-.316-.158-.114-.061-.224-.123-.331-.188-.09-.054-.19-.116-.29-.182-.098-.064-.193-.133-.288-.2-.101-.074-.197-.149-.291-.225-.096-.079-.185-.154-.272-.232-.083-.072-.168-.154-.251-.234-.094-.092-.178-.18-.259-.268-.085-.095-.163-.183-.238-.272-.071-.083-.145-.177-.218-.272-.07-.093-.137-.186-.202-.278-.077-.111-.143-.213-.206-.316-.062-.099-.123-.204-.183-.312-.055-.101-.111-.207-.164-.315-.053-.108-.104-.218-.151-.329-.049-.113-.095-.227-.137-.344s-.081-.228-.116-.339c-.036-.117-.069-.23-.1-.345-.032-.119-.06-.241-.085-.363-.025-.12-.048-.243-.067-.364-.019-.127-.036-.25-.048-.372-.013-.128-.022-.251-.029-.373-.006-.133-.01-.259-.01-.384v-5.105c0-.828.671-1.5,1.5-1.5s1.5.672,1.5,1.5v5.105c0,.076.002.153.006.23.004.068.01.146.018.224.007.067.017.144.028.218.01.064.024.137.039.207s.03.14.048.208l.136.43c.021.059.048.123.075.187.028.065.058.13.088.193.031.063.063.125.098.187.036.065.07.124.105.182.04.064.078.123.117.181l.124.17c.043.057.087.111.132.165l.151.173c.04.043.088.094.137.143l.325.29c.048.039.103.083.159.123l.364.241c.068.041.126.074.185.105l.411.194c.062.026.123.05.183.071.081.029.148.053.215.072l.435.109c.08.018.149.028.218.039.085.013.156.021.226.028.076.008.153.015.23.019.078.004.157.006.236.006h4.012c.829,0,1.5.672,1.5,1.5s-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M160.512,133h-4.012c-.828,0-1.5-.672-1.5-1.5s.672-1.5,1.5-1.5h4.012c.079,0,.158-.002.237-.006.066-.004.135-.008.201-.016.09-.01.165-.018.239-.029.085-.013.153-.024.221-.038.093-.019.166-.036.238-.054.062-.016.136-.036.207-.058l.206-.069c.077-.027.141-.054.203-.079.068-.028.137-.06.203-.091s.133-.065.197-.1c.059-.031.117-.064.174-.099l.196-.123c.059-.038.115-.079.172-.12l.334-.271c.058-.051.105-.097.152-.143l.282-.308c.057-.065.097-.117.136-.169l.134-.186c.04-.057.075-.111.109-.167l.11-.188c.033-.062.065-.124.097-.187l.092-.199c.023-.055.049-.116.07-.179l.136-.425c.021-.077.035-.143.049-.207.018-.081.029-.148.039-.215.014-.081.022-.15.028-.218.008-.074.015-.15.019-.225.004-.076.006-.152.006-.229v-3.105c0-.828.672-1.5,1.5-1.5s1.5.672,1.5,1.5v3.105c0,.126-.004.251-.01.375-.006.126-.017.251-.028.374-.014.13-.03.26-.051.387-.017.112-.04.24-.066.365-.022.112-.053.238-.086.363-.026.1-.06.213-.094.325-.042.132-.081.245-.122.355-.038.104-.085.223-.135.34-.05.114-.1.222-.151.328-.053.109-.108.217-.167.321-.057.104-.117.206-.179.307-.065.106-.134.212-.206.314-.064.093-.134.189-.205.282-.066.089-.146.188-.226.283-.061.073-.139.161-.217.247-.092.099-.171.181-.251.261-.096.095-.187.18-.279.262-.079.07-.167.146-.258.22-.101.081-.192.152-.287.222-.098.072-.195.141-.297.208-.1.065-.199.128-.301.188-.097.059-.206.121-.317.18-.105.056-.211.109-.318.16s-.219.1-.328.146c-.115.049-.233.095-.353.137-.098.036-.211.073-.323.109-.129.039-.247.071-.365.102-.128.032-.248.06-.37.084-.11.023-.237.045-.368.064-.106.017-.232.032-.358.045-.12.015-.255.024-.391.03-.127.006-.253.01-.381.01Z' /%3e%3cpath d='M166.5,29c-.828,0-1.5-.671-1.5-1.5v-5.105c0-.077-.002-.154-.006-.23-.004-.075-.011-.15-.019-.225-.007-.068-.015-.137-.026-.204-.012-.082-.024-.149-.038-.215-.017-.08-.031-.144-.049-.208l-.136-.429c-.025-.074-.049-.131-.073-.187l-.192-.392c-.033-.062-.069-.122-.106-.181-.034-.056-.069-.11-.106-.164-.045-.065-.083-.12-.125-.173-.052-.069-.095-.123-.14-.177l-.149-.17c-.039-.043-.083-.089-.129-.134l-.327-.293c-.055-.044-.109-.087-.166-.128l-.362-.241c-.061-.036-.122-.072-.185-.105-.064-.035-.131-.068-.197-.1l-.216-.096c-.064-.028-.13-.052-.195-.076-.06-.021-.128-.044-.194-.065-.075-.022-.144-.042-.214-.06l-.23-.053c-.074-.015-.146-.027-.22-.039-.071-.011-.15-.021-.228-.029-.07-.007-.15-.013-.229-.017-.071-.003-.15-.006-.229-.006h-4.012c-.828,0-1.5-.671-1.5-1.5s.672-1.5,1.5-1.5h4.012c.128,0,.254.003.38.009.132.007.257.017.379.029.129.013.253.029.374.047.131.02.256.042.378.067.116.023.233.05.351.08.119.029.24.063.357.1.112.033.233.074.35.116.122.045.24.09.354.138.105.045.212.092.315.141.107.051.213.105.318.16.107.057.214.118.317.179.102.061.201.124.301.189.102.068.194.134.287.201.103.075.198.149.292.226.092.074.179.149.267.227.088.077.172.158.256.239.091.087.178.178.263.271.083.091.16.179.235.268.073.087.147.181.221.278.065.084.137.185.207.286.064.092.133.197.198.303.062.103.124.207.183.313.057.104.111.208.163.314s.102.215.148.324c.052.12.101.241.145.365.035.095.074.206.11.319.038.125.072.241.102.356.03.112.061.237.086.365.024.111.047.238.066.365.018.113.035.242.048.372.012.124.022.248.028.373s.01.251.01.376v5.105c0,.829-.672,1.5-1.5,1.5Z' /%3e%3c/g%3e%3c/svg%3e\");\n                -webkit-mask: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 148'%3e%3cpath d='M157.887,44.459L52.274,10.298c-4.204-1.36-8.714.946-10.074,5.15l-25.236,78.02c-1.36,4.204.946,8.714,5.15,10.074l105.612,34.161c4.204,1.36,8.714-.946,10.074-5.15l25.236-78.02c1.36-4.204-.946-8.714-5.15-10.074ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3cg%3e%3cpath d='M26.525,101.386l23.854-17.207c1.309-.944,3.057-1.009,4.433-.165l13.435,8.247c1.432.879,3.261.769,4.577-.274l46.564-36.911c1.516-1.202,3.674-1.146,5.127.131l28.984,25.498-.033,34.303c-.004,4.415-3.585,7.992-8,7.992H34.5c-4.424,0-8.008-3.591-8-8.015l.025-13.599Z' /%3e%3cpath d='M145.5,25H34.5c-4.418,0-8,3.582-8,8v82c0,4.418,3.582,8,8,8h111c4.418,0,8-3.582,8-8V33c0-4.418-3.582-8-8-8ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3c/g%3e%3cg%3e%3cpath d='M13.5,27c-.829,0-1.5-.671-1.5-1.5v-3.105c0-.126.003-.252.01-.376.007-.129.016-.251.028-.373.013-.13.029-.251.047-.372.021-.129.043-.251.068-.372.026-.123.054-.243.085-.363.031-.117.064-.233.1-.346.035-.112.075-.227.115-.338.045-.121.091-.237.141-.351.045-.104.095-.215.148-.324.054-.11.111-.22.17-.327.055-.099.116-.204.179-.306.06-.097.126-.198.194-.297.071-.103.14-.2.211-.292.068-.089.142-.182.217-.272.078-.093.156-.181.234-.267.076-.084.16-.169.243-.252.087-.087.179-.173.271-.255.091-.081.18-.157.271-.23.086-.071.179-.142.272-.212.105-.077.202-.145.302-.21.104-.069.204-.132.306-.193.104-.062.209-.123.317-.179.108-.057.213-.11.319-.16.108-.051.219-.101.33-.147.113-.047.226-.091.339-.132.116-.041.229-.079.343-.114.112-.036.237-.069.361-.101.115-.029.234-.056.354-.081.124-.025.247-.047.37-.066.123-.018.241-.033.361-.046.135-.015.262-.023.391-.03.133-.006.259-.01.386-.01h4.012c.829,0,1.5.671,1.5,1.5s-.671,1.5-1.5,1.5h-4.012c-.079,0-.158.002-.236.006-.068.004-.143.009-.217.017l-.238.03c-.068.01-.142.023-.216.038s-.149.032-.222.05c-.068.018-.135.035-.2.055l-.226.076c-.066.024-.136.052-.204.08-.063.026-.13.057-.196.088l-.201.101c-.059.032-.121.067-.182.104l-.185.117c-.056.037-.115.078-.173.12l-.337.275c-.045.041-.094.085-.141.131-.05.05-.101.101-.149.154l-.41.514c-.036.052-.074.11-.11.169-.041.065-.075.126-.109.188-.036.065-.067.125-.097.187-.031.063-.061.127-.088.192-.029.068-.055.132-.079.196-.023.064-.048.133-.07.203l-.063.218c-.018.066-.034.136-.048.206-.015.071-.029.143-.04.215-.01.067-.02.143-.028.218-.007.07-.013.148-.017.225-.003.068-.006.145-.006.223v3.105c0,.829-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M24.5,133h-4.012c-.127,0-.253-.004-.379-.01-.127-.006-.253-.017-.378-.028-.13-.013-.261-.029-.389-.05-.114-.017-.243-.039-.37-.065-.104-.021-.221-.048-.337-.076-.127-.032-.241-.064-.351-.098-.132-.04-.252-.081-.369-.123-.106-.038-.229-.085-.348-.136-.104-.045-.21-.092-.314-.141-.106-.05-.211-.104-.316-.158-.114-.061-.224-.123-.331-.188-.09-.054-.19-.116-.29-.182-.098-.064-.193-.133-.288-.2-.101-.074-.197-.149-.291-.225-.096-.079-.185-.154-.272-.232-.083-.072-.168-.154-.251-.234-.094-.092-.178-.18-.259-.268-.085-.095-.163-.183-.238-.272-.071-.083-.145-.177-.218-.272-.07-.093-.137-.186-.202-.278-.077-.111-.143-.213-.206-.316-.062-.099-.123-.204-.183-.312-.055-.101-.111-.207-.164-.315-.053-.108-.104-.218-.151-.329-.049-.113-.095-.227-.137-.344s-.081-.228-.116-.339c-.036-.117-.069-.23-.1-.345-.032-.119-.06-.241-.085-.363-.025-.12-.048-.243-.067-.364-.019-.127-.036-.25-.048-.372-.013-.128-.022-.251-.029-.373-.006-.133-.01-.259-.01-.384v-5.105c0-.828.671-1.5,1.5-1.5s1.5.672,1.5,1.5v5.105c0,.076.002.153.006.23.004.068.01.146.018.224.007.067.017.144.028.218.01.064.024.137.039.207s.03.14.048.208l.136.43c.021.059.048.123.075.187.028.065.058.13.088.193.031.063.063.125.098.187.036.065.07.124.105.182.04.064.078.123.117.181l.124.17c.043.057.087.111.132.165l.151.173c.04.043.088.094.137.143l.325.29c.048.039.103.083.159.123l.364.241c.068.041.126.074.185.105l.411.194c.062.026.123.05.183.071.081.029.148.053.215.072l.435.109c.08.018.149.028.218.039.085.013.156.021.226.028.076.008.153.015.23.019.078.004.157.006.236.006h4.012c.829,0,1.5.672,1.5,1.5s-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M160.512,133h-4.012c-.828,0-1.5-.672-1.5-1.5s.672-1.5,1.5-1.5h4.012c.079,0,.158-.002.237-.006.066-.004.135-.008.201-.016.09-.01.165-.018.239-.029.085-.013.153-.024.221-.038.093-.019.166-.036.238-.054.062-.016.136-.036.207-.058l.206-.069c.077-.027.141-.054.203-.079.068-.028.137-.06.203-.091s.133-.065.197-.1c.059-.031.117-.064.174-.099l.196-.123c.059-.038.115-.079.172-.12l.334-.271c.058-.051.105-.097.152-.143l.282-.308c.057-.065.097-.117.136-.169l.134-.186c.04-.057.075-.111.109-.167l.11-.188c.033-.062.065-.124.097-.187l.092-.199c.023-.055.049-.116.07-.179l.136-.425c.021-.077.035-.143.049-.207.018-.081.029-.148.039-.215.014-.081.022-.15.028-.218.008-.074.015-.15.019-.225.004-.076.006-.152.006-.229v-3.105c0-.828.672-1.5,1.5-1.5s1.5.672,1.5,1.5v3.105c0,.126-.004.251-.01.375-.006.126-.017.251-.028.374-.014.13-.03.26-.051.387-.017.112-.04.24-.066.365-.022.112-.053.238-.086.363-.026.1-.06.213-.094.325-.042.132-.081.245-.122.355-.038.104-.085.223-.135.34-.05.114-.1.222-.151.328-.053.109-.108.217-.167.321-.057.104-.117.206-.179.307-.065.106-.134.212-.206.314-.064.093-.134.189-.205.282-.066.089-.146.188-.226.283-.061.073-.139.161-.217.247-.092.099-.171.181-.251.261-.096.095-.187.18-.279.262-.079.07-.167.146-.258.22-.101.081-.192.152-.287.222-.098.072-.195.141-.297.208-.1.065-.199.128-.301.188-.097.059-.206.121-.317.18-.105.056-.211.109-.318.16s-.219.1-.328.146c-.115.049-.233.095-.353.137-.098.036-.211.073-.323.109-.129.039-.247.071-.365.102-.128.032-.248.06-.37.084-.11.023-.237.045-.368.064-.106.017-.232.032-.358.045-.12.015-.255.024-.391.03-.127.006-.253.01-.381.01Z' /%3e%3cpath d='M166.5,29c-.828,0-1.5-.671-1.5-1.5v-5.105c0-.077-.002-.154-.006-.23-.004-.075-.011-.15-.019-.225-.007-.068-.015-.137-.026-.204-.012-.082-.024-.149-.038-.215-.017-.08-.031-.144-.049-.208l-.136-.429c-.025-.074-.049-.131-.073-.187l-.192-.392c-.033-.062-.069-.122-.106-.181-.034-.056-.069-.11-.106-.164-.045-.065-.083-.12-.125-.173-.052-.069-.095-.123-.14-.177l-.149-.17c-.039-.043-.083-.089-.129-.134l-.327-.293c-.055-.044-.109-.087-.166-.128l-.362-.241c-.061-.036-.122-.072-.185-.105-.064-.035-.131-.068-.197-.1l-.216-.096c-.064-.028-.13-.052-.195-.076-.06-.021-.128-.044-.194-.065-.075-.022-.144-.042-.214-.06l-.23-.053c-.074-.015-.146-.027-.22-.039-.071-.011-.15-.021-.228-.029-.07-.007-.15-.013-.229-.017-.071-.003-.15-.006-.229-.006h-4.012c-.828,0-1.5-.671-1.5-1.5s.672-1.5,1.5-1.5h4.012c.128,0,.254.003.38.009.132.007.257.017.379.029.129.013.253.029.374.047.131.02.256.042.378.067.116.023.233.05.351.08.119.029.24.063.357.1.112.033.233.074.35.116.122.045.24.09.354.138.105.045.212.092.315.141.107.051.213.105.318.16.107.057.214.118.317.179.102.061.201.124.301.189.102.068.194.134.287.201.103.075.198.149.292.226.092.074.179.149.267.227.088.077.172.158.256.239.091.087.178.178.263.271.083.091.16.179.235.268.073.087.147.181.221.278.065.084.137.185.207.286.064.092.133.197.198.303.062.103.124.207.183.313.057.104.111.208.163.314s.102.215.148.324c.052.12.101.241.145.365.035.095.074.206.11.319.038.125.072.241.102.356.03.112.061.237.086.365.024.111.047.238.066.365.018.113.035.242.048.372.012.124.022.248.028.373s.01.251.01.376v5.105c0,.829-.672,1.5-1.5,1.5Z' /%3e%3c/g%3e%3c/svg%3e\");\n                mask-repeat: no-repeat;\n                -webkit-mask-repeat: no-repeat;\n                mask-size: 125px 66px;\n                -webkit-mask-size: 125px 66px;\n                mask-position: 50% 30%;\n                -webkit-mask-position: 50% 30%;\n                position: absolute;\n                right: 0;\n                top: 0;\n            }\n\n            &::after {\n                content: attr(data-translation);\n                color: var(--label-color);\n                position: absolute;\n                top: 50%;\n                transform: translateX(-50%) translateY(-50%);\n            }\n        }\n\n        &-item {\n            align-items: center;\n            display: grid;\n            grid-template-columns: auto 1fr 15px;\n            padding: 1rem 0;\n\n            &:first-child {\n\n                .gallery-popup-images-list-operations {\n                    & > a[href=\"#up\"] {\n                        display: none;\n                    }\n                }\n            }\n\n            &:last-child {\n                .gallery-popup-images-list-operations {\n                    & > a[href=\"#down\"] {\n                        display: none;\n                    }\n                }\n            }\n\n            img {\n                cursor: move;\n                max-height: 90px;\n                width: 90px;\n            }\n\n            & > div {\n                display: flex;\n                flex-wrap: wrap;\n                padding: 0 2rem;\n\n                & > * {\n                    width: 100%;\n                }\n\n                & > span {\n                    font-size: 1.3rem;\n                    font-weight: var(--font-weight-semibold);\n                    line-height: 1;\n                    padding: 0 0 .5rem 0;\n                    text-align: left;\n                }\n\n                & > input {\n                    margin: 5px 0;\n                }\n            }\n\n            & > .gallery-popup-images-list-operations {\n                align-self: normal;\n                margin: 5px 0;\n                padding: 0;\n                position: relative;\n\n                & > a {\n                    border-radius: 50%;\n                    font-size: 2.4rem;\n                    height: 3rem;\n                    left: 0;\n                    line-height: 1.1;\n                    text-align: center;\n                    transition: var(--transition);\n                    position: absolute;\n                    width: 3rem;\n\n                    &[href=\"#remove\"] {\n                        color: var(--warning);\n                        top: 50%;\n                        transform: translateY(-60%);\n                    }\n\n                    &[href=\"#up\"],\n                    &[href=\"#down\"] {\n                         color: var(--icon-secondary-color);\n\n                         &:active,\n                         &:focus,\n                         &:hover {\n                             color: var(--icon-tertiary-color);\n                         }\n                    }\n\n                    &[href=\"#up\"] {\n                        top: 0;\n                        transform: rotate(90deg);\n                    }\n\n                    &[href=\"#down\"] {\n                        bottom: 0;\n                        transform: rotate(-90deg);\n                    }\n\n                    &:hover {\n                        background: var(--input-border-color);\n                    }\n                }\n            }\n        }\n    }\n}\n\n.buttons {\n    display: flex;\n    margin: 0 0 -4rem 0;\n    position: relative;\n    text-align: center;\n    top: 1px;\n}\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/HelpPanelBlockEditor.vue",
    "content": "<template>\n    <div :class=\"{ 'help-panel': true, 'is-visible': isOpen }\">\n        <div>\n            <p\n                class=\"help-panel-desc\"\n                v-pure-html=\"$t('editor.blockEditorHelpPanelDesc')\"></p>\n            <table class=\"help-panel-table\">\n                <tr>\n                    <th>{{ $t('editor.shortcuts') }}</th>\n                    <th>{{ $t('editor.markdown') }}</th>\n                </tr>\n                <tr>\n                    <td>/separator</td>\n                    <td>---</td>\n                </tr>\n                <tr>\n                    <td>/hr</td>\n                    <td>---</td>\n                </tr>\n                <tr>\n                    <td>/header</td>\n                    <td>##</td>\n                </tr>\n                <tr>\n                    <td>/h1</td>\n                    <td>#</td>\n                </tr>\n                <tr>\n                    <td>/h2</td>\n                    <td>##</td>\n                </tr>\n                <tr>\n                    <td>/h3</td>\n                    <td>###</td>\n                </tr>\n                <tr>\n                    <td>/h4</td>\n                    <td>####</td>\n                </tr>\n                <tr>\n                    <td>/h5</td>\n                    <td>#####</td>\n                </tr>\n                <tr>\n                    <td>/h6</td>\n                    <td>######</td>\n                </tr>\n                <tr>\n                    <td>/list</td>\n                    <td>*</td>\n                </tr>\n                <tr>\n                    <td>/quote</td>\n                    <td>></td>\n                </tr>\n                <tr>\n                    <td>/blockquote</td>\n                    <td>></td>\n                </tr>\n                <tr>\n                    <td>/code</td>\n                    <td>```</td>\n                </tr>\n                <tr>\n                    <td>/readmore</td>\n                    <td>***</td>\n                </tr>\n                <tr>\n                    <td>/more</td>\n                    <td>***</td>\n                </tr>\n                <tr>\n                    <td>/html</td>\n                    <td></td>\n                </tr>\n                <tr>\n                    <td>/toc</td>\n                    <td></td>\n                </tr>\n                <!-- <tr>\n                    <td>/embed</td>\n                    <td></td>\n                </tr> -->\n                <tr>\n                    <td>/image</td>\n                    <td></td>\n                </tr>\n                <tr>\n                    <td>/img</td>\n                    <td></td>\n                </tr>\n            </table>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'help-panel-block-editor',\n    props: {\n        isOpen: {\n            default: false,\n            type: Boolean\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/help-panel-common.scss';\n\n\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/HelpPanelMarkdown.vue",
    "content": "<template>\n    <div :class=\"{ 'help-panel': true, 'is-visible': isOpen }\">\n        <div>\n            <p class=\"help-panel-desc\">\n                {{ $t('editor.markdownHelpPanelDesc') }}\n            </p>\n            <table class=\"help-panel-table col-3\">\n                <tr>\n                    <th>{{ $t('editor.element') }}</th>\n                    <th>{{ $t('editor.markdown') }}</th>\n                    <th>{{ $t('editor.shortcuts') }}</th>\n                </tr>\n                <tr>\n                    <td><b>Bold</b></td>\n                    <td>**text**</td>\n                    <td>Ctrl/⌘ + B</td>\n                </tr>\n                <tr>\n                    <td><i>Emphasize</i></td>\n                    <td>*text*</td>\n                    <td>Ctrl/⌘ + I</td>\n                </tr>\n                <tr>\n                    <td><s>Strikethrough</s></td>\n                    <td>~~text~~</td>\n                    <td>Ctrl/⌘ + Alt + U</td>\n                </tr>\n                <tr>\n                    <td><a href=\"\">Link</a></td>\n                    <td>[title](http://)</td>\n                    <td>Ctrl/⌘ + K</td>\n                </tr>\n                <tr>\n                    <td>List</td>\n                    <td>* item</td>\n                    <td>Ctrl/⌘ + L</td>\n                </tr>\n                <tr>\n                    <td>Ordered List</td>\n                    <td>1. item</td>\n                    <td>Ctrl/⌘ + Alt/⌥ + L</td>\n                </tr>\n                <tr>\n                    <td>Blockquote</td>\n                    <td>> quote</td>\n                    <td>Ctrl/⌘ + Q/'</td>\n                </tr>\n                <tr>\n                    <td>Inline code</td>\n                    <td>`code`</td>\n                    <td></td>\n                </tr>\n                <tr>\n                    <td>Code</td>\n                    <td>```code```</td>\n                    <td></td>\n                </tr>\n                <tr>\n                    <td>H1</td>\n                    <td># Heading</td>\n                    <td>\n                        <template v-if=\"!isMac\">Ctrl + H</template>\n                    </td>\n                </tr>\n                <tr>\n                    <td>H2</td>\n                    <td>## Heading</td>\n                    <td>\n                        <template v-if=\"!isMac\">Ctrl + H (x2)</template>\n                    </td>\n                </tr>\n                <tr>\n                    <td>H3</td>\n                    <td>### Heading</td>\n                    <td>\n                        <template v-if=\"!isMac\">Ctrl + H (x3)</template>\n                    </td>\n                </tr>\n                <tr>\n                    <td>H4</td>\n                    <td>#### </td>\n                    <td>\n                        <template v-if=\"!isMac\">Ctrl + H (x4)</template>\n                    </td>\n                </tr>\n                <tr>\n                    <td>H5</td>\n                    <td>##### </td>\n                    <td>\n                        <template v-if=\"!isMac\">Ctrl + H (x5)</template>\n                    </td>\n                </tr>\n                <tr>\n                    <td>H6</td>\n                    <td>###### </td>\n                    <td>\n                        <template v-if=\"!isMac\">Ctrl + H (x6)</template>\n                    </td>\n                </tr>\n                <tr>\n                    <td>Readmore</td>\n                    <td>---READMORE---</td>\n                    <td></td>\n                </tr>\n                 <tr>\n                    <td>{{ $t('image.image') }}</td>\n                    <td colspan=\"2\">\n                        {{ $t('editor.dragAndDropImgToEditor') }}\n                    </td>\n                </tr>\n            </table>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'help-panel-markdown',\n    props: {\n        isOpen: {\n            default: false,\n            type: Boolean\n        }\n    },\n    computed: {\n        isMac () {\n            return document.body.getAttribute('data-os') === 'osx';\n        }\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/help-panel-common.scss';\n\n\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/InlineEditor.vue",
    "content": "<template>\n    <div\n        id=\"inline-toolbar\"\n        ref=\"toolbar\"\n        contenteditable=\"false\"\n        @click.stop>\n       <button\n            id=\"inline-toolbar-bold\"\n            class=\"tox-icon tox-tbtn__icon-wrap\"\n            @click=\"toggle('bold')\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"></path><path d=\"M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"></path></svg>\n        </button>\n        <button\n            id=\"inline-toolbar-italic\"\n            class=\"tox-icon tox-tbtn__icon-wrap\"\n            @click=\"toggle('italic')\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"19\" y1=\"4\" x2=\"10\" y2=\"4\"></line><line x1=\"14\" y1=\"20\" x2=\"5\" y2=\"20\"></line><line x1=\"15\" y1=\"4\" x2=\"9\" y2=\"20\"></line></svg>\n        </button>\n        <button\n            id=\"inline-toolbar-underline\"\n            class=\"tox-icon tox-tbtn__icon-wrap\"\n            @click=\"toggle('underline')\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4\"></path><line x1=\"4\" y1=\"20\" x2=\"20\" y2=\"20\"></line></svg>\n        </button>\n        <button\n            id=\"inline-toolbar-strikethrough\"\n            class=\"tox-icon tox-tbtn__icon-wrap\"\n            @click=\"toggle('strikethrough')\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 4H9a3 3 0 0 0-2.83 4\"></path><path d=\"M14 12a4 4 0 0 1 0 8H6\"></path><line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\"></line></svg>\n        </button>\n        <button\n            id=\"inline-toolbar-url\"\n            class=\"tox-icon tox-tbtn__icon-wrap\"\n            @click=\"addLink\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"></path><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"></path></svg>\n        </button>\n        <button\n            id=\"inline-toolbar-unurl\"\n            class=\"tox-icon tox-tbtn__icon-wrap\"\n            @click=\"unLink\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71\"></path><path d=\"m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71\"></path><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"5\"></line><line x1=\"2\" y1=\"8\" x2=\"5\" y2=\"8\"></line><line x1=\"16\" y1=\"19\" x2=\"16\" y2=\"22\"></line><line x1=\"19\" y1=\"16\" x2=\"22\" y2=\"16\"></line></svg>\n        </button>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'inline-editor',\n    data () {\n        return {\n            win: null,\n            doc: null,\n            body: null,\n            customFormats: []\n        };\n    },\n    mounted () {\n        this.$bus.$on('init-inline-editor', customFormats => {\n            this.init(customFormats);\n        });\n\n        this.$bus.$on('update-inline-editor', config => {\n            this.update(config.sel, config.text);\n        });\n\n        this.$bus.$on('link-popup-updated', response => {\n            if ($(this.$refs.toolbar).css('display') === 'none') {\n                return;\n            }\n\n            if(response) {\n                let relAttr = [];\n\n                if (response.rel && response.rel.nofollow) {\n                    relAttr.push('nofollow');\n                }\n\n                if (response.rel && response.rel.sponsored) {\n                    relAttr.push('sponsored');\n                }\n\n                if (response.rel && response.rel.ugc) {\n                    relAttr.push('ugc');\n                }\n\n                if (response.target.indexOf('_blank') > -1) {\n                    relAttr.push('noopener');\n                    relAttr.push('noreferrer');\n                }\n\n                if (relAttr.length) {\n                    relAttr = ' rel=\"' + relAttr.join(' ') + '\"';\n                }\n\n                let linkHTMLStart = `<a href=\"${response.url}\"${response.title}${response.cssClass}${response.target}${relAttr}>`;\n                let linkHTMLContent = response.text;\n                let linkHTMLEnd = `</a>`;\n\n                if (linkHTMLContent === '') {\n                    linkHTMLContent = tinymce.activeEditor.selection.getContent();\n                }\n\n                tinymce.activeEditor.selection.setContent(linkHTMLStart + linkHTMLContent + linkHTMLEnd);\n\n                setTimeout(() => {\n                    this.updateLinkButtons();\n                }, 200);\n            }\n        });\n    },\n    methods: {\n        init () {\n            let iframe = document.getElementById('post-editor_ifr');\n            this.win = iframe.contentWindow.window;\n            this.doc = this.win.document;\n            this.body = this.doc.body;\n        },\n\n        toggle (format) {\n            tinymce.activeEditor.formatter.toggle(format);\n        },\n\n        addLink () {\n            this.$bus.$emit('init-link-popup', {\n                postID: this.postID,\n                selection: tinymce.activeEditor.selection.getContent()\n            });\n        },\n\n        unLink () {\n            tinymce.activeEditor.execCommand('Unlink', false);\n\n            setTimeout(() => {\n                this.updateLinkButtons();\n            }, 100);\n        },\n\n        update (sel, text) {\n            if ($('#link-toolbar').css('display') !== 'none') {\n                return;\n            }\n\n            if(text.trim() !== '') {\n                let range = sel.getRangeAt(0);\n                let rect = range.getBoundingClientRect();\n\n                $(this.$refs.toolbar).css({\n                    display: 'flex',\n                    left: this.calculateLeft(rect) + \"px\",\n                    top: this.calculateTop(rect) + \"px\"\n                });\n\n                this.updateLinkButtons();\n            } else {\n                $(this.$refs.toolbar).css('display', 'none');\n            }\n        },\n\n        calculateTop(rect) {\n            let iframe = $('#post-editor_ifr');\n            return (rect.top - 60) + iframe.offset().top;\n        },\n\n        calculateLeft(rect) {\n            let iframe = $('#post-editor_ifr');\n            let toolbar = $('#inline-toolbar');\n            let halfWidth = toolbar.outerWidth() / 2;\n            let base = (rect.left + (rect.width / 2) - halfWidth) + iframe.offset().left;\n\n            if(base <= 10) {\n                return 10;\n            }\n\n            if(base >= window.outerWidth - (base + 10)) {\n                return window.outerWidth - (base + 10);\n            }\n\n            return base;\n        },\n\n        updateLinkButtons() {\n            let selectedContent = tinymce.activeEditor.selection.getContent();\n\n            if(this.textContainsLink(selectedContent)) {\n                $('#inline-toolbar-url').css('display', 'none');\n                $('#inline-toolbar-unurl').css('display', 'inline-flex');\n            } else {\n                $('#inline-toolbar-url').css('display', 'inline-flex');\n                $('#inline-toolbar-unurl').css('display', 'none');\n            }\n        },\n\n        textContainsLink(text) {\n            return text.indexOf('<a href=') !== -1;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('init-link-editor');\n        this.$bus.$off('update-link-editor');\n        this.$bus.$off('link-popup-updated');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/ItemHelper.js",
    "content": "class ItemHelper {\n    static async prepareItemData (newStatus, itemID, $store, itemData, itemType = 'post') {\n        let finalStatus = newStatus;\n        let mediaPath = ItemHelper.getMediaPath($store, itemID, itemType);\n        let preparedText = '';\n        \n        if (itemData.editor === 'tinymce') {\n            preparedText = $('#post-editor').val();\n        }\n\n        if (itemData.editor === 'markdown') {\n            preparedText = itemData.text;\n        }\n\n        if (itemData.editor === 'blockeditor') {\n            preparedText = $('#post-editor').val();\n        }\n        \n        // Remove directxory path from images src attribute\n        preparedText = preparedText.replace(/file:(\\/){1,}/gmi, 'file:///');\n        preparedText = preparedText.split(mediaPath).join('#DOMAIN_NAME#');\n        preparedText = preparedText.replace(/file:(\\/){1,}\\#DOMAIN_NAME\\#/gmi, '#DOMAIN_NAME#');\n\n        if (itemType === 'page') {\n            finalStatus += ',is-page';\n        }\n\n        if (itemData.isHidden) {\n            finalStatus += ',hidden';\n        }\n\n        if (itemData.isTrashed) {\n            finalStatus += ',trashed';\n        }\n\n        if (itemData.isFeatured) {\n            finalStatus += ',featured';\n        }\n\n        if (itemData.isExcludedOnHomepage) {\n            finalStatus += ',excluded_homepage';\n        }\n\n        let itemViewSettings = {};\n\n        if (itemType === 'post') {\n            $store.state.currentSite.themeSettings.postConfig.forEach(field => {\n                let fieldType = 'select';\n\n                if (typeof field.type !== 'undefined') {\n                    fieldType = field.type;\n                }\n\n                itemViewSettings[field.name] = {\n                    type: fieldType,\n                    value: itemData.viewOptions[field.name]\n                };\n            });\n        } else if (itemType === 'page') {\n            $store.state.currentSite.themeSettings.pageConfig.forEach(field => {\n                let fieldType = 'select';\n\n                if (typeof field.type !== 'undefined') {\n                    fieldType = field.type;\n                }\n\n                itemViewSettings[field.name] = {\n                    type: fieldType,\n                    value: itemData.viewOptions[field.name]\n                };\n            });\n        }\n\n        if (itemData.slug === '') {\n            itemData.slug = await mainProcessAPI.invoke('app-main-process-create-slug', itemData.title);\n        }\n\n        let creationDate = itemData.creationDate.timestamp;\n\n        if (!itemData.creationDate.timestamp) {\n            creationDate = Date.now();\n        }\n\n        let preparedData = {\n            'site': $store.state.currentSite.config.name,\n            'title': itemData.title,\n            'slug': itemData.slug,\n            'text': preparedText,\n            'status': finalStatus,\n            'creationDate': creationDate,\n            'modificationDate': Date.now(),\n            'template': itemData.template,\n            'featuredImage': itemData.featuredImage.path === null ? '' : 'file:///' + ItemHelper.getMediaPath($store, itemID, itemType) + itemData.featuredImage.path,\n            'featuredImageFilename': itemData.featuredImage.path,\n            'featuredImageData': {\n                alt: itemData.featuredImage.alt,\n                caption: itemData.featuredImage.caption,\n                credits: itemData.featuredImage.credits\n            },\n            'additionalData': {\n                metaTitle: itemData.metaTitle,\n                metaDesc: itemData.metaDescription,\n                metaRobots: itemData.metaRobots,\n                canonicalUrl: itemData.canonicalUrl,\n                editor: itemData.editor\n            },\n            'id': itemID,\n            'author': parseInt(itemData.author, 10)\n        };\n\n        if (itemType === 'post') {\n            preparedData.tags = itemData.tags;\n\n            if (preparedData.tags && preparedData.tags.length) {\n                preparedData.tags = [...new Set(preparedData.tags)];\n            }\n            \n            preparedData.additionalData.mainTag = itemData.mainTag;\n            preparedData.postViewSettings = itemViewSettings;\n        } else if (itemType === 'page') {\n            preparedData.pageViewSettings = itemViewSettings;\n        }\n\n        return preparedData;\n    }\n\n    static loadItemData (data, $store, $moment, itemType = 'post') {\n        let itemData = {\n            title: '',\n            text: '',\n            slug: '',\n            author: 1,\n            template: '',\n            creationDate: {\n                text: '',\n                timestamp: 0\n            },\n            modificationDate: {\n                text: '',\n                timestamp: 0\n            },\n            isTrashed: false,\n            status: '',\n            metaTitle: '',\n            metaDescription: '',\n            metaRobots: 'index, follow',\n            canonicalUrl: '',\n            featuredImage: {\n                path: '',\n                alt: '',\n                caption: '',\n                credits: ''\n            }\n        };\n\n        if (itemType === 'post') {\n            itemData.tags = [];\n            itemData.viewOptions = {};\n            itemData.mainTag = '';\n            itemData.isFeatured = false;\n            itemData.isHidden = false;\n            itemData.isExcludedOnHomepage = false;\n        } else if (itemType === 'page') {\n            itemData.viewOptions = {};\n        }\n\n        // Set post elements\n        itemData.title = data[itemType + 's'][0].title;\n        let mediaPath = ItemHelper.getMediaPath($store, data[itemType + 's'][0].id, itemType);\n        let preparedText = data[itemType + 's'][0].text;\n        preparedText = preparedText.split('#DOMAIN_NAME#').join('file:///' + mediaPath);\n        preparedText = ItemHelper.setWebpCompatibility($store, preparedText);\n        itemData.text = preparedText;\n\n        // Set tags\n        if (itemType === 'post') {\n            itemData.tags = [];\n\n            if (data.tags.length) {\n                for (let i = 0; i < data.tags.length; i++) {\n                    itemData.tags.push(data.tags[i].name);\n                }\n            }\n\n            itemData.mainTag = data.additionalData.mainTag || '';\n        }\n\n        // Set author\n        itemData.author = data.author[0].id;\n\n        // Dates\n        let format = 'MMM DD, YYYY  HH:mm';\n\n        if($store.state.app.config.timeFormat == 12) {\n            format = 'MMM DD, YYYY  hh:mm a';\n        }\n\n        itemData.creationDate.text = $moment(data[itemType + 's'][0].created_at).format(format);\n        itemData.modificationDate.text = $moment(data[itemType + 's'][0].modified_at).format(format);\n        itemData.creationDate.timestamp = data[itemType + 's'][0].created_at;\n        itemData.modificationDate.timestamp = data[itemType + 's'][0].modified_at;\n        itemData.status = data[itemType + 's'][0].status.split(',').join(', ');\n        itemData.isTrashed = data[itemType + 's'][0].status.indexOf('trashed') > -1;\n\n        if (itemType === 'post') {\n            itemData.isHidden = data[itemType + 's'][0].status.indexOf('hidden') > -1;\n            itemData.isFeatured = data[itemType + 's'][0].status.indexOf('featured') > -1;\n            itemData.isExcludedOnHomepage = data[itemType + 's'][0].status.indexOf('excluded_homepage') > -1;\n        }\n\n        // Set image\n        if (data.featuredImage) {\n            itemData.featuredImage.path = !data.featuredImage.url ? '' : data.featuredImage.url;\n\n            if(data.featuredImage.additional_data) {\n                try {\n                    let imageData = JSON.parse(data.featuredImage.additional_data);\n                    itemData.featuredImage.alt = imageData.alt;\n                    itemData.featuredImage.caption = imageData.caption;\n                    itemData.featuredImage.credits = imageData.credits;\n                } catch(e) {\n                    console.warning('Unable to load featured image data: ');\n                    console.warning(data.featuredImage.additional_data);\n                }\n            }\n        }\n\n        // Set SEO\n        itemData.slug = data[itemType + 's'][0].slug;\n        itemData.metaTitle = data.additionalData.metaTitle || \"\";\n        itemData.metaDescription = data.additionalData.metaDesc || \"\";\n        itemData.metaRobots = data.additionalData.metaRobots || \"\";\n        itemData.canonicalUrl = data.additionalData.canonicalUrl || \"\";\n\n        // Update post template\n        itemData.template = data[itemType + 's'][0].template;\n\n        // Update item view settings\n        if (itemType === 'post' || itemType === 'page') {\n            ItemHelper.updateViewSettings(itemType, data, itemData);\n        }\n\n        return itemData;\n    }\n\n    static updateViewSettings(itemType, data, itemData) {\n        let settingsKey = itemType + 'ViewSettings';\n        let viewSettings = data[settingsKey];\n        let viewFields = Object.keys(viewSettings);\n\n        for (let i = 0; i < viewFields.length; i++) {\n            let newValue = '';\n    \n            if (viewSettings[viewFields[i]] && viewSettings[viewFields[i]].value) {\n                newValue = viewSettings[viewFields[i]].value;\n            } else {\n                newValue = viewSettings[viewFields[i]];\n            }\n    \n            itemData.viewOptions[viewFields[i]] = newValue;\n        }\n    }\n\n    static getMediaPath ($store, itemID, itemType = 'post') {\n        let mediaPath = $store.state.currentSite.siteDir.replace(/&/gmi, '&amp;');\n        mediaPath = mediaPath.replace(/\\\\/g, '/');\n        mediaPath += '/input/media/';\n        \n        if (itemType === 'post' || itemType === 'page') {\n            mediaPath += 'posts/';\n        }\n\n        mediaPath += itemID === 0 ? 'temp' : itemID;\n        mediaPath += '/';\n\n        return mediaPath;\n    }\n\n    static setWebpCompatibility ($store, text) {\n        let forceWebp = !!$store.state.currentSite.config.advanced.forceWebp;\n\n        text = text.replace(/\\<figure class=\"gallery__item\">[\\s\\S]*?<a[\\s\\S]*?href=\"(.*?)\"[\\s\\S]+?>[\\s\\S]*?<img[\\s\\S]*?src=\"(.*?)\"/gmi, (matches, linkUrl, imgUrl) => {\n            if (linkUrl && imgUrl) {\n                if (\n                    forceWebp && \n                    ItemHelper.getImageType(linkUrl) === 'webp-compatible' && \n                    !ItemHelper.isWebpImage(imgUrl)\n                ) {\n                    let imgExtension = ItemHelper.getImageExtension(imgUrl);\n                    let newImgUrl = imgUrl.substr(0, imgUrl.length + (-1 * imgExtension.length)) + '.webp';\n                    matches = matches.replace(imgUrl, newImgUrl);\n                } else if (\n                    !forceWebp && \n                    ItemHelper.getImageType(linkUrl) === 'webp-compatible' && \n                    ItemHelper.isWebpImage(imgUrl)\n                ) {\n                    let imgExtension = ItemHelper.getImageExtension(linkUrl);\n                    let newImgUrl = imgUrl.substr(0, imgUrl.length - 5) + imgExtension;\n                    matches = matches.replace(imgUrl, newImgUrl);\n                }\n            }\n\n            return matches;\n        });\n\n        return text;\n    }\n\n    static isWebpImage (url) {\n        if (url.substr(-5).toLowerCase() === '.webp') {\n            return true;\n        }\n\n        return false;\n    }\n\n    static getImageType (url) {\n        if (url.substr(-5).toLowerCase() === '.webp') {\n            return 'webp';\n        }\n\n        if (\n            url.substr(-5).toLowerCase() === '.jpeg' ||\n            url.substr(-4).toLowerCase() === '.jpg' ||\n            url.substr(-4).toLowerCase() === '.png'\n        ) {\n            return 'webp-compatible';\n        }\n\n        return 'other';\n    }\n\n    static getImageExtension (url) {\n        if (url.substr(-5).toLowerCase() === '.webp' || url.substr(-5).toLowerCase() === '.jpeg') {\n            return url.substr(-5);\n        } \n\n        if (url.substr(-4).toLowerCase() === '.jpg' || url.substr(-4).toLowerCase() === '.png') {\n            return url.substr(-4);\n        } \n\n        return false;\n    }\n}\n\nexport default ItemHelper;\n"
  },
  {
    "path": "app/src/components/post-editor/LinkPopup.vue",
    "content": "<template>\n    <div\n        v-if=\"isVisible\"\n        class=\"overlay\">\n        <div class=\"popup popup-link-add\">\n            <div class=\"message\">\n                <h1>{{ $t('link.insertEditLink') }}</h1>\n\n                <field :label=\"$t('link.selectLinkType') + ':'\">\n                    <v-select\n                        slot=\"field\"\n                        v-model=\"type\"\n                        :options=\"linkTypes\"\n                        :searchable=\"false\"\n                        :custom-label=\"customTypeLabels\"\n                        :show-labels=\"false\"\n                        :placeholder=\"$t('link.selectLinkType')\"></v-select>\n                </field>\n\n                <field\n                    v-if=\"type === 'post'\"\n                    :label=\"$t('post.postName')\">\n                    <v-select\n                        slot=\"field\"\n                        ref=\"postPagesSelect\"\n                        :options=\"postPages\"\n                        v-model=\"post\"\n                        :custom-label=\"customPostLabels\"\n                        :close-on-select=\"true\"\n                        :show-labels=\"false\"\n                        @select=\"closeDropdown('postPagesSelect')\"\n                        :placeholder=\"$t('post.selectPostPage')\"></v-select>\n                </field>\n\n                <field\n                    v-if=\"type === 'page'\"\n                    :label=\"$t('page.pageName')\">\n                    <v-select\n                        slot=\"field\"\n                        ref=\"pageItemsSelect\"\n                        :options=\"pageItems\"\n                        v-model=\"page\"\n                        :custom-label=\"customPageLabels\"\n                        :close-on-select=\"true\"\n                        :show-labels=\"false\"\n                        @select=\"closeDropdown('pageItemsSelect')\"\n                        :placeholder=\"$t('page.selectPage')\"></v-select>\n                </field>\n\n                <field\n                    v-if=\"type === 'tag'\"\n                    :label=\"$t('tag.tagName')\">\n                    <v-select\n                        slot=\"field\"\n                        ref=\"tagPagesSelect\"\n                        :options=\"tagPages\"\n                        v-model=\"tag\"\n                        :custom-label=\"customTagLabels\"\n                        :close-on-select=\"true\"\n                        :show-labels=\"false\"\n                        @select=\"closeDropdown('tagPagesSelect')\"\n                        :placeholder=\"$t('tag.selectTagPage')\"></v-select>\n                </field>\n\n                <field\n                    v-if=\"type === 'author'\"\n                    :label=\"$t('author.authorName') + ':'\">\n                    <v-select\n                        slot=\"field\"\n                        ref=\"authorPagesSelect\"\n                        :options=\"authorPages\"\n                        v-model=\"author\"\n                        :custom-label=\"customAuthorsLabels\"\n                        :close-on-select=\"true\"\n                        :show-labels=\"false\"\n                        @select=\"closeDropdown('authorPagesSelect')\"\n                        :placeholder=\"$t('author.selectAuthorPage')\"></v-select>\n                </field>\n\n                <field\n                    v-if=\"type === 'external'\"\n                    :label=\"$t('ui.customLink') + ':'\">\n                    <input\n                        slot=\"field\"\n                        type=\"text\"\n                        spellcheck=\"false\"\n                        v-model=\"external\"\n                        class=\"link-popup-field-external\" />\n                </field>\n\n                <field\n                    v-if=\"type === 'file'\"\n                    :label=\"$t('file.fileSemicolon')\">\n                    <v-select\n                        slot=\"field\"\n                        ref=\"fileSelect\"\n                        :options=\"filesList\"\n                        v-model=\"file\"\n                        :close-on-select=\"true\"\n                        :show-labels=\"false\"\n                        @select=\"closeDropdown('fileSelect')\"\n                        :placeholder=\"$t('file.selectFile')\"></v-select>\n                </field>\n\n                <field\n                    v-if=\"!markdown\"\n                    :label=\"$t('ui.linkTarget') + ':'\">\n                    <v-select\n                        slot=\"field\"\n                        ref=\"targetSelect\"\n                        :options=\"targetList\"\n                        v-model=\"target\"\n                        :custom-label=\"customTargetLabels\"\n                        :close-on-select=\"true\"\n                        :show-labels=\"false\"\n                        @select=\"closeDropdown('targetSelect')\"\n                        :placeholder=\"$t('ui.selectOption')\"></v-select>\n                </field>\n\n                <field :label=\"$t('settings.linkLabel') + ':'\">\n                    <input\n                        slot=\"field\"\n                        type=\"text\"\n                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                        v-model=\"label\"\n                        class=\"link-popup-field-label\" />\n                </field>\n\n                <field\n                    v-if=\"!markdown\"\n                    :label=\"$t('link.linkTitleAttribute')\">\n                    <input\n                        slot=\"field\"\n                        type=\"text\"\n                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                        v-model=\"title\"\n                        class=\"link-popup-field-title\" />\n                </field>\n\n                <field\n                    v-if=\"!markdown\"\n                    :label=\"$t('link.linkClassAttribute')\">\n                    <input\n                        slot=\"field\"\n                        type=\"text\"\n                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                        v-model=\"cssClass\"\n                        class=\"link-popup-field-class\" />\n                </field>\n\n                <field\n                    v-if=\"!markdown\"\n                    :label=\"$t('link.linkRelAttribute')\">\n                    <switcher\n                        slot=\"field\"\n                        label=\"nofollow\"\n                        v-model=\"rel.nofollow\" />\n                    <switcher\n                        slot=\"field\"\n                        label=\"sponsored\"\n                        v-model=\"rel.sponsored\" />\n                    <switcher\n                        slot=\"field\"\n                        label=\"ugc\"\n                        v-model=\"rel.ugc\" />\n                </field>\n\n                <field\n                    v-if=\"!markdown && type === 'file'\"\n                    :label=\"$t('link.downloadAttribute')\">\n                    <switcher\n                        slot=\"field\"\n                        label=\"\"\n                        v-model=\"downloadAttr\" />\n                </field>\n            </div>\n\n            <div class=\"buttons\">\n                <p-button\n                    type=\"medium no-border-radius half-width\"\n                    @click.native=\"setLink\">\n                    {{ $t('ui.ok') }}\n                </p-button>\n\n                <p-button\n                    type=\"medium no-border-radius half-width cancel-popup\"\n                    @click.native=\"cancel\">\n                    {{ $t('ui.cancel') }}\n                </p-button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'link-popup',\n    props: {\n        'markdown': {\n            default: false\n        }\n    },\n    data () {\n        return {\n            postID: 0,\n            isVisible: false,\n            easymdeInstance: null,\n            type: 'external',\n            post: null,\n            page: null,\n            tag: null,\n            author: null,\n            file: null,\n            external: '',\n            target: '',\n            label: '',\n            title: '',\n            cssClass: '',\n            downloadAttr: false,\n            rel: {\n                nofollow: false,\n                sponsored: false,\n                ugc: false\n            },\n            filesList: []\n        };\n    },\n    computed: {\n        linkTypes () {\n            return [ 'external', 'post', 'page', 'tag', 'tags', 'author', 'frontpage', 'blogpage', 'file' ];\n        },\n        tagPages () {\n            return this.$store.state.currentSite.tags.filter(tag => tag.additionalData.indexOf('\"isHidden\":true') === -1).sort((a, b) => {\n                return a.name.localeCompare(b.name);\n            }).map(tag => tag.id);\n        },\n        authorPages () {\n            return this.$store.state.currentSite.authors.map(author => author.username).sort((a, b) => {\n                if (a.toLowerCase() < b.toLowerCase()) {\n                    return -1;\n                }\n\n                if (a.toLowerCase() > b.toLowerCase()) {\n                    return 1;\n                }\n\n                return 0;\n            });\n        },\n        postPages () {\n            return this.$store.state.currentSite.posts.filter(post => post.status.indexOf('published') > -1).sort((a, b) => {\n                return a.title.localeCompare(b.title);\n            }).map(post => post.id);\n        },\n        pageItems () {\n            return this.$store.state.currentSite.pages.filter(page => page.status.indexOf('published') > -1).sort((a, b) => {\n                return a.title.localeCompare(b.title);\n            }).map(page => page.id);\n        },\n        targetList () {\n            return [ '-', '_blank' ];\n        }\n    },\n    mounted () {\n        this.$bus.$on('init-link-popup', config => {\n            this.postID = config.postID;\n            this.parseContent(config.selection);\n            this.isVisible = true;\n        });\n\n        this.loadFiles();\n        this.$bus.$on('link-popup-updated', this.addLink);\n    },\n    methods: {\n        customTypeLabels (value) {\n            switch (value) {\n                case 'post': return this.$t('post.postLink');\n                case 'page': return this.$t('page.pageLink');\n                case 'tag': return this.$t('tag.tagLink');\n                case 'tags': return this.$t('tag.tagsListLink');\n                case 'author': return this.$t('author.authorLink');\n                case 'frontpage': return this.$t('ui.frontpageLink');\n                case 'blogpage': return this.$t('ui.blogIndexLink');\n                case 'external': return this.$t('ui.customLink');\n                case 'file': return this.$t('file.fileFromFileManager');\n            }\n        },\n        customTagLabels (value) {\n            return this.$store.state.currentSite.tags.filter(tag => tag.additionalData.indexOf('\"isHidden\":true') === -1 && tag.id === value).map(tag => tag.name)[0];\n        },\n        customAuthorsLabels (value) {\n            return this.$store.state.currentSite.authors.filter(author => author.username === value).map(author => author.name)[0];\n        },\n        customPostLabels (value) {\n            return this.$store.state.currentSite.posts.filter(post => post.id === value).map(post => post.title)[0];\n        },\n        customPageLabels (value) {\n            return this.$store.state.currentSite.pages.filter(page => page.id === value).map(page => page.title)[0];\n        },\n        customTargetLabels (value) {\n            if (value === '-') {\n                return this.$t('ui.sameWindow');\n            }\n\n            if (value === '_blank') {\n                return this.$t('ui.newWindow');\n            }\n        },\n        closeDropdown (refID) {\n            this.$refs[refID].isOpen = false;\n        },\n        cleanPopup () {\n            this.type = 'external';\n            this.post = null;\n            this.page = null;\n            this.tag = null;\n            this.author = null;\n            this.file = null;\n            this.external = '';\n            this.target = '';\n            this.downloadAttr = false;\n            this.label = '';\n            this.title = '';\n            this.cssClass = '';\n            this.rel = {\n                nofollow: false,\n                sponsored: false,\n                ugc: false\n            };\n        },\n        parseContent (content) {\n            if (!content) {\n                return;\n            }\n\n            if (this.markdown) {\n                this.parseMarkdownContent(content);\n            } else {\n                this.parseHTMLContent(content);\n            }\n        },\n        parseHTMLContent (content) {\n            let linkContent = content.match(/>(.*?)<\\/a>/);\n            let titleContent = content.match(/title=\"(.*?)\"/);\n            let classContent = content.match(/class=\"(.*?)\"/);\n            let targetContent = content.match(/target=\"(.*?)\"/);\n            let urlContent = content.match(/href=\"(.*?)\"/);\n            let relContent = content.match(/rel=\"(.*?)\"/);\n            let downloadContent = content.match(/<a.*?(download=\"download\").*?>/);\n\n            this.type = 'external';\n\n            if (linkContent && linkContent[1]) {\n                this.label = linkContent[1];\n            } else {\n                this.label = content;\n            }\n\n            if (titleContent && titleContent[1]) {\n                this.title = titleContent[1];\n            }\n\n            if (classContent && classContent[1]) {\n                this.cssClass = classContent[1];\n            }\n\n            if (targetContent && targetContent[1]) {\n                this.target = targetContent[1];\n            }\n\n            if (downloadContent && downloadContent[1]) {\n                this.downloadAttr = true;\n            }\n\n            this.parseUrlContent(urlContent);\n\n            let relValues = ['nofollow', 'sponsored', 'ugc'];\n\n            for (let i = 0; i < relValues.length; i++) {\n                if (relContent && relContent[1].indexOf(relValues[i]) > -1) {\n                    this.rel[relValues[i]] = true;\n                }\n            }\n        },\n        parseMarkdownContent (content) {\n            let linkContent = content.match(/\\[(.*?)\\]/);\n            let urlContent = content.match(/\\((.*?)\\)/);\n            this.type = 'external';\n\n            if (linkContent && linkContent[1]) {\n                this.label = linkContent[1];\n            } else {\n                this.label = content;\n            }\n\n            this.parseUrlContent(urlContent);\n        },\n        parseUrlContent (urlContent) {\n            if (urlContent && urlContent[1]) {\n                if (urlContent[1].indexOf('/post/') !== -1) {\n                    let id = urlContent[1].replace('#INTERNAL_LINK#/post/', '');\n                    this.type = 'post';\n                    this.post = parseInt(id, 10);\n                } else if (urlContent[1].indexOf('/page/') !== -1) {\n                    let id = urlContent[1].replace('#INTERNAL_LINK#/page/', '');\n                    this.type = 'page';\n                    this.page = parseInt(id, 10);\n                } else if (urlContent[1].indexOf('/tag/') !== -1) {\n                    let id = urlContent[1].replace('#INTERNAL_LINK#/tag/', '');\n                    this.type = 'tag';\n                    this.tag = parseInt(id, 10);\n                } else if (urlContent[1].indexOf('/tags/') !== -1) {\n                    this.type = 'tags';\n                } else if (urlContent[1].indexOf('/author/') !== -1) {\n                    let id = urlContent[1].replace('#INTERNAL_LINK#/author/', '');\n                    this.type = 'author';\n                    this.author = parseInt(id, 10);\n                } else if (urlContent[1].indexOf('/frontpage/') !== -1) {\n                    this.type = 'frontpage';\n                } else if (urlContent[1].indexOf('/blogpage/') !== -1) {\n                    this.type = 'blogpage';\n                } else if (urlContent[1].indexOf('/file/') !== -1) {\n                    this.type = 'file';\n                    this.file = urlContent[1].replace('#INTERNAL_LINK#/file/', '');\n                } else {\n                    this.type = 'external';\n                    this.external = urlContent[1];\n                }\n            }\n        },\n        setLink () {\n            let response = {\n                url: '',\n                title: '',\n                cssClass: '',\n                target: '',\n                text: this.label,\n                rel: this.rel,\n                downloadAttr: this.downloadAttr\n            };\n\n            if (this.type !== 'external') {\n                if (this.type === 'tags') {\n                    response.url = '#INTERNAL_LINK#/tags/1';\n                } else if (this.type === 'frontpage') {\n                    response.url = '#INTERNAL_LINK#/frontpage/1';\n                } else if (this.type === 'blogpage') {\n                    response.url = '#INTERNAL_LINK#/blogpage/1';\n                } else {\n                    response.url = '#INTERNAL_LINK#/' + this.type + '/' + this[this.type];\n                } \n            } else {\n                response.url = this.external;\n            }\n\n            if (this.target !== '' && this.target !== '-') {\n                response.target = ' target=\"' + this.target + '\"';\n            }\n\n            if (this.title.trim() !== '') {\n                response.title = ' title=\"' + this.title + '\"';\n            }\n\n            if (this.cssClass.trim() !== '') {\n                response.cssClass = ' class=\"' + this.cssClass + '\"'\n            }\n\n            this.cleanPopup();\n            this.isVisible = false;\n            this.$bus.$emit('link-popup-updated', response);\n        },\n        cancel () {\n            this.cleanPopup();\n            this.isVisible = false;\n            this.$bus.$emit('link-popup-updated', false);\n        },\n        addLink (response) {\n            if (this.markdown) {\n                this.addLinkMarkdown(response);\n            } else {\n                this.addLinkHTML(response);\n            }\n        },\n        addLinkMarkdown (response) {\n            if (response) {\n                this.easymdeInstance.codemirror.replaceSelections([`[${response.text}](${response.url})`]);\n            }\n        },\n        addLinkHTML (response) {\n            if ($('#link-toolbar').css('display') !== 'none' || $('#inline-toolbar').css('display') !== 'none') {\n                console.log('STOP1');\n                return;\n            }\n\n            if (response) {\n                let relAttr = [];\n                let downloadAttr = '';\n\n                if (response.rel && response.rel.nofollow) {\n                    relAttr.push('nofollow');\n                }\n\n                if (response.rel && response.rel.sponsored) {\n                    relAttr.push('sponsored');\n                }\n\n                if (response.rel && response.rel.ugc) {\n                    relAttr.push('ugc');\n                }\n\n                if (response.target.indexOf('_blank') > -1) {\n                    relAttr.push('noopener');\n                    relAttr.push('noreferrer');\n                }\n\n                if (relAttr.length) {\n                    relAttr = ' rel=\"' + relAttr.join(' ') + '\"';\n                }\n\n                if (response.downloadAttr && response.url.indexOf('#INTERNAL_LINK#/file/') > -1) {\n                    downloadAttr = ' download=\"download\" '\n                }\n\n                let linkHTMLStart = `<a href=\"${response.url}\"${response.title}${response.cssClass}${response.target}${relAttr}${downloadAttr}>`;\n                let linkHTMLContent = response.text;\n                let linkHTMLEnd = `</a>`;\n\n                if (linkHTMLContent === '') {\n                    linkHTMLContent = tinymce.activeEditor.selection.getContent();\n                }\n\n                tinymce.activeEditor.selection.setContent(linkHTMLStart + linkHTMLContent + linkHTMLEnd);\n            }\n        },\n        setEasyMdeInstance (instance) {\n            this.easymdeInstance = instance;\n        },\n        loadFiles () {\n            mainProcessAPI.send('app-file-manager-list', {\n                siteName: this.$store.state.currentSite.config.name,\n                dirPath: 'root-files'\n            });\n\n            mainProcessAPI.receiveOnce('app-file-manager-listed', (data) => {\n                this.filesList = data.map(file => file.name);\n\n                mainProcessAPI.send('app-file-manager-list', {\n                    siteName: this.$store.state.currentSite.config.name,\n                    dirPath: 'media/files'\n                });\n\n                mainProcessAPI.receiveOnce('app-file-manager-listed', (data) => {\n                    this.filesList = this.filesList.concat(data.map(file => 'media/files/' + file.name));\n                });\n            });\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('init-link-popup');\n        this.$bus.$off('link-popup-updated', this.addLink);\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/popup-common.scss';\n\n.overlay {\n    z-index: 100005;\n}\n\nh1 {\n    text-align: center;\n}\n\n.popup {\n    max-width: 60rem;\n    min-width: 60rem;\n    padding: 4rem;\n\n    &.popup-link-add {\n        overflow: visible;\n    }\n\n    .field {\n        .switcher {\n            float: left;\n            top: -3px;\n        }\n    }\n}\n\n.message {\n    font-size: 1.6rem;\n    padding: 0;\n}\n\n.buttons {\n    display: flex;\n    margin: 4rem -4rem -4rem -4rem;\n    position: relative;\n    text-align: center;\n    top: 1px;\n\n    & > .button {\n        border-radius: 0 0 0 .6rem;\n\n        & + .button {\n            border-radius: 0 0 .6rem 0;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/LinkToolbar.vue",
    "content": "<template>\n    <div\n        id=\"link-toolbar\"\n        contenteditable=\"false\">\n        <button\n            id=\"link-toolbar-url\"\n            @click.stop=\"showLinkPopup\"\n            class=\"tox-icon tox-tbtn__icon-wrap\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"></path><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"></path></svg>\n        </button>\n        <button\n            id=\"link-toolbar-unurl\"\n            @click.stop=\"unlink\"\n            class=\"tox-icon tox-tbtn__icon-wrap\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71\"></path><path d=\"m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71\"></path><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"5\"></line><line x1=\"2\" y1=\"8\" x2=\"5\" y2=\"8\"></line><line x1=\"16\" y1=\"19\" x2=\"16\" y2=\"22\"></line><line x1=\"19\" y1=\"16\" x2=\"22\" y2=\"16\"></line></svg>\n        </button>\n        <button\n            id=\"link-toolbar-preview\"\n            @click.stop=\"showPreview\"\n            class=\"tox-icon tox-tbtn__icon-wrap\">\n            <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z\"></path><circle cx=\"12\" cy=\"12\" r=\"3\"></circle></svg>\n        </button>\n    </div>\n</template>\n\n<script>\nimport Utils from './../../helpers/utils.js';\n\nexport default {\n    name: 'link-toolbar',\n    data () {\n        return {\n            win: null\n        };\n    },\n    mounted () {\n        this.$bus.$on('init-link-editor', iframe => {\n            this.init(iframe);\n        });\n\n        this.$bus.$on('update-link-editor', config => {\n            this.update(config.sel, config.text);\n        });\n\n        this.$bus.$on('link-popup-updated', response => {\n            if ($('#link-toolbar').css('display') === 'none') {\n                return false;\n            }\n\n            if(response) {\n                let relAttr = [];\n                let downloadAttr = '';\n\n                if (response.rel && response.rel.nofollow) {\n                    relAttr.push('nofollow');\n                }\n\n                if (response.rel && response.rel.sponsored) {\n                    relAttr.push('sponsored');\n                }\n\n                if (response.rel && response.rel.ugc) {\n                    relAttr.push('ugc');\n                }\n\n                if (response.target.indexOf('_blank') > -1) {\n                    relAttr.push('noopener');\n                    relAttr.push('noreferrer');\n                }\n\n                if (relAttr.length) {\n                    relAttr = ' rel=\"' + relAttr.join(' ') + '\"';\n                }\n\n                if (response.downloadAttr && response.url.indexOf('#INTERNAL_LINK#/file/') > -1) {\n                    downloadAttr = ' download=\"download\" '\n                }\n\n                let linkHTML = `<a href=\"${response.url}\"${response.title}${response.cssClass}${response.target}${relAttr}${downloadAttr}>${response.text}</a>`;\n                tinymce.activeEditor.selection.setContent(linkHTML);\n            } else {\n                let sel = this.win.getSelection();\n                sel.removeAllRanges();\n            }\n\n            $('#link-toolbar').css('display', 'none');\n            tinymce.activeEditor.selection.collapse();\n        });\n    },\n    methods: {\n        init(iframe) {\n            this.win = iframe.contentWindow.window;\n            $('#link-toolbar').css('display', 'none');\n        },\n\n        showLinkPopup () {\n            let selectedNode = tinymce.activeEditor.selection.getNode();\n\n            if (selectedNode.tagName === 'IMG' && selectedNode.parentNode && selectedNode.parentNode.tagName === 'A') {\n                this.$bus.$emit('init-link-popup', {\n                    postID: this.postID,\n                    selection: selectedNode.parentNode.outerHTML\n                });\n            } else {\n                this.$bus.$emit('init-link-popup', {\n                    postID: this.postID,\n                    selection: tinymce.activeEditor.selection.getContent()\n                });\n            }\n        },\n\n        showPreview () {\n            let selectedText = tinymce.activeEditor.selection.getContent();\n            let link = selectedText.match(/href=\"(.*?)\"/);\n\n            if (link && link[1] && link[1].indexOf('#INTERNAL_LINK#') === -1) {\n                let urlToOpen = Utils.getValidUrl(link[1]);\n\n                if (urlToOpen) {\n                    mainProcessAPI.shellOpenExternal(urlToOpen);\n                } else {\n                    alert(this.$t('link.linkInvalidMsg'));\n                }\n            }\n        },\n\n        unlink () {\n            tinymce.activeEditor.execCommand('Unlink', false);\n            $('#link-toolbar').css('display', 'none');\n            tinymce.activeEditor.selection.collapse();\n        },\n\n        update(selection, link) {\n            if ($('#inline-toolbar').css('display') !== 'none') {\n                return;\n            }\n\n            if(link) {\n                let range = selection.getRangeAt(0);\n                let rect = range.getBoundingClientRect();\n\n                $('#link-toolbar').css({\n                    display: 'flex',\n                    left: this.calculateLeft(rect) + \"px\",\n                    top: (this.calculateTop(rect) + 10) + \"px\"\n                });\n\n                let url = link.outerHTML.match(/href=\"(.*?)\"/);\n                let previewButton = $('#link-toolbar-preview')\n\n                if(url && url[1] && url[1].indexOf('#INTERNAL_LINK#') === -1) {\n                    previewButton.css('opacity', 1);\n                    previewButton.css('cursor', 'pointer');\n                    previewButton.attr('title', this.$t('link.previewLinkInBrowser'));\n                } else {\n                    previewButton.css('opacity', .25);\n                    previewButton.css('cursor', 'not-allowed');\n                    previewButton.attr('title', this.$t('link.previewOnlyExternalLinksMsg'));\n                }\n            } else {\n                $('#link-toolbar').css('display', 'none');\n            }\n        },\n\n        calculateTop(rect) {\n            let iframe = $('#post-editor_ifr');\n            return (rect.top - 60) + iframe.offset().top;\n        },\n\n        calculateLeft(rect) {\n            let iframe = $('#post-editor_ifr');\n            let toolbar = $('#link-toolbar');\n            let halfWidth = toolbar.outerWidth() / 2;\n            let base = (rect.left + (rect.width / 2) - halfWidth) + iframe.offset().left;\n\n            if(base <= 10) {\n                return 10;\n            }\n\n            if(base >= window.outerWidth - (base + 10)) {\n                return window.outerWidth - (base + 10);\n            }\n\n            return base;\n        },\n    },\n    beforeDestroy () {\n        this.$bus.$off('init-link-editor');\n        this.$bus.$off('update-link-editor');\n        this.$bus.$off('link-popup-updated');\n    }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/SearchPopup.vue",
    "content": "<template>\n    <div :class=\"{ 'search-popup': true, 'is-visible': isVisible }\">\n        <input \n            @keyup=\"doKeyboardNavigation\"\n            ref=\"search-phrase-input\"\n            spellcheck=\"false\"\n            v-model=\"searchPhrase\" />\n        <span>\n            <template v-if=\"resultsCount > 0\">\n                {{ currentResultIndex }} / \n            </template> \n            {{ resultsCount }}\n        </span>        \n        <button @click.prevent=\"getPrevResult()\">\n            <icon\n                size=\"m\"\n                name=\"arrow-up\"  />\n        </button>\n        <button @click.prevent=\"getNextResult()\">\n            <icon\n                size=\"m\"\n                name=\"arrow-down\"  />\n        </button>\n        <button @click.prevent=\"finishSearch()\">\n            <icon\n                size=\"m\"\n                name=\"close\"  />\n        </button>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'search-popup',\n    data () {\n        return {\n            searchPhrase: '',\n            isVisible: false,\n            currentResultIndex: 0,\n            resultsCount: 0\n        };\n    },\n    watch: {\n        searchPhrase (newValue) {\n            if (this.isVisible && newValue.trim() !== '') {\n                mainProcessAPI.invoke('app-main-webview-search-find-in-page', newValue);\n            } else {\n                this.resultsCount = 0;\n                this.currentResultIndex = 0;\n                mainProcessAPI.invoke('app-main-webview-search-stop-find-in-page');\n            }\n        }\n    },\n    mounted () {\n        /*document.querySelector('webview').addEventListener('dom-ready', () => {\n            document.querySelector('webview').addEventListener('found-in-page', e => {\n                this.currentResultIndex = e.result.activeMatchOrdinal;\n                this.resultsCount = e.result.matches;\n            });\n        });*/\n\n        mainProcessAPI.receive('app-show-search-form', this.showSearch);\n        this.$bus.$on('app-show-search-form', this.showSearch);\n    },\n    methods: {\n        getNextResult () {\n            if (this.resultsCount === 0) {\n                return;\n            }\n\n            mainProcessAPI.invoke('app-main-webview-search-find-in-page', this.searchPhrase);\n        },\n        getPrevResult () {\n            if (this.resultsCount === 0) {\n                return;\n            }\n\n            mainProcessAPI.invoke('app-main-webview-search-find-in-page', this.searchPhrase, {\n                forward: false\n            });\n        },\n        showSearch () {\n            this.isVisible = true;\n            this.$refs['search-phrase-input'].focus();\n        },\n        finishSearch () {\n            mainProcessAPI.invoke('app-main-webview-search-stop-find-in-page');\n            this.isVisible = false;\n        },\n        doKeyboardNavigation (e) {\n            if (e.code === 'Enter' && !event.isComposing) {\n                this.getNextResult();\n            }\n        }\n    },\n    beforeDestroy () {\n        mainProcessAPI.stopReceiveAll('app-show-search-form');\n        this.$bus.$off('app-show-search-form', this.showSearch);\n    }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.search-popup {\n    background: var(--popup-bg);\n    border-radius: 6px;\n    box-shadow: 0 0 5px rgba(black, .125);\n    left: 50%;\n    opacity: 0;\n    padding: 15px 22px 15px 30px;\n    pointer-events: none;\n    position: fixed;\n    top: 20px;\n    transform: translateX(-50%);\n    transition: var(--transition);\n    width: auto;\n    z-index: 10000000;\n\n    &.is-visible {\n        opacity: 1;\n        pointer-events: auto;\n        top: 40px;\n    }\n\n    input {\n        background: none;\n        border: none;\n        border-bottom: 1px solid var(--input-border-focus);        \n        color: var(--text-primary-color);\n        font-size: 18px;\n        font-weight: var(--font-weight-semibold);\n        width: 300px;\n    }\n    \n    span {\n        color: var(--text-primary-color);\n        display: inline-block;\n        font-size: 12px;  \n        margin-right: 10px;\n        min-width: 3.5rem;\n    }\n    \n    button {\n        background: none;\n        border: none;\n        border-radius: 50%;        \n        cursor: pointer;        \n        height: 2.8rem;         \n        padding: 0;\n        position: relative;       \n        text-align: center;       \n        transition: var(--transition);             \n        width: 2.8rem;\n        \n        & > svg {           \n            stroke: var(--icon-secondary-color);\n            vertical-align: middle;\n        }       \n        \n        &:active,\n        &:focus,\n        &:hover {\n            & > svg {           \n                stroke: var(--icon-tertiary-color);\n            }\n        }\n        \n        &:hover {\n            background: var(--input-border-color);            \n        }  \n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/Sidebar.vue",
    "content": "<template>\n    <div :class=\"{ \n        'options-sidebar-container': true, \n        'post-editor-sidebar': true, \n        'is-visible': isVisible \n    }\">\n        <div class=\"options-sidebar\">\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'status' }\"\n                    class=\"is-first\"\n                    @click=\"openItem('status')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-status\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('post.status') }}</span>\n                </div>\n\n                <div\n                    class=\"post-editor-settings\"\n                    style=\"max-height: none;\"\n                    ref=\"status-content-wrapper\">\n                    <div\n                        class=\"post-editor-settings-content\"\n                        ref=\"status-content\">\n                        <div\n                            v-if=\"isEdit\"\n                            class=\"post-info\">\n                            <dl>\n                                <dt v-if=\"itemType === 'post'\">{{ $t('post.postState') }}</dt>\n                                <dt v-if=\"itemType === 'page'\">{{ $t('page.pageState') }}</dt>\n                                <dd id=\"post-status\">\n                                    {{ filteredStatus }}\n                                </dd>\n                            </dl>\n\n                            <dl>\n                                <dt>{{ $t('author.author') }}</dt>\n                                <dd>\n                                    <a\n                                        href=\"#\"\n                                        @click.prevent=\"changeAuthor\">\n                                        {{ authorName }}\n                                    </a>\n                                </dd>\n                            </dl>\n\n                            <dl>\n                                <dt>{{ $t('post.published') }}</dt>\n                                <dd>\n                                    <a\n                                        href=\"#\"\n                                        @click.prevent=\"changeDate\">\n                                        {{ $parent.postData.creationDate.text }}\n                                    </a>\n                                </dd>\n                            </dl>\n\n                            <dl>\n                                <dt>{{ $t('post.updatedOn') }}</dt>\n                                <dd id=\"post-date-modified\">\n                                    {{ $parent.postData.modificationDate.text }}\n                                </dd>\n                            </dl>\n\n                            <dl \n                                v-if=\"itemType === 'page' && pagesStructureLoaded\"\n                                class=\"page-parent-page-wrapper\">\n                                <dt>{{ $t('page.parentPage') }}</dt>\n                                <dd id=\"page-parent-page\">\n                                    <dropdown\n                                        key=\"page-parent-dropdown\"\n                                        id=\"page-parent-page-dropdown\"\n                                        :value=\"parentPage\"\n                                        :onChange=\"changeParentPage\"\n                                        :items=\"flatPagesList\"></dropdown>\n                                </dd>\n                            </dl>\n                        </div>\n\n                        <div\n                            v-if=\"!isEdit\"\n                            class=\"post-info post-info--nogrid\">\n\n                            <dl>\n                                <dt v-if=\"itemType === 'post'\">{{ $t('post.postAuthor') }}</dt>\n                                <dt v-if=\"itemType === 'page'\">{{ $t('page.pageAuthor') }}</dt>\n                                <dd>\n                                    <dropdown\n                                        id=\"post-author-id\"\n                                        v-model=\"$parent.postData.author\"\n                                        :items=\"authors\"></dropdown>\n                                </dd>\n                            </dl>\n\n                            <dl class=\"post-date\">\n                                <dt>{{ $t('post.published') }}</dt>\n                                <dd>\n                                    <a\n                                        href=\"#\"\n                                        @click.prevent=\"changeDate\">\n\n                                        <template v-if=\"!$parent.postData.creationDate.text\">\n                                            <template v-if=\"itemType === 'post'\">\n                                                {{ $t('post.setCustomPostDate') }}\n                                            </template>\n                                            <template v-if=\"itemType === 'page'\">\n                                                {{ $t('page.setCustomPageDate') }}\n                                            </template>\n                                        </template>\n\n                                        <template v-if=\"$parent.postData.creationDate.text\">\n                                            {{ $t('post.changePostDate') }}\n\n                                            <small>\n                                                ({{ $parent.postData.creationDate.text }})\n                                            </small>\n\n                                            <span\n                                                class=\"post-date-reset\"\n                                                @click.stop.prevent=\"resetCreationDate()\">\n                                                &times;\n                                            </span>\n\n                                        </template>\n                                    </a>\n                                </dd>\n                            </dl>\n\n                            <dl v-if=\"itemType === 'page' && pagesStructureLoaded\">\n                                <dt>{{ $t('page.parentPage') }}</dt>\n                                <dd id=\"page-parent-page\">\n                                    <dropdown\n                                        key=\"page-parent-dropdown\"\n                                        id=\"page-parent-page-dropdown\"\n                                        :value=\"parentPage\"\n                                        :onChange=\"changeParentPage\"\n                                        :items=\"flatPagesList\"></dropdown>\n                                </dd>\n                            </dl>\n                        </div>\n\n                        <div \n                            v-if=\"itemType === 'post'\"\n                            class=\"post-action\">\n                            <label id=\"post-featured-wrapper\">\n                                <switcher\n                                    v-model=\"$parent.postData.isFeatured\" />\n                                <icon\n                                    :title=\"$t('post.markAsFeatured')\"\n                                    class=\"switcher-item-icon-helper\"\n                                    name=\"featured-post\"\n                                    size=\"xs\"\n                                    strokeColor=\"color-helper-6\" />\n                                <span>\n                                    {{ $t('post.markAsFeatured') }}\n                                </span>\n                            </label>\n\n                            <label id=\"post-hidden-wrapper\">\n                                <switcher\n                                    :title=\"$t('post.postWillNotAppearOnListMsg')\"\n                                    v-model=\"$parent.postData.isHidden\" />\n                                <icon\n                                    :title=\"$t('post.hidePost')\"\n                                    class=\"switcher-item-icon-helper\"\n                                    name=\"hidden-post\"\n                                    size=\"xs\"\n                                    strokeColor=\"color-6\" />\n                                <span :title=\"$t('post.postWillNotAppearOnListMsg')\">\n                                    {{ $t('post.hidePost') }}\n                                </span>\n                            </label>\n\n                            <label id=\"post-excluded-homepage-wrapper\">\n                                <switcher\n                                    :title=\"$t('post.postWillNotAppearOnHomepageListMsg')\"\n                                    v-model=\"$parent.postData.isExcludedOnHomepage\" />\n                                <icon\n                                    :title=\"$t('post.excludeFromHomepage')\"\n                                    class=\"switcher-item-icon-helper\"\n                                    name=\"excluded-post\"\n                                    size=\"xs\"\n                                    strokeColor=\"color-3\"/>\n                                <span :title=\"$t('post.postWillNotAppearOnHomepageListMsg')\">\n                                    {{ $t('post.excludeFromHomepage') }}\n                                </span>\n                            </label>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'image' }\"\n                    @click=\"openItem('image')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-image\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.featuredImage') }}</span>\n                </div>\n\n                <div\n                    class=\"post-editor-settings\"\n                    ref=\"image-content-wrapper\">\n                    <div\n                        class=\"post-editor-settings-content\"\n                        ref=\"image-content\">\n                        <image-upload\n                            ref=\"featured-image\"\n                            :item-id=\"$parent.postID\"\n                            v-model=\"$parent.postData.featuredImage.path\"\n                            imageType=\"featuredImages\" />\n\n                        <div\n                            v-if=\"$parent.postData.featuredImage.path\"\n                            class=\"image-uploader-settings-form\">\n                            <label>{{ $t('ui.alternativeText') }}\n                                <text-input\n                                    ref=\"featured-image-alt\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                    v-model=\"$parent.postData.featuredImage.alt\" />\n                            </label>\n\n                            <label>{{ $t('ui.caption') }}\n                                <text-input\n                                    ref=\"featured-image-caption\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                    v-model=\"$parent.postData.featuredImage.caption\" />\n                            </label>\n\n                            <label>{{ $t('ui.credits') }}\n                                <text-input\n                                    ref=\"featured-image-credits\"\n                                    :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                    v-model=\"$parent.postData.featuredImage.credits\" />\n                            </label>\n                        </div>\n                    </div>\n                </div>\n\n                <div \n                    v-if=\"itemType === 'post'\"\n                    class=\"options-sidebar-item\">\n                    <div\n                        :class=\"{ \n                            'options-sidebar-header': true, \n                            'is-open': openedItem === 'tags' \n                        }\"\n                        @click=\"openItem('tags')\">\n                        <icon\n                            class=\"options-sidebar-icon\"\n                            size=\"s\"\n                            name=\"sidebar-tags\"/>\n\n                        <span class=\"options-sidebar-label\">{{ $t('ui.tags') }}</span>\n                    </div>\n\n                    <div\n                        class=\"post-editor-settings\"\n                        ref=\"tags-content-wrapper\">\n                        <div\n                            class=\"post-editor-settings-content post-editor-settings-content-tags\"\n                            ref=\"tags-content\">\n                            <div class=\"post-tags\">\n                                <label id=\"post-tags-wrapper\">\n                                    <v-select\n                                        v-model=\"$parent.postData.tags\"\n                                        :tag-placeholder=\"$t('tag.addThisAsNewTag')\"\n                                        :options=\"availableTags\"\n                                        :searchable=\"true\"\n                                        :show-labels=\"false\"\n                                        placeholder=\"\"\n                                        :multiple=\"true\"\n                                        :taggable=\"true\"\n                                        @remove=\"removeTag\"\n                                        @tag=\"addTag\"></v-select>\n\n                                    <small\n                                        v-if=\"tagIsRestricted\"\n                                        class=\"post-tags-error\">\n                                        {{ $t('tag.tagIsNotAllowed') }}\n                                    </small>\n                                </label>\n                            </div>\n\n                            <div\n                                v-if=\"$parent.postData.tags.length > 1\"\n                                class=\"post-main-tag\">\n                                <label>\n                                    {{ $t('tag.mainTag') }}:\n                                    <dropdown\n                                        id=\"post-main-tag\"\n                                        v-model=\"$parent.postData.mainTag\"\n                                        :items=\"tagsForDropdown\">\n                                    </dropdown>\n\n                                    <small class=\"note\">\n                                        {{ $t('tag.noMainTagForPostMsg') }}\n                                    </small>\n                                </label>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"options-sidebar-item\">\n                    <div\n                        :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'seo' }\"\n                        @click=\"openItem('seo')\">\n                        <icon\n                            class=\"options-sidebar-icon\"\n                            size=\"s\"\n                            name=\"sidebar-seo\"/>\n\n                        <span class=\"options-sidebar-label\">\n                            {{ $t('ui.seo') }}\n\n                            <span\n                                v-if=\"$parent.postData.slug.length > 250\"\n                                class=\"options-sidebar-label-warning\">\n                                <template v-if=\"itemType === 'post'\">{{ $t('post.postSlugTooLong') }}</template>\n                                <template v-if=\"itemType === 'page'\">{{ $t('page.pageSlugTooLong') }}</template>\n                            </span>\n                        </span>\n                    </div>\n\n                    <div\n                        class=\"post-editor-settings\"\n                        ref=\"seo-content-wrapper\">\n                        <div\n                            class=\"post-editor-settings-content\"\n                            ref=\"seo-content\">\n                            <div class=\"post-seo\">\n                                <label>\n                                    <template v-if=\"itemType === 'post'\">{{ $t('post.postSlug') }}:</template>\n                                    <template v-if=\"itemType === 'page'\">{{ $t('page.pageSlug') }}:</template>\n                                    <div class=\"options-sidebar-item-slug\">\n                                        <input\n                                            type=\"text\"\n                                            v-model=\"$parent.postData.slug\"\n                                            spellcheck=\"false\"\n                                            @keyup=\"$parent.slugUpdated\">\n                                        <p-button \n                                            :onClick=\"updateSlug\" \n                                            :title=\"$t('ui.updateSlug')\"\n                                            icon=\"refresh\"\n                                            type=\"secondary icon\">\n                                        </p-button>\n                                    </div>\n                                    <small\n                                        v-if=\"$parent.postData.slug.length > 250\"\n                                        class=\"note is-warning\">\n                                        {{ $t('post.postSlugLengthWarning') }}\n                                    </small>\n                                </label>\n\n                                <label class=\"with-char-counter\">\n                                    {{ $t('settings.pageTitle') }}:\n                                    <text-input\n                                        type=\"text\"\n                                        v-model=\"$parent.postData.metaTitle\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        :placeholder=\"$t('settings.leaveBlankToUseDefaultPageTitle')\"\n                                        :charCounter=\"true\"\n                                        :preferredCount=\"70\" />\n                                    <small class=\"note\">\n                                        <template v-if=\"itemType === 'post'\">{{ $t('settings.postPageTitleVariables') }}:</template>\n                                        <template v-if=\"itemType === 'page'\">{{ $t('settings.pageTitleVariables') }}:</template>\n                                    </small>\n                                </label>\n\n                                <label class=\"with-char-counter\">\n                                    {{ $t('ui.metaDescription') }}:\n                                    <text-area\n                                        v-model=\"$parent.postData.metaDescription\"\n                                        :charCounter=\"true\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        :preferredCount=\"160\"></text-area>\n                                    <small class=\"note\">\n                                        <template v-if=\"itemType === 'post'\">{{ $t('settings.postPageTitleVariables') }}:</template>\n                                        <template v-if=\"itemType === 'page'\">{{ $t('settings.pageTitleVariables') }}:</template>\n                                    </small>\n                                </label>\n\n                                <label>\n                                    {{ $t('ui.metaRobotsIndex') }}:\n                                    <dropdown\n                                        v-if=\"!$parent.postData.canonicalUrl\"\n                                        id=\"post-meta-robots\"\n                                        v-model=\"$parent.postData.metaRobots\"\n                                        :items=\"metaRobotsOptions\">\n                                    </dropdown>\n                                    <div v-else>\n                                        <small>{{ $t('ui.ifCanonicalUrlIsSetMetaRobotsTagIsIgnored') }}</small>\n                                    </div>\n                                </label>\n\n                                <label>\n                                    {{ $t('ui.canonicalURL') }}:\n                                    <input\n                                        type=\"text\"\n                                        v-model=\"$parent.postData.canonicalUrl\"\n                                        spellcheck=\"false\"\n                                        :placeholder=\"$t('tag.leaveBlankToUseDefaultTagPageURL')\" />\n                                </label>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"options-sidebar-item\">\n                <div\n                    :class=\"{ 'options-sidebar-header': true, 'is-open': openedItem === 'other' }\"\n                    @click=\"openItem('other')\">\n                    <icon\n                        class=\"options-sidebar-icon\"\n                        size=\"s\"\n                        name=\"sidebar-options\"/>\n\n                    <span class=\"options-sidebar-label\">{{ $t('ui.otherOptions') }}</span>\n                </div>\n\n                <div\n                    class=\"post-editor-settings\"\n                    ref=\"other-content-wrapper\">\n                    <div\n                        class=\"post-editor-settings-content\"\n                        ref=\"other-content\">\n                        <div class=\"post-other\" id=\"post-view-settings\">\n                            <label id=\"post-template-wrapper\">\n                                <template v-if=\"itemType === 'post'\">\n                                    {{ $t('post.postTemplate') }}:\n                                </template>\n                                <template v-if=\"itemType === 'page'\">\n                                    {{ $t('page.pageTemplate') }}:\n                                </template>\n\n                                <dropdown\n                                    :items=\"itemType === 'post' ? postTemplates : pageTemplates\"\n                                    :disabled=\"!hasTemplates\"\n                                    v-model=\"$parent.postData.template\"\n                                    id=\"post-template\">\n                                    <option\n                                        v-if=\"hasTemplates\"\n                                        value=\"*\"\n                                        :selected=\"$parent.postData.template === '*'\"\n                                        slot=\"first-choice\">\n                                        {{ $t('settings.useGlobalConfiguration') }}\n                                    </option>\n                                    <option\n                                        v-if=\"hasTemplates\"\n                                        value=\"\"\n                                        :selected=\"$parent.postData.template === ''\"\n                                        slot=\"first-choice\">\n                                        {{ $t('theme.defaultTemplate') }}\n                                    </option>\n                                    <option\n                                        v-if=\"!hasTemplates\"\n                                        value=\"\"\n                                        slot=\"first-choice\">\n                                        {{ $t('ui.notAvailableInYourTheme') }}\n                                    </option>\n                                </dropdown>\n\n                                <template v-if=\"itemType === 'post'\">\n                                    <small\n                                        v-if=\"$parent.postData.template === '*'\"\n                                        slot=\"note\">\n                                        {{ $t('post.currentDefaultTemplate') }}:\n                                        <strong>\n                                            {{ $store.state.currentSite.themeSettings.postTemplates[$store.state.currentSite.themeSettings.defaultTemplates.post] }}\n                                        </strong>\n                                    </small>\n                                </template>\n                                <template v-else-if=\"itemType === 'page'\">\n                                    <small\n                                        v-if=\"$parent.postData.template === '*'\"\n                                        slot=\"note\">\n                                        {{ $t('page.currentDefaultTemplate') }}:\n                                        <strong>\n                                            {{ $store.state.currentSite.themeSettings.pageTemplates[$store.state.currentSite.themeSettings.defaultTemplates.page] }}\n                                        </strong>\n                                    </small>\n                                </template>\n                            </label>\n\n                            <template v-for=\"(field, index) of viewThemeSettings\">\n                                <separator\n                                    v-if=\"displayField(field) && field.type === 'separator'\"\n                                    :label=\"field.label\"\n                                    :is-line=\"true\"\n                                    :note=\"field.note\" />\n\n                                <label\n                                    v-if=\"displayField(field) && field.type !== 'separator'\"\n                                    :key=\"'post-view-field-' + index\">\n                                    {{ field.label }}\n\n                                    <dropdown\n                                        v-if=\"!field.type || field.type === 'select'\"\n                                        :id=\"field.name + '-select'\"\n                                        class=\"post-view-settings\"\n                                        v-model=\"$parent.postData.viewOptions[field.name]\"\n                                        :items=\"generateItems(field.options)\">\n                                        <option slot=\"first-choice\" value=\"\">{{ $t('settings.useGlobalConfiguration') }}</option>\n                                    </dropdown>\n\n                                    <text-input\n                                        v-if=\"field.type === 'text' || field.type === 'number'\"\n                                        :type=\"field.type\"\n                                        class=\"post-view-settings\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        :placeholder=\"fieldPlaceholder(field)\"\n                                        v-model=\"$parent.postData.viewOptions[field.name]\" />\n\n                                    <text-area\n                                        v-if=\"field.type === 'textarea'\"\n                                        class=\"post-view-settings\"\n                                        :placeholder=\"fieldPlaceholder(field)\"\n                                        :spellcheck=\"$store.state.currentSite.config.spellchecking\"\n                                        v-model=\"$parent.postData.viewOptions[field.name]\" />\n\n                                    <color-picker\n                                        v-if=\"field.type === 'colorpicker'\"\n                                        class=\"post-view-settings\"\n                                        v-model=\"$parent.postData.viewOptions[field.name]\"\n                                        :outputFormat=\"field.outputFormat ? field.outputFormat : 'RGBAorHEX'\">\n                                    </color-picker>\n\n                                    <image-upload\n                                        v-if=\"field.type === 'image'\"\n                                        slot=\"field\"\n                                        v-model=\"$parent.postData.viewOptions[field.name]\"\n                                        :item-id=\"$parent.postID\"\n                                        imageType=\"contentImages\" />\n\n                                    <small\n                                        v-if=\"field.note\"\n                                        class=\"note\">\n                                        {{ field.note }}\n                                    </small>\n                                </label>\n                            </template>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script>\nimport Vue from 'vue';\n\nexport default {\n    name: 'post-editor-sidebar',\n    props: {\n        'itemType': {\n            default: 'post',\n            type: String\n        },\n        'isVisible': {\n            default: false,\n            type: Boolean\n        }\n    },\n    data () {\n        return {\n            openedItem: 'status',\n            tagIsRestricted: false,\n            initialParentPage: null,\n            parentPage: 0,\n            pagesStructureLoaded: false,\n            pagesHierarchy: [],\n            flatStructure: []\n        };\n    },\n    computed: {\n        isEdit () {\n            return !!this.$route.params.post_id;\n        },\n        authors () {\n            let authors = [];\n            let sortedAuthors = this.$store.state.currentSite.authors.slice().sort((a, b) => {\n                if (a.name.toLowerCase() > b.name.toLowerCase()) {\n                    return 1;\n                }\n\n                if (a.name.toLowerCase() < b.name.toLowerCase()) {\n                    return -1;\n                }\n\n                return 0;\n            });\n\n            for (let author of sortedAuthors) {\n                authors.push({\n                    value: author.id,\n                    label: author.name\n                });\n            }\n\n            return authors;\n        },\n        authorName () {\n            return this.$store.state.currentSite.authors.filter(author => author.id === this.$parent.postData.author)[0].name;\n        },\n        availableTags () {\n            return this.$store.state.currentSite.tags.map(tag => tag.name);\n        },\n        metaRobotsOptions () {\n            return {\n                'index, follow': this.$t('ui.indexFollow'),\n                'index, nofollow': this.$t('ui.indexNofollow'),\n                'index, follow, noarchive': this.$t('ui.indexFollowNoArchive'),\n                'index, nofollow, noarchive': this.$t('ui.indexNofollowNoArchive'),\n                'noindex, follow': this.$t('ui.noindexFollow'),\n                'noindex, nofollow': this.$t('ui.noindexNofollow')\n            };\n        },\n        defaultPostTemplate () {\n            let defaultTemplate = this.$store.state.currentSite.themeSettings.defaultTemplates.post;\n\n            if (Object.keys(this.postTemplates).indexOf(defaultTemplate) > -1) {\n                return defaultTemplate;\n            }\n\n            return '';\n        },\n        defaultPageTemplate () {\n            let defaultTemplate = this.$store.state.currentSite.themeSettings.defaultTemplates.page;\n\n            if (Object.keys(this.pageTemplates).indexOf(defaultTemplate) > -1) {\n                return defaultTemplate;\n            }\n\n            return '';\n        },\n        postTemplates () {\n            return this.$store.state.currentSite.themeSettings.postTemplates || [];\n        },\n        pageTemplates () {\n            return this.$store.state.currentSite.themeSettings.pageTemplates || [];\n        },\n        hasTemplates () {\n            if (this.itemType === 'page') {\n                if (!this.pageTemplates) {\n                    return false;\n                }\n\n                return !!Object.keys(this.pageTemplates).length;\n            }\n\n            return !!Object.keys(this.postTemplates).length;\n        },\n        viewThemeSettings () {\n            if (this.itemType === 'page') {\n                return this.$store.state.currentSite.themeSettings.pageConfig;\n            }\n\n            return this.$store.state.currentSite.themeSettings.postConfig;\n        },\n        tagsForDropdown () {\n            return [{\n                value: '',\n                label: this.$t('ui.notSet')\n            }].concat(this.$parent.postData.tags.map(tag => ({\n                value: this.getTagIdByName(tag),\n                label: tag\n            })));\n        },\n        flatPagesList () {\n            let pages = this.$store.getters.sitePages('');\n            let flatPages = this.flatStructure.map(pageItem => {\n                let pageData = pages.find(page => page.id === pageItem.id);\n                return {\n                    value: pageItem.id,\n                    label: pageData ? '—'.repeat(pageItem.depth) + ' ' + pageData.title : '??'\n                };\n            });\n\n            let noParentLabel = this.$t('page.noParentPage');\n            return [{\n                value: 0, \n                label: noParentLabel, \n            }].concat(flatPages);\n        },\n        filteredStatus () {\n            let originalStatus = this.$parent.postData.status;\n            originalStatus = originalStatus.split(',');\n            originalStatus = originalStatus.map(status => status.trim());\n            originalStatus = originalStatus.filter(status => status !== 'is-page');\n            \n            return originalStatus.join(', ');\n        }\n    },\n    mounted () {\n        this.$bus.$on('author-changed', (newAuthor) => {\n            this.$parent.postData.author = parseInt(newAuthor, 10);\n        });\n\n        if (!this.isEdit) {\n            this.$parent.postData.template = this.defaultPostTemplate;\n        } else if (!!this.$parent.postData.mainTag && this.itemType === 'post') {\n            let foundedTag = this.$store.state.currentSite.tags.filter(tag => tag.id === this.$parent.postData.mainTag);\n\n            if (!foundedTag.length) {\n                this.$parent.postData.mainTag = '';\n            }\n        }\n\n        if (this.itemType === 'page') {\n            mainProcessAPI.send('app-pages-hierarchy-load', this.$store.state.currentSite.config.name);\n\n            mainProcessAPI.receiveOnce('app-pages-hierarchy-loaded', (data) => {\n                if (!data) {\n                    this.pagesHierarchy = this.$store.getters.sitePages('')\n                                                                .filter(item => item.title !== null)\n                                                                .map(item => ({id: item.id, subpages: []}));\n                    this.hierarchySave();\n                    this.flatStructure = this.flattenPages(0, this.pagesHierarchy, 0);\n                    this.pagesStructureLoaded = true;\n                    return;\n                }\n\n                this.pagesHierarchy = JSON.parse(JSON.stringify(data));\n                this.flatStructure = this.flattenPages(0, this.pagesHierarchy, 0);\n                this.pagesStructureLoaded = true;\n            });\n        }\n\n        this.$bus.$on('page-data-updated', this.prepareStructureBeforeSave);\n    },\n    methods: {\n        openItem (itemName) {\n            if (this.openedItem === itemName) {\n                this.closeItem();\n                return;\n            }\n\n            this.closeItem();\n            this.openedItem = itemName;\n            let contentWrapper = this.$refs[this.openedItem + '-content-wrapper'];\n            let content = this.$refs[this.openedItem + '-content'];\n            contentWrapper.style.maxHeight = content.clientHeight + \"px\";\n\n            setTimeout(function() {\n                contentWrapper.style.maxHeight = 'none';\n\n                if (content.classList.contains('post-editor-settings-content-tags')) {\n                    contentWrapper.style.overflow = 'visible';\n                }\n            }, 300);\n        },\n        closeItem () {\n            if (this.openedItem === '') {\n                return;\n            }\n\n            let contentWrapper = this.$refs[this.openedItem + '-content-wrapper'];\n            let content = this.$refs[this.openedItem + '-content'];\n            this.openedItem = '';\n\n            if (content.classList.contains('post-editor-settings-content-tags')) {\n                contentWrapper.style.overflow = 'hidden';\n            }\n\n            contentWrapper.style.maxHeight = content.clientHeight + \"px\";\n\n            setTimeout(function () {\n                contentWrapper.style.maxHeight = 0;\n            }, 50);\n        },\n        addTag (newTag) {\n            this.tagIsRestricted = false;\n\n            let restrictedSlugs = [\n                'assets',\n                'media',\n                this.$store.state.currentSite.config.advanced.urls.authorsPrefix,\n                this.$store.state.currentSite.config.advanced.urls.pageName\n            ];\n\n            if (this.$store.state.currentSite.config.advanced.urls.tagsPrefix !== '') {\n                restrictedSlugs = [];\n            }\n\n            if (restrictedSlugs.indexOf(newTag.toLowerCase()) >= 0) {\n                this.tagIsRestricted = true;\n                return;\n            }\n\n            this.$parent.postData.tags.push(newTag);\n        },\n        removeTag (removedTagName) {\n            let removedTagID = this.getTagIdByName(removedTagName);\n            let mainTagName = this.getTagNameById(this.$parent.postData.mainTag);\n\n            if (typeof removedTagID === 'string') {\n                if (mainTagName === removedTagName) {\n                    this.$parent.postData.mainTag = '';\n                }\n            } else {\n                if (parseInt(removedTagID, 10) === parseInt(this.$parent.postData.mainTag, 10)) {\n                    this.$parent.postData.mainTag = '';\n                }\n            }\n        },\n        getTagNameById (tagID) {\n            let foundedTag = this.$store.state.currentSite.tags.filter(tag => tag.id === tagID);\n\n            if (foundedTag.length) {\n                return foundedTag[0].name;\n            }\n\n            return '';\n        },\n        getTagIdByName (tagName) {\n            let foundedTag = this.$store.state.currentSite.tags.filter(tag => tag.name === tagName);\n\n            if (foundedTag.length) {\n                return foundedTag[0].id;\n            }\n\n            return tagName;\n        },\n        generateItems (arrayToConvert) {\n            let options = {};\n\n            for (let i = 0; i < arrayToConvert.length; i++) {\n                options[arrayToConvert[i].value] = arrayToConvert[i].label;\n            }\n\n            return options;\n        },\n        changeAuthor () {\n            this.$bus.$emit('author-popup-display', this.$parent.postData.author);\n        },\n        changeDate () {\n            this.$bus.$emit('date-popup-display', this.$parent.postData.creationDate.timestamp);\n        },\n        displayField (field) {\n            let templateField = this.itemType === 'page' ? 'pageTemplates' : 'postTemplates';\n\n            if (!field[templateField]) {\n                return true;\n            }\n\n            if (field[templateField].indexOf('!') === 0) {\n                return !(field[templateField].replace('!', '').split(',').indexOf(this.$parent.postData.template) > -1);\n            }\n\n            return field[templateField].split(',').indexOf(this.$parent.postData.template) > -1;\n        },\n        fieldPlaceholder (field) {\n            if (field.placeholder || field.placeholder === '') {\n                return field.placeholder;\n            }\n\n\t\t\treturn this.$t('theme.leaveBlankToUseDefault');\n        },\n        resetCreationDate () {\n            this.$parent.postData.creationDate.timestamp = 0;\n            this.$parent.postData.creationDate.text = '';\n        },\n        updateSlug () {\n            this.$bus.$emit('update-post-slug', true);\n        },\n        hierarchySave () {\n            mainProcessAPI.send('app-pages-hierarchy-save', {\n                siteName: this.$store.state.currentSite.config.name,\n                hierarchy: this.pagesHierarchy\n            });\n        },\n        flattenPages(parentID = 0, pages, depth = 0) {\n            let result = [];\n            \n            pages.forEach(page => {\n                if (this.$parent.postID && parseInt(page.id, 10) === parseInt(this.$parent.postID, 10)) {\n                    this.parentPage = parseInt(parentID, 10);\n\n                    if (this.initialParentPage === null) {\n                        this.initialParentPage = this.parentPage;\n                    }\n\n                    return result;\n                }\n\n                result.push({ id: parseInt(page.id, 10), depth });\n\n                if (page.subpages && page.subpages.length > 0) {\n                    result = result.concat(this.flattenPages(page.id, page.subpages, depth + 1));\n                }\n            });\n\n            return result;\n        },\n        findAndRemoveItem(pages, selectedItem) {\n            for (let i = 0; i < pages.length; i++) {\n                if (pages[i].id === selectedItem) {\n                    return pages.splice(i, 1)[0];\n                } else if (pages[i].subpages.length > 0) {\n                    let result = this.findAndRemoveItem(pages[i].subpages, selectedItem);\n                    \n                    if (result) {\n                        return result;\n                    }\n                }\n            }\n\n            return null;\n        },\n        findItemAndParent(pages, id) {\n            for (let i = 0; i < pages.length; i++) {\n                if (pages[i].id === id) {\n                    return { \n                        item: pages[i], \n                        parent: pages \n                    };\n                } else if (pages[i].subpages.length > 0) {\n                    let result = this.findItemAndParent(pages[i].subpages, id);\n                    \n                    if (result) {\n                        return result;\n                    }\n                }\n            }\n\n            return null;\n        },\n        prepareStructureBeforeSave (pageID) {\n            pageID = parseInt(pageID, 10);\n\n            if (this.initialParentPage === this.parentPage) {\n                return;\n            }\n\n            if (this.initialParentPage === null && this.parentPage === 0) {\n                this.pagesHierarchy.push({ id: pageID, subpages: [] });\n                this.hierarchySave();\n                this.initialParentPage = this.parentPage;\n                return;\n            }\n\n            if (this.initialParentPage === null && this.parentPage > 0) {\n                let target = this.findItemAndParent(this.pagesHierarchy, this.parentPage);\n                let { item: targetItem, parent: targetParent } = target;\n                targetItem.subpages.push({ id: pageID, subpages: [] });\n                this.hierarchySave();\n                this.initialParentPage = this.parentPage;\n                return;\n            }\n\n            if (this.initialParentPage !== null && this.parentPage === 0) {\n                let selectedItem = this.findAndRemoveItem(this.pagesHierarchy, pageID);\n                this.pagesHierarchy.push(selectedItem);\n                this.hierarchySave();\n                this.initialParentPage = this.parentPage;\n                return 0;\n            }\n\n            let selectedItem = this.findAndRemoveItem(this.pagesHierarchy, pageID);\n            let target = this.findItemAndParent(this.pagesHierarchy, this.parentPage);\n            let { item: targetItem, parent: targetParent } = target;\n            targetItem.subpages.push(selectedItem);\n            this.hierarchySave();\n            this.initialParentPage = this.parentPage;\n        },\n        changeParentPage (newParentPage) {\n            Vue.set(this, 'parentPage', parseInt(newParentPage, 10));\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('post-editor-featured-image-loaded');\n        this.$bus.$off('page-data-updated');\n        this.$bus.$off('author-changed');\n    }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../../scss/variables.scss';\n@import '../../scss/options-sidebar.scss';\n@import '../../scss/mixins.scss';\n\n.post-editor {\n    &-sidebar {\n        box-shadow: var(--box-shadow-medium);\n        height: calc(100vh - var(--topbar-height));\n        opacity: 0;\n        pointer-events: none;\n        top: var(--topbar-height);\n        z-index: 99999;\n\n        &.is-visible {\n            opacity: 1;\n            pointer-events: auto;\n        }\n\n        &:before {\n            background: linear-gradient(to bottom, var(--option-sidebar-bg) 0%,var(--option-sidebar-bg) 75%,transparent 100%);\n            content: \"\";\n            height: 8rem;\n            position: fixed;\n            top: var(--topbar-height);\n            right: 0;\n            width: $options-sidebar-width;\n            z-index: 1;\n        }\n\n        .options-sidebar {\n            padding-top: 8rem;\n        }\n\n        &-header {\n            color: var(--gray-3);\n            font-size: 1.2rem;\n            font-weight: 600;\n            margin-top: 0;\n            padding: 0 3.6rem 1.5rem;\n            text-transform: uppercase;\n        }\n\n        .post-info {\n            display: grid;\n            grid-template-columns: repeat(2, 49%);\n            grid-gap: 0 2%;\n            margin-bottom: 1rem;\n\n            &--nogrid {\n                 display: block;\n            }\n\n            dl {\n                margin: 0 0 3rem 0;\n\n                &.page-parent-page-wrapper {\n                    grid-column: span 2;\n                    margin-bottom: 0;\n                }\n            }\n\n            dt {\n                color: var(--label-color);\n                font-size: 1.4rem;\n                margin: 0 0 .5rem 0;\n            }\n\n            dd {\n                color: var(--gray-4);\n                font-size: 1.3rem;\n                margin: 0;\n\n                a {\n                    display: block;\n                    position: relative;\n                    white-space: nowrap;\n                }\n            }\n        }\n\n        .post-editor-settings {\n            .post-author-selector {\n                border-bottom: 1px solid var(--gray-1);\n                margin-bottom: 2rem;\n                padding-bottom: 0;\n            }\n\n            .post-date {\n                margin-bottom: 2rem;\n\n                dd {\n                    font-size: $app-font-base;\n                }\n\n                small {\n                    color: var(--gray-4);\n                    padding: 0 .5rem;\n                    position: relative;\n                    top: -1px;\n                }\n\n                &-reset {\n                    border-radius: 50%;\n                    color: var(--icon-secondary-color);\n                    font-size: 2.4rem;\n                    font-weight: 300;\n                    height: 3rem;\n                    line-height: 1;\n                    position: absolute;\n                    right: 0;\n                    text-align: center;\n                    transition: var(--transition);\n                    top: 50%;\n                    transform: translate(0, -50%);\n                    width: 3rem;\n\n                    .icon {\n                        cursor: pointer;\n                        fill: currentColor;\n                    }\n\n                    &:active,\n                    &:focus,\n                    &:hover {\n                        color: var(--headings-color);\n                    }\n\n                    &:hover {\n                        background: var(--input-border-color);\n                    }\n                }\n            }\n\n            .post-action {\n               label {\n                  font-size: 1.4rem;\n                  font-weight: var(--font-weight-normal);\n                  line-height: 1.8;\n               }\n            }\n        }\n\n        .post-tags {\n            line-height: 2;\n            margin-bottom: 0;\n\n            &-error {\n                color: var(--warning);\n                display: block;\n                font-size: 1.4rem;\n                padding: .5rem 0;\n\n                &.is-hidden {\n                    display: none;\n                }\n            }\n\n            .multiselect,\n            .multiselect__tags {\n                min-height: 52px;\n            }\n\n            .multiselect__tags {\n                padding: 0 4rem 0 0.5rem;\n            }\n\n            .multiselect__input {\n                max-width: 120px;\n            }\n        }\n\n        .post-editor-settings {\n            max-height: 0;\n            overflow: hidden;\n            transition: max-height .25s ease-out;\n\n            &-content {\n                padding: 0 0 1rem;\n\n                .image-uploader {\n                    margin-top: 0;\n                }\n            }\n\n        \n            #post-featured-wrapper {\n                margin-top: 0;\n            }\n        }\n\n        .switcher-item-icon-helper {\n            margin: 0 .5rem 0 0;\n            position: relative;\n            top: .2rem;\n        }\n    }\n}\n\n/*\n * Special styles for linux\n */\n\nbody[data-os=\"linux\"] {\n    .post-editor-sidebar {\n        height: 100vh;\n        top: 0;\n\n        &:before {\n            top: 0;\n        }\n    }\n}\n\n/*\n * Select2 adjustments\n */\n.tags-dropdown .select2-results__option[aria-selected=\"true\"] {\n    display: none;\n}\n\nbody > .select2-container {\n    font-size: 1.4rem;\n\n    .select2-results__option--highlighted[aria-selected] {\n        background: var(--color-primary);\n    }\n\n    .select2-dropdown {\n        background-color: var(--white);\n        border: 1px solid var(--input-border-color);\n        border-radius: 3px;\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/SourceCodeEditor.vue",
    "content": "<template>\n    <div :class=\"{ 'source-code-editor': true, 'is-hidden': !isVisible }\">\n        <codemirror-editor\n            id=\"source-code-editor\"\n            ref=\"codemirror\"\n            mode=\"xml\">\n        </codemirror-editor>\n    </div>\n</template>\n\n<script>\nexport default {\n    name: 'source-code-editor',\n    data () {\n        return {\n            isVisible: false,\n            tinymceHandler: null\n        };\n    },\n    mounted () {\n        this.$bus.$on('source-code-editor-show', (content, editor) => {\n            this.show(content, editor);\n        });\n    },\n    methods: {\n        show (content, editor) {\n            this.isVisible = true;\n            this.tinymceHandler = editor;\n\n            setTimeout(() => {\n                this.$refs.codemirror.editor.setValue(content);\n                this.$refs.codemirror.editor.refresh();\n                this.$refs.codemirror.editor.focus();\n\n                if (this.$store.state.app.config.experimentalFeatureAppAutoBeautifySourceCode) {\n                    this.$bus.$emit('source-code-editor-beautify-code');\n                }\n            }, 500);\n        },\n        applyChanges () {\n            this.tinymceHandler.focus();\n            this.tinymceHandler.undoManager.transact(() => {\n                let content = this.$refs.codemirror.editor.getValue();\n                this.tinymceHandler.setContent(content);\n            });\n            this.tinymceHandler.selection.setCursorLocation();\n            this.tinymceHandler.nodeChanged();\n            this.$refs.codemirror.editor.setValue('');\n            this.$bus.$emit('source-code-editor-close');\n            this.isVisible = false;\n\n            let advancedDialog = $('.CodeMirror-advanced-dialog');\n\n            if(advancedDialog.length) {\n                advancedDialog.find('.buttons button').last().trigger('click');\n            }\n        },\n        cancelChanges () {\n            this.isVisible = false;\n            this.$refs.codemirror.editor.setValue('');\n            this.$bus.$emit('source-code-editor-close');\n\n            let advancedDialog = $('.CodeMirror-advanced-dialog');\n\n            if(advancedDialog.length) {\n                advancedDialog.find('.buttons button').last().trigger('click');\n            }\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('source-code-editor-show');\n    }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.source-code-editor {\n    background: var(--bg-primary);\n    height: calc(100vh - 10rem)!important;\n    left: 0;   \n    overflow: hidden;\n    position: absolute;\n    top: 10rem;\n    width: 100vw;\n    z-index: 99999;\n    \n    &::before {\n        background: var(--bg-primary);\n        content: \"\";        \n        height: 10rem;\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100%;         \n    }\n\n    &.is-hidden {\n        display: none;\n    }\n\n    .CodeMirror-wrap {\n        display: block;\n        min-height: calc(100vh - 10rem); \n        padding: 0;\n        width: 100%;\n    }\n\n    .CodeMirror-vscrollbar {\n        overflow-y: hidden!important;\n    }\n}\n\n/*\n * Special styles for win & linux\n */\n\nbody[data-os=\"linux\"] {\n    .source-code-editor {\n        height: calc(100vh - 6rem)!important;\n        top: 6rem;\n        \n        &::before {\n            height: 6rem;            \n        }\n\n        .CodeMirror-wrap {\n            min-height: calc(100vh - 6rem);\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/TopBar.vue",
    "content": "<template>\n    <div class=\"post-editor-topbar\">\n        <label v-if=\"sourceCodeEditorVisible\">\n            {{ $t('editor.sourceCode') }}\n        </label>\n        \n        <p-button\n            v-if=\"!sourceCodeEditorVisible\"\n            id=\"post-back-to-posts-button\"\n            type=\"clean-invert icon\"\n            icon=\"arrow-left\"\n            @click.native=\"cancelItem\">\n            <template v-if=\"itemType === 'post'\">\n                {{ $t('ui.backToPosts') }}\n            </template>\n            <template v-else-if=\"itemType === 'page'\">\n                {{ $t('ui.backToPages') }}\n            </template>\n        </p-button>\n\n        <p-button\n            v-if=\"!sourceCodeEditorVisible\"\n            id=\"post-preview-button\"\n            type=\"clean-invert\"\n            :disabled=\"!themeConfigured\"\n            :title=\"themeConfigured ? $t('post.configureThemeBeforeGeneratingPreview') : ''\"\n            @click.native=\"generatePostPreview\">\n            {{ $t('ui.preview') }}\n        </p-button>\n\n        <div\n            v-if=\"!sourceCodeEditorVisible\"\n            class=\"post-editor-actions\">\n\n            <btn-dropdown\n                ref=\"dropdown-button\"\n                :items=\"dropdownItems\"\n                :defaultValue=\"retrieveCurrentAction()\" />\n\n            <p-button\n                icon=\"settings\"\n                :type=\"$parent.sidebarVisible ? 'clean clean-invert icon only-icon-color' : 'clean clean-invert icon only-icon'\"\n                @click.native=\"$parent.toggleSidebar\" />\n        </div>\n\n        <div\n            v-if=\"sourceCodeEditorVisible\"\n            class=\"post-editor-source-code-actions\">\n            <p-button\n                type=\"clean\"\n                @click.native=\"sourceCodeCancel\">\n                {{ $t('ui.goBack') }}\n            </p-button>\n\n            <p-button\n                v-if=\"sourceCodeEditorVisible\"\n                type=\"secondary icon\"\n                icon=\"source-code\"\n                @click.native=\"$bus.$emit('source-code-editor-beautify-code')\">\n                {{ $t('ui.beautifyCode') }}\n            </p-button>\n\n            <p-button\n                type=\"primary\"\n                @click.native=\"sourceCodeApply\">\n                {{ $t('ui.applyChanges') }}\n            </p-button>\n\n        </div>\n    </div>\n</template>\n\n<script>\nimport ItemHelper from './ItemHelper';\n\nexport default {\n    name: 'post-editor-top-bar',\n    props: [\n        'itemType'\n    ],\n    computed: {\n        dropdownItems () {\n            return [\n                {\n                    label: this.isDraft ? this.$t('ui.saveDraftAndClose') : this.$t('ui.saveAndClose'),\n                    value: 'save-and-close',\n                    isVisible: () => true,\n                    onClick: this.dropdownSaveAndClose\n                },\n                {\n                    label: this.isDraft ? this.$t('ui.saveDraft') : this.$t('ui.save'),\n                    value: 'save',\n                    isVisible: () => true,\n                    onClick: this.dropdownSave\n                },\n                {\n                    label: this.$t('ui.saveAsDraft'),\n                    value: 'save-as-draft',\n                    isVisible: () => !this.isDraft,\n                    onClick: this.dropdownSaveAsDraft\n                },\n                {\n                    label: this.$store.state.app.config.closeEditorOnSave ? this.$t('ui.publishAndClose') : this.$t('ui.publishPost'),\n                    value: 'publish-post',\n                    isVisible: () => this.isDraft,\n                    onClick: this.dropdownPublish\n                }\n            ]\n        },\n        isDraft () {\n            return this.$parent.postData.status.indexOf('draft') > -1;\n        },\n        themeConfigured () {\n            return !!this.$store.state.currentSite.config.theme;\n        }\n    },\n    data () {\n        return {\n            sourceCodeEditorVisible: false\n        };\n    },\n    mounted () {\n        this.$bus.$on('source-code-editor-show', () => {\n            this.sourceCodeEditorVisible = true;\n        });\n\n        this.$bus.$on('source-code-editor-close', () => {\n            this.sourceCodeEditorVisible = false;\n        });\n\n        this.$bus.$on('document-body-clicked', this.closeDropdownButton);\n        this.$bus.$on('update-inline-editor', this.closeDropdownButton);\n    },\n    methods: {\n        dropdownSave () {\n            let status = this.$parent.postData.status.indexOf('draft') > -1 ? 'draft' : 'published';\n            this.$parent.savePost(status, false, false);\n            localStorage.setItem('publii-post-editor-current-action', 'save');\n        },\n        dropdownSaveAndClose () {\n            let status = this.$parent.postData.status.indexOf('draft') > -1 ? 'draft' : 'published';\n            this.$parent.savePost(status, false, true);\n            localStorage.setItem('publii-post-editor-current-action', 'save-and-close');\n        },\n        dropdownSaveAsDraft () {\n            this.$parent.savePost('draft');\n            this.retrieveCurrentAction();\n        },\n        dropdownPublish () {\n            this.$parent.savePost('published', false, this.$store.state.app.config.closeEditorOnSave);\n            this.retrieveCurrentAction();\n        },\n        retrieveCurrentAction () {\n            let currentAction = localStorage.getItem('publii-post-editor-current-action');\n\n            if (!currentAction) {\n                if (this.$store.state.app.config.closeEditorOnSave) {\n                    currentAction = 'save-and-close';\n                } else {\n                    currentAction = 'save';\n                }\n            }\n\n            if (this.$refs['dropdown-button']) {\n                this.$refs['dropdown-button'].setValue(currentAction);\n            }\n\n            return currentAction;\n        },\n        closeDropdownButton () {\n            if (this.$refs['dropdown-button']) {\n                this.$refs['dropdown-button'].hideDropdown();\n            }\n        },\n        cancelItem () {\n            if (!this.$parent.possibleDataLoss) {\n                this.$parent.closeEditor();\n                return;\n            }\n\n            this.$bus.$emit('confirm-display', {\n                message: this.$t('ui.cancelWarningMsg'),\n                isDanger: true,\n                okClick: this.cleanUpItem\n            });\n        },\n        cleanUpItem () {\n            // Get the text data\n            let preparedText = this.$parent.postData.text;\n            // Remove directory path from images src attribute\n            let mediaPath = ItemHelper.getMediaPath(this.$store, this.$parent.postID, this.itemType);\n            preparedText = preparedText.replace(/file:(\\/){1,}/gmi, 'file:///');\n            preparedText = preparedText.split(mediaPath).join('#DOMAIN_NAME#');\n            preparedText = preparedText.replace(/file:(\\/){1,}\\#DOMAIN_NAME\\#/gmi, '#DOMAIN_NAME#');\n            // Send an event which will remove unused images from the post editor\n            let eventToSend = 'app-post-cancel';\n\n            if (this.itemType === 'page') {\n                eventToSend = 'app-page-cancel';\n            }\n\n            mainProcessAPI.send(eventToSend, {\n                'site': this.$store.state.currentSite.config.name,\n                'id': this.$parent.postID,\n                'text': preparedText,\n                'featuredImageFilename': this.$parent.postData.featuredImage.path,\n                'featuredImageData': {\n                    alt: this.$parent.postData.featuredImage.alt,\n                    caption: this.$parent.postData.featuredImage.caption,\n                    credits: this.$parent.postData.featuredImage.credits\n                }\n            });\n\n            this.$parent.closeEditor();\n        },\n        sourceCodeApply () {\n            this.$parent.$refs['source-code-editor'].applyChanges();\n        },\n        sourceCodeCancel () {\n            this.$parent.$refs['source-code-editor'].cancelChanges();\n        },\n        generatePostPreview () {\n            this.$parent.savePost('published', true);\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('source-code-editor-show');\n        this.$bus.$off('source-code-editor-close');\n        this.$bus.$off('document-body-clicked', this.closeDropdownButton);\n        this.$bus.$off('update-inline-editor', this.closeDropdownButton);\n    }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.post-editor {\n    &-topbar {\n        align-items: center;\n        background: transparent;\n        font-size: 1.4rem;\n        display: flex;\n        height: 5.6rem;\n        justify-content: space-between;\n        padding: 0 3.2rem;\n        position: absolute;\n        top: 3.6rem;\n        width: 100%;\n        z-index: 100001;\n    }\n\n    &-actions {\n        display: flex;\n        margin-left: auto;\n\n        .button {\n            text-align: center;\n\n            &:nth-child(2) {\n                margin-left: 1rem;\n                margin-right: -1.7rem; // button padding\n            }\n        }\n    }\n\n    &-source-code-actions {\n        margin-left: auto;\n    }\n\n    #post-preview-button {\n        background: var(--bg-primary);\n    }\n\n    #post-back-to-posts-button {\n        background: var(--bg-primary);\n        margin-left: -2.1rem;\n        margin-right: .625rem;\n        padding-left: 3.4rem;\n        padding-right: .625rem;\n        position: relative;\n\n        &::after {\n            border-right: 1px solid var(--gray-2);\n            content: \"\";\n            height: 1.4rem;\n            right: -.9375rem;\n            @include centerXY(false, true);\n        }\n    }\n}\n\n/*\n * Windows & linux adjustments\n */\n\nbody[data-os=\"linux\"] {\n    .post-editor {\n        &-topbar {\n            top: 0;\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/components/post-editor/WritersPanel.vue",
    "content": "<template>\n    <div :class=\"{ 'post-editor-writers-panel': true, 'is-hidden': !isVisible }\">\n        <dl>\n            <dt id=\"counter-words\">{{ words }}</dt>\n            <dd>{{ $t('post.words') }}</dd>\n            <dt id=\"counter-unique-words\">{{ uniqueWords }}</dt>\n            <dd>{{ $t('post.uniqueWords') }}</dd>\n            <dt id=\"counter-characters\">{{ characters }}</dt>\n            <dd>{{ $t('post.characters') }}</dd>\n            <dt id=\"counter-sentences\">{{ sentences }}</dt>\n            <dd>{{ $t('post.sentences') }}</dd>\n            <dt id=\"counter-paragraphs\">{{ paragraphs }}</dt>\n            <dd>{{ $t('post.paragraphs') }}</dd>\n            <dt id=\"counter-reading-time\"><span v-pure-html=\"readingTime\"></span><small>{{ $t('post.min') }}</small></dt>\n            <dd>{{ $t('post.readingTime') }}</dd>\n        </dl>\n    </div>\n</template>\n\n<script>\nimport strip_tags from './../../helpers/vendor/locutus/strings/strip_tags';\n\nexport default {\n    name: 'post-editor-writers-panel',\n    props: {\n        'isVisible': {\n            default: false,\n            type: Boolean\n        }\n    },\n    data () {\n        return {\n            words: 0,\n            uniqueWords: 0,\n            characters: 0,\n            sentences: 0,\n            paragraphs: 0,\n            readingTime: 0\n        };\n    },\n    mounted () {\n        this.$bus.$on('writers-panel-refresh', () => this.refresh());\n        this.refresh();\n    },\n    methods: {\n        refresh() {\n            let iframe = document.getElementById('post-editor_ifr');\n\n            if(!iframe) {\n                return false;\n            }\n\n            let content = iframe.contentWindow.window.document.body.innerHTML;\n            let paragraphs = content.match(/<(p|blockquote|ul|ol|h1|h2|h3|h4|h5|h6|pre).*?>/g);\n            let sentencesSource = content.split('</p>').map(sentence => strip_tags(sentence.replace(/&nbsp;/g, ' ').replace(/\\s+/g, ' '))).join(\"\\n\");\n            let words = sentencesSource.toLowerCase().split(/[\\n\\s]/).filter(fragment => fragment.trim() !== '');\n            let sentences = sentencesSource.split(/[\\.\\?!\\n]/).filter(fragment => fragment.trim() !== '');\n            let uniqueWords = [...new Set(words)];\n            let readingTime = Math.floor(words.length / 180);\n            let readingTimePrefix = '';\n\n            if(words.length < 180) {\n                readingTimePrefix = '&lt; ';\n                readingTime = 1;\n            }\n\n            this.uniqueWords = uniqueWords.length;\n            this.words = words.length;\n            this.characters = sentences.join(' ').length;\n            this.sentences = sentences.length;\n            this.paragraphs = paragraphs ? paragraphs.length : 0;\n            this.readingTime = readingTimePrefix + readingTime;\n        }\n    },\n    beforeDestroy () {\n        this.$bus.$off('writers-panel-open');\n    }\n};\n</script>\n\n<style lang=\"scss\">\n@import '../../scss/variables.scss';\n@import '../../scss/mixins.scss';\n\n.post-editor {\n    &-writers-panel {\n       background: var(--option-sidebar-bg);\n        border-right: 1px solid var(--input-border-color);\n        bottom: 0;\n        color: var(--label-color);\n        height: calc(100vh - var(--topbar-height));\n        left: 0;\n        opacity: 1;\n        position: absolute;\n        text-align: center;\n        top: var(--topbar-height);\n        transition: var(--transition);\n        width: $writers-panel-width;\n        z-index: 100;\n\n        &.is-hidden {\n            opacity: 0;\n            pointer-events: none;\n        }\n\n        dl {\n            margin: 10rem 15% 0 15%;\n            width: 70%;\n\n            dt {\n                color: var(--text-primary-color);\n                font-size: 2.6rem;\n                font-family: Georgia, serif;\n            }\n\n            dd {\n                border-bottom: 1px solid var(--input-border-color);\n                font-size: 1.4rem;\n                margin: 0 0 1rem 0;\n                padding: 0 0 2rem 0;\n\n                &:last-child {\n                    border-bottom: none;\n                }\n            }\n        }\n    }\n}\n\n/*\n * Windows adjustments\n */\nbody[data-os=\"linux\"] {\n    .post-editor {\n        &-writers-panel {\n            height: 100vh;\n            top: 0;\n        }\n    }\n}\n\n/*\n * Responsive improvements\n */\n@media (max-height: 900px) {\n    .post-editor-writers-panel {\n        dl {\n            dd {\n                line-height: 1.1;\n            }\n        }\n    }\n}\n</style>\n"
  },
  {
    "path": "app/src/config/langs.js",
    "content": "module.exports = [\n    {\n        name: 'English (International)',\n        code: 'en'\n    },\n    {\n        name: 'Custom language code',\n        code: 'custom'\n    },\n    {\n        name: 'Bambara',\n        code: 'bm'\n    },\n    {\n        name: 'Bashkir',\n        code: 'ba'\n    },\n    {\n        name: 'Basque',\n        code: 'eu'\n    },\n    {\n        name: 'Belarusian',\n        code: 'be'\n    },\n    {\n        name: 'Bengali (Bangla)',\n        code: 'bn'\n    },\n    {\n        name: 'Bihari',\n        code: 'bh'\n    },\n    {\n        name: 'Bislama',\n        code: 'bi'\n    },\n    {\n        name: 'Bosnian',\n        code: 'bs'\n    },\n    {\n        name: 'Breton',\n        code: 'br'\n    },\n    {\n        name: 'Bulgarian',\n        code: 'bg'\n    },\n    {\n        name: 'Chinese (China)',\n        code: 'zh-cn'\n    },\n    {\n        name: 'Chinese (Hong Kong)',\n        code: 'zh-hk'\n    },\n    {\n        name: 'Chinese (Taiwan)',\n        code: 'zh-tw'\n    },\n    {\n        name: 'Croatian',\n        code: 'hr'\n    },\n    {\n        name: 'Czech',\n        code: 'cs'\n    },\n    {\n        name: 'Danish',\n        code: 'da'\n    },\n    {\n        name: 'Dutch',\n        code: 'nl'\n    },\n    {\n        name: 'Dutch (Belgium)',\n        code: 'nl-be'\n    },\n    {\n        name: 'English (Australia)',\n        code: 'en-au'\n    },\n    {\n        name: 'English (Canada)',\n        code: 'en-ca'\n    },\n    {\n        name: 'English (India)',\n        code: 'en-in'\n    },\n    {\n        name: 'English (Ireland)',\n        code: 'en-ie'\n    },\n    {\n        name: 'English (Israel)',\n        code: 'en-il'\n    },\n    {\n        name: 'English (Nigeria)',\n        code: 'en-ng'\n    },\n    {\n        name: 'English (United Kingdom)',\n        code: 'en-gb'\n    },\n    {\n        name: 'English (New Zealand)',\n        code: 'en-nz'\n    },\n    {\n        name: 'English (USA)',\n        code: 'en-us'\n    },\n    {\n        name: 'English (South Africa)',\n        code: 'en-za'\n    },\n    {\n        name: 'Estonian',\n        code: 'et'\n    },\n    {\n        name: 'Finnish',\n        code: 'fi'\n    },\n    {\n        name: 'French',\n        code: 'fr'\n    },\n    {\n        name: 'French (Algeria)',\n        code: 'fr-dz'\n    },\n    {\n        name: 'French (Belgium)',\n        code: 'fr-be'\n    },\n    {\n        name: 'French (Cameroon)',\n        code: 'fr-cm'\n    },\n    {\n        name: 'French (Canada)',\n        code: 'fr-ca'\n    },\n    {\n        name: 'French (Central African Republic)',\n        code: 'fr-cf'\n    },\n    {\n        name: 'French (DR Congo)',\n        code: 'fr-cd'\n    },\n    {\n        name: 'French (Ivory Coast)',\n        code: 'fr-ci'\n    },\n    {\n        name: 'French (Luxembourg)',\n        code: 'fr-lu'\n    },\n    {\n        name: 'French (Madagascar)',\n        code: 'fr-mg'\n    },\n    {\n        name: 'French (Morocco)',\n        code: 'fr-ma'\n    },\n    {\n        name: 'French (Senegal)',\n        code: 'fr-sn'\n    },\n    {\n        name: 'French (Tunisia)',\n        code: 'fr-tn'\n    },\n    {\n        name: 'French (Switzerland)',\n        code: 'fr-ch'\n    },\n    {\n        name: 'German',\n        code: 'de'\n    },\n    {\n        name: 'German (Austria)',\n        code: 'de-at'\n    },\n    {\n        name: 'German (Switzerland)',\n        code: 'de-ch'\n    },\n    {\n        name: 'Greek',\n        code: 'el'\n    },\n    {\n        name: 'Hebrew',\n        code: 'he'\n    },\n    {\n        name: 'Hindi',\n        code: 'hi'\n    },\n    {\n        name: 'Hungarian',\n        code: 'hu'\n    },\n    {\n        name: 'Icelandic',\n        code: 'is'\n    },\n    {\n        name: 'Italian',\n        code: 'it'\n    },\n    {\n        name: 'Japanese',\n        code: 'ja'\n    },\n    {\n        name: 'Korean',\n        code: 'ko'\n    },\n    {\n        name: 'Luxembourgish',\n        code: 'lb'\n    },\n    {\n        name: 'Lithuanian',\n        code: 'lt'\n    },\n    {\n        name: 'Latvian',\n        code: 'lv'\n    },\n    {\n        name: 'Montenegrin',\n        code: 'me'\n    },\n    {\n        name: 'Macedonian',\n        code: 'mk'\n    },\n    {\n        name: 'Norwegian (Nynorsk)',\n        code: 'nn'\n    },\n    {\n        name: 'Norwegian (Bokmål)',\n        code: 'nb'\n    },\n    {\n        name: 'Polish',\n        code: 'pl'\n    },\n    {\n        name: 'Portuguese',\n        code: 'pt'\n    },\n    {\n        name: 'Portuguese (Brazil)',\n        code: 'pt-br'\n    },\n    {\n        name: 'Romanian',\n        code: 'ro'\n    },\n    {\n        name: 'Russian',\n        code: 'ru'\n    },\n    {\n        name: 'Serbian',\n        code: 'sr'\n    },\n    {\n        name: 'Slovak',\n        code: 'sk'\n    },\n    {\n        name: 'Slovenian',\n        code: 'sl'\n    },\n    {\n        name: 'Spanish',\n        code: 'es'\n    },\n    {\n        name: 'Swedish',\n        code: 'sv'\n    },\n    {\n        name: 'Swahili',\n        code: 'sw'\n    },\n    {\n        name: 'Thai',\n        code: 'th'\n    },\n    {\n        name: 'Turkish',\n        code: 'tr'\n    },\n    {\n        name: 'Ukrainian',\n        code: 'uk'\n    },\n    {\n        name: 'Urdu',\n        code: 'ur'\n    },\n    {\n        name: 'Vietnamese',\n        code: 'vi'\n    }\n];\n"
  },
  {
    "path": "app/src/helpers/sass-colors.js",
    "content": "export default {\n    'current-color': 'currentColor',\n    \n    'color-0': '#71c0ff',\n    'color-1': '#42a5f5',\n    'color-2': '#40b771',\n    'color-3': '#ef5350',\n    'color-4': '#2a2e30',\n    'color-5': '#4e5360',\n    'color-6': '#707277',\n    'color-7': '#8e929d',\n    'color-8': '#cbcdd4',\n    'color-9': '#edeef1',\n    'color-10': '#fff',\n\n    'color-helper-1': '#FFD54F',\n    'color-helper-2': '#5765A2',\n    'color-helper-3': '#A57C3A',\n    'color-helper-4': '#D48CDA',\n    'color-helper-5': '#FFF8E1',\n    'color-helper-6': '#F2B900',\n};\n"
  },
  {
    "path": "app/src/helpers/utils.js",
    "content": "class Utils {\n    /**\n     * Function used to deeply merge objects\n     *\n     * @param target\n     * @param source\n     * @returns {*}\n     */\n    static deepMerge(target, source) {\n        if (typeof target !== 'object') {\n            target = {};\n        }\n\n        for (let property in source) {\n            if (source.hasOwnProperty(property)) {\n                let sourceProperty = source[property];\n\n                if (typeof sourceProperty === 'object' && !Array.isArray(sourceProperty) && !(sourceProperty instanceof Date)) {\n                    target[property] = Utils.deepMerge(target[property], sourceProperty);\n                    continue;\n                } else if(sourceProperty instanceof Date) {\n                    target[property] = new Date(sourceProperty.getTime());\n                    continue;\n                }\n\n                target[property] = sourceProperty;\n            }\n        }\n\n        for (let a = 2, l = arguments.length; a < l; a++) {\n            Utils.deepMerge(target, arguments[a]);\n        }\n\n        return target;\n    }\n\n    /**\n     * Function used to create error log from given data\n     *\n     * @param errorData\n     *\n     * @returns {string}\n     */\n    static generateErrorLog(errorData, returnText = false) {\n        let output = '';\n\n        if (!Array.isArray(errorData) && errorData.message) {\n            errorData = errorData.message;\n        }\n        \n        if (returnText) {\n            for (let i = 0; i < errorData.length; i++) {\n                if (i > 0) {\n                    output += \"\\n\";\n                }\n\n                output += errorData[i].message + \"\\n\" + errorData[i].desc + \"\\n\\n\";\n            }\n        } else {\n            for (let i = 0; i < errorData.length; i++) {\n                let preparedDesc = errorData[i].desc.replace(/\\</gmi, '&lt;').replace(/\\>/gmi, '&gt;');\n                output += '<strong>' + errorData[i].message + '</strong><pre>' + preparedDesc + '</pre>';\n            }\n        }\n\n        if (output === '') {\n            return false;\n        }\n\n        return output;\n    }\n\n    /*\n     * Function used to create function which will be called\n     * with delay if they are invoked many times\n     */\n    static debouncedFunction(fn, wait) {\n        var timeout;\n\n        return function () {\n            var context = this;\n            var args = arguments;\n\n            var later = function () {\n                timeout = null;\n                fn.apply(context, args);\n            };\n\n            clearTimeout(timeout);\n            timeout = setTimeout(later, wait);\n        };\n    }\n\n    /*\n     * Function used to create function which will be called\n     * at maximum with the specified threshhold\n     */\n    static throttledFunction(fn, threshhold, scope) {\n        threshhold || (threshhold = 250);\n        var last,\n            deferTimer;\n        return function () {\n            var context = scope || this;\n\n            var now = +new Date,\n                args = arguments;\n            if (last && now < last + threshhold) {\n                // hold on to it\n                clearTimeout(deferTimer);\n                deferTimer = setTimeout(function () {\n                    last = now;\n                    fn.apply(context, args);\n                }, threshhold);\n            } else {\n                last = now;\n                fn.apply(context, args);\n            }\n        };\n    }\n\n    /*\n     * Run function if it is not invoked since X ms.\n     */\n    static debounce(func, wait, immediate) {\n        var timeout;\n\n        return function() {\n            var context = this, args = arguments;\n            var later = function() {\n                timeout = null;\n\n                if (!immediate) {\n                    func.apply(context, args);\n                }\n            };\n\n            var callNow = immediate && !timeout;\n            clearTimeout(timeout);\n\n            timeout = setTimeout(later, wait);\n\n            if (callNow) {\n                func.apply(context, args);\n            }\n        };\n    }\n\n    /*\n     * Create a function which can be called only once\n     */\n    static once(fn, context) {\n        var result;\n\n        return function() {\n            if(fn) {\n                result = fn.apply(context || this, arguments);\n                fn = null;\n            }\n\n            return result;\n        };\n    }\n\n    /*!\n     * normalize-path <https://github.com/jonschlinkert/normalize-path>\n     *\n     * Copyright (c) 2014-2015, Jon Schlinkert.\n     * Licensed under the MIT License\n     */\n    static normalizePath(str, stripTrailing) {\n        if (typeof str !== 'string') {\n            throw new TypeError('expected a string');\n        }\n        str = str.replace(/[\\\\\\/]+/g, '/');\n        if (stripTrailing !== false) {\n            str = str.replace(/\\/$/, '');\n        }\n        return str;\n    }\n\n    /**\n     * Check if the provided link is a valid URL\n     */\n    static getValidUrl(urlToCheck) {\n        if (typeof urlToCheck !== 'string') {\n            return false;\n        }\n\n        let url;\n        let allowedProtocols = ['http:', 'https:', 'file:', 'dat:', 'ipfs:', 'dweb:'];\n\n        try {\n            url = new URL(urlToCheck);\n        } catch (e) {\n            return false;\n        }\n\n        if (allowedProtocols.indexOf(url.protocol) > -1) {\n            return url.href.replace(/\\s/gmi, '');\n        }\n\n        return false;\n    }\n}\n\nmodule.exports = Utils;\n"
  },
  {
    "path": "app/src/helpers/vendor/locutus/strings/strip_tags.js",
    "content": "export default (input, allowed) => { // eslint-disable-line camelcase\n                                       //  discuss at: http://locutus.io/php/strip_tags/\n                                       // original by: Kevin van Zonneveld (http://kvz.io)\n                                       // improved by: Luke Godfrey\n                                       // improved by: Kevin van Zonneveld (http://kvz.io)\n                                       //    input by: Pul\n                                       //    input by: Alex\n                                       //    input by: Marc Palau\n                                       //    input by: Brett Zamir (http://brett-zamir.me)\n                                       //    input by: Bobby Drake\n                                       //    input by: Evertjan Garretsen\n                                       // bugfixed by: Kevin van Zonneveld (http://kvz.io)\n                                       // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman)\n                                       // bugfixed by: Kevin van Zonneveld (http://kvz.io)\n                                       // bugfixed by: Kevin van Zonneveld (http://kvz.io)\n                                       // bugfixed by: Eric Nagel\n                                       // bugfixed by: Kevin van Zonneveld (http://kvz.io)\n                                       // bugfixed by: Tomasz Wesolowski\n                                       //  revised by: Rafał Kukawski (http://blog.kukawski.pl)\n                                       //   example 1: strip_tags('<p>Kevin</p> <br /><b>van</b> <i>Zonneveld</i>', '<i><b>')\n                                       //   returns 1: 'Kevin <b>van</b> <i>Zonneveld</i>'\n                                       //   example 2: strip_tags('<p>Kevin <img src=\"someimage.png\" onmouseover=\"someFunction()\">van <i>Zonneveld</i></p>', '<p>')\n                                       //   returns 2: '<p>Kevin van Zonneveld</p>'\n                                       //   example 3: strip_tags(\"<a href='http://kvz.io'>Kevin van Zonneveld</a>\", \"<a>\")\n                                       //   returns 3: \"<a href='http://kvz.io'>Kevin van Zonneveld</a>\"\n                                       //   example 4: strip_tags('1 < 5 5 > 1')\n                                       //   returns 4: '1 < 5 5 > 1'\n                                       //   example 5: strip_tags('1 <br/> 1')\n                                       //   returns 5: '1  1'\n                                       //   example 6: strip_tags('1 <br/> 1', '<br>')\n                                       //   returns 6: '1 <br/> 1'\n                                       //   example 7: strip_tags('1 <br/> 1', '<br><br/>')\n                                       //   returns 7: '1 <br/> 1'\n                                       // making sure the allowed arg is a string containing only tags in lowercase (<a><b><c>)\n    allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('')\n    var tags = /<\\/?([a-z][a-z0-9]*)\\b[^>]*>/gi\n    var commentsAndPhpTags = /<!--[\\s\\S]*?-->|<\\?(?:php)?[\\s\\S]*?\\?>/gi\n    return input.replace(commentsAndPhpTags, '').replace(tags, function ($0, $1) {\n        return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''\n    })\n};\n"
  },
  {
    "path": "app/src/helpers/vendor/locutus/xml/index.js",
    "content": "module.exports['utf8_decode'] = require('./utf8_decode')\nmodule.exports['utf8_encode'] = require('./utf8_encode')\n"
  },
  {
    "path": "app/src/helpers/vendor/locutus/xml/utf8_decode.js",
    "content": "module.exports = function utf8_decode (strData) { // eslint-disable-line camelcase\n  //  discuss at: http://locutus.io/php/utf8_decode/\n  // original by: Webtoolkit.info (http://www.webtoolkit.info/)\n  //    input by: Aman Gupta\n  //    input by: Brett Zamir (http://brett-zamir.me)\n  // improved by: Kevin van Zonneveld (http://kvz.io)\n  // improved by: Norman \"zEh\" Fuchs\n  // bugfixed by: hitwork\n  // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman)\n  // bugfixed by: Kevin van Zonneveld (http://kvz.io)\n  // bugfixed by: kirilloid\n  // bugfixed by: w35l3y (http://www.wesley.eti.br)\n  //   example 1: utf8_decode('Kevin van Zonneveld')\n  //   returns 1: 'Kevin van Zonneveld'\n\n  var tmpArr = []\n  var i = 0\n  var c1 = 0\n  var seqlen = 0\n\n  strData += ''\n\n  while (i < strData.length) {\n    c1 = strData.charCodeAt(i) & 0xFF\n    seqlen = 0\n\n    // http://en.wikipedia.org/wiki/UTF-8#Codepage_layout\n    if (c1 <= 0xBF) {\n      c1 = (c1 & 0x7F)\n      seqlen = 1\n    } else if (c1 <= 0xDF) {\n      c1 = (c1 & 0x1F)\n      seqlen = 2\n    } else if (c1 <= 0xEF) {\n      c1 = (c1 & 0x0F)\n      seqlen = 3\n    } else {\n      c1 = (c1 & 0x07)\n      seqlen = 4\n    }\n\n    for (var ai = 1; ai < seqlen; ++ai) {\n      c1 = ((c1 << 0x06) | (strData.charCodeAt(ai + i) & 0x3F))\n    }\n\n    if (seqlen === 4) {\n      c1 -= 0x10000\n      tmpArr.push(String.fromCharCode(0xD800 | ((c1 >> 10) & 0x3FF)))\n      tmpArr.push(String.fromCharCode(0xDC00 | (c1 & 0x3FF)))\n    } else {\n      tmpArr.push(String.fromCharCode(c1))\n    }\n\n    i += seqlen\n  }\n\n  return tmpArr.join('')\n}\n"
  },
  {
    "path": "app/src/helpers/vendor/locutus/xml/utf8_encode.js",
    "content": "module.exports = function utf8_encode (argString) { // eslint-disable-line camelcase\n  //  discuss at: http://locutus.io/php/utf8_encode/\n  // original by: Webtoolkit.info (http://www.webtoolkit.info/)\n  // improved by: Kevin van Zonneveld (http://kvz.io)\n  // improved by: sowberry\n  // improved by: Jack\n  // improved by: Yves Sucaet\n  // improved by: kirilloid\n  // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman)\n  // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman)\n  // bugfixed by: Ulrich\n  // bugfixed by: Rafał Kukawski (http://blog.kukawski.pl)\n  // bugfixed by: kirilloid\n  //   example 1: utf8_encode('Kevin van Zonneveld')\n  //   returns 1: 'Kevin van Zonneveld'\n\n  if (argString === null || typeof argString === 'undefined') {\n    return ''\n  }\n\n  // .replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n  var string = (argString + '')\n  var utftext = ''\n  var start\n  var end\n  var stringl = 0\n\n  start = end = 0\n  stringl = string.length\n  for (var n = 0; n < stringl; n++) {\n    var c1 = string.charCodeAt(n)\n    var enc = null\n\n    if (c1 < 128) {\n      end++\n    } else if (c1 > 127 && c1 < 2048) {\n      enc = String.fromCharCode(\n        (c1 >> 6) | 192, (c1 & 63) | 128\n      )\n    } else if ((c1 & 0xF800) !== 0xD800) {\n      enc = String.fromCharCode(\n        (c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128\n      )\n    } else {\n      // surrogate pairs\n      if ((c1 & 0xFC00) !== 0xD800) {\n        throw new RangeError('Unmatched trail surrogate at ' + n)\n      }\n      var c2 = string.charCodeAt(++n)\n      if ((c2 & 0xFC00) !== 0xDC00) {\n        throw new RangeError('Unmatched lead surrogate at ' + (n - 1))\n      }\n      c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000\n      enc = String.fromCharCode(\n        (c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128\n      )\n    }\n    if (enc !== null) {\n      if (end > start) {\n        utftext += string.slice(start, end)\n      }\n      utftext += enc\n      start = end = n + 1\n    }\n  }\n\n  if (end > start) {\n    utftext += string.slice(start, stringl)\n  }\n\n  return utftext\n}\n"
  },
  {
    "path": "app/src/helpers/vendor/tinymce/icons/publii/icons.js",
    "content": "tinymce.IconManager.add(\"publii\", {\n   icons: {\n       \"accessibility-check\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"5\" r=\"1\"></circle><path d=\"m9 20 3-6 3 6\"></path><path d=\"m6 8 6 2 6-2\"></path><path d=\"M12 10v4\"></path></svg>',\n\n       \"action-next\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>',\n\n       \"action-prev\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"18 15 12 9 6 15\"></polyline></svg>',\n\n       \"align-center\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" y1=\"6\" x2=\"3\" y2=\"6\"></line><line x1=\"17\" y1=\"12\" x2=\"7\" y2=\"12\"></line><line x1=\"19\" y1=\"18\" x2=\"5\" y2=\"18\"></line></svg>',\n\n       \"align-justify\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" x2=\"21\" y1=\"6\" y2=\"6\"></line><line x1=\"3\" x2=\"21\" y1=\"12\" y2=\"12\"></line><line x1=\"3\" x2=\"21\" y1=\"18\" y2=\"18\"></line></svg>',\n\n       \"align-left\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" y1=\"6\" x2=\"3\" y2=\"6\"></line><line x1=\"15\" y1=\"12\" x2=\"3\" y2=\"12\"></line><line x1=\"17\" y1=\"18\" x2=\"3\" y2=\"18\"></line></svg>',\n\n       \"align-none\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" x2=\"21\" y1=\"6\" y2=\"6\"></line><line x1=\"3\" x2=\"21\" y1=\"12\" y2=\"12\"></line><line x1=\"3\" x2=\"21\" y1=\"18\" y2=\"18\"></line><line x1=\"3\" x2=\"21\" y1=\"20\" y2=\"4\"></line></svg>',\n\n       \"align-right\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"21\" y1=\"6\" x2=\"3\" y2=\"6\"></line><line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\"></line><line x1=\"21\" y1=\"18\" x2=\"7\" y2=\"18\"></line></svg>',\n\n       \"arrow-left\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"15 18 9 12 15 6\"></polyline></svg>',\n\n       \"arrow-right\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 18 15 12 9 6\"></polyline></svg>',\n\n       bold: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"></path><path d=\"M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"></path></svg>',\n\n       bookmark: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z\"></path></svg>',\n\n       \"border-style\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" x2=\"21\" y1=\"6\" y2=\"6\"></line><line x1=\"3\" x2=\"9\" y1=\"12\" y2=\"12\"></line><line x1=\"15\" x2=\"21\" y1=\"12\" y2=\"12\"></line><line x1=\"3\" x2=\"5\" y1=\"18\" y2=\"18\"></line><line x1=\"11\" x2=\"13\" y1=\"18\" y2=\"18\"></line><line x1=\"19\" x2=\"21\" y1=\"18\" y2=\"18\"></line></svg>',\n\n       \"border-width\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" x2=\"21\" y1=\"6\" y2=\"6\"></line><line x1=\"3\" x2=\"21\" y1=\"7\" y2=\"7\"></line><line x1=\"3\" x2=\"21\" y1=\"8\" y2=\"8\"></line><line x1=\"3\" x2=\"21\" y1=\"12\" y2=\"12\"></line><line x1=\"3\" x2=\"21\" y1=\"13\" y2=\"13\"></line><line x1=\"3\" x2=\"21\" y1=\"18\" y2=\"18\"></line></svg>',\n\n       brightness: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z\"></path><path d=\"M12 4h.01\"></path><path d=\"M20 12h.01\"></path><path d=\"M12 20h.01\"></path><path d=\"M4 12h.01\"></path><path d=\"M17.657 6.343h.01\"></path><path d=\"M17.657 17.657h.01\"></path><path d=\"M6.343 17.657h.01\"></path><path d=\"M6.343 6.343h.01\"></path></svg>',\n\n       browse: '<svg width=\"24\" height=\"24\"><path d=\"M19 4a2 2 0 012 2v12a2 2 0 01-2 2h-4v-2h4V8H5v10h4v2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h14zm-8 9.4l-2.3 2.3a1 1 0 11-1.4-1.4l4-4a1 1 0 011.4 0l4 4a1 1 0 01-1.4 1.4L13 13.4V20a1 1 0 01-2 0v-6.6z\" fill-rule=\"nonzero\"/></svg>',\n\n       cancel: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><line x1=\"15\" x2=\"9\" y1=\"9\" y2=\"15\"></line><line x1=\"9\" x2=\"15\" y1=\"9\" y2=\"15\"></line></svg>',\n\n       \"cell-background-color\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18.37 2.63 14 7l-1.59-1.59a2 2 0 0 0-2.82 0L8 7l9 9 1.59-1.59a2 2 0 0 0 0-2.82L17 10l4.37-4.37a2.12 2.12 0 1 0-3-3Z\"></path><path d=\"M9 8c-2 3-4 3.5-7 4l8 10c2-1 6-5 6-7\"></path><path d=\"M14.5 17.5 4.5 15\"></path></svg>',\n\n       \"cell-border-color\": '<svg width=\"24\" height=\"24\"><g fill-rule=\"evenodd\"><path fill-rule=\"nonzero\" d=\"M5 13v5h2v2H5a2 2 0 01-2-2v-5h2zm8-7V4h6a2 2 0 012 2h-8z\" opacity=\".2\"/><path fill-rule=\"nonzero\" d=\"M13 4v2H5v7H3V6c0-1.1.9-2 2-2h8zm-2.6 14.1l.1-.1.1.1.2.3.2.2.2.2c.4.6.8 1.2.8 1.7 0 .8-.7 1.5-1.5 1.5S9 21.3 9 20.5c0-.5.4-1.1.8-1.7l.2-.2.2-.2.2-.3z\"/><path d=\"M13 11l-2 2H5v-2h6V6h2z\"/><path fill-rule=\"nonzero\" d=\"M18.4 8l1 1-1.8 1.9 4 4c.5.4.5 1.1 0 1.6l-4.3 4.2a1.2 1.2 0 01-1.6 0l-4.4-4.2c-.4-.5-.4-1.2 0-1.7l7-6.8zm1.6 7l-3-3-3 3h6z\"/></g></svg>',\n\n       \"change-case\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3,15 7,7 11,15\"></polyline><line x1=\"4\" x2=\"10\" y1=\"13\" y2=\"13\"></line><circle cx=\"18\" cy=\"12\" r=\"3\"></circle><line x1=\"21\" x2=\"21\" y1=\"9\" y2=\"15\"></line></svg>',\n\n       \"character-count\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"17\" cy=\"12\" r=\"3\"></circle><line x1=\"14\" x2=\"14\" y1=\"7\" y2=\"15\"></line><circle cx=\"7\" cy=\"12\" r=\"3\"></circle><line x1=\"10\" x2=\"10\" y1=\"9\" y2=\"15\"></line><polyline points=\"22,17 22,19 2,19 2,17\"></polyline></svg>',\n\n       checklist: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"10\" x2=\"21\" y1=\"6\" y2=\"6\"></line><line x1=\"10\" x2=\"21\" y1=\"12\" y2=\"12\"></line><line x1=\"10\" x2=\"21\" y1=\"18\" y2=\"18\"></line><polyline points=\"3 6 4 7 6 5\"></polyline><polyline points=\"3 12 4 13 6 11\"></polyline><polyline points=\"3 18 4 19 6 17\"></polyline></svg>',\n\n       checkmark: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"></polyline></svg>',\n\n       \"chevron-down\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"></polyline></svg>',\n\n       \"chevron-left\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"15 18 9 12 15 6\"></polyline></svg>',\n\n       \"chevron-right\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 18 15 12 9 6\"></polyline></svg>',\n\n       \"chevron-up\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"18 15 12 9 6 15\"></polyline></svg>',\n\n       close: '<svg width=\"24\" height=\"24\"><path d=\"M17.3 8.2L13.4 12l3.9 3.8a1 1 0 01-1.5 1.5L12 13.4l-3.8 3.9a1 1 0 01-1.5-1.5l3.9-3.8-3.9-3.8a1 1 0 011.5-1.5l3.8 3.9 3.8-3.9a1 1 0 011.5 1.5z\" fill-rule=\"evenodd\"/></svg>',\n\n       \"code-sample\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1\"></path><path d=\"M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1\"></path></svg>',\n       \n\n       \"color-levels\": '<svg width=\"24\" height=\"24\"><path d=\"M17.5 11.4A9 9 0 0118 14c0 .5 0 1-.2 1.4 0 .4-.3.9-.5 1.3a6.2 6.2 0 01-3.7 3 5.7 5.7 0 01-3.2 0A5.9 5.9 0 017.6 18a6.2 6.2 0 01-1.4-2.6 6.7 6.7 0 010-2.8c0-.4.1-.9.3-1.3a13.6 13.6 0 012.3-4A20 20 0 0112 4a26.4 26.4 0 013.2 3.4 18.2 18.2 0 012.3 4zm-2 4.5c.4-.7.5-1.4.5-2a7.3 7.3 0 00-1-3.2c.2.6.2 1.2.2 1.9a4.5 4.5 0 01-1.3 3 5.3 5.3 0 01-2.3 1.5 4.9 4.9 0 01-2 .1 4.3 4.3 0 002.4.8 4 4 0 002-.6 4 4 0 001.5-1.5z\" fill-rule=\"evenodd\"/></svg>',\n\n       \"color-picker\": '<svg width=\"24\" height=\"24\"><path d=\"M12 3a9 9 0 000 18 1.5 1.5 0 001.1-2.5c-.2-.3-.4-.6-.4-1 0-.8.7-1.5 1.5-1.5H16a5 5 0 005-5c0-4.4-4-8-9-8zm-5.5 9a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm3-4a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm5 0a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm3 4a1.5 1.5 0 110-3 1.5 1.5 0 010 3z\" fill-rule=\"nonzero\"/></svg>',\n\n       \"color-swatch-remove-color\": '<svg width=\"24\" height=\"24\"><path stroke=\"#000\" stroke-width=\"2\" d=\"M21 3L3 21\" fill-rule=\"evenodd\"/></svg>',\n\n       \"color-swatch\": '<svg width=\"24\" height=\"24\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"1\" fill-rule=\"evenodd\"/></svg>',\n\n       \"comment-add\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"></path><path d=\"M12 7v6\"></path><path d=\"M9 10h6\"></path></svg>',\n\n       comment: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"></path></svg>',\n\n       contrast: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M12 18a6 6 0 0 0 0-12v12z\"></path></svg>',\n\n       copy: '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"13\" height=\"13\" x=\"9\" y=\"9\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>',\n\n       crop: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 2v14a2 2 0 0 0 2 2h14\"></path><path d=\"M18 22V8a2 2 0 0 0-2-2H2\"></path></svg>',\n\n       \"cut-column\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"3\"></circle><circle cx=\"6\" cy=\"18\" r=\"3\"></circle><line x1=\"20\" y1=\"4\" x2=\"8.12\" y2=\"15.88\"></line><line x1=\"14.47\" y1=\"14.48\" x2=\"20\" y2=\"20\"></line><line x1=\"8.12\" y1=\"8.12\" x2=\"12\" y2=\"12\"></line></svg>',\n\n       \"cut-row\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"3\"></circle><circle cx=\"6\" cy=\"18\" r=\"3\"></circle><line x1=\"20\" y1=\"4\" x2=\"8.12\" y2=\"15.88\"></line><line x1=\"14.47\" y1=\"14.48\" x2=\"20\" y2=\"20\"></line><line x1=\"8.12\" y1=\"8.12\" x2=\"12\" y2=\"12\"></line></svg>',\n\n       cut: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"3\"></circle><circle cx=\"6\" cy=\"18\" r=\"3\"></circle><line x1=\"20\" y1=\"4\" x2=\"8.12\" y2=\"15.88\"></line><line x1=\"14.47\" y1=\"14.48\" x2=\"20\" y2=\"20\"></line><line x1=\"8.12\" y1=\"8.12\" x2=\"12\" y2=\"12\"></line></svg>',\n\n       \"document-properties\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z\"></path><polyline points=\"14 2 14 8 20 8\"></polyline></svg>',\n\n       drag: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"9\" cy=\"12\" r=\"1\"></circle><circle cx=\"9\" cy=\"5\" r=\"1\"></circle><circle cx=\"9\" cy=\"19\" r=\"1\"></circle><circle cx=\"15\" cy=\"12\" r=\"1\"></circle><circle cx=\"15\" cy=\"5\" r=\"1\"></circle><circle cx=\"15\" cy=\"19\" r=\"1\"></circle></svg>',\n\n       \"duplicate-column\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>',\n\n       \"duplicate-row\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>',\n\n       duplicate: '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>',\n\n       \"edit-image\": '<svg width=\"24\" height=\"24\"><path d=\"M18 16h2V7a2 2 0 00-2-2H7v2h11v9zM6 17h15a1 1 0 010 2h-1v1a1 1 0 01-2 0v-1H6a2 2 0 01-2-2V7H3a1 1 0 110-2h1V4a1 1 0 112 0v13zm3-5.3l1.3 2 3-4.7 3.7 6H7l2-3.3z\" fill-rule=\"nonzero\"/></svg>',\n\n       \"embed-page\": '<svg width=\"24\" height=\"24\"><path d=\"M19 6V5H5v14h2A13 13 0 0119 6zm0 1.4c-.8.8-1.6 2.4-2.2 4.6H19V7.4zm0 5.6h-2.4c-.4 1.8-.6 3.8-.6 6h3v-6zm-4 6c0-2.2.2-4.2.6-6H13c-.7 1.8-1.1 3.8-1.1 6h3zm-4 0c0-2.2.4-4.2 1-6H9.6A12 12 0 008 19h3zM4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 01-1-1V4c0-.6.4-1 1-1zm11.8 9c.4-1.9 1-3.4 1.8-4.5a9.2 9.2 0 00-4 4.5h2.2zm-3.4 0a12 12 0 012.8-4 12 12 0 00-5 4h2.2z\" fill-rule=\"nonzero\"/></svg>',\n\n       embed: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m22 8-6 4 6 4V8Z\"></path><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\" ry=\"2\"></rect></svg>',\n\n       emoji: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M8 14s1.5 2 4 2 4-2 4-2\"></path><line x1=\"9\" y1=\"9\" x2=\"9.01\" y2=\"9\"></line><line x1=\"15\" y1=\"9\" x2=\"15.01\" y2=\"9\"></line></svg>',\n\n       export: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z\"></path><polyline points=\"14 2 14 8 20 8\"></polyline><path d=\"M12 18v-6\"></path><path d=\"m9 15 3 3 3-3\"></path></svg>',\n\n       fill: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m19 11-8-8-8.6 8.6a2 2 0 0 0 0 2.8l5.2 5.2c.8.8 2 .8 2.8 0L19 11Z\"></path><path d=\"m5 2 5 5\"></path><path d=\"M2 13h15\"></path><path d=\"M22 20a2 2 0 1 1-4 0c0-1.6 1.7-2.4 2-4 .3 1.6 2 2.4 2 4Z\"></path></svg>',\n\n       \"flip-horizontally\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3\"></path><path d=\"M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3\"></path><path d=\"M12 20v2\"></path><path d=\"M12 14v2\"></path><path d=\"M12 8v2\"></path><path d=\"M12 2v2\"></path></svg>',\n\n       \"flip-vertically\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3\"></path><path d=\"M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3\"></path><path d=\"M4 12H2\"></path><path d=\"M10 12H8\"></path><path d=\"M16 12h-2\"></path><path d=\"M22 12h-2\"></path></svg>',\n\n       \"format-painter\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 19.9V16h3a2 2 0 0 0 2-2v-2H5v2c0 1.1.9 2 2 2h3v3.9a2 2 0 1 0 4 0Z\"></path><path d=\"M6 12V2h12v10\"></path><path d=\"M14 2v4\"></path><path d=\"M10 2v2\"></path></svg>',\n\n       format: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 7V4h16v3\"></path><path d=\"M5 20h6\"></path><path d=\"M13 4 8 20\"></path></svg>',\n\n       fullscreen: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21 21-6-6m6 6v-4.8m0 4.8h-4.8\"></path><path d=\"M3 16.2V21m0 0h4.8M3 21l6-6\"></path><path d=\"M21 7.8V3m0 0h-4.8M21 3l-6 6\"></path><path d=\"M3 7.8V3m0 0h4.8M3 3l6 6\"></path></svg>',\n\n       gallery: '<svg width=\"27\" height=\"19\" viewBox=\"0 0 34 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"18\" height=\"18\" rx=\"2\" transform=\"translate(3 3)\"/><circle cx=\"2\" cy=\"2\" r=\"2\" transform=\"translate(7 7)\"/><path d=\"M21,15l-3.086-3.086a2,2,0,0,0-2.828,0L6,21\"/><path d=\"M0,18\" transform=\"translate(26 3)\"/><path d=\"M3,21a2,2,0,0,0,2-2V5A2,2,0,0,0,3,3\" transform=\"translate(21)\"/><path d=\"M3,21a2,2,0,0,0,2-2V5A2,2,0,0,0,3,3\" transform=\"translate(26)\"/>',\n       gamma: '<svg width=\"24\" height=\"24\"><path d=\"M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 01-1-1V4c0-.6.4-1 1-1zm1 2v14h14V5H5zm6.5 11.8V14L9.2 8.7a5.1 5.1 0 00-.4-.8l-.1-.2H8 8v-1l.3-.1.3-.1h.7a1 1 0 01.6.5l.1.3a8.5 8.5 0 01.3.6l1.9 4.6 2-5.2a1 1 0 011-.6.5.5 0 01.5.6L13 14v2.8a.7.7 0 01-1.4 0z\" fill-rule=\"nonzero\"/></svg>',\n\n       help: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path><line x1=\"12\" x2=\"12.01\" y1=\"17\" y2=\"17\"></line></svg>',\n\n       \"highlight-bg-color\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 11-6 6v3h9l3-3\"></path><path d=\"m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4\"></path></svg>',\n\n       home: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"></path><polyline points=\"9 22 9 12 15 12 15 22\"></polyline></svg>',\n\n       \"horizontal-rule\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"6\" r=\"1\"></circle><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line><circle cx=\"12\" cy=\"18\" r=\"1\"></circle></svg>',\n\n       \"image-options\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"1\"></circle><circle cx=\"19\" cy=\"12\" r=\"1\"></circle><circle cx=\"5\" cy=\"12\" r=\"1\"></circle></svg>',\n\n       image: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><circle cx=\"9\" cy=\"9\" r=\"2\"></circle><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"></path></svg>',\n\n       indent: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 8 7 12 3 16\"></polyline><line x1=\"21\" x2=\"11\" y1=\"12\" y2=\"12\"></line><line x1=\"21\" x2=\"11\" y1=\"6\" y2=\"6\"></line><line x1=\"21\" x2=\"11\" y1=\"18\" y2=\"18\"></line></svg>',\n\n       info: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><line x1=\"12\" x2=\"12\" y1=\"16\" y2=\"12\"></line><line x1=\"12\" x2=\"12.01\" y1=\"8\" y2=\"8\"></line></svg>',\n\n       \"insert-character\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path><path d=\"M4 19h5v-1a7.35 7.35 0 1 1 6 0v1h5\"></path></svg>',\n\n       \"insert-time\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polyline points=\"12 6 12 12 16.5 12\"></polyline></svg>',\n\n       invert: '<svg width=\"24\" height=\"24\"><path d=\"M18 19.3L16.5 18a5.8 5.8 0 01-3.1 1.9 6.1 6.1 0 01-5.5-1.6A5.8 5.8 0 016 14v-.3l.1-1.2A13.9 13.9 0 017.7 9l-3-3 .7-.8 2.8 2.9 9 8.9 1.5 1.6-.7.6zm0-5.5v.3l-.1 1.1-.4 1-1.2-1.2a4.3 4.3 0 00.2-1v-.2c0-.4 0-.8-.2-1.3l-.5-1.4a14.8 14.8 0 00-3-4.2L12 6a26.1 26.1 0 00-2.2 2.5l-1-1a20.9 20.9 0 012.9-3.3L12 4l1 .8a22.2 22.2 0 014 5.4c.6 1.2 1 2.4 1 3.6z\" fill-rule=\"evenodd\"/></svg>',\n\n       italic: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"19\" y1=\"4\" x2=\"10\" y2=\"4\"></line><line x1=\"14\" y1=\"20\" x2=\"5\" y2=\"20\"></line><line x1=\"15\" y1=\"4\" x2=\"9\" y2=\"20\"></line></svg>',\n\n       language: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21.54 15H17a2 2 0 0 0-2 2v4.54\"></path><path d=\"M7 3.34V5a3 3 0 0 0 3 3v0a2 2 0 0 1 2 2v0c0 1.1.9 2 2 2v0a2 2 0 0 0 2-2v0c0-1.1.9-2 2-2h3.17\"></path><path d=\"M11 21.95V18a2 2 0 0 0-2-2v0a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05\"></path><circle cx=\"12\" cy=\"12\" r=\"10\"></circle></svg>',\n\n       \"line-height\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"1.663 15 4.996 18.333 8.329 15\" /><polyline points=\"1.663 5 4.996 1.667 8.329 5\" /><line x1=\"5\" y1=\"2\" x2=\"5\" y2=\"18\"/><line x1=\"18\" y1=\"5\" x2=\"12\" y2=\"5\" /><line x1=\"18\" y1=\"10\" x2=\"12\" y2=\"10\" /><line x1=\"18\" y1=\"15\" x2=\"12\" y2=\"15\" /></svg>',\n\n       line: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"></path><path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\"></path></svg>',\n\n       link: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"></path><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"></path></svg>',\n\n       \"list-bull-circle\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".4\"><path d=\"M9 6h12M9 12h12M9 18h12\"/></g><circle cx=\"3\" cy=\"6\" r=\"1.5\" stroke-width=\"1\"/><circle cx=\"3\" cy=\"12\" r=\"1.5\" stroke-width=\"1\"/><circle cx=\"3\" cy=\"18\" r=\"1.5\" stroke-width=\"1\"/></svg>',\n\n       \"list-bull-square\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".4\"><path d=\"M9 6h12M9 12h12M9 18h12\"/></g><path d=\"M3 5h2v2H3zM3 11h2v2H3zM3 17h2v2H3z\"/></svg>',\n\n       \"list-bull-default\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".5\"><path d=\"M9 6h12M9 12h12M9 18h12\"/></g><path stroke-width=\"4\" d=\"M3 6h.01M3 12h.01M3 18h.01\"/></svg>',\n\n       \"list-num-default\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".6\"><path d=\"M11 6h10M11 12h10M11 18h10\"/></g><g stroke-width=\"1\"><path d=\"M4 6h1v4M4 10h2M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/></g></svg>',\n\n       \"list-num-lower-alpha\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".6\"><path d=\"M11 6h10M11 12h10M11 18h10\"/></g><g stroke-width=\"1\"><path d=\"M6.5 7a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 6.5 7Zm0-1.5v3M5 15.5a1.5 1.5 0 1 1-.001 3.001A1.5 1.5 0 0 1 5 15.5Zm-1.5 3v-5M8.5 8.736h0M8.5 18.456h0\"/></g></svg>',\n\n       \"list-num-lower-greek\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".6\"><path d=\"M11 6h10M11 12h10M11 18h10\"/></g><g stroke-width=\"1\"><path d=\"M6.061 8.061C5.79 8.332 5.415 8.5 5 8.5A1.497 1.497 0 0 1 3.5 7 1.497 1.497 0 0 1 5 5.5c.415 0 .789.168 1.061.439m.855 2.443c-.179-.076-.341-.186-.477-.321s-.246-.297-.321-.477a1.515 1.515 0 0 1 0-1.168 1.513 1.513 0 0 1 .798-.798M8.5 8.5h0M8.5 18.456h0M3.5 15a1.5 1.5 0 0 1 3 0c0 .828-.672 1.171-1.5 1.171S3.5 15.828 3.5 15Zm0 4.5V15m2.561.941c.271.271.439.646.439 1.061a1.497 1.497 0 0 1-1.5 1.5 1.497 1.497 0 0 1-1.5-1.5c0-.415.168-.789.439-1.061\"/></g></svg>',\n\n       \"list-num-lower-roman\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".6\"><path d=\"M10 6h11M10 12h11M10 18h11\"/></g><path stroke-width=\"1\" d=\"M5 7v3M5 5h0M7 10h0M3 14h0M3 19v-3M5 19v-3M5 14h0M7 19h0\"/></svg>',\n\n       \"list-num-upper-alpha\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".6\"><path d=\"M11 6h10M11 12h10M11 18h10\"/></g><g stroke-width=\"1\"><path d=\"M8.5 9.5h0M8.5 18.956h0M2.835 9.5l2-5M6.835 9.5l-2-5M3.835 8h2M5.945 16.5c.239.266.389.614.389 1 0 .828-.389 1.5-1.5 1.5h-1.5v-5h1.5c1.111 0 1.5.672 1.5 1.5 0 .386-.15.734-.389 1Zm-2.611 0h2.611\"/></g></svg>',\n\n       \"list-num-upper-roman\": '<svg width=\"32\" height=\"32\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g opacity=\".6\"><path d=\"M10 6h11M10 12h11M10 18h11\"/></g><path stroke-width=\"1\" d=\"M5 5v4M7 9h0M3 19v-5M5 19v-5M7 19h0\"/></svg>',\n\n       lock: '<svg width=\"24\" height=\"24\"><path d=\"M16.3 11c.2 0 .3 0 .5.2l.2.6v7.4c0 .3 0 .4-.2.6l-.6.2H7.8c-.3 0-.4 0-.6-.2a.7.7 0 01-.2-.6v-7.4c0-.3 0-.4.2-.6l.5-.2H8V8c0-.8.3-1.5.9-2.1.6-.6 1.3-.9 2.1-.9h2c.8 0 1.5.3 2.1.9.6.6.9 1.3.9 2.1v3h.3zM10 8v3h4V8a1 1 0 00-.3-.7A1 1 0 0013 7h-2a1 1 0 00-.7.3 1 1 0 00-.3.7z\" fill-rule=\"evenodd\"/></svg>',\n\n       ltr: '<svg width=\"24\" height=\"24\"><path d=\"M11 5h7a1 1 0 010 2h-1v11a1 1 0 01-2 0V7h-2v11a1 1 0 01-2 0v-6c-.5 0-1 0-1.4-.3A3.4 3.4 0 017.8 10a3.3 3.3 0 010-2.8 3.4 3.4 0 011.8-1.8L11 5zM4.4 16.2L6.2 15l-1.8-1.2a1 1 0 011.2-1.6l3 2a1 1 0 010 1.6l-3 2a1 1 0 11-1.2-1.6z\" fill-rule=\"evenodd\"/></svg>',\n\n       \"more-drawer\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"1\"></circle><circle cx=\"19\" cy=\"12\" r=\"1\"></circle><circle cx=\"5\" cy=\"12\" r=\"1\"></circle></svg>',\n\n       \"new-document\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z\"></path><polyline points=\"14 2 14 8 20 8\"></polyline></svg>',\n\n       \"new-tab\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"></path><polyline points=\"15 3 21 3 21 9\"></polyline><line x1=\"10\" x2=\"21\" y1=\"14\" y2=\"3\"></line></svg>',\n\n       \"non-breaking\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22,17 22,19 2,19 2,17\"></polyline></svg>',\n\n       notice: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2\"></polygon><line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\"></line><line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\"></line></svg>',\n\n       \"ordered-list\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"10\" y1=\"6\" x2=\"21\" y2=\"6\"></line><line x1=\"10\" y1=\"12\" x2=\"21\" y2=\"12\"></line><line x1=\"10\" y1=\"18\" x2=\"21\" y2=\"18\"></line><path d=\"M4 6h1v4\"></path><path d=\"M4 10h2\"></path><path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"></path></svg>',\n\n       orientation: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16.466 7.5C15.643 4.237 13.952 2 12 2 9.239 2 7 6.477 7 12s2.239 10 5 10c.342 0 .677-.069 1-.2\"></path><path d=\"m15.194 13.707 3.814 1.86-1.86 3.814\"></path><path d=\"M19 15.57c-1.804.885-4.274 1.43-7 1.43-5.523 0-10-2.239-10-5s4.477-5 10-5c4.838 0 8.873 1.718 9.8 4\"></path></svg>',\n\n       outdent: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"7 8 3 12 7 16\"></polyline><line x1=\"21\" x2=\"11\" y1=\"12\" y2=\"12\"></line><line x1=\"21\" x2=\"11\" y1=\"6\" y2=\"6\"></line><line x1=\"21\" x2=\"11\" y1=\"18\" y2=\"18\"></line></svg>',\n\n       \"page-break\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 8V5c0-1 1-2 2-2h10c1 0 2 1 2 2v3\"></path><path d=\"M19 16v3c0 1-1 2-2 2H7c-1 0-2-1-2-2v-3\"></path><line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\"></line></svg>',\n\n       paragraph: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M13 4v16\"></path><path d=\"M17 4v16\"></path><path d=\"M19 4H9.5a4.5 4.5 0 0 0 0 9H13\"></path></svg>',\n\n       \"paste-column-after\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"></rect><path d=\"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\"></path><path d=\"M16 4h2a2 2 0 0 1 2 2v4\"></path><path d=\"m17.5,13.668h-8.333\"/><path d=\"m14.167,17.002l3.333-3.333-3.333-3.333\"/></svg>',\n\n       \"paste-column-before\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" transform=\"scale(-1 1)\"><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"></rect><path d=\"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\"></path><path d=\"M16 4h2a2 2 0 0 1 2 2v4\"></path><path d=\"m17.5,13.668h-8.333\"/><path d=\"m14.167,17.002l3.333-3.333-3.333-3.333\"/></svg>',\n\n       \"paste-row-after\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-2M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2M12.332 19.5v-8.333M8.998 16.167l3.333 3.333 3.333-3.333\"/></svg>',\n\n       \"paste-row-before\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-2M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2M12.168 12v8.333M15.502 15.333 12.169 12l-3.333 3.333\"/></svg>',\n\n       \"paste-text\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\"></rect><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"></path><path d=\"M9 12v-1h6v1\"></path><path d=\"M11 17h2\"></path><path d=\"M12 11v6\"></path></svg>',\n\n       paste: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 2H9a1 1 0 0 0-1 1v2c0 .6.4 1 1 1h6c.6 0 1-.4 1-1V3c0-.6-.4-1-1-1Z\"></path><path d=\"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2M16 4h2a2 2 0 0 1 2 2v2M11 14h10\"></path><path d=\"m17 10 4 4-4 4\"></path></svg>',\n\n       \"permanent-pen\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"18\" x2=\"22\" y1=\"2\" y2=\"6\"></line><path d=\"M7.5 20.5 19 9l-4-4L3.5 16.5 2 22z\"></path></svg>',\n\n       plus: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\"></rect><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"></path></svg>',\n\n       preferences: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\"></path><circle cx=\"12\" cy=\"12\" r=\"3\"></circle></svg>',\n\n       preview: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z\"></path><circle cx=\"12\" cy=\"12\" r=\"3\"></circle></svg>',\n\n       print: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 6 2 18 2 18 9\"></polyline><path d=\"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2\"></path><rect width=\"12\" height=\"8\" x=\"6\" y=\"14\"></rect></svg>',\n\n       quote: '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z\"></path><path d=\"M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z\"></path></svg>',\n\n       readmore: '<svg width=\"19\" height=\"19\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3\"></path><path d=\"M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3\"></path><path d=\"M4 12H2\"></path><path d=\"M10 12H8\"></path><path d=\"M16 12h-2\"></path><path d=\"M22 12h-2\"></path></svg>',\n\n       redo: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"15 14 20 9 15 4\"></polyline><path d=\"M4 20v-7a4 4 0 0 1 4-4h12\"></path></svg>',\n\n       reload: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 2v6h-6\"></path><path d=\"M3 12a9 9 0 0 1 15-6.7L21 8\"></path><path d=\"M3 22v-6h6\"></path><path d=\"M21 12a9 9 0 0 1-15 6.7L3 16\"></path></svg>',\n\n       \"remove-formatting\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21\"></path><path d=\"M22 21H7\"></path><path d=\"m5 11 9 9\"></path></svg>',\n\n       remove: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"></path><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"></path><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"></path><line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"></line><line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"></line></svg>',\n\n       \"resize-handle\": '<svg width=\"10\" height=\"10\"><g fill-rule=\"nonzero\"><path d=\"M8.1 1.1A.5.5 0 119 2l-7 7A.5.5 0 111 8l7-7zM8.1 5.1A.5.5 0 119 6l-3 3A.5.5 0 115 8l3-3z\"/></g></svg>',\n\n       resize: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"5 11 5 5 11 5\"></polyline><polyline points=\"19 13 19 19 13 19\"></polyline><line x1=\"5\" x2=\"19\" y1=\"5\" y2=\"19\"></line></svg>',\n\n       \"restore-draft\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 3v5h5\"></path><path d=\"M3.05 13A9 9 0 1 0 6 5.3L3 8\"></path><path d=\"M12 7v5l4 2\"></path></svg>',\n\n       \"rotate-left\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 2v6h6\"></path><path d=\"M3 13a9 9 0 1 0 3-7.7L3 8\"></path></svg>',\n\n       \"rotate-right\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 2v6h-6\"></path><path d=\"M21 13a9 9 0 1 1-3-7.7L21 8\"></path></svg>',\n\n       save: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3v12\"></path><path d=\"m8 11 4 4 4-4\"></path><path d=\"M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4\"></path></svg>',\n\n       search: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line></svg>',\n\n       \"select-all\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 3a2 2 0 0 0-2 2\"></path><path d=\"M19 3a2 2 0 0 1 2 2\"></path><path d=\"M21 19a2 2 0 0 1-2 2\"></path><path d=\"M5 21a2 2 0 0 1-2-2\"></path><path d=\"M9 3h1\"></path><path d=\"M9 21h1\"></path><path d=\"M14 3h1\"></path><path d=\"M14 21h1\"></path><path d=\"M3 9v1\"></path><path d=\"M21 9v1\"></path><path d=\"M3 14v1\"></path><path d=\"M21 14v1\"></path></svg>',\n\n       selected: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 11 12 14 22 4\"></polyline><path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"></path></svg>',\n\n       settings: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 7h-9\"></path><path d=\"M14 17H5\"></path><circle cx=\"17\" cy=\"17\" r=\"3\"></circle><circle cx=\"7\" cy=\"7\" r=\"3\"></circle></svg>',\n\n       sharpen: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path><path d=\"M6 5h12l3 5l-8.5 9.5a.7 .7 0 0 1 -1 0l-8.5 -9.5l3 -5\"></path><path d=\"M10 12l-2 -2.2l.6 -1\"></path></svg>',\n\n       sourcecode: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"16 18 22 12 16 6\"></polyline><polyline points=\"8 6 2 12 8 18\"></polyline></svg>',\n\n       \"spell-check\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"17\" cy=\"10\" r=\"3\"></circle><line x1=\"14\" x2=\"14\" y1=\"5\" y2=\"13\"></line><circle cx=\"7\" cy=\"10\" r=\"3\"></circle><line x1=\"10\" x2=\"10\" y1=\"7\" y2=\"13\"></line><path d=\"m9 19 2 2 4-4\"></path></svg>',\n\n       \"strike-through\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 4H9a3 3 0 0 0-2.83 4\"></path><path d=\"M14 12a4 4 0 0 1 0 8H6\"></path><line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\"></line></svg>',\n\n       subscript: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m4 5 8 8\"></path><path d=\"m12 5-8 8\"></path><path d=\"M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07\"></path></svg>',\n\n       superscript: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m4 19 8-8\"></path><path d=\"m12 19-8-8\"></path><path d=\"M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 0 0-2.617-.436c-.42.239-.738.614-.899 1.06\"></path></svg>',\n\n       \"table-caption\": '<svg width=\"24\" height=\"24\"><g fill-rule=\"nonzero\"><rect width=\"12\" height=\"2\" x=\"3\" y=\"4\" rx=\"1\"/><path d=\"M19 8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2v-8c0-1.1.9-2 2-2h14zM5 15v3h6v-3H5zm14 0h-6v3h6v-3zm0-5h-6v3h6v-3zM5 13h6v-3H5v3z\"/></g></svg>',\n\n       \"table-cell-classes\": '<svg width=\"24\" height=\"24\"><g fill-rule=\"evenodd\"><path fill-rule=\"nonzero\" d=\"M13 4v9H3V6c0-1.1.9-2 2-2h8zm-2 2H5v5h6V6z\"/><path fill-rule=\"nonzero\" d=\"M13 4h6a2 2 0 012 2v7h-8v-2h6V6h-6V4z\" opacity=\".2\"/><path d=\"M18 20l-2.6 1.6.7-3-2.4-2 3.1-.2 1.2-2.9 1.2 2.9 3 .2-2.3 2 .7 3z\"/><path fill-rule=\"nonzero\" d=\"M3 13v5c0 1.1.9 2 2 2h8v-7h-2v5H5v-5H3z\" opacity=\".2\"/></g></svg>',\n\n       \"table-cell-properties\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 7h-9\"></path><path d=\"M14 17H5\"></path><circle cx=\"17\" cy=\"17\" r=\"3\"></circle><circle cx=\"7\" cy=\"7\" r=\"3\"></circle></svg>',\n\n       \"table-cell-select-all\": '<svg width=\"24\" height=\"24\"><g fill-rule=\"evenodd\"><path fill-rule=\"nonzero\" d=\"M19 4a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h14zm0 2H5v12h14V6z\"/><path d=\"M13 6v5h6v2h-6v5h-2v-5H5v-2h6V6h2z\" opacity=\".2\"/></g></svg>',\n\n       \"table-cell-select-inner\": '<svg width=\"24\" height=\"24\"><g fill-rule=\"evenodd\"><path fill-rule=\"nonzero\" d=\"M19 4a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h14zm0 2H5v12h14V6z\" opacity=\".2\"/><path d=\"M13 6v5h6v2h-6v5h-2v-5H5v-2h6V6h2z\"/></g></svg>',\n\n       \"table-classes\": '<svg width=\"24\" height=\"24\"><g fill-rule=\"evenodd\"><path fill-rule=\"nonzero\" d=\"M19 4a2 2 0 012 2v7h-8v7H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h14zm-8 9H5v5h6v-5zm8-7h-6v5h6V6zm-8 0H5v5h6V6z\"/><path d=\"M18 20l-2.6 1.6.7-3-2.4-2 3.1-.2 1.2-2.9 1.2 2.9 3 .2-2.3 2 .7 3z\"/></g></svg>',\n\n       \"table-delete-column\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"></path><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"></path><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"></path><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"></line><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"></line></svg>',\n\n       \"table-delete-row\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"></path><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"></path><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"></path><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"></line><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"></line></svg>',\n\n       \"table-delete-table\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"></path><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"></path><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"></path><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\"></line><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\"></line></svg>',\n\n       \"table-insert-column-after\": '<svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path><path d=\"M6 4h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1z\"></path><path d=\"M15 12l4 0\"></path><path d=\"M17 10l0 4\"></path></svg>',\n\n       \"table-insert-column-before\": '<svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path><path d=\"M14 4h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1z\"></path><path d=\"M5 12l4 0\"></path><path d=\"M7 10l0 4\"></path></svg>',\n\n       \"table-insert-row-above\": '<svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path><path d=\"M4 18v-4a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1z\"></path><path d=\"M12 9v-4\"></path><path d=\"M10 7l4 0\"></path></svg>',\n\n       \"table-insert-row-after\": '<svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path><path d=\"M20 6v4a1 1 0 0 1 -1 1h-14a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h14a1 1 0 0 1 1 1z\"></path><path d=\"M12 15l0 4\"></path><path d=\"M14 17l-4 0\"></path></svg>',\n\n       \"table-left-header\": '<svg width=\"24\" height=\"24\"><path d=\"M19 4a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h14zm0 9h-4v5h4v-5zm-6 0H9v5h4v-5zm0-7H9v5h4V6zm6 0h-4v5h4V6z\"/></svg>',\n\n       \"table-merge-cells\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"></line><line x1=\"3\" y1=\"15\" x2=\"9\" y2=\"15\"></line><line x1=\"9\" y1=\"3\" x2=\"9\" y2=\"21\"></line></svg>',\n\n       \"table-row-numbering\": '<svg width=\"24\" height=\"24\"><path d=\"M18 4a2 2 0 012 2v13a2 2 0 01-2 2H6a2 2 0 01-2-2V6c0-1.1.9-2 2-2h12zm0 12h-8v3h8v-3zM7 16a1 1 0 00-1 1v1a1 1 0 002 0v-1c0-.6-.4-1-1-1zm11-5h-8v3h8v-3zM7 11a1 1 0 00-1 1v1a1 1 0 002 0v-1c0-.6-.4-1-1-1zm11-5h-8v3h8V6zM7 6a1 1 0 00-1 1v1a1 1 0 102 0V7c0-.6-.4-1-1-1z\"/></svg>',\n\n       \"table-row-properties\": '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 7h-9\"></path><path d=\"M14 17H5\"></path><circle cx=\"17\" cy=\"17\" r=\"3\"></circle><circle cx=\"7\" cy=\"7\" r=\"3\"></circle></svg>',\n\n       \"table-split-cells\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"></line><line x1=\"3\" y1=\"15\" x2=\"9\" y2=\"15\"></line><line x1=\"9\" y1=\"3\" x2=\"9\" y2=\"21\"></line><line x1=\"13\" y1=\"17\" x2=\"17\" y2=\"13\" ></line><line x1=\"13\" y1=\"13\" x2=\"17\" y2=\"17\"></line></svg>',\n\n       \"table-top-header\": '<svg width=\"24\" height=\"24\"><path d=\"M19 4a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h14zm-8 11H5v3h6v-3zm8 0h-6v3h6v-3zm0-5h-6v3h6v-3zM5 13h6v-3H5v3z\"/></svg>',\n\n       table: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><line x1=\"3\" y1=\"9\" x2=\"21\" y2=\"9\"></line><line x1=\"3\" y1=\"15\" x2=\"21\" y2=\"15\"></line><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"21\"></line></svg>',\n\n       template: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"></path><path d=\"M19.27 13.73A2.5 2.5 0 0 0 17.5 13h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-1.5c0-.66-.26-1.3-.73-1.77Z\"></path><path d=\"M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-3-3c-1.66 0-3 1-3 3s1 2 1 3.5V13\"></path></svg>',\n\n       \"temporary-placeholder\": '<svg width=\"24\" height=\"24\"><g fill-rule=\"evenodd\"><path d=\"M9 7.6V6h2.5V4.5a.5.5 0 111 0V6H15v1.6a8 8 0 11-6 0zm-2.6 5.3a.5.5 0 00.3.6c.3 0 .6 0 .6-.3l.1-.2a5 5 0 013.3-2.8c.3-.1.4-.4.4-.6-.1-.3-.4-.5-.6-.4a6 6 0 00-4.1 3.7z\"/><circle cx=\"14\" cy=\"4\" r=\"1\"/><circle cx=\"12\" cy=\"2\" r=\"1\"/><circle cx=\"10\" cy=\"4\" r=\"1\"/></g></svg>',\n\n       \"text-color\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m20,21H4c-.552,0-1-.447-1-1s.448-1,1-1h16c.553,0,1,.447,1,1s-.447,1-1,1Z\" id=\"tox-icon-text-color__color\" stroke=\"none\"></path><path d=\"m6 16 6-12 6 12\"></path><path d=\"M8 12h8\"></path></svg>',\n\n       toc: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6H5a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h13l4-3.5L18 6Z\"></path><path d=\"M12 13v8\"></path><path d=\"M12 3v3\"></path></svg>',\n\n       translate: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m5 8 6 6\"></path><path d=\"m4 14 6-6 2-3\"></path><path d=\"M2 5h12\"></path><path d=\"M7 2h1\"></path><path d=\"m22 22-5-10-5 10\"></path><path d=\"M14 18h6\"></path></svg>',\n\n       underline: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4v6a6 6 0 0 0 12 0V4\"></path><line x1=\"4\" y1=\"20\" x2=\"20\" y2=\"20\"></line></svg>',\n\n       undo: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 14 4 9 9 4\"></polyline><path d=\"M20 20v-7a4 4 0 0 0-4-4H4\"></path></svg>',\n\n       unlink: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71\"></path><path d=\"m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71\"></path><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"5\"></line><line x1=\"2\" y1=\"8\" x2=\"5\" y2=\"8\"></line><line x1=\"16\" y1=\"19\" x2=\"16\" y2=\"22\"></line><line x1=\"19\" y1=\"16\" x2=\"22\" y2=\"16\"></line></svg>',\n\n       unlock: '<svg width=\"24\" height=\"24\"><path d=\"M16 5c.8 0 1.5.3 2.1.9.6.6.9 1.3.9 2.1v3h-2V8a1 1 0 00-.3-.7A1 1 0 0016 7h-2a1 1 0 00-.7.3 1 1 0 00-.3.7v3h.3c.2 0 .3 0 .5.2l.2.6v7.4c0 .3 0 .4-.2.6l-.6.2H4.8c-.3 0-.4 0-.6-.2a.7.7 0 01-.2-.6v-7.4c0-.3 0-.4.2-.6l.5-.2H11V8c0-.8.3-1.5.9-2.1.6-.6 1.3-.9 2.1-.9h2z\" fill-rule=\"evenodd\"/></svg>',\n\n       \"unordered-list\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\"></line><line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\"></line><line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\"></line><line x1=\"3\" y1=\"6\" x2=\"3.01\" y2=\"6\"></line><line x1=\"3\" y1=\"12\" x2=\"3.01\" y2=\"12\"></line><line x1=\"3\" y1=\"18\" x2=\"3.01\" y2=\"18\"></line></svg>',\n\n       unselected: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6\"></path><path d=\"M21 19a2 2 0 0 1-2 2\"></path><path d=\"M14 21h1\"></path><path d=\"M21 14v1\"></path></svg>',\n\n       upload: '<svg width=\"24\" height=\"24\"><path d=\"M18 19v-2a1 1 0 012 0v3c0 .6-.4 1-1 1H5a1 1 0 01-1-1v-3a1 1 0 012 0v2h12zM11 6.4L8.7 8.7a1 1 0 01-1.4-1.4l4-4a1 1 0 011.4 0l4 4a1 1 0 11-1.4 1.4L13 6.4V16a1 1 0 01-2 0V6.4z\" fill-rule=\"nonzero\"/></svg>',\n\n       user: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"></path><circle cx=\"12\" cy=\"7\" r=\"4\"></circle></svg>',\n\n       \"vertical-align\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m17 3-5 5-5-5h10\"></path><path d=\"m17 21-5-5-5 5h10\"></path><path d=\"M4 12H2\"></path><path d=\"M10 12H8\"></path><path d=\"M16 12h-2\"></path><path d=\"M22 12h-2\"></path></svg>',\n\n       visualblocks: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"7\" height=\"9\" x=\"3\" y=\"3\"></rect><rect width=\"7\" height=\"5\" x=\"14\" y=\"3\"></rect><rect width=\"7\" height=\"9\" x=\"14\" y=\"12\"></rect><rect width=\"7\" height=\"5\" x=\"3\" y=\"16\"></rect></svg>',\n\n       visualchars: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M13 4v16\"></path><path d=\"M17 4v16\"></path><path d=\"M19 4H9.5a4.5 4.5 0 0 0 0 9H13\"></path></svg>',\n\n       warning: '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"></path><line x1=\"12\" x2=\"12\" y1=\"9\" y2=\"13\"></line><line x1=\"12\" x2=\"12.01\" y1=\"17\" y2=\"17\"></line></svg>',\n\n       \"zoom-in\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle><line x1=\"21\" x2=\"16.65\" y1=\"21\" y2=\"16.65\"></line><line x1=\"11\" x2=\"11\" y1=\"8\" y2=\"14\"></line><line x1=\"8\" x2=\"14\" y1=\"11\" y2=\"11\"></line></svg>',\n\n       \"zoom-out\": '<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle><line x1=\"21\" x2=\"16.65\" y1=\"21\" y2=\"16.65\"></line><line x1=\"8\" x2=\"14\" y1=\"11\" y2=\"11\"></line></svg>'\n   }\n});"
  },
  {
    "path": "app/src/helpers/vendor/tinymce/langs/readme.md",
    "content": "This is where language files should be placed.\n\nPlease DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/\n"
  },
  {
    "path": "app/src/helpers/vendor/tinymce/license.txt",
    "content": "      GNU LESSER GENERAL PUBLIC LICENSE\n           Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n          Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\n      GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n          NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n         END OF TERMS AND CONDITIONS\n\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n\n\n"
  },
  {
    "path": "app/src/helpers/vendor/tinymce/plugins/emoticons/js/emojiimages.js",
    "content": "// Source: npm package: emojilib\n// Images provided by twemoji: https://github.com/twitter/twemoji\nwindow.tinymce.Resource.add(\"tinymce.plugins.emoticons\", {\n  100: {\n    keywords: [ \"score\", \"perfect\", \"numbers\", \"century\", \"exam\", \"quiz\", \"test\", \"pass\", \"hundred\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcaf\" src=\"1f4af.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  1234: {\n    keywords: [ \"numbers\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd22\" src=\"1f522.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  grinning: {\n    keywords: [ \"face\", \"smile\", \"happy\", \"joy\", \":D\", \"grin\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude00\" src=\"1f600.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  grimacing: {\n    keywords: [ \"face\", \"grimace\", \"teeth\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude2c\" src=\"1f62c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  grin: {\n    keywords: [ \"face\", \"happy\", \"smile\", \"joy\", \"kawaii\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude01\" src=\"1f601.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  joy: {\n    keywords: [ \"face\", \"cry\", \"tears\", \"weep\", \"happy\", \"happytears\", \"haha\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude02\" src=\"1f602.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  rofl: {\n    keywords: [ \"face\", \"rolling\", \"floor\", \"laughing\", \"lol\", \"haha\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd23\" src=\"1f923.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  partying: {\n    keywords: [ \"face\", \"celebration\", \"woohoo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd73\" src=\"1f973.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiley: {\n    keywords: [ \"face\", \"happy\", \"joy\", \"haha\", \":D\", \":)\", \"smile\", \"funny\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude03\" src=\"1f603.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smile: {\n    keywords: [ \"face\", \"happy\", \"joy\", \"funny\", \"haha\", \"laugh\", \"like\", \":D\", \":)\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude04\" src=\"1f604.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sweat_smile: {\n    keywords: [ \"face\", \"hot\", \"happy\", \"laugh\", \"sweat\", \"smile\", \"relief\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude05\" src=\"1f605.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  laughing: {\n    keywords: [ \"happy\", \"joy\", \"lol\", \"satisfied\", \"haha\", \"face\", \"glad\", \"XD\", \"laugh\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude06\" src=\"1f606.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  innocent: {\n    keywords: [ \"face\", \"angel\", \"heaven\", \"halo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude07\" src=\"1f607.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  wink: {\n    keywords: [ \"face\", \"happy\", \"mischievous\", \"secret\", \";)\", \"smile\", \"eye\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude09\" src=\"1f609.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  blush: {\n    keywords: [ \"face\", \"smile\", \"happy\", \"flushed\", \"crush\", \"embarrassed\", \"shy\", \"joy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude0a\" src=\"1f60a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  slightly_smiling_face: {\n    keywords: [ \"face\", \"smile\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude42\" src=\"1f642.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  upside_down_face: {\n    keywords: [ \"face\", \"flipped\", \"silly\", \"smile\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude43\" src=\"1f643.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  relaxed: {\n    keywords: [ \"face\", \"blush\", \"massage\", \"happiness\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u263a\\ufe0f\" src=\"263a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  yum: {\n    keywords: [ \"happy\", \"joy\", \"tongue\", \"smile\", \"face\", \"silly\", \"yummy\", \"nom\", \"delicious\", \"savouring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude0b\" src=\"1f60b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  relieved: {\n    keywords: [ \"face\", \"relaxed\", \"phew\", \"massage\", \"happiness\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude0c\" src=\"1f60c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  heart_eyes: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"crush\", \"heart\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude0d\" src=\"1f60d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiling_face_with_three_hearts: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"crush\", \"hearts\", \"adore\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd70\" src=\"1f970.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_heart: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude18\" src=\"1f618.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing: {\n    keywords: [ \"love\", \"like\", \"face\", \"3\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude17\" src=\"1f617.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_smiling_eyes: {\n    keywords: [ \"face\", \"affection\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude19\" src=\"1f619.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_closed_eyes: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude1a\" src=\"1f61a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  stuck_out_tongue_winking_eye: {\n    keywords: [ \"face\", \"prank\", \"childish\", \"playful\", \"mischievous\", \"smile\", \"wink\", \"tongue\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude1c\" src=\"1f61c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  zany: {\n    keywords: [ \"face\", \"goofy\", \"crazy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd2a\" src=\"1f92a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  raised_eyebrow: {\n    keywords: [ \"face\", \"distrust\", \"scepticism\", \"disapproval\", \"disbelief\", \"surprise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd28\" src=\"1f928.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  monocle: {\n    keywords: [ \"face\", \"stuffy\", \"wealthy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd0\" src=\"1f9d0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  stuck_out_tongue_closed_eyes: {\n    keywords: [ \"face\", \"prank\", \"playful\", \"mischievous\", \"smile\", \"tongue\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude1d\" src=\"1f61d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  stuck_out_tongue: {\n    keywords: [ \"face\", \"prank\", \"childish\", \"playful\", \"mischievous\", \"smile\", \"tongue\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude1b\" src=\"1f61b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  money_mouth_face: {\n    keywords: [ \"face\", \"rich\", \"dollar\", \"money\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd11\" src=\"1f911.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  nerd_face: {\n    keywords: [ \"face\", \"nerdy\", \"geek\", \"dork\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd13\" src=\"1f913.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sunglasses: {\n    keywords: [ \"face\", \"cool\", \"smile\", \"summer\", \"beach\", \"sunglass\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude0e\" src=\"1f60e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  star_struck: {\n    keywords: [ \"face\", \"smile\", \"starry\", \"eyes\", \"grinning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd29\" src=\"1f929.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  clown_face: {\n    keywords: [ \"face\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd21\" src=\"1f921.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cowboy_hat_face: {\n    keywords: [ \"face\", \"cowgirl\", \"hat\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd20\" src=\"1f920.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hugs: {\n    keywords: [ \"face\", \"smile\", \"hug\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd17\" src=\"1f917.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smirk: {\n    keywords: [ \"face\", \"smile\", \"mean\", \"prank\", \"smug\", \"sarcasm\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude0f\" src=\"1f60f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  no_mouth: {\n    keywords: [ \"face\", \"hellokitty\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude36\" src=\"1f636.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  neutral_face: {\n    keywords: [ \"indifference\", \"meh\", \":|\", \"neutral\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude10\" src=\"1f610.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  expressionless: {\n    keywords: [ \"face\", \"indifferent\", \"-_-\", \"meh\", \"deadpan\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude11\" src=\"1f611.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  unamused: {\n    keywords: [ \"indifference\", \"bored\", \"straight face\", \"serious\", \"sarcasm\", \"unimpressed\", \"skeptical\", \"dubious\", \"side_eye\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude12\" src=\"1f612.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  roll_eyes: {\n    keywords: [ \"face\", \"eyeroll\", \"frustrated\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude44\" src=\"1f644.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  thinking: {\n    keywords: [ \"face\", \"hmmm\", \"think\", \"consider\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd14\" src=\"1f914.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  lying_face: {\n    keywords: [ \"face\", \"lie\", \"pinocchio\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd25\" src=\"1f925.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hand_over_mouth: {\n    keywords: [ \"face\", \"whoops\", \"shock\", \"surprise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd2d\" src=\"1f92d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  shushing: {\n    keywords: [ \"face\", \"quiet\", \"shhh\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd2b\" src=\"1f92b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  symbols_over_mouth: {\n    keywords: [ \"face\", \"swearing\", \"cursing\", \"cussing\", \"profanity\", \"expletive\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd2c\" src=\"1f92c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  exploding_head: {\n    keywords: [ \"face\", \"shocked\", \"mind\", \"blown\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd2f\" src=\"1f92f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  flushed: {\n    keywords: [ \"face\", \"blush\", \"shy\", \"flattered\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude33\" src=\"1f633.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  disappointed: {\n    keywords: [ \"face\", \"sad\", \"upset\", \"depressed\", \":(\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude1e\" src=\"1f61e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  worried: {\n    keywords: [ \"face\", \"concern\", \"nervous\", \":(\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude1f\" src=\"1f61f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  angry: {\n    keywords: [ \"mad\", \"face\", \"annoyed\", \"frustrated\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude20\" src=\"1f620.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  rage: {\n    keywords: [ \"angry\", \"mad\", \"hate\", \"despise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude21\" src=\"1f621.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pensive: {\n    keywords: [ \"face\", \"sad\", \"depressed\", \"upset\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude14\" src=\"1f614.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  confused: {\n    keywords: [ \"face\", \"indifference\", \"huh\", \"weird\", \"hmmm\", \":/\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude15\" src=\"1f615.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  slightly_frowning_face: {\n    keywords: [ \"face\", \"frowning\", \"disappointed\", \"sad\", \"upset\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude41\" src=\"1f641.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  frowning_face: {\n    keywords: [ \"face\", \"sad\", \"upset\", \"frown\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2639\" src=\"2639.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  persevere: {\n    keywords: [ \"face\", \"sick\", \"no\", \"upset\", \"oops\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude23\" src=\"1f623.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  confounded: {\n    keywords: [ \"face\", \"confused\", \"sick\", \"unwell\", \"oops\", \":S\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude16\" src=\"1f616.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tired_face: {\n    keywords: [ \"sick\", \"whine\", \"upset\", \"frustrated\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude2b\" src=\"1f62b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  weary: {\n    keywords: [ \"face\", \"tired\", \"sleepy\", \"sad\", \"frustrated\", \"upset\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude29\" src=\"1f629.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pleading: {\n    keywords: [ \"face\", \"begging\", \"mercy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd7a\" src=\"1f97a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  triumph: {\n    keywords: [ \"face\", \"gas\", \"phew\", \"proud\", \"pride\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude24\" src=\"1f624.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  open_mouth: {\n    keywords: [ \"face\", \"surprise\", \"impressed\", \"wow\", \"whoa\", \":O\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude2e\" src=\"1f62e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  scream: {\n    keywords: [ \"face\", \"munch\", \"scared\", \"omg\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude31\" src=\"1f631.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  fearful: {\n    keywords: [ \"face\", \"scared\", \"terrified\", \"nervous\", \"oops\", \"huh\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude28\" src=\"1f628.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cold_sweat: {\n    keywords: [ \"face\", \"nervous\", \"sweat\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude30\" src=\"1f630.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hushed: {\n    keywords: [ \"face\", \"woo\", \"shh\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude2f\" src=\"1f62f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  frowning: {\n    keywords: [ \"face\", \"aw\", \"what\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude26\" src=\"1f626.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  anguished: {\n    keywords: [ \"face\", \"stunned\", \"nervous\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude27\" src=\"1f627.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cry: {\n    keywords: [ \"face\", \"tears\", \"sad\", \"depressed\", \"upset\", \":'(\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude22\" src=\"1f622.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  disappointed_relieved: {\n    keywords: [ \"face\", \"phew\", \"sweat\", \"nervous\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude25\" src=\"1f625.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  drooling_face: {\n    keywords: [ \"face\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd24\" src=\"1f924.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sleepy: {\n    keywords: [ \"face\", \"tired\", \"rest\", \"nap\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude2a\" src=\"1f62a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sweat: {\n    keywords: [ \"face\", \"hot\", \"sad\", \"tired\", \"exercise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude13\" src=\"1f613.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hot: {\n    keywords: [ \"face\", \"feverish\", \"heat\", \"red\", \"sweating\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd75\" src=\"1f975.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cold: {\n    keywords: [ \"face\", \"blue\", \"freezing\", \"frozen\", \"frostbite\", \"icicles\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd76\" src=\"1f976.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sob: {\n    keywords: [ \"face\", \"cry\", \"tears\", \"sad\", \"upset\", \"depressed\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude2d\" src=\"1f62d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dizzy_face: {\n    keywords: [ \"spent\", \"unconscious\", \"xox\", \"dizzy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude35\" src=\"1f635.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  astonished: {\n    keywords: [ \"face\", \"xox\", \"surprised\", \"poisoned\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude32\" src=\"1f632.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  zipper_mouth_face: {\n    keywords: [ \"face\", \"sealed\", \"zipper\", \"secret\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd10\" src=\"1f910.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  nauseated_face: {\n    keywords: [ \"face\", \"vomit\", \"gross\", \"green\", \"sick\", \"throw up\", \"ill\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd22\" src=\"1f922.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sneezing_face: {\n    keywords: [ \"face\", \"gesundheit\", \"sneeze\", \"sick\", \"allergy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd27\" src=\"1f927.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  vomiting: {\n    keywords: [ \"face\", \"sick\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd2e\" src=\"1f92e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mask: {\n    keywords: [ \"face\", \"sick\", \"ill\", \"disease\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude37\" src=\"1f637.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  face_with_thermometer: {\n    keywords: [ \"sick\", \"temperature\", \"thermometer\", \"cold\", \"fever\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd12\" src=\"1f912.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  face_with_head_bandage: {\n    keywords: [ \"injured\", \"clumsy\", \"bandage\", \"hurt\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd15\" src=\"1f915.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  woozy: {\n    keywords: [ \"face\", \"dizzy\", \"intoxicated\", \"tipsy\", \"wavy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd74\" src=\"1f974.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sleeping: {\n    keywords: [ \"face\", \"tired\", \"sleepy\", \"night\", \"zzz\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude34\" src=\"1f634.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  zzz: {\n    keywords: [ \"sleepy\", \"tired\", \"dream\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca4\" src=\"1f4a4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  poop: {\n    keywords: [ \"hankey\", \"shitface\", \"fail\", \"turd\", \"shit\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca9\" src=\"1f4a9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiling_imp: {\n    keywords: [ \"devil\", \"horns\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude08\" src=\"1f608.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  imp: {\n    keywords: [ \"devil\", \"angry\", \"horns\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc7f\" src=\"1f47f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  japanese_ogre: {\n    keywords: [ \"monster\", \"red\", \"mask\", \"halloween\", \"scary\", \"creepy\", \"devil\", \"demon\", \"japanese\", \"ogre\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc79\" src=\"1f479.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  japanese_goblin: {\n    keywords: [ \"red\", \"evil\", \"mask\", \"monster\", \"scary\", \"creepy\", \"japanese\", \"goblin\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc7a\" src=\"1f47a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  skull: {\n    keywords: [ \"dead\", \"skeleton\", \"creepy\", \"death\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc80\" src=\"1f480.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  ghost: {\n    keywords: [ \"halloween\", \"spooky\", \"scary\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc7b\" src=\"1f47b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  alien: {\n    keywords: [ \"UFO\", \"paul\", \"weird\", \"outer_space\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc7d\" src=\"1f47d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  robot: {\n    keywords: [ \"computer\", \"machine\", \"bot\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd16\" src=\"1f916.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiley_cat: {\n    keywords: [ \"animal\", \"cats\", \"happy\", \"smile\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude3a\" src=\"1f63a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smile_cat: {\n    keywords: [ \"animal\", \"cats\", \"smile\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude38\" src=\"1f638.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  joy_cat: {\n    keywords: [ \"animal\", \"cats\", \"haha\", \"happy\", \"tears\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude39\" src=\"1f639.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  heart_eyes_cat: {\n    keywords: [ \"animal\", \"love\", \"like\", \"affection\", \"cats\", \"valentines\", \"heart\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude3b\" src=\"1f63b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smirk_cat: {\n    keywords: [ \"animal\", \"cats\", \"smirk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude3c\" src=\"1f63c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_cat: {\n    keywords: [ \"animal\", \"cats\", \"kiss\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude3d\" src=\"1f63d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  scream_cat: {\n    keywords: [ \"animal\", \"cats\", \"munch\", \"scared\", \"scream\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude40\" src=\"1f640.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  crying_cat_face: {\n    keywords: [ \"animal\", \"tears\", \"weep\", \"sad\", \"cats\", \"upset\", \"cry\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude3f\" src=\"1f63f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pouting_cat: {\n    keywords: [ \"animal\", \"cats\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude3e\" src=\"1f63e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  palms_up: {\n    keywords: [ \"hands\", \"gesture\", \"cupped\", \"prayer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd32\" src=\"1f932.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_hands: {\n    keywords: [ \"gesture\", \"hooray\", \"yea\", \"celebration\", \"hands\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4c\" src=\"1f64c.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  clap: {\n    keywords: [ \"hands\", \"praise\", \"applause\", \"congrats\", \"yay\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc4f\" src=\"1f44f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  wave: {\n    keywords: [ \"hands\", \"gesture\", \"goodbye\", \"solong\", \"farewell\", \"hello\", \"hi\", \"palm\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc4b\" src=\"1f44b.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  call_me_hand: {\n    keywords: [ \"hands\", \"gesture\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd19\" src=\"1f919.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  \"+1\": {\n    keywords: [ \"thumbsup\", \"yes\", \"awesome\", \"good\", \"agree\", \"accept\", \"cool\", \"hand\", \"like\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc4d\" src=\"1f44d.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  \"-1\": {\n    keywords: [ \"thumbsdown\", \"no\", \"dislike\", \"hand\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc4e\" src=\"1f44e.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  facepunch: {\n    keywords: [ \"angry\", \"violence\", \"fist\", \"hit\", \"attack\", \"hand\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc4a\" src=\"1f44a.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fist: {\n    keywords: [ \"fingers\", \"hand\", \"grasp\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u270a\" src=\"270a.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fist_left: {\n    keywords: [ \"hand\", \"fistbump\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd1b\" src=\"1f91b.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fist_right: {\n    keywords: [ \"hand\", \"fistbump\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd1c\" src=\"1f91c.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  v: {\n    keywords: [ \"fingers\", \"ohyeah\", \"hand\", \"peace\", \"victory\", \"two\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u270c\" src=\"270c.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  ok_hand: {\n    keywords: [ \"fingers\", \"limbs\", \"perfect\", \"ok\", \"okay\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc4c\" src=\"1f44c.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_hand: {\n    keywords: [ \"fingers\", \"stop\", \"highfive\", \"palm\", \"ban\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u270b\" src=\"270b.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_back_of_hand: {\n    keywords: [ \"fingers\", \"raised\", \"backhand\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd1a\" src=\"1f91a.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  open_hands: {\n    keywords: [ \"fingers\", \"butterfly\", \"hands\", \"open\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc50\" src=\"1f450.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  muscle: {\n    keywords: [ \"arm\", \"flex\", \"hand\", \"summer\", \"strong\", \"biceps\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcaa\" src=\"1f4aa.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pray: {\n    keywords: [ \"please\", \"hope\", \"wish\", \"namaste\", \"highfive\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4f\" src=\"1f64f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  foot: {\n    keywords: [ \"kick\", \"stomp\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb6\" src=\"1f9b6.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  leg: {\n    keywords: [ \"kick\", \"limb\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb5\" src=\"1f9b5.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  handshake: {\n    keywords: [ \"agreement\", \"shake\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd1d\" src=\"1f91d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  point_up: {\n    keywords: [ \"hand\", \"fingers\", \"direction\", \"up\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u261d\" src=\"261d.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_up_2: {\n    keywords: [ \"fingers\", \"hand\", \"direction\", \"up\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc46\" src=\"1f446.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_down: {\n    keywords: [ \"fingers\", \"hand\", \"direction\", \"down\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc47\" src=\"1f447.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_left: {\n    keywords: [ \"direction\", \"fingers\", \"hand\", \"left\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc48\" src=\"1f448.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_right: {\n    keywords: [ \"fingers\", \"hand\", \"direction\", \"right\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc49\" src=\"1f449.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fu: {\n    keywords: [ \"hand\", \"fingers\", \"rude\", \"middle\", \"flipping\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd95\" src=\"1f595.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_hand_with_fingers_splayed: {\n    keywords: [ \"hand\", \"fingers\", \"palm\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd90\" src=\"1f590.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  love_you: {\n    keywords: [ \"hand\", \"fingers\", \"gesture\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd1f\" src=\"1f91f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  metal: {\n    keywords: [ \"hand\", \"fingers\", \"evil_eye\", \"sign_of_horns\", \"rock_on\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd18\" src=\"1f918.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  crossed_fingers: {\n    keywords: [ \"good\", \"lucky\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd1e\" src=\"1f91e.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  vulcan_salute: {\n    keywords: [ \"hand\", \"fingers\", \"spock\", \"star trek\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd96\" src=\"1f596.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  writing_hand: {\n    keywords: [ \"lower_left_ballpoint_pen\", \"stationery\", \"write\", \"compose\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u270d\" src=\"270d.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  selfie: {\n    keywords: [ \"camera\", \"phone\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd33\" src=\"1f933.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  nail_care: {\n    keywords: [ \"beauty\", \"manicure\", \"finger\", \"fashion\", \"nail\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc85\" src=\"1f485.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  lips: {\n    keywords: [ \"mouth\", \"kiss\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc44\" src=\"1f444.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tooth: {\n    keywords: [ \"teeth\", \"dentist\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb7\" src=\"1f9b7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tongue: {\n    keywords: [ \"mouth\", \"playful\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc45\" src=\"1f445.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  ear: {\n    keywords: [ \"face\", \"hear\", \"sound\", \"listen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc42\" src=\"1f442.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  nose: {\n    keywords: [ \"smell\", \"sniff\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc43\" src=\"1f443.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  eye: {\n    keywords: [ \"face\", \"look\", \"see\", \"watch\", \"stare\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc41\" src=\"1f441.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  eyes: {\n    keywords: [ \"look\", \"watch\", \"stalk\", \"peek\", \"see\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc40\" src=\"1f440.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  brain: {\n    keywords: [ \"smart\", \"intelligent\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde0\" src=\"1f9e0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  bust_in_silhouette: {\n    keywords: [ \"user\", \"person\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc64\" src=\"1f464.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  busts_in_silhouette: {\n    keywords: [ \"user\", \"person\", \"human\", \"group\", \"team\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc65\" src=\"1f465.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  speaking_head: {\n    keywords: [ \"user\", \"person\", \"human\", \"sing\", \"say\", \"talk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udde3\" src=\"1f5e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  baby: {\n    keywords: [ \"child\", \"boy\", \"girl\", \"toddler\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc76\" src=\"1f476.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  child: {\n    keywords: [ \"gender-neutral\", \"young\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd2\" src=\"1f9d2.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  boy: {\n    keywords: [ \"man\", \"male\", \"guy\", \"teenager\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc66\" src=\"1f466.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  girl: {\n    keywords: [ \"female\", \"woman\", \"teenager\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc67\" src=\"1f467.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  adult: {\n    keywords: [ \"gender-neutral\", \"person\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd1\" src=\"1f9d1.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man: {\n    keywords: [ \"mustache\", \"father\", \"dad\", \"guy\", \"classy\", \"sir\", \"moustache\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\" src=\"1f468.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman: {\n    keywords: [ \"female\", \"girls\", \"lady\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\" src=\"1f469.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  blonde_woman: {\n    keywords: [ \"woman\", \"female\", \"girl\", \"blonde\", \"person\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc71\\u200d\\u2640\\ufe0f\" src=\"1f471-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  blonde_man: {\n    keywords: [ \"man\", \"male\", \"boy\", \"blonde\", \"guy\", \"person\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc71\" src=\"1f471.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  bearded_person: {\n    keywords: [ \"person\", \"bewhiskered\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd4\" src=\"1f9d4.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  older_adult: {\n    keywords: [ \"human\", \"elder\", \"senior\", \"gender-neutral\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd3\" src=\"1f9d3.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  older_man: {\n    keywords: [ \"human\", \"male\", \"men\", \"old\", \"elder\", \"senior\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc74\" src=\"1f474.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  older_woman: {\n    keywords: [ \"human\", \"female\", \"women\", \"lady\", \"old\", \"elder\", \"senior\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc75\" src=\"1f475.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_with_gua_pi_mao: {\n    keywords: [ \"male\", \"boy\", \"chinese\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc72\" src=\"1f472.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_with_headscarf: {\n    keywords: [ \"female\", \"hijab\", \"mantilla\", \"tichel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd5\" src=\"1f9d5.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_with_turban: {\n    keywords: [ \"female\", \"indian\", \"hinduism\", \"arabs\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc73\\u200d\\u2640\\ufe0f\" src=\"1f473-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_with_turban: {\n    keywords: [ \"male\", \"indian\", \"hinduism\", \"arabs\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc73\" src=\"1f473.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  policewoman: {\n    keywords: [ \"woman\", \"police\", \"law\", \"legal\", \"enforcement\", \"arrest\", \"911\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6e\\u200d\\u2640\\ufe0f\" src=\"1f46e-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  policeman: {\n    keywords: [ \"man\", \"police\", \"law\", \"legal\", \"enforcement\", \"arrest\", \"911\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6e\" src=\"1f46e.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  construction_worker_woman: {\n    keywords: [ \"female\", \"human\", \"wip\", \"build\", \"construction\", \"worker\", \"labor\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc77\\u200d\\u2640\\ufe0f\" src=\"1f477-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  construction_worker_man: {\n    keywords: [ \"male\", \"human\", \"wip\", \"guy\", \"build\", \"construction\", \"worker\", \"labor\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc77\" src=\"1f477.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  guardswoman: {\n    keywords: [ \"uk\", \"gb\", \"british\", \"female\", \"royal\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc82\\u200d\\u2640\\ufe0f\" src=\"1f482-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  guardsman: {\n    keywords: [ \"uk\", \"gb\", \"british\", \"male\", \"guy\", \"royal\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc82\" src=\"1f482.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  female_detective: {\n    keywords: [ \"human\", \"spy\", \"detective\", \"female\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd75\\ufe0f\\u200d\\u2640\\ufe0f\" src=\"1f575-fe0f-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  male_detective: {\n    keywords: [ \"human\", \"spy\", \"detective\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd75\" src=\"1f575.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_health_worker: {\n    keywords: [ \"doctor\", \"nurse\", \"therapist\", \"healthcare\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\u2695\\ufe0f\" src=\"1f469-200d-2695-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_health_worker: {\n    keywords: [ \"doctor\", \"nurse\", \"therapist\", \"healthcare\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\u2695\\ufe0f\" src=\"1f468-200d-2695-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_farmer: {\n    keywords: [ \"rancher\", \"gardener\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83c\\udf3e\" src=\"1f469-200d-1f33e.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_farmer: {\n    keywords: [ \"rancher\", \"gardener\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83c\\udf3e\" src=\"1f468-200d-1f33e.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_cook: {\n    keywords: [ \"chef\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83c\\udf73\" src=\"1f469-200d-1f373.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_cook: {\n    keywords: [ \"chef\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83c\\udf73\" src=\"1f468-200d-1f373.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_student: {\n    keywords: [ \"graduate\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83c\\udf93\" src=\"1f469-200d-1f393.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_student: {\n    keywords: [ \"graduate\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83c\\udf93\" src=\"1f468-200d-1f393.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_singer: {\n    keywords: [ \"rockstar\", \"entertainer\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83c\\udfa4\" src=\"1f469-200d-1f3a4.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_singer: {\n    keywords: [ \"rockstar\", \"entertainer\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83c\\udfa4\" src=\"1f468-200d-1f3a4.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_teacher: {\n    keywords: [ \"instructor\", \"professor\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83c\\udfeb\" src=\"1f469-200d-1f3eb.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_teacher: {\n    keywords: [ \"instructor\", \"professor\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83c\\udfeb\" src=\"1f468-200d-1f3eb.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_factory_worker: {\n    keywords: [ \"assembly\", \"industrial\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83c\\udfed\" src=\"1f469-200d-1f3ed.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_factory_worker: {\n    keywords: [ \"assembly\", \"industrial\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83c\\udfed\" src=\"1f468-200d-1f3ed.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_technologist: {\n    keywords: [ \"coder\", \"developer\", \"engineer\", \"programmer\", \"software\", \"woman\", \"human\", \"laptop\", \"computer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udcbb\" src=\"1f469-200d-1f4bb.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_technologist: {\n    keywords: [ \"coder\", \"developer\", \"engineer\", \"programmer\", \"software\", \"man\", \"human\", \"laptop\", \"computer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udcbb\" src=\"1f468-200d-1f4bb.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_office_worker: {\n    keywords: [ \"business\", \"manager\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udcbc\" src=\"1f469-200d-1f4bc.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_office_worker: {\n    keywords: [ \"business\", \"manager\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udcbc\" src=\"1f468-200d-1f4bc.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_mechanic: {\n    keywords: [ \"plumber\", \"woman\", \"human\", \"wrench\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udd27\" src=\"1f469-200d-1f527.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_mechanic: {\n    keywords: [ \"plumber\", \"man\", \"human\", \"wrench\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udd27\" src=\"1f468-200d-1f527.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_scientist: {\n    keywords: [ \"biologist\", \"chemist\", \"engineer\", \"physicist\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udd2c\" src=\"1f469-200d-1f52c.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_scientist: {\n    keywords: [ \"biologist\", \"chemist\", \"engineer\", \"physicist\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udd2c\" src=\"1f468-200d-1f52c.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_artist: {\n    keywords: [ \"painter\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83c\\udfa8\" src=\"1f469-200d-1f3a8.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_artist: {\n    keywords: [ \"painter\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83c\\udfa8\" src=\"1f468-200d-1f3a8.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_firefighter: {\n    keywords: [ \"fireman\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\ude92\" src=\"1f469-200d-1f692.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_firefighter: {\n    keywords: [ \"fireman\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\ude92\" src=\"1f468-200d-1f692.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_pilot: {\n    keywords: [ \"aviator\", \"plane\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\u2708\\ufe0f\" src=\"1f469-200d-2708-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_pilot: {\n    keywords: [ \"aviator\", \"plane\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\u2708\\ufe0f\" src=\"1f468-200d-2708-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_astronaut: {\n    keywords: [ \"space\", \"rocket\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\ude80\" src=\"1f469-200d-1f680.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_astronaut: {\n    keywords: [ \"space\", \"rocket\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\ude80\" src=\"1f468-200d-1f680.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_judge: {\n    keywords: [ \"justice\", \"court\", \"woman\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\u2696\\ufe0f\" src=\"1f469-200d-2696-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_judge: {\n    keywords: [ \"justice\", \"court\", \"man\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\u2696\\ufe0f\" src=\"1f468-200d-2696-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_superhero: {\n    keywords: [ \"woman\", \"female\", \"good\", \"heroine\", \"superpowers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb8\\u200d\\u2640\\ufe0f\" src=\"1f9b8-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_superhero: {\n    keywords: [ \"man\", \"male\", \"good\", \"hero\", \"superpowers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb8\\u200d\\u2642\\ufe0f\" src=\"1f9b8-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_supervillain: {\n    keywords: [ \"woman\", \"female\", \"evil\", \"bad\", \"criminal\", \"heroine\", \"superpowers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb9\\u200d\\u2640\\ufe0f\" src=\"1f9b9-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_supervillain: {\n    keywords: [ \"man\", \"male\", \"evil\", \"bad\", \"criminal\", \"hero\", \"superpowers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb9\\u200d\\u2642\\ufe0f\" src=\"1f9b9-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  mrs_claus: {\n    keywords: [ \"woman\", \"female\", \"xmas\", \"mother christmas\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd36\" src=\"1f936.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  santa: {\n    keywords: [ \"festival\", \"man\", \"male\", \"xmas\", \"father christmas\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf85\" src=\"1f385.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  sorceress: {\n    keywords: [ \"woman\", \"female\", \"mage\", \"witch\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd9\\u200d\\u2640\\ufe0f\" src=\"1f9d9-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  wizard: {\n    keywords: [ \"man\", \"male\", \"mage\", \"sorcerer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd9\\u200d\\u2642\\ufe0f\" src=\"1f9d9-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_elf: {\n    keywords: [ \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddd\\u200d\\u2640\\ufe0f\" src=\"1f9dd-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_elf: {\n    keywords: [ \"man\", \"male\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddd\\u200d\\u2642\\ufe0f\" src=\"1f9dd-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_vampire: {\n    keywords: [ \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddb\\u200d\\u2640\\ufe0f\" src=\"1f9db-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_vampire: {\n    keywords: [ \"man\", \"male\", \"dracula\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddb\\u200d\\u2642\\ufe0f\" src=\"1f9db-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_zombie: {\n    keywords: [ \"woman\", \"female\", \"undead\", \"walking dead\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddf\\u200d\\u2640\\ufe0f\" src=\"1f9df-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  man_zombie: {\n    keywords: [ \"man\", \"male\", \"dracula\", \"undead\", \"walking dead\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddf\\u200d\\u2642\\ufe0f\" src=\"1f9df-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  woman_genie: {\n    keywords: [ \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddde\\u200d\\u2640\\ufe0f\" src=\"1f9de-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  man_genie: {\n    keywords: [ \"man\", \"male\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddde\\u200d\\u2642\\ufe0f\" src=\"1f9de-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mermaid: {\n    keywords: [ \"woman\", \"female\", \"merwoman\", \"ariel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddc\\u200d\\u2640\\ufe0f\" src=\"1f9dc-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  merman: {\n    keywords: [ \"man\", \"male\", \"triton\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udddc\\u200d\\u2642\\ufe0f\" src=\"1f9dc-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_fairy: {\n    keywords: [ \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddda\\u200d\\u2640\\ufe0f\" src=\"1f9da-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_fairy: {\n    keywords: [ \"man\", \"male\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddda\\u200d\\u2642\\ufe0f\" src=\"1f9da-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  angel: {\n    keywords: [ \"heaven\", \"wings\", \"halo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc7c\" src=\"1f47c.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pregnant_woman: {\n    keywords: [ \"baby\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd30\" src=\"1f930.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  breastfeeding: {\n    keywords: [ \"nursing\", \"baby\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd31\" src=\"1f931.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  princess: {\n    keywords: [ \"girl\", \"woman\", \"female\", \"blond\", \"crown\", \"royal\", \"queen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc78\" src=\"1f478.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  prince: {\n    keywords: [ \"boy\", \"man\", \"male\", \"crown\", \"royal\", \"king\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd34\" src=\"1f934.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  bride_with_veil: {\n    keywords: [ \"couple\", \"marriage\", \"wedding\", \"woman\", \"bride\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc70\" src=\"1f470.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_in_tuxedo: {\n    keywords: [ \"couple\", \"marriage\", \"wedding\", \"groom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd35\" src=\"1f935.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  running_woman: {\n    keywords: [ \"woman\", \"walking\", \"exercise\", \"race\", \"running\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc3\\u200d\\u2640\\ufe0f\" src=\"1f3c3-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  running_man: {\n    keywords: [ \"man\", \"walking\", \"exercise\", \"race\", \"running\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc3\" src=\"1f3c3.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  walking_woman: {\n    keywords: [ \"human\", \"feet\", \"steps\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb6\\u200d\\u2640\\ufe0f\" src=\"1f6b6-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  walking_man: {\n    keywords: [ \"human\", \"feet\", \"steps\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb6\" src=\"1f6b6.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  dancer: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"fun\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc83\" src=\"1f483.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_dancing: {\n    keywords: [ \"male\", \"boy\", \"fun\", \"dancer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd7a\" src=\"1f57a.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  dancing_women: {\n    keywords: [ \"female\", \"bunny\", \"women\", \"girls\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6f\" src=\"1f46f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dancing_men: {\n    keywords: [ \"male\", \"bunny\", \"men\", \"boys\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6f\\u200d\\u2642\\ufe0f\" src=\"1f46f-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couple: {\n    keywords: [ \"pair\", \"people\", \"human\", \"love\", \"date\", \"dating\", \"like\", \"affection\", \"valentines\", \"marriage\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6b\" src=\"1f46b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  two_men_holding_hands: {\n    keywords: [ \"pair\", \"couple\", \"love\", \"like\", \"bromance\", \"friendship\", \"people\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6c\" src=\"1f46c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  two_women_holding_hands: {\n    keywords: [ \"pair\", \"friendship\", \"couple\", \"love\", \"like\", \"female\", \"people\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6d\" src=\"1f46d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  bowing_woman: {\n    keywords: [ \"woman\", \"female\", \"girl\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude47\\u200d\\u2640\\ufe0f\" src=\"1f647-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  bowing_man: {\n    keywords: [ \"man\", \"male\", \"boy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude47\" src=\"1f647.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_facepalming: {\n    keywords: [ \"man\", \"male\", \"boy\", \"disbelief\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd26\\u200d\\u2642\\ufe0f\" src=\"1f926-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_facepalming: {\n    keywords: [ \"woman\", \"female\", \"girl\", \"disbelief\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd26\\u200d\\u2640\\ufe0f\" src=\"1f926-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_shrugging: {\n    keywords: [ \"woman\", \"female\", \"girl\", \"confused\", \"indifferent\", \"doubt\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd37\" src=\"1f937.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_shrugging: {\n    keywords: [ \"man\", \"male\", \"boy\", \"confused\", \"indifferent\", \"doubt\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd37\\u200d\\u2642\\ufe0f\" src=\"1f937-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  tipping_hand_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"human\", \"information\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc81\" src=\"1f481.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  tipping_hand_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"human\", \"information\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc81\\u200d\\u2642\\ufe0f\" src=\"1f481-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  no_good_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"nope\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude45\" src=\"1f645.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  no_good_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"nope\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude45\\u200d\\u2642\\ufe0f\" src=\"1f645-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  ok_woman: {\n    keywords: [ \"women\", \"girl\", \"female\", \"pink\", \"human\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude46\" src=\"1f646.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  ok_man: {\n    keywords: [ \"men\", \"boy\", \"male\", \"blue\", \"human\", \"man\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude46\\u200d\\u2642\\ufe0f\" src=\"1f646-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raising_hand_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4b\" src=\"1f64b.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raising_hand_man: {\n    keywords: [ \"male\", \"boy\", \"man\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4b\\u200d\\u2642\\ufe0f\" src=\"1f64b-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pouting_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4e\" src=\"1f64e.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pouting_man: {\n    keywords: [ \"male\", \"boy\", \"man\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4e\\u200d\\u2642\\ufe0f\" src=\"1f64e-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  frowning_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"sad\", \"depressed\", \"discouraged\", \"unhappy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4d\" src=\"1f64d.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  frowning_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"sad\", \"depressed\", \"discouraged\", \"unhappy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4d\\u200d\\u2642\\ufe0f\" src=\"1f64d-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  haircut_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc87\" src=\"1f487.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  haircut_man: {\n    keywords: [ \"male\", \"boy\", \"man\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc87\\u200d\\u2642\\ufe0f\" src=\"1f487-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  massage_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"head\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc86\" src=\"1f486.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  massage_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"head\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc86\\u200d\\u2642\\ufe0f\" src=\"1f486-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_in_steamy_room: {\n    keywords: [ \"female\", \"woman\", \"spa\", \"steamroom\", \"sauna\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd6\\u200d\\u2640\\ufe0f\" src=\"1f9d6-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_in_steamy_room: {\n    keywords: [ \"male\", \"man\", \"spa\", \"steamroom\", \"sauna\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd6\\u200d\\u2642\\ufe0f\" src=\"1f9d6-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  couple_with_heart_woman_man: {\n    keywords: [ \"pair\", \"love\", \"like\", \"affection\", \"human\", \"dating\", \"valentines\", \"marriage\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc91\" src=\"1f491.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couple_with_heart_woman_woman: {\n    keywords: [ \"pair\", \"love\", \"like\", \"affection\", \"human\", \"dating\", \"valentines\", \"marriage\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc69\" src=\"1f469-200d-2764-fe0f-200d-1f469.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couple_with_heart_man_man: {\n    keywords: [ \"pair\", \"love\", \"like\", \"affection\", \"human\", \"dating\", \"valentines\", \"marriage\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc68\" src=\"1f468-200d-2764-fe0f-200d-1f468.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couplekiss_man_woman: {\n    keywords: [ \"pair\", \"valentines\", \"love\", \"like\", \"dating\", \"marriage\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc8f\" src=\"1f48f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couplekiss_woman_woman: {\n    keywords: [ \"pair\", \"valentines\", \"love\", \"like\", \"dating\", \"marriage\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc8b\\u200d\\ud83d\\udc69\" src=\"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couplekiss_man_man: {\n    keywords: [ \"pair\", \"valentines\", \"love\", \"like\", \"dating\", \"marriage\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc8b\\u200d\\ud83d\\udc68\" src=\"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_boy: {\n    keywords: [ \"home\", \"parents\", \"child\", \"mom\", \"dad\", \"father\", \"mother\", \"people\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc6a\" src=\"1f46a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"child\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\" src=\"1f468-200d-1f469-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_girl_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f469-200d-1f467-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_boy_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f469-200d-1f466-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_girl_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\" src=\"1f468-200d-1f469-200d-1f467-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc66\" src=\"1f469-200d-1f469-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\" src=\"1f469-200d-1f469-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_girl_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\" src=\"1f469-200d-1f469-200d-1f467-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_boy_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\" src=\"1f469-200d-1f469-200d-1f466-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_girl_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\" src=\"1f469-200d-1f469-200d-1f467-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f468-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc67\" src=\"1f468-200d-1f468-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_girl_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f468-200d-1f467-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_boy_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f468-200d-1f466-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_girl_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\" src=\"1f468-200d-1f468-200d-1f467-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc66\" src=\"1f469-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc67\" src=\"1f469-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_girl_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\" src=\"1f469-200d-1f467-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_boy_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\" src=\"1f469-200d-1f466-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_girl_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\" src=\"1f469-200d-1f467-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc67\" src=\"1f468-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_girl_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f467-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_boy_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\" src=\"1f468-200d-1f466-200d-1f466.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_girl_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\" src=\"1f468-200d-1f467-200d-1f467.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  yarn: {\n    keywords: [ \"ball\", \"crochet\", \"knit\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf6\" src=\"1f9f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  thread: {\n    keywords: [ \"needle\", \"sewing\", \"spool\", \"string\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf5\" src=\"1f9f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  coat: {\n    keywords: [ \"jacket\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde5\" src=\"1f9e5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  labcoat: {\n    keywords: [ \"doctor\", \"experiment\", \"scientist\", \"chemist\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd7c\" src=\"1f97c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  womans_clothes: {\n    keywords: [ \"fashion\", \"shopping_bags\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc5a\" src=\"1f45a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tshirt: {\n    keywords: [ \"fashion\", \"cloth\", \"casual\", \"shirt\", \"tee\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc55\" src=\"1f455.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  jeans: {\n    keywords: [ \"fashion\", \"shopping\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc56\" src=\"1f456.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  necktie: {\n    keywords: [ \"shirt\", \"suitup\", \"formal\", \"fashion\", \"cloth\", \"business\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc54\" src=\"1f454.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dress: {\n    keywords: [ \"clothes\", \"fashion\", \"shopping\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc57\" src=\"1f457.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  bikini: {\n    keywords: [ \"swimming\", \"female\", \"woman\", \"girl\", \"fashion\", \"beach\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc59\" src=\"1f459.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kimono: {\n    keywords: [ \"dress\", \"fashion\", \"women\", \"female\", \"japanese\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc58\" src=\"1f458.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  lipstick: {\n    keywords: [ \"female\", \"girl\", \"fashion\", \"woman\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc84\" src=\"1f484.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kiss: {\n    keywords: [ \"face\", \"lips\", \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc8b\" src=\"1f48b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  footprints: {\n    keywords: [ \"feet\", \"tracking\", \"walking\", \"beach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc63\" src=\"1f463.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  flat_shoe: {\n    keywords: [ \"ballet\", \"slip-on\", \"slipper\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd7f\" src=\"1f97f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  high_heel: {\n    keywords: [ \"fashion\", \"shoes\", \"female\", \"pumps\", \"stiletto\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc60\" src=\"1f460.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sandal: {\n    keywords: [ \"shoes\", \"fashion\", \"flip flops\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc61\" src=\"1f461.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  boot: {\n    keywords: [ \"shoes\", \"fashion\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc62\" src=\"1f462.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mans_shoe: {\n    keywords: [ \"fashion\", \"male\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc5e\" src=\"1f45e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  athletic_shoe: {\n    keywords: [ \"shoes\", \"sports\", \"sneakers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc5f\" src=\"1f45f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hiking_boot: {\n    keywords: [ \"backpacking\", \"camping\", \"hiking\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd7e\" src=\"1f97e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  socks: {\n    keywords: [ \"stockings\", \"clothes\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde6\" src=\"1f9e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  gloves: {\n    keywords: [ \"hands\", \"winter\", \"clothes\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde4\" src=\"1f9e4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  scarf: {\n    keywords: [ \"neck\", \"winter\", \"clothes\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde3\" src=\"1f9e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  womans_hat: {\n    keywords: [ \"fashion\", \"accessories\", \"female\", \"lady\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc52\" src=\"1f452.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tophat: {\n    keywords: [ \"magic\", \"gentleman\", \"classy\", \"circus\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa9\" src=\"1f3a9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  billed_hat: {\n    keywords: [ \"cap\", \"baseball\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde2\" src=\"1f9e2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  rescue_worker_helmet: {\n    keywords: [ \"construction\", \"build\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26d1\" src=\"26d1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mortar_board: {\n    keywords: [ \"school\", \"college\", \"degree\", \"university\", \"graduation\", \"cap\", \"hat\", \"legal\", \"learn\", \"education\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf93\" src=\"1f393.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  crown: {\n    keywords: [ \"king\", \"kod\", \"leader\", \"royalty\", \"lord\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc51\" src=\"1f451.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  school_satchel: {\n    keywords: [ \"student\", \"education\", \"bag\", \"backpack\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf92\" src=\"1f392.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  luggage: {\n    keywords: [ \"packing\", \"travel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf3\" src=\"1f9f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pouch: {\n    keywords: [ \"bag\", \"accessories\", \"shopping\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc5d\" src=\"1f45d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  purse: {\n    keywords: [ \"fashion\", \"accessories\", \"money\", \"sales\", \"shopping\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc5b\" src=\"1f45b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  handbag: {\n    keywords: [ \"fashion\", \"accessory\", \"accessories\", \"shopping\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc5c\" src=\"1f45c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  briefcase: {\n    keywords: [ \"business\", \"documents\", \"work\", \"law\", \"legal\", \"job\", \"career\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcbc\" src=\"1f4bc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  eyeglasses: {\n    keywords: [ \"fashion\", \"accessories\", \"eyesight\", \"nerdy\", \"dork\", \"geek\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc53\" src=\"1f453.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dark_sunglasses: {\n    keywords: [ \"face\", \"cool\", \"accessories\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd76\" src=\"1f576.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  goggles: {\n    keywords: [ \"eyes\", \"protection\", \"safety\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd7d\" src=\"1f97d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  ring: {\n    keywords: [ \"wedding\", \"propose\", \"marriage\", \"valentines\", \"diamond\", \"fashion\", \"jewelry\", \"gem\", \"engagement\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc8d\" src=\"1f48d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  closed_umbrella: {\n    keywords: [ \"weather\", \"rain\", \"drizzle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf02\" src=\"1f302.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dog: {\n    keywords: [ \"animal\", \"friend\", \"nature\", \"woof\", \"puppy\", \"pet\", \"faithful\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc36\" src=\"1f436.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cat: {\n    keywords: [ \"animal\", \"meow\", \"nature\", \"pet\", \"kitten\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc31\" src=\"1f431.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mouse: {\n    keywords: [ \"animal\", \"nature\", \"cheese_wedge\", \"rodent\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc2d\" src=\"1f42d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hamster: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc39\" src=\"1f439.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rabbit: {\n    keywords: [ \"animal\", \"nature\", \"pet\", \"spring\", \"magic\", \"bunny\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc30\" src=\"1f430.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fox_face: {\n    keywords: [ \"animal\", \"nature\", \"face\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd8a\" src=\"1f98a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bear: {\n    keywords: [ \"animal\", \"nature\", \"wild\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc3b\" src=\"1f43b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  panda_face: {\n    keywords: [ \"animal\", \"nature\", \"panda\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc3c\" src=\"1f43c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  koala: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc28\" src=\"1f428.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tiger: {\n    keywords: [ \"animal\", \"cat\", \"danger\", \"wild\", \"nature\", \"roar\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc2f\" src=\"1f42f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  lion: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd81\" src=\"1f981.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cow: {\n    keywords: [ \"beef\", \"ox\", \"animal\", \"nature\", \"moo\", \"milk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc2e\" src=\"1f42e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  pig: {\n    keywords: [ \"animal\", \"oink\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc37\" src=\"1f437.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  pig_nose: {\n    keywords: [ \"animal\", \"oink\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc3d\" src=\"1f43d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  frog: {\n    keywords: [ \"animal\", \"nature\", \"croak\", \"toad\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc38\" src=\"1f438.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  squid: {\n    keywords: [ \"animal\", \"nature\", \"ocean\", \"sea\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd91\" src=\"1f991.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  octopus: {\n    keywords: [ \"animal\", \"creature\", \"ocean\", \"sea\", \"nature\", \"beach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc19\" src=\"1f419.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shrimp: {\n    keywords: [ \"animal\", \"ocean\", \"nature\", \"seafood\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd90\" src=\"1f990.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  monkey_face: {\n    keywords: [ \"animal\", \"nature\", \"circus\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc35\" src=\"1f435.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  gorilla: {\n    keywords: [ \"animal\", \"nature\", \"circus\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd8d\" src=\"1f98d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  see_no_evil: {\n    keywords: [ \"monkey\", \"animal\", \"nature\", \"haha\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude48\" src=\"1f648.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hear_no_evil: {\n    keywords: [ \"animal\", \"monkey\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude49\" src=\"1f649.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  speak_no_evil: {\n    keywords: [ \"monkey\", \"animal\", \"nature\", \"omg\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude4a\" src=\"1f64a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  monkey: {\n    keywords: [ \"animal\", \"nature\", \"banana\", \"circus\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc12\" src=\"1f412.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  chicken: {\n    keywords: [ \"animal\", \"cluck\", \"nature\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc14\" src=\"1f414.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  penguin: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc27\" src=\"1f427.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bird: {\n    keywords: [ \"animal\", \"nature\", \"fly\", \"tweet\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc26\" src=\"1f426.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  baby_chick: {\n    keywords: [ \"animal\", \"chicken\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc24\" src=\"1f424.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hatching_chick: {\n    keywords: [ \"animal\", \"chicken\", \"egg\", \"born\", \"baby\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc23\" src=\"1f423.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hatched_chick: {\n    keywords: [ \"animal\", \"chicken\", \"baby\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc25\" src=\"1f425.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  duck: {\n    keywords: [ \"animal\", \"nature\", \"bird\", \"mallard\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd86\" src=\"1f986.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  eagle: {\n    keywords: [ \"animal\", \"nature\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd85\" src=\"1f985.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  owl: {\n    keywords: [ \"animal\", \"nature\", \"bird\", \"hoot\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd89\" src=\"1f989.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bat: {\n    keywords: [ \"animal\", \"nature\", \"blind\", \"vampire\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd87\" src=\"1f987.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  wolf: {\n    keywords: [ \"animal\", \"nature\", \"wild\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc3a\" src=\"1f43a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  boar: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc17\" src=\"1f417.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  horse: {\n    keywords: [ \"animal\", \"brown\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc34\" src=\"1f434.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  unicorn: {\n    keywords: [ \"animal\", \"nature\", \"mystical\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd84\" src=\"1f984.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  honeybee: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"bug\", \"spring\", \"honey\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc1d\" src=\"1f41d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bug: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"worm\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc1b\" src=\"1f41b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  butterfly: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"caterpillar\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd8b\" src=\"1f98b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snail: {\n    keywords: [ \"slow\", \"animal\", \"shell\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc0c\" src=\"1f40c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  beetle: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"ladybug\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc1e\" src=\"1f41e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ant: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"bug\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc1c\" src=\"1f41c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  grasshopper: {\n    keywords: [ \"animal\", \"cricket\", \"chirp\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd97\" src=\"1f997.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  spider: {\n    keywords: [ \"animal\", \"arachnid\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd77\" src=\"1f577.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  scorpion: {\n    keywords: [ \"animal\", \"arachnid\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd82\" src=\"1f982.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  crab: {\n    keywords: [ \"animal\", \"crustacean\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd80\" src=\"1f980.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snake: {\n    keywords: [ \"animal\", \"evil\", \"nature\", \"hiss\", \"python\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc0d\" src=\"1f40d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  lizard: {\n    keywords: [ \"animal\", \"nature\", \"reptile\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd8e\" src=\"1f98e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  \"t-rex\": {\n    keywords: [ \"animal\", \"nature\", \"dinosaur\", \"tyrannosaurus\", \"extinct\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd96\" src=\"1f996.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sauropod: {\n    keywords: [ \"animal\", \"nature\", \"dinosaur\", \"brachiosaurus\", \"brontosaurus\", \"diplodocus\", \"extinct\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd95\" src=\"1f995.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  turtle: {\n    keywords: [ \"animal\", \"slow\", \"nature\", \"tortoise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc22\" src=\"1f422.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tropical_fish: {\n    keywords: [ \"animal\", \"swim\", \"ocean\", \"beach\", \"nemo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc20\" src=\"1f420.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fish: {\n    keywords: [ \"animal\", \"food\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc1f\" src=\"1f41f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  blowfish: {\n    keywords: [ \"animal\", \"nature\", \"food\", \"sea\", \"ocean\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc21\" src=\"1f421.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dolphin: {\n    keywords: [ \"animal\", \"nature\", \"fish\", \"sea\", \"ocean\", \"flipper\", \"fins\", \"beach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc2c\" src=\"1f42c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shark: {\n    keywords: [ \"animal\", \"nature\", \"fish\", \"sea\", \"ocean\", \"jaws\", \"fins\", \"beach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd88\" src=\"1f988.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  whale: {\n    keywords: [ \"animal\", \"nature\", \"sea\", \"ocean\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc33\" src=\"1f433.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  whale2: {\n    keywords: [ \"animal\", \"nature\", \"sea\", \"ocean\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc0b\" src=\"1f40b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  crocodile: {\n    keywords: [ \"animal\", \"nature\", \"reptile\", \"lizard\", \"alligator\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc0a\" src=\"1f40a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  leopard: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc06\" src=\"1f406.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  zebra: {\n    keywords: [ \"animal\", \"nature\", \"stripes\", \"safari\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd93\" src=\"1f993.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tiger2: {\n    keywords: [ \"animal\", \"nature\", \"roar\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc05\" src=\"1f405.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  water_buffalo: {\n    keywords: [ \"animal\", \"nature\", \"ox\", \"cow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc03\" src=\"1f403.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ox: {\n    keywords: [ \"animal\", \"cow\", \"beef\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc02\" src=\"1f402.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cow2: {\n    keywords: [ \"beef\", \"ox\", \"animal\", \"nature\", \"moo\", \"milk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc04\" src=\"1f404.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  deer: {\n    keywords: [ \"animal\", \"nature\", \"horns\", \"venison\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd8c\" src=\"1f98c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dromedary_camel: {\n    keywords: [ \"animal\", \"hot\", \"desert\", \"hump\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc2a\" src=\"1f42a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  camel: {\n    keywords: [ \"animal\", \"nature\", \"hot\", \"desert\", \"hump\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc2b\" src=\"1f42b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  giraffe: {\n    keywords: [ \"animal\", \"nature\", \"spots\", \"safari\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd92\" src=\"1f992.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  elephant: {\n    keywords: [ \"animal\", \"nature\", \"nose\", \"th\", \"circus\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc18\" src=\"1f418.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rhinoceros: {\n    keywords: [ \"animal\", \"nature\", \"horn\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd8f\" src=\"1f98f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  goat: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc10\" src=\"1f410.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ram: {\n    keywords: [ \"animal\", \"sheep\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc0f\" src=\"1f40f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sheep: {\n    keywords: [ \"animal\", \"nature\", \"wool\", \"shipit\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc11\" src=\"1f411.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  racehorse: {\n    keywords: [ \"animal\", \"gamble\", \"luck\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc0e\" src=\"1f40e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  pig2: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc16\" src=\"1f416.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rat: {\n    keywords: [ \"animal\", \"mouse\", \"rodent\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc00\" src=\"1f400.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mouse2: {\n    keywords: [ \"animal\", \"nature\", \"rodent\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc01\" src=\"1f401.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rooster: {\n    keywords: [ \"animal\", \"nature\", \"chicken\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc13\" src=\"1f413.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  turkey: {\n    keywords: [ \"animal\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd83\" src=\"1f983.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dove: {\n    keywords: [ \"animal\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd4a\" src=\"1f54a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dog2: {\n    keywords: [ \"animal\", \"nature\", \"friend\", \"doge\", \"pet\", \"faithful\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc15\" src=\"1f415.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  poodle: {\n    keywords: [ \"dog\", \"animal\", \"101\", \"nature\", \"pet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc29\" src=\"1f429.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cat2: {\n    keywords: [ \"animal\", \"meow\", \"pet\", \"cats\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc08\" src=\"1f408.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rabbit2: {\n    keywords: [ \"animal\", \"nature\", \"pet\", \"magic\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc07\" src=\"1f407.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  chipmunk: {\n    keywords: [ \"animal\", \"nature\", \"rodent\", \"squirrel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc3f\" src=\"1f43f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hedgehog: {\n    keywords: [ \"animal\", \"nature\", \"spiny\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd94\" src=\"1f994.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  raccoon: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd9d\" src=\"1f99d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  llama: {\n    keywords: [ \"animal\", \"nature\", \"alpaca\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd99\" src=\"1f999.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hippopotamus: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd9b\" src=\"1f99b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  kangaroo: {\n    keywords: [ \"animal\", \"nature\", \"australia\", \"joey\", \"hop\", \"marsupial\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd98\" src=\"1f998.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  badger: {\n    keywords: [ \"animal\", \"nature\", \"honey\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udda1\" src=\"1f9a1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  swan: {\n    keywords: [ \"animal\", \"nature\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udda2\" src=\"1f9a2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  peacock: {\n    keywords: [ \"animal\", \"nature\", \"peahen\", \"bird\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd9a\" src=\"1f99a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  parrot: {\n    keywords: [ \"animal\", \"nature\", \"bird\", \"pirate\", \"talk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd9c\" src=\"1f99c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  lobster: {\n    keywords: [ \"animal\", \"nature\", \"bisque\", \"claws\", \"seafood\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd9e\" src=\"1f99e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mosquito: {\n    keywords: [ \"animal\", \"nature\", \"insect\", \"malaria\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd9f\" src=\"1f99f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  paw_prints: {\n    keywords: [ \"animal\", \"tracking\", \"footprints\", \"dog\", \"cat\", \"pet\", \"feet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc3e\" src=\"1f43e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dragon: {\n    keywords: [ \"animal\", \"myth\", \"nature\", \"chinese\", \"green\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc09\" src=\"1f409.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dragon_face: {\n    keywords: [ \"animal\", \"myth\", \"nature\", \"chinese\", \"green\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc32\" src=\"1f432.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cactus: {\n    keywords: [ \"vegetable\", \"plant\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf35\" src=\"1f335.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  christmas_tree: {\n    keywords: [ \"festival\", \"vacation\", \"december\", \"xmas\", \"celebration\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf84\" src=\"1f384.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  evergreen_tree: {\n    keywords: [ \"plant\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf32\" src=\"1f332.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  deciduous_tree: {\n    keywords: [ \"plant\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf33\" src=\"1f333.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  palm_tree: {\n    keywords: [ \"plant\", \"vegetable\", \"nature\", \"summer\", \"beach\", \"mojito\", \"tropical\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf34\" src=\"1f334.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  seedling: {\n    keywords: [ \"plant\", \"nature\", \"grass\", \"lawn\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf31\" src=\"1f331.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  herb: {\n    keywords: [ \"vegetable\", \"plant\", \"medicine\", \"weed\", \"grass\", \"lawn\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf3f\" src=\"1f33f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shamrock: {\n    keywords: [ \"vegetable\", \"plant\", \"nature\", \"irish\", \"clover\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2618\" src=\"2618.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  four_leaf_clover: {\n    keywords: [ \"vegetable\", \"plant\", \"nature\", \"lucky\", \"irish\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf40\" src=\"1f340.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bamboo: {\n    keywords: [ \"plant\", \"nature\", \"vegetable\", \"panda\", \"pine_decoration\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf8d\" src=\"1f38d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tanabata_tree: {\n    keywords: [ \"plant\", \"nature\", \"branch\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf8b\" src=\"1f38b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  leaves: {\n    keywords: [ \"nature\", \"plant\", \"tree\", \"vegetable\", \"grass\", \"lawn\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf43\" src=\"1f343.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fallen_leaf: {\n    keywords: [ \"nature\", \"plant\", \"vegetable\", \"leaves\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf42\" src=\"1f342.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  maple_leaf: {\n    keywords: [ \"nature\", \"plant\", \"vegetable\", \"ca\", \"fall\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf41\" src=\"1f341.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ear_of_rice: {\n    keywords: [ \"nature\", \"plant\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf3e\" src=\"1f33e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hibiscus: {\n    keywords: [ \"plant\", \"vegetable\", \"flowers\", \"beach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf3a\" src=\"1f33a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sunflower: {\n    keywords: [ \"nature\", \"plant\", \"fall\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf3b\" src=\"1f33b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rose: {\n    keywords: [ \"flowers\", \"valentines\", \"love\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf39\" src=\"1f339.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  wilted_flower: {\n    keywords: [ \"plant\", \"nature\", \"flower\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd40\" src=\"1f940.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tulip: {\n    keywords: [ \"flowers\", \"plant\", \"nature\", \"summer\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf37\" src=\"1f337.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  blossom: {\n    keywords: [ \"nature\", \"flowers\", \"yellow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf3c\" src=\"1f33c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cherry_blossom: {\n    keywords: [ \"nature\", \"plant\", \"spring\", \"flower\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf38\" src=\"1f338.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bouquet: {\n    keywords: [ \"flowers\", \"nature\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc90\" src=\"1f490.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mushroom: {\n    keywords: [ \"plant\", \"vegetable\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf44\" src=\"1f344.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  chestnut: {\n    keywords: [ \"food\", \"squirrel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf30\" src=\"1f330.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  jack_o_lantern: {\n    keywords: [ \"halloween\", \"light\", \"pumpkin\", \"creepy\", \"fall\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf83\" src=\"1f383.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shell: {\n    keywords: [ \"nature\", \"sea\", \"beach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc1a\" src=\"1f41a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  spider_web: {\n    keywords: [ \"animal\", \"insect\", \"arachnid\", \"silk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd78\" src=\"1f578.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  earth_americas: {\n    keywords: [ \"globe\", \"world\", \"USA\", \"international\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf0e\" src=\"1f30e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  earth_africa: {\n    keywords: [ \"globe\", \"world\", \"international\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf0d\" src=\"1f30d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  earth_asia: {\n    keywords: [ \"globe\", \"world\", \"east\", \"international\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf0f\" src=\"1f30f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  full_moon: {\n    keywords: [ \"nature\", \"yellow\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf15\" src=\"1f315.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waning_gibbous_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\", \"waxing_gibbous_moon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf16\" src=\"1f316.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  last_quarter_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf17\" src=\"1f317.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waning_crescent_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf18\" src=\"1f318.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  new_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf11\" src=\"1f311.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waxing_crescent_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf12\" src=\"1f312.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  first_quarter_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf13\" src=\"1f313.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waxing_gibbous_moon: {\n    keywords: [ \"nature\", \"night\", \"sky\", \"gray\", \"twilight\", \"planet\", \"space\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf14\" src=\"1f314.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  new_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf1a\" src=\"1f31a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  full_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf1d\" src=\"1f31d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  first_quarter_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf1b\" src=\"1f31b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  last_quarter_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf1c\" src=\"1f31c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_with_face: {\n    keywords: [ \"nature\", \"morning\", \"sky\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf1e\" src=\"1f31e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  crescent_moon: {\n    keywords: [ \"night\", \"sleep\", \"sky\", \"evening\", \"magic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf19\" src=\"1f319.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  star: {\n    keywords: [ \"night\", \"yellow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2b50\" src=\"2b50.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  star2: {\n    keywords: [ \"night\", \"sparkle\", \"awesome\", \"good\", \"magic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf1f\" src=\"1f31f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dizzy: {\n    keywords: [ \"star\", \"sparkle\", \"shoot\", \"magic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcab\" src=\"1f4ab.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sparkles: {\n    keywords: [ \"stars\", \"shine\", \"shiny\", \"cool\", \"awesome\", \"good\", \"magic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2728\" src=\"2728.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  comet: {\n    keywords: [ \"space\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2604\" src=\"2604.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sunny: {\n    keywords: [ \"weather\", \"nature\", \"brightness\", \"summer\", \"beach\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2600\\ufe0f\" src=\"2600.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_behind_small_cloud: {\n    keywords: [ \"weather\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf24\" src=\"1f324.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  partly_sunny: {\n    keywords: [ \"weather\", \"nature\", \"cloudy\", \"morning\", \"fall\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26c5\" src=\"26c5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_behind_large_cloud: {\n    keywords: [ \"weather\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf25\" src=\"1f325.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_behind_rain_cloud: {\n    keywords: [ \"weather\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf26\" src=\"1f326.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud: {\n    keywords: [ \"weather\", \"sky\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2601\\ufe0f\" src=\"2601.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_rain: {\n    keywords: [ \"weather\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf27\" src=\"1f327.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_lightning_and_rain: {\n    keywords: [ \"weather\", \"lightning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26c8\" src=\"26c8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_lightning: {\n    keywords: [ \"weather\", \"thunder\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf29\" src=\"1f329.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  zap: {\n    keywords: [ \"thunder\", \"weather\", \"lightning bolt\", \"fast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26a1\" src=\"26a1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fire: {\n    keywords: [ \"hot\", \"cook\", \"flame\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd25\" src=\"1f525.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  boom: {\n    keywords: [ \"bomb\", \"explode\", \"explosion\", \"collision\", \"blown\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca5\" src=\"1f4a5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snowflake: {\n    keywords: [ \"winter\", \"season\", \"cold\", \"weather\", \"christmas\", \"xmas\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2744\\ufe0f\" src=\"2744.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_snow: {\n    keywords: [ \"weather\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf28\" src=\"1f328.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snowman: {\n    keywords: [ \"winter\", \"season\", \"cold\", \"weather\", \"christmas\", \"xmas\", \"frozen\", \"without_snow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26c4\" src=\"26c4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snowman_with_snow: {\n    keywords: [ \"winter\", \"season\", \"cold\", \"weather\", \"christmas\", \"xmas\", \"frozen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2603\" src=\"2603.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  wind_face: {\n    keywords: [ \"gust\", \"air\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf2c\" src=\"1f32c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dash: {\n    keywords: [ \"wind\", \"air\", \"fast\", \"shoo\", \"fart\", \"smoke\", \"puff\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca8\" src=\"1f4a8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tornado: {\n    keywords: [ \"weather\", \"cyclone\", \"twister\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf2a\" src=\"1f32a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fog: {\n    keywords: [ \"weather\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf2b\" src=\"1f32b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  open_umbrella: {\n    keywords: [ \"weather\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2602\" src=\"2602.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  umbrella: {\n    keywords: [ \"rainy\", \"weather\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2614\" src=\"2614.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  droplet: {\n    keywords: [ \"water\", \"drip\", \"faucet\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca7\" src=\"1f4a7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sweat_drops: {\n    keywords: [ \"water\", \"drip\", \"oops\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca6\" src=\"1f4a6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ocean: {\n    keywords: [ \"sea\", \"water\", \"wave\", \"nature\", \"tsunami\", \"disaster\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf0a\" src=\"1f30a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  green_apple: {\n    keywords: [ \"fruit\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf4f\" src=\"1f34f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  apple: {\n    keywords: [ \"fruit\", \"mac\", \"school\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf4e\" src=\"1f34e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pear: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf50\" src=\"1f350.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tangerine: {\n    keywords: [ \"food\", \"fruit\", \"nature\", \"orange\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf4a\" src=\"1f34a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  lemon: {\n    keywords: [ \"fruit\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf4b\" src=\"1f34b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  banana: {\n    keywords: [ \"fruit\", \"food\", \"monkey\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf4c\" src=\"1f34c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  watermelon: {\n    keywords: [ \"fruit\", \"food\", \"picnic\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf49\" src=\"1f349.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  grapes: {\n    keywords: [ \"fruit\", \"food\", \"wine\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf47\" src=\"1f347.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  strawberry: {\n    keywords: [ \"fruit\", \"food\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf53\" src=\"1f353.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  melon: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf48\" src=\"1f348.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cherries: {\n    keywords: [ \"food\", \"fruit\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf52\" src=\"1f352.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  peach: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf51\" src=\"1f351.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pineapple: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf4d\" src=\"1f34d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  coconut: {\n    keywords: [ \"fruit\", \"nature\", \"food\", \"palm\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd65\" src=\"1f965.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  kiwi_fruit: {\n    keywords: [ \"fruit\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd5d\" src=\"1f95d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  mango: {\n    keywords: [ \"fruit\", \"food\", \"tropical\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd6d\" src=\"1f96d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  avocado: {\n    keywords: [ \"fruit\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd51\" src=\"1f951.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  broccoli: {\n    keywords: [ \"fruit\", \"food\", \"vegetable\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd66\" src=\"1f966.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tomato: {\n    keywords: [ \"fruit\", \"vegetable\", \"nature\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf45\" src=\"1f345.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  eggplant: {\n    keywords: [ \"vegetable\", \"nature\", \"food\", \"aubergine\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf46\" src=\"1f346.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cucumber: {\n    keywords: [ \"fruit\", \"food\", \"pickle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd52\" src=\"1f952.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  carrot: {\n    keywords: [ \"vegetable\", \"food\", \"orange\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd55\" src=\"1f955.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  hot_pepper: {\n    keywords: [ \"food\", \"spicy\", \"chilli\", \"chili\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf36\" src=\"1f336.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  potato: {\n    keywords: [ \"food\", \"tuber\", \"vegatable\", \"starch\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd54\" src=\"1f954.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  corn: {\n    keywords: [ \"food\", \"vegetable\", \"plant\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf3d\" src=\"1f33d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  leafy_greens: {\n    keywords: [ \"food\", \"vegetable\", \"plant\", \"bok choy\", \"cabbage\", \"kale\", \"lettuce\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd6c\" src=\"1f96c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sweet_potato: {\n    keywords: [ \"food\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf60\" src=\"1f360.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  peanuts: {\n    keywords: [ \"food\", \"nut\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd5c\" src=\"1f95c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  honey_pot: {\n    keywords: [ \"bees\", \"sweet\", \"kitchen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf6f\" src=\"1f36f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  croissant: {\n    keywords: [ \"food\", \"bread\", \"french\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd50\" src=\"1f950.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bread: {\n    keywords: [ \"food\", \"wheat\", \"breakfast\", \"toast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf5e\" src=\"1f35e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  baguette_bread: {\n    keywords: [ \"food\", \"bread\", \"french\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd56\" src=\"1f956.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bagel: {\n    keywords: [ \"food\", \"bread\", \"bakery\", \"schmear\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd6f\" src=\"1f96f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pretzel: {\n    keywords: [ \"food\", \"bread\", \"twisted\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd68\" src=\"1f968.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cheese: {\n    keywords: [ \"food\", \"chadder\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddc0\" src=\"1f9c0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  egg: {\n    keywords: [ \"food\", \"chicken\", \"breakfast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd5a\" src=\"1f95a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bacon: {\n    keywords: [ \"food\", \"breakfast\", \"pork\", \"pig\", \"meat\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd53\" src=\"1f953.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  steak: {\n    keywords: [ \"food\", \"cow\", \"meat\", \"cut\", \"chop\", \"lambchop\", \"porkchop\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd69\" src=\"1f969.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pancakes: {\n    keywords: [ \"food\", \"breakfast\", \"flapjacks\", \"hotcakes\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd5e\" src=\"1f95e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  poultry_leg: {\n    keywords: [ \"food\", \"meat\", \"drumstick\", \"bird\", \"chicken\", \"turkey\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf57\" src=\"1f357.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  meat_on_bone: {\n    keywords: [ \"good\", \"food\", \"drumstick\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf56\" src=\"1f356.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bone: {\n    keywords: [ \"skeleton\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddb4\" src=\"1f9b4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fried_shrimp: {\n    keywords: [ \"food\", \"animal\", \"appetizer\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf64\" src=\"1f364.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fried_egg: {\n    keywords: [ \"food\", \"breakfast\", \"kitchen\", \"egg\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf73\" src=\"1f373.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  hamburger: {\n    keywords: [ \"meat\", \"fast food\", \"beef\", \"cheeseburger\", \"mcdonalds\", \"burger king\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf54\" src=\"1f354.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fries: {\n    keywords: [ \"chips\", \"snack\", \"fast food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf5f\" src=\"1f35f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  stuffed_flatbread: {\n    keywords: [ \"food\", \"flatbread\", \"stuffed\", \"gyro\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd59\" src=\"1f959.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  hotdog: {\n    keywords: [ \"food\", \"frankfurter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf2d\" src=\"1f32d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pizza: {\n    keywords: [ \"food\", \"party\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf55\" src=\"1f355.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sandwich: {\n    keywords: [ \"food\", \"lunch\", \"bread\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd6a\" src=\"1f96a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  canned_food: {\n    keywords: [ \"food\", \"soup\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd6b\" src=\"1f96b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  spaghetti: {\n    keywords: [ \"food\", \"italian\", \"noodle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf5d\" src=\"1f35d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  taco: {\n    keywords: [ \"food\", \"mexican\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf2e\" src=\"1f32e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  burrito: {\n    keywords: [ \"food\", \"mexican\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf2f\" src=\"1f32f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  green_salad: {\n    keywords: [ \"food\", \"healthy\", \"lettuce\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd57\" src=\"1f957.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  shallow_pan_of_food: {\n    keywords: [ \"food\", \"cooking\", \"casserole\", \"paella\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd58\" src=\"1f958.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  ramen: {\n    keywords: [ \"food\", \"japanese\", \"noodle\", \"chopsticks\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf5c\" src=\"1f35c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  stew: {\n    keywords: [ \"food\", \"meat\", \"soup\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf72\" src=\"1f372.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fish_cake: {\n    keywords: [ \"food\", \"japan\", \"sea\", \"beach\", \"narutomaki\", \"pink\", \"swirl\", \"kamaboko\", \"surimi\", \"ramen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf65\" src=\"1f365.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fortune_cookie: {\n    keywords: [ \"food\", \"prophecy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd60\" src=\"1f960.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sushi: {\n    keywords: [ \"food\", \"fish\", \"japanese\", \"rice\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf63\" src=\"1f363.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bento: {\n    keywords: [ \"food\", \"japanese\", \"box\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf71\" src=\"1f371.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  curry: {\n    keywords: [ \"food\", \"spicy\", \"hot\", \"indian\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf5b\" src=\"1f35b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  rice_ball: {\n    keywords: [ \"food\", \"japanese\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf59\" src=\"1f359.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  rice: {\n    keywords: [ \"food\", \"china\", \"asian\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf5a\" src=\"1f35a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  rice_cracker: {\n    keywords: [ \"food\", \"japanese\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf58\" src=\"1f358.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  oden: {\n    keywords: [ \"food\", \"japanese\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf62\" src=\"1f362.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  dango: {\n    keywords: [ \"food\", \"dessert\", \"sweet\", \"japanese\", \"barbecue\", \"meat\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf61\" src=\"1f361.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  shaved_ice: {\n    keywords: [ \"hot\", \"dessert\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf67\" src=\"1f367.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  ice_cream: {\n    keywords: [ \"food\", \"hot\", \"dessert\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf68\" src=\"1f368.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  icecream: {\n    keywords: [ \"food\", \"hot\", \"dessert\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf66\" src=\"1f366.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pie: {\n    keywords: [ \"food\", \"dessert\", \"pastry\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd67\" src=\"1f967.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cake: {\n    keywords: [ \"food\", \"dessert\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf70\" src=\"1f370.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cupcake: {\n    keywords: [ \"food\", \"dessert\", \"bakery\", \"sweet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddc1\" src=\"1f9c1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  moon_cake: {\n    keywords: [ \"food\", \"autumn\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd6e\" src=\"1f96e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  birthday: {\n    keywords: [ \"food\", \"dessert\", \"cake\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf82\" src=\"1f382.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  custard: {\n    keywords: [ \"dessert\", \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf6e\" src=\"1f36e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  candy: {\n    keywords: [ \"snack\", \"dessert\", \"sweet\", \"lolly\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf6c\" src=\"1f36c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  lollipop: {\n    keywords: [ \"food\", \"snack\", \"candy\", \"sweet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf6d\" src=\"1f36d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  chocolate_bar: {\n    keywords: [ \"food\", \"snack\", \"dessert\", \"sweet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf6b\" src=\"1f36b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  popcorn: {\n    keywords: [ \"food\", \"movie theater\", \"films\", \"snack\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf7f\" src=\"1f37f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  dumpling: {\n    keywords: [ \"food\", \"empanada\", \"pierogi\", \"potsticker\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd5f\" src=\"1f95f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  doughnut: {\n    keywords: [ \"food\", \"dessert\", \"snack\", \"sweet\", \"donut\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf69\" src=\"1f369.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cookie: {\n    keywords: [ \"food\", \"snack\", \"oreo\", \"chocolate\", \"sweet\", \"dessert\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf6a\" src=\"1f36a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  milk_glass: {\n    keywords: [ \"beverage\", \"drink\", \"cow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd5b\" src=\"1f95b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  beer: {\n    keywords: [ \"relax\", \"beverage\", \"drink\", \"drunk\", \"party\", \"pub\", \"summer\", \"alcohol\", \"booze\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf7a\" src=\"1f37a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  beers: {\n    keywords: [ \"relax\", \"beverage\", \"drink\", \"drunk\", \"party\", \"pub\", \"summer\", \"alcohol\", \"booze\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf7b\" src=\"1f37b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  clinking_glasses: {\n    keywords: [ \"beverage\", \"drink\", \"party\", \"alcohol\", \"celebrate\", \"cheers\", \"wine\", \"champagne\", \"toast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd42\" src=\"1f942.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  wine_glass: {\n    keywords: [ \"drink\", \"beverage\", \"drunk\", \"alcohol\", \"booze\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf77\" src=\"1f377.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tumbler_glass: {\n    keywords: [ \"drink\", \"beverage\", \"drunk\", \"alcohol\", \"liquor\", \"booze\", \"bourbon\", \"scotch\", \"whisky\", \"glass\", \"shot\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd43\" src=\"1f943.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cocktail: {\n    keywords: [ \"drink\", \"drunk\", \"alcohol\", \"beverage\", \"booze\", \"mojito\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf78\" src=\"1f378.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tropical_drink: {\n    keywords: [ \"beverage\", \"cocktail\", \"summer\", \"beach\", \"alcohol\", \"booze\", \"mojito\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf79\" src=\"1f379.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  champagne: {\n    keywords: [ \"drink\", \"wine\", \"bottle\", \"celebration\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf7e\" src=\"1f37e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sake: {\n    keywords: [ \"wine\", \"drink\", \"drunk\", \"beverage\", \"japanese\", \"alcohol\", \"booze\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf76\" src=\"1f376.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tea: {\n    keywords: [ \"drink\", \"bowl\", \"breakfast\", \"green\", \"british\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf75\" src=\"1f375.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cup_with_straw: {\n    keywords: [ \"drink\", \"soda\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd64\" src=\"1f964.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  coffee: {\n    keywords: [ \"beverage\", \"caffeine\", \"latte\", \"espresso\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2615\" src=\"2615.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  baby_bottle: {\n    keywords: [ \"food\", \"container\", \"milk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf7c\" src=\"1f37c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  salt: {\n    keywords: [ \"condiment\", \"shaker\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddc2\" src=\"1f9c2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  spoon: {\n    keywords: [ \"cutlery\", \"kitchen\", \"tableware\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd44\" src=\"1f944.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fork_and_knife: {\n    keywords: [ \"cutlery\", \"kitchen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf74\" src=\"1f374.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  plate_with_cutlery: {\n    keywords: [ \"food\", \"eat\", \"meal\", \"lunch\", \"dinner\", \"restaurant\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf7d\" src=\"1f37d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bowl_with_spoon: {\n    keywords: [ \"food\", \"breakfast\", \"cereal\", \"oatmeal\", \"porridge\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd63\" src=\"1f963.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  takeout_box: {\n    keywords: [ \"food\", \"leftovers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd61\" src=\"1f961.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  chopsticks: {\n    keywords: [ \"food\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd62\" src=\"1f962.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  soccer: {\n    keywords: [ \"sports\", \"football\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26bd\" src=\"26bd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  basketball: {\n    keywords: [ \"sports\", \"balls\", \"NBA\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc0\" src=\"1f3c0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  football: {\n    keywords: [ \"sports\", \"balls\", \"NFL\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc8\" src=\"1f3c8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  baseball: {\n    keywords: [ \"sports\", \"balls\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26be\" src=\"26be.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  softball: {\n    keywords: [ \"sports\", \"balls\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd4e\" src=\"1f94e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  tennis: {\n    keywords: [ \"sports\", \"balls\", \"green\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfbe\" src=\"1f3be.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  volleyball: {\n    keywords: [ \"sports\", \"balls\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd0\" src=\"1f3d0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  rugby_football: {\n    keywords: [ \"sports\", \"team\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc9\" src=\"1f3c9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  flying_disc: {\n    keywords: [ \"sports\", \"frisbee\", \"ultimate\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd4f\" src=\"1f94f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"8ball\": {\n    keywords: [ \"pool\", \"hobby\", \"game\", \"luck\", \"magic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb1\" src=\"1f3b1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  golf: {\n    keywords: [ \"sports\", \"business\", \"flag\", \"hole\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f3\" src=\"26f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  golfing_woman: {\n    keywords: [ \"sports\", \"business\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfcc\\ufe0f\\u200d\\u2640\\ufe0f\" src=\"1f3cc-fe0f-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  golfing_man: {\n    keywords: [ \"sports\", \"business\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfcc\" src=\"1f3cc.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  ping_pong: {\n    keywords: [ \"sports\", \"pingpong\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd3\" src=\"1f3d3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  badminton: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff8\" src=\"1f3f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  goal_net: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd45\" src=\"1f945.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  ice_hockey: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd2\" src=\"1f3d2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  field_hockey: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd1\" src=\"1f3d1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  lacrosse: {\n    keywords: [ \"sports\", \"ball\", \"stick\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd4d\" src=\"1f94d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  cricket: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfcf\" src=\"1f3cf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  ski: {\n    keywords: [ \"sports\", \"winter\", \"cold\", \"snow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfbf\" src=\"1f3bf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  skier: {\n    keywords: [ \"sports\", \"winter\", \"snow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f7\" src=\"26f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  snowboarder: {\n    keywords: [ \"sports\", \"winter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc2\" src=\"1f3c2.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  person_fencing: {\n    keywords: [ \"sports\", \"fencing\", \"sword\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd3a\" src=\"1f93a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  women_wrestling: {\n    keywords: [ \"sports\", \"wrestlers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd3c\\u200d\\u2640\\ufe0f\" src=\"1f93c-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  men_wrestling: {\n    keywords: [ \"sports\", \"wrestlers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd3c\\u200d\\u2642\\ufe0f\" src=\"1f93c-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  woman_cartwheeling: {\n    keywords: [ \"gymnastics\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd38\\u200d\\u2640\\ufe0f\" src=\"1f938-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_cartwheeling: {\n    keywords: [ \"gymnastics\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd38\\u200d\\u2642\\ufe0f\" src=\"1f938-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  woman_playing_handball: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd3e\\u200d\\u2640\\ufe0f\" src=\"1f93e-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_playing_handball: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd3e\\u200d\\u2642\\ufe0f\" src=\"1f93e-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  ice_skate: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f8\" src=\"26f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  curling_stone: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd4c\" src=\"1f94c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  skateboard: {\n    keywords: [ \"board\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef9\" src=\"1f6f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  sled: {\n    keywords: [ \"sleigh\", \"luge\", \"toboggan\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef7\" src=\"1f6f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  bow_and_arrow: {\n    keywords: [ \"sports\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff9\" src=\"1f3f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  fishing_pole_and_fish: {\n    keywords: [ \"food\", \"hobby\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa3\" src=\"1f3a3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  boxing_glove: {\n    keywords: [ \"sports\", \"fighting\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd4a\" src=\"1f94a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  martial_arts_uniform: {\n    keywords: [ \"judo\", \"karate\", \"taekwondo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd4b\" src=\"1f94b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  rowing_woman: {\n    keywords: [ \"sports\", \"hobby\", \"water\", \"ship\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea3\\u200d\\u2640\\ufe0f\" src=\"1f6a3-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  rowing_man: {\n    keywords: [ \"sports\", \"hobby\", \"water\", \"ship\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea3\" src=\"1f6a3.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  climbing_woman: {\n    keywords: [ \"sports\", \"hobby\", \"woman\", \"female\", \"rock\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd7\\u200d\\u2640\\ufe0f\" src=\"1f9d7-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  climbing_man: {\n    keywords: [ \"sports\", \"hobby\", \"man\", \"male\", \"rock\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd7\\u200d\\u2642\\ufe0f\" src=\"1f9d7-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  swimming_woman: {\n    keywords: [ \"sports\", \"exercise\", \"human\", \"athlete\", \"water\", \"summer\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfca\\u200d\\u2640\\ufe0f\" src=\"1f3ca-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  swimming_man: {\n    keywords: [ \"sports\", \"exercise\", \"human\", \"athlete\", \"water\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfca\" src=\"1f3ca.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  woman_playing_water_polo: {\n    keywords: [ \"sports\", \"pool\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd3d\\u200d\\u2640\\ufe0f\" src=\"1f93d-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_playing_water_polo: {\n    keywords: [ \"sports\", \"pool\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd3d\\u200d\\u2642\\ufe0f\" src=\"1f93d-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  woman_in_lotus_position: {\n    keywords: [ \"woman\", \"female\", \"meditation\", \"yoga\", \"serenity\", \"zen\", \"mindfulness\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd8\\u200d\\u2640\\ufe0f\" src=\"1f9d8-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_in_lotus_position: {\n    keywords: [ \"man\", \"male\", \"meditation\", \"yoga\", \"serenity\", \"zen\", \"mindfulness\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddd8\\u200d\\u2642\\ufe0f\" src=\"1f9d8-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  surfing_woman: {\n    keywords: [ \"sports\", \"ocean\", \"sea\", \"summer\", \"beach\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc4\\u200d\\u2640\\ufe0f\" src=\"1f3c4-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  surfing_man: {\n    keywords: [ \"sports\", \"ocean\", \"sea\", \"summer\", \"beach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc4\" src=\"1f3c4.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  bath: {\n    keywords: [ \"clean\", \"shower\", \"bathroom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udec0\" src=\"1f6c0.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  basketball_woman: {\n    keywords: [ \"sports\", \"human\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f9\\ufe0f\\u200d\\u2640\\ufe0f\" src=\"26f9-fe0f-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  basketball_man: {\n    keywords: [ \"sports\", \"human\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f9\" src=\"26f9.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  weight_lifting_woman: {\n    keywords: [ \"sports\", \"training\", \"exercise\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfcb\\ufe0f\\u200d\\u2640\\ufe0f\" src=\"1f3cb-fe0f-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  weight_lifting_man: {\n    keywords: [ \"sports\", \"training\", \"exercise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfcb\" src=\"1f3cb.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  biking_woman: {\n    keywords: [ \"sports\", \"bike\", \"exercise\", \"hipster\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb4\\u200d\\u2640\\ufe0f\" src=\"1f6b4-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  biking_man: {\n    keywords: [ \"sports\", \"bike\", \"exercise\", \"hipster\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb4\" src=\"1f6b4.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  mountain_biking_woman: {\n    keywords: [ \"transportation\", \"sports\", \"human\", \"race\", \"bike\", \"woman\", \"female\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb5\\u200d\\u2640\\ufe0f\" src=\"1f6b5-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  mountain_biking_man: {\n    keywords: [ \"transportation\", \"sports\", \"human\", \"race\", \"bike\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb5\" src=\"1f6b5.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  horse_racing: {\n    keywords: [ \"animal\", \"betting\", \"competition\", \"gambling\", \"luck\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc7\" src=\"1f3c7.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  business_suit_levitating: {\n    keywords: [ \"suit\", \"business\", \"levitate\", \"hover\", \"jump\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd74\" src=\"1f574.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  trophy: {\n    keywords: [ \"win\", \"award\", \"contest\", \"place\", \"ftw\", \"ceremony\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc6\" src=\"1f3c6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  running_shirt_with_sash: {\n    keywords: [ \"play\", \"pageant\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfbd\" src=\"1f3bd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  medal_sports: {\n    keywords: [ \"award\", \"winning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc5\" src=\"1f3c5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  medal_military: {\n    keywords: [ \"award\", \"winning\", \"army\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf96\" src=\"1f396.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"1st_place_medal\": {\n    keywords: [ \"award\", \"winning\", \"first\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd47\" src=\"1f947.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"2nd_place_medal\": {\n    keywords: [ \"award\", \"second\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd48\" src=\"1f948.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"3rd_place_medal\": {\n    keywords: [ \"award\", \"third\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd49\" src=\"1f949.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  reminder_ribbon: {\n    keywords: [ \"sports\", \"cause\", \"support\", \"awareness\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf97\" src=\"1f397.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  rosette: {\n    keywords: [ \"flower\", \"decoration\", \"military\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff5\" src=\"1f3f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  ticket: {\n    keywords: [ \"event\", \"concert\", \"pass\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfab\" src=\"1f3ab.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  tickets: {\n    keywords: [ \"sports\", \"concert\", \"entrance\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf9f\" src=\"1f39f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  performing_arts: {\n    keywords: [ \"acting\", \"theater\", \"drama\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfad\" src=\"1f3ad.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  art: {\n    keywords: [ \"design\", \"paint\", \"draw\", \"colors\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa8\" src=\"1f3a8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  circus_tent: {\n    keywords: [ \"festival\", \"carnival\", \"party\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfaa\" src=\"1f3aa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  woman_juggling: {\n    keywords: [ \"juggle\", \"balance\", \"skill\", \"multitask\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd39\\u200d\\u2640\\ufe0f\" src=\"1f939-200d-2640-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_juggling: {\n    keywords: [ \"juggle\", \"balance\", \"skill\", \"multitask\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd39\\u200d\\u2642\\ufe0f\" src=\"1f939-200d-2642-fe0f.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  microphone: {\n    keywords: [ \"sound\", \"music\", \"PA\", \"sing\", \"talkshow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa4\" src=\"1f3a4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  headphones: {\n    keywords: [ \"music\", \"score\", \"gadgets\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa7\" src=\"1f3a7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  musical_score: {\n    keywords: [ \"treble\", \"clef\", \"compose\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfbc\" src=\"1f3bc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  musical_keyboard: {\n    keywords: [ \"piano\", \"instrument\", \"compose\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb9\" src=\"1f3b9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  drum: {\n    keywords: [ \"music\", \"instrument\", \"drumsticks\", \"snare\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udd41\" src=\"1f941.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  saxophone: {\n    keywords: [ \"music\", \"instrument\", \"jazz\", \"blues\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb7\" src=\"1f3b7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  trumpet: {\n    keywords: [ \"music\", \"brass\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfba\" src=\"1f3ba.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  guitar: {\n    keywords: [ \"music\", \"instrument\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb8\" src=\"1f3b8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  violin: {\n    keywords: [ \"music\", \"instrument\", \"orchestra\", \"symphony\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfbb\" src=\"1f3bb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  clapper: {\n    keywords: [ \"movie\", \"film\", \"record\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfac\" src=\"1f3ac.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  video_game: {\n    keywords: [ \"play\", \"console\", \"PS4\", \"controller\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfae\" src=\"1f3ae.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  space_invader: {\n    keywords: [ \"game\", \"arcade\", \"play\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc7e\" src=\"1f47e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  dart: {\n    keywords: [ \"game\", \"play\", \"bar\", \"target\", \"bullseye\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfaf\" src=\"1f3af.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  game_die: {\n    keywords: [ \"dice\", \"random\", \"tabletop\", \"play\", \"luck\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb2\" src=\"1f3b2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  chess_pawn: {\n    keywords: [ \"expendable\" ],\n    char: \"\\u265f\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  slot_machine: {\n    keywords: [ \"bet\", \"gamble\", \"vegas\", \"fruit machine\", \"luck\", \"casino\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb0\" src=\"1f3b0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  jigsaw: {\n    keywords: [ \"interlocking\", \"puzzle\", \"piece\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde9\" src=\"1f9e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  bowling: {\n    keywords: [ \"sports\", \"fun\", \"play\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb3\" src=\"1f3b3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  red_car: {\n    keywords: [ \"red\", \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude97\" src=\"1f697.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  taxi: {\n    keywords: [ \"uber\", \"vehicle\", \"cars\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude95\" src=\"1f695.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  blue_car: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude99\" src=\"1f699.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bus: {\n    keywords: [ \"car\", \"vehicle\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude8c\" src=\"1f68c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  trolleybus: {\n    keywords: [ \"bart\", \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude8e\" src=\"1f68e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  racing_car: {\n    keywords: [ \"sports\", \"race\", \"fast\", \"formula\", \"f1\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfce\" src=\"1f3ce.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  police_car: {\n    keywords: [ \"vehicle\", \"cars\", \"transportation\", \"law\", \"legal\", \"enforcement\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude93\" src=\"1f693.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ambulance: {\n    keywords: [ \"health\", \"911\", \"hospital\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude91\" src=\"1f691.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fire_engine: {\n    keywords: [ \"transportation\", \"cars\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude92\" src=\"1f692.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  minibus: {\n    keywords: [ \"vehicle\", \"car\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude90\" src=\"1f690.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  truck: {\n    keywords: [ \"cars\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude9a\" src=\"1f69a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  articulated_lorry: {\n    keywords: [ \"vehicle\", \"cars\", \"transportation\", \"express\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude9b\" src=\"1f69b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tractor: {\n    keywords: [ \"vehicle\", \"car\", \"farming\", \"agriculture\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude9c\" src=\"1f69c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  kick_scooter: {\n    keywords: [ \"vehicle\", \"kick\", \"razor\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef4\" src=\"1f6f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motorcycle: {\n    keywords: [ \"race\", \"sports\", \"fast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfcd\" src=\"1f3cd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bike: {\n    keywords: [ \"sports\", \"bicycle\", \"exercise\", \"hipster\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb2\" src=\"1f6b2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motor_scooter: {\n    keywords: [ \"vehicle\", \"vespa\", \"sasha\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef5\" src=\"1f6f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rotating_light: {\n    keywords: [ \"police\", \"ambulance\", \"911\", \"emergency\", \"alert\", \"error\", \"pinged\", \"law\", \"legal\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea8\" src=\"1f6a8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_police_car: {\n    keywords: [ \"vehicle\", \"law\", \"legal\", \"enforcement\", \"911\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude94\" src=\"1f694.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_bus: {\n    keywords: [ \"vehicle\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude8d\" src=\"1f68d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_automobile: {\n    keywords: [ \"car\", \"vehicle\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude98\" src=\"1f698.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_taxi: {\n    keywords: [ \"vehicle\", \"cars\", \"uber\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude96\" src=\"1f696.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  aerial_tramway: {\n    keywords: [ \"transportation\", \"vehicle\", \"ski\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea1\" src=\"1f6a1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain_cableway: {\n    keywords: [ \"transportation\", \"vehicle\", \"ski\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea0\" src=\"1f6a0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  suspension_railway: {\n    keywords: [ \"vehicle\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude9f\" src=\"1f69f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  railway_car: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude83\" src=\"1f683.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  train: {\n    keywords: [ \"transportation\", \"vehicle\", \"carriage\", \"public\", \"travel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude8b\" src=\"1f68b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  monorail: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude9d\" src=\"1f69d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bullettrain_side: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude84\" src=\"1f684.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bullettrain_front: {\n    keywords: [ \"transportation\", \"vehicle\", \"speed\", \"fast\", \"public\", \"travel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude85\" src=\"1f685.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  light_rail: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude88\" src=\"1f688.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain_railway: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude9e\" src=\"1f69e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  steam_locomotive: {\n    keywords: [ \"transportation\", \"vehicle\", \"train\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude82\" src=\"1f682.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  train2: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude86\" src=\"1f686.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  metro: {\n    keywords: [ \"transportation\", \"blue-square\", \"mrt\", \"underground\", \"tube\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude87\" src=\"1f687.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tram: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude8a\" src=\"1f68a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  station: {\n    keywords: [ \"transportation\", \"vehicle\", \"public\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude89\" src=\"1f689.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  flying_saucer: {\n    keywords: [ \"transportation\", \"vehicle\", \"ufo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef8\" src=\"1f6f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  helicopter: {\n    keywords: [ \"transportation\", \"vehicle\", \"fly\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude81\" src=\"1f681.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  small_airplane: {\n    keywords: [ \"flight\", \"transportation\", \"fly\", \"vehicle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udee9\" src=\"1f6e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  airplane: {\n    keywords: [ \"vehicle\", \"transportation\", \"flight\", \"fly\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2708\\ufe0f\" src=\"2708.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  flight_departure: {\n    keywords: [ \"airport\", \"flight\", \"landing\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeeb\" src=\"1f6eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  flight_arrival: {\n    keywords: [ \"airport\", \"flight\", \"boarding\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeec\" src=\"1f6ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sailboat: {\n    keywords: [ \"ship\", \"summer\", \"transportation\", \"water\", \"sailing\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f5\" src=\"26f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motor_boat: {\n    keywords: [ \"ship\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udee5\" src=\"1f6e5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  speedboat: {\n    keywords: [ \"ship\", \"transportation\", \"vehicle\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea4\" src=\"1f6a4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ferry: {\n    keywords: [ \"boat\", \"ship\", \"yacht\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f4\" src=\"26f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  passenger_ship: {\n    keywords: [ \"yacht\", \"cruise\", \"ferry\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef3\" src=\"1f6f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rocket: {\n    keywords: [ \"launch\", \"ship\", \"staffmode\", \"NASA\", \"outer space\", \"outer_space\", \"fly\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude80\" src=\"1f680.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  artificial_satellite: {\n    keywords: [ \"communication\", \"gps\", \"orbit\", \"spaceflight\", \"NASA\", \"ISS\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef0\" src=\"1f6f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  seat: {\n    keywords: [ \"sit\", \"airplane\", \"transport\", \"bus\", \"flight\", \"fly\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcba\" src=\"1f4ba.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  canoe: {\n    keywords: [ \"boat\", \"paddle\", \"water\", \"ship\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udef6\" src=\"1f6f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  anchor: {\n    keywords: [ \"ship\", \"ferry\", \"sea\", \"boat\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2693\" src=\"2693.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  construction: {\n    keywords: [ \"wip\", \"progress\", \"caution\", \"warning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea7\" src=\"1f6a7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fuelpump: {\n    keywords: [ \"gas station\", \"petroleum\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26fd\" src=\"26fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  busstop: {\n    keywords: [ \"transportation\", \"wait\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\ude8f\" src=\"1f68f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  vertical_traffic_light: {\n    keywords: [ \"transportation\", \"driving\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea6\" src=\"1f6a6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  traffic_light: {\n    keywords: [ \"transportation\", \"signal\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea5\" src=\"1f6a5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  checkered_flag: {\n    keywords: [ \"contest\", \"finishline\", \"race\", \"gokart\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfc1\" src=\"1f3c1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ship: {\n    keywords: [ \"transportation\", \"titanic\", \"deploy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea2\" src=\"1f6a2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ferris_wheel: {\n    keywords: [ \"photo\", \"carnival\", \"londoneye\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa1\" src=\"1f3a1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  roller_coaster: {\n    keywords: [ \"carnival\", \"playground\", \"photo\", \"fun\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa2\" src=\"1f3a2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  carousel_horse: {\n    keywords: [ \"photo\", \"carnival\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa0\" src=\"1f3a0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  building_construction: {\n    keywords: [ \"wip\", \"working\", \"progress\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd7\" src=\"1f3d7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  foggy: {\n    keywords: [ \"photo\", \"mountain\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf01\" src=\"1f301.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tokyo_tower: {\n    keywords: [ \"photo\", \"japanese\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddfc\" src=\"1f5fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  factory: {\n    keywords: [ \"building\", \"industry\", \"pollution\", \"smoke\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfed\" src=\"1f3ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fountain: {\n    keywords: [ \"photo\", \"summer\", \"water\", \"fresh\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f2\" src=\"26f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rice_scene: {\n    keywords: [ \"photo\", \"japan\", \"asia\", \"tsukimi\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf91\" src=\"1f391.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain: {\n    keywords: [ \"photo\", \"nature\", \"environment\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f0\" src=\"26f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain_snow: {\n    keywords: [ \"photo\", \"nature\", \"environment\", \"winter\", \"cold\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd4\" src=\"1f3d4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mount_fuji: {\n    keywords: [ \"photo\", \"mountain\", \"nature\", \"japanese\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddfb\" src=\"1f5fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  volcano: {\n    keywords: [ \"photo\", \"nature\", \"disaster\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf0b\" src=\"1f30b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  japan: {\n    keywords: [ \"nation\", \"country\", \"japanese\", \"asia\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddfe\" src=\"1f5fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  camping: {\n    keywords: [ \"photo\", \"outdoors\", \"tent\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd5\" src=\"1f3d5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tent: {\n    keywords: [ \"photo\", \"camping\", \"outdoors\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26fa\" src=\"26fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  national_park: {\n    keywords: [ \"photo\", \"environment\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfde\" src=\"1f3de.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motorway: {\n    keywords: [ \"road\", \"cupertino\", \"interstate\", \"highway\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udee3\" src=\"1f6e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  railway_track: {\n    keywords: [ \"train\", \"transportation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udee4\" src=\"1f6e4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sunrise: {\n    keywords: [ \"morning\", \"view\", \"vacation\", \"photo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf05\" src=\"1f305.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sunrise_over_mountains: {\n    keywords: [ \"view\", \"vacation\", \"photo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf04\" src=\"1f304.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  desert: {\n    keywords: [ \"photo\", \"warm\", \"saharah\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfdc\" src=\"1f3dc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  beach_umbrella: {\n    keywords: [ \"weather\", \"summer\", \"sunny\", \"sand\", \"mojito\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd6\" src=\"1f3d6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  desert_island: {\n    keywords: [ \"photo\", \"tropical\", \"mojito\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfdd\" src=\"1f3dd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  city_sunrise: {\n    keywords: [ \"photo\", \"good morning\", \"dawn\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf07\" src=\"1f307.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  city_sunset: {\n    keywords: [ \"photo\", \"evening\", \"sky\", \"buildings\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf06\" src=\"1f306.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  cityscape: {\n    keywords: [ \"photo\", \"night life\", \"urban\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd9\" src=\"1f3d9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  night_with_stars: {\n    keywords: [ \"evening\", \"city\", \"downtown\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf03\" src=\"1f303.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bridge_at_night: {\n    keywords: [ \"photo\", \"sanfrancisco\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf09\" src=\"1f309.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  milky_way: {\n    keywords: [ \"photo\", \"space\", \"stars\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf0c\" src=\"1f30c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  stars: {\n    keywords: [ \"night\", \"photo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf20\" src=\"1f320.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sparkler: {\n    keywords: [ \"stars\", \"night\", \"shine\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf87\" src=\"1f387.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fireworks: {\n    keywords: [ \"photo\", \"festival\", \"carnival\", \"congratulations\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf86\" src=\"1f386.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rainbow: {\n    keywords: [ \"nature\", \"happy\", \"unicorn_face\", \"photo\", \"sky\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf08\" src=\"1f308.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  houses: {\n    keywords: [ \"buildings\", \"photo\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfd8\" src=\"1f3d8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  european_castle: {\n    keywords: [ \"building\", \"royalty\", \"history\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff0\" src=\"1f3f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  japanese_castle: {\n    keywords: [ \"photo\", \"building\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfef\" src=\"1f3ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  stadium: {\n    keywords: [ \"photo\", \"place\", \"sports\", \"concert\", \"venue\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfdf\" src=\"1f3df.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  statue_of_liberty: {\n    keywords: [ \"american\", \"newyork\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddfd\" src=\"1f5fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  house: {\n    keywords: [ \"building\", \"home\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe0\" src=\"1f3e0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  house_with_garden: {\n    keywords: [ \"home\", \"plant\", \"nature\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe1\" src=\"1f3e1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  derelict_house: {\n    keywords: [ \"abandon\", \"evict\", \"broken\", \"building\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfda\" src=\"1f3da.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  office: {\n    keywords: [ \"building\", \"bureau\", \"work\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe2\" src=\"1f3e2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  department_store: {\n    keywords: [ \"building\", \"shopping\", \"mall\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfec\" src=\"1f3ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  post_office: {\n    keywords: [ \"building\", \"envelope\", \"communication\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe3\" src=\"1f3e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  european_post_office: {\n    keywords: [ \"building\", \"email\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe4\" src=\"1f3e4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  hospital: {\n    keywords: [ \"building\", \"health\", \"surgery\", \"doctor\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe5\" src=\"1f3e5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bank: {\n    keywords: [ \"building\", \"money\", \"sales\", \"cash\", \"business\", \"enterprise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe6\" src=\"1f3e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  hotel: {\n    keywords: [ \"building\", \"accomodation\", \"checkin\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe8\" src=\"1f3e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  convenience_store: {\n    keywords: [ \"building\", \"shopping\", \"groceries\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfea\" src=\"1f3ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  school: {\n    keywords: [ \"building\", \"student\", \"education\", \"learn\", \"teach\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfeb\" src=\"1f3eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  love_hotel: {\n    keywords: [ \"like\", \"affection\", \"dating\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe9\" src=\"1f3e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  wedding: {\n    keywords: [ \"love\", \"like\", \"affection\", \"couple\", \"marriage\", \"bride\", \"groom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc92\" src=\"1f492.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  classical_building: {\n    keywords: [ \"art\", \"culture\", \"history\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfdb\" src=\"1f3db.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  church: {\n    keywords: [ \"building\", \"religion\", \"christ\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26ea\" src=\"26ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mosque: {\n    keywords: [ \"islam\", \"worship\", \"minaret\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd4c\" src=\"1f54c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  synagogue: {\n    keywords: [ \"judaism\", \"worship\", \"temple\", \"jewish\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd4d\" src=\"1f54d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  kaaba: {\n    keywords: [ \"mecca\", \"mosque\", \"islam\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd4b\" src=\"1f54b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  shinto_shrine: {\n    keywords: [ \"temple\", \"japan\", \"kyoto\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26e9\" src=\"26e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  watch: {\n    keywords: [ \"time\", \"accessories\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u231a\" src=\"231a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  iphone: {\n    keywords: [ \"technology\", \"apple\", \"gadgets\", \"dial\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf1\" src=\"1f4f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  calling: {\n    keywords: [ \"iphone\", \"incoming\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf2\" src=\"1f4f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  computer: {\n    keywords: [ \"technology\", \"laptop\", \"screen\", \"display\", \"monitor\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcbb\" src=\"1f4bb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  keyboard: {\n    keywords: [ \"technology\", \"computer\", \"type\", \"input\", \"text\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2328\" src=\"2328.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  desktop_computer: {\n    keywords: [ \"technology\", \"computing\", \"screen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udda5\" src=\"1f5a5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  printer: {\n    keywords: [ \"paper\", \"ink\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udda8\" src=\"1f5a8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  computer_mouse: {\n    keywords: [ \"click\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddb1\" src=\"1f5b1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  trackball: {\n    keywords: [ \"technology\", \"trackpad\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddb2\" src=\"1f5b2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  joystick: {\n    keywords: [ \"game\", \"play\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd79\" src=\"1f579.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  clamp: {\n    keywords: [ \"tool\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udddc\" src=\"1f5dc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  minidisc: {\n    keywords: [ \"technology\", \"record\", \"data\", \"disk\", \"90s\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcbd\" src=\"1f4bd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  floppy_disk: {\n    keywords: [ \"oldschool\", \"technology\", \"save\", \"90s\", \"80s\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcbe\" src=\"1f4be.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  cd: {\n    keywords: [ \"technology\", \"dvd\", \"disk\", \"disc\", \"90s\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcbf\" src=\"1f4bf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dvd: {\n    keywords: [ \"cd\", \"disk\", \"disc\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc0\" src=\"1f4c0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  vhs: {\n    keywords: [ \"record\", \"video\", \"oldschool\", \"90s\", \"80s\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcfc\" src=\"1f4fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  camera: {\n    keywords: [ \"gadgets\", \"photography\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf7\" src=\"1f4f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  camera_flash: {\n    keywords: [ \"photography\", \"gadgets\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf8\" src=\"1f4f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  video_camera: {\n    keywords: [ \"film\", \"record\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf9\" src=\"1f4f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  movie_camera: {\n    keywords: [ \"film\", \"record\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa5\" src=\"1f3a5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  film_projector: {\n    keywords: [ \"video\", \"tape\", \"record\", \"movie\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcfd\" src=\"1f4fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  film_strip: {\n    keywords: [ \"movie\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf9e\" src=\"1f39e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  telephone_receiver: {\n    keywords: [ \"technology\", \"communication\", \"dial\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcde\" src=\"1f4de.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  phone: {\n    keywords: [ \"technology\", \"communication\", \"dial\", \"telephone\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u260e\\ufe0f\" src=\"260e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pager: {\n    keywords: [ \"bbcall\", \"oldschool\", \"90s\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcdf\" src=\"1f4df.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  fax: {\n    keywords: [ \"communication\", \"technology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce0\" src=\"1f4e0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  tv: {\n    keywords: [ \"technology\", \"program\", \"oldschool\", \"show\", \"television\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcfa\" src=\"1f4fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  radio: {\n    keywords: [ \"communication\", \"music\", \"podcast\", \"program\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcfb\" src=\"1f4fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  studio_microphone: {\n    keywords: [ \"sing\", \"recording\", \"artist\", \"talkshow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf99\" src=\"1f399.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  level_slider: {\n    keywords: [ \"scale\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf9a\" src=\"1f39a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  control_knobs: {\n    keywords: [ \"dial\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf9b\" src=\"1f39b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  compass: {\n    keywords: [ \"magnetic\", \"navigation\", \"orienteering\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udded\" src=\"1f9ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  stopwatch: {\n    keywords: [ \"time\", \"deadline\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23f1\" src=\"23f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  timer_clock: {\n    keywords: [ \"alarm\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23f2\" src=\"23f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  alarm_clock: {\n    keywords: [ \"time\", \"wake\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23f0\" src=\"23f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mantelpiece_clock: {\n    keywords: [ \"time\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd70\" src=\"1f570.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hourglass_flowing_sand: {\n    keywords: [ \"oldschool\", \"time\", \"countdown\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23f3\" src=\"23f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hourglass: {\n    keywords: [ \"time\", \"clock\", \"oldschool\", \"limit\", \"exam\", \"quiz\", \"test\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u231b\" src=\"231b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  satellite: {\n    keywords: [ \"communication\", \"future\", \"radio\", \"space\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce1\" src=\"1f4e1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  battery: {\n    keywords: [ \"power\", \"energy\", \"sustain\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd0b\" src=\"1f50b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  electric_plug: {\n    keywords: [ \"charger\", \"power\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd0c\" src=\"1f50c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bulb: {\n    keywords: [ \"light\", \"electricity\", \"idea\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca1\" src=\"1f4a1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  flashlight: {\n    keywords: [ \"dark\", \"camping\", \"sight\", \"night\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd26\" src=\"1f526.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  candle: {\n    keywords: [ \"fire\", \"wax\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd6f\" src=\"1f56f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  fire_extinguisher: {\n    keywords: [ \"quench\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddef\" src=\"1f9ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  wastebasket: {\n    keywords: [ \"bin\", \"trash\", \"rubbish\", \"garbage\", \"toss\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddd1\" src=\"1f5d1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  oil_drum: {\n    keywords: [ \"barrell\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udee2\" src=\"1f6e2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  money_with_wings: {\n    keywords: [ \"dollar\", \"bills\", \"payment\", \"sale\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb8\" src=\"1f4b8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dollar: {\n    keywords: [ \"money\", \"sales\", \"bill\", \"currency\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb5\" src=\"1f4b5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  yen: {\n    keywords: [ \"money\", \"sales\", \"japanese\", \"dollar\", \"currency\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb4\" src=\"1f4b4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  euro: {\n    keywords: [ \"money\", \"sales\", \"dollar\", \"currency\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb6\" src=\"1f4b6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pound: {\n    keywords: [ \"british\", \"sterling\", \"money\", \"sales\", \"bills\", \"uk\", \"england\", \"currency\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb7\" src=\"1f4b7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  moneybag: {\n    keywords: [ \"dollar\", \"payment\", \"coins\", \"sale\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb0\" src=\"1f4b0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  credit_card: {\n    keywords: [ \"money\", \"sales\", \"dollar\", \"bill\", \"payment\", \"shopping\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb3\" src=\"1f4b3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gem: {\n    keywords: [ \"blue\", \"ruby\", \"diamond\", \"jewelry\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc8e\" src=\"1f48e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  balance_scale: {\n    keywords: [ \"law\", \"fairness\", \"weight\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2696\" src=\"2696.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  toolbox: {\n    keywords: [ \"tools\", \"diy\", \"fix\", \"maintainer\", \"mechanic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf0\" src=\"1f9f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  wrench: {\n    keywords: [ \"tools\", \"diy\", \"ikea\", \"fix\", \"maintainer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd27\" src=\"1f527.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hammer: {\n    keywords: [ \"tools\", \"build\", \"create\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd28\" src=\"1f528.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hammer_and_pick: {\n    keywords: [ \"tools\", \"build\", \"create\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2692\" src=\"2692.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hammer_and_wrench: {\n    keywords: [ \"tools\", \"build\", \"create\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udee0\" src=\"1f6e0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pick: {\n    keywords: [ \"tools\", \"dig\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26cf\" src=\"26cf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  nut_and_bolt: {\n    keywords: [ \"handy\", \"tools\", \"fix\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd29\" src=\"1f529.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gear: {\n    keywords: [ \"cog\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2699\" src=\"2699.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  brick: {\n    keywords: [ \"bricks\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf1\" src=\"1f9f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  chains: {\n    keywords: [ \"lock\", \"arrest\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26d3\" src=\"26d3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  magnet: {\n    keywords: [ \"attraction\", \"magnetic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf2\" src=\"1f9f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gun: {\n    keywords: [ \"violence\", \"weapon\", \"pistol\", \"revolver\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd2b\" src=\"1f52b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bomb: {\n    keywords: [ \"boom\", \"explode\", \"explosion\", \"terrorism\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca3\" src=\"1f4a3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  firecracker: {\n    keywords: [ \"dynamite\", \"boom\", \"explode\", \"explosion\", \"explosive\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde8\" src=\"1f9e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hocho: {\n    keywords: [ \"knife\", \"blade\", \"cutlery\", \"kitchen\", \"weapon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd2a\" src=\"1f52a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dagger: {\n    keywords: [ \"weapon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udde1\" src=\"1f5e1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crossed_swords: {\n    keywords: [ \"weapon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2694\" src=\"2694.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shield: {\n    keywords: [ \"protection\", \"security\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udee1\" src=\"1f6e1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  smoking: {\n    keywords: [ \"kills\", \"tobacco\", \"cigarette\", \"joint\", \"smoke\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeac\" src=\"1f6ac.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  skull_and_crossbones: {\n    keywords: [ \"poison\", \"danger\", \"deadly\", \"scary\", \"death\", \"pirate\", \"evil\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2620\" src=\"2620.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  coffin: {\n    keywords: [ \"vampire\", \"dead\", \"die\", \"death\", \"rip\", \"graveyard\", \"cemetery\", \"casket\", \"funeral\", \"box\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26b0\" src=\"26b0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  funeral_urn: {\n    keywords: [ \"dead\", \"die\", \"death\", \"rip\", \"ashes\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26b1\" src=\"26b1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  amphora: {\n    keywords: [ \"vase\", \"jar\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udffa\" src=\"1f3fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crystal_ball: {\n    keywords: [ \"disco\", \"party\", \"magic\", \"circus\", \"fortune_teller\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd2e\" src=\"1f52e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  prayer_beads: {\n    keywords: [ \"dhikr\", \"religious\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcff\" src=\"1f4ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  nazar_amulet: {\n    keywords: [ \"bead\", \"charm\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddff\" src=\"1f9ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  barber: {\n    keywords: [ \"hair\", \"salon\", \"style\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc88\" src=\"1f488.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  alembic: {\n    keywords: [ \"distilling\", \"science\", \"experiment\", \"chemistry\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2697\" src=\"2697.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  telescope: {\n    keywords: [ \"stars\", \"space\", \"zoom\", \"science\", \"astronomy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd2d\" src=\"1f52d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  microscope: {\n    keywords: [ \"laboratory\", \"experiment\", \"zoomin\", \"science\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd2c\" src=\"1f52c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hole: {\n    keywords: [ \"embarrassing\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd73\" src=\"1f573.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pill: {\n    keywords: [ \"health\", \"medicine\", \"doctor\", \"pharmacy\", \"drug\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc8a\" src=\"1f48a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  syringe: {\n    keywords: [ \"health\", \"hospital\", \"drugs\", \"blood\", \"medicine\", \"needle\", \"doctor\", \"nurse\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc89\" src=\"1f489.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dna: {\n    keywords: [ \"biologist\", \"genetics\", \"life\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddec\" src=\"1f9ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  microbe: {\n    keywords: [ \"amoeba\", \"bacteria\", \"germs\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udda0\" src=\"1f9a0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  petri_dish: {\n    keywords: [ \"bacteria\", \"biology\", \"culture\", \"lab\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddeb\" src=\"1f9eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  test_tube: {\n    keywords: [ \"chemistry\", \"experiment\", \"lab\", \"science\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddea\" src=\"1f9ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  thermometer: {\n    keywords: [ \"weather\", \"temperature\", \"hot\", \"cold\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf21\" src=\"1f321.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  broom: {\n    keywords: [ \"cleaning\", \"sweeping\", \"witch\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf9\" src=\"1f9f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  basket: {\n    keywords: [ \"laundry\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddfa\" src=\"1f9fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  toilet_paper: {\n    keywords: [ \"roll\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddfb\" src=\"1f9fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  label: {\n    keywords: [ \"sale\", \"tag\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff7\" src=\"1f3f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bookmark: {\n    keywords: [ \"favorite\", \"label\", \"save\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd16\" src=\"1f516.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  toilet: {\n    keywords: [ \"restroom\", \"wc\", \"washroom\", \"bathroom\", \"potty\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udebd\" src=\"1f6bd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shower: {\n    keywords: [ \"clean\", \"water\", \"bathroom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udebf\" src=\"1f6bf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bathtub: {\n    keywords: [ \"clean\", \"shower\", \"bathroom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udec1\" src=\"1f6c1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  soap: {\n    keywords: [ \"bar\", \"bathing\", \"cleaning\", \"lather\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddfc\" src=\"1f9fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  sponge: {\n    keywords: [ \"absorbing\", \"cleaning\", \"porous\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddfd\" src=\"1f9fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  lotion_bottle: {\n    keywords: [ \"moisturizer\", \"sunscreen\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf4\" src=\"1f9f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  key: {\n    keywords: [ \"lock\", \"door\", \"password\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd11\" src=\"1f511.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  old_key: {\n    keywords: [ \"lock\", \"door\", \"password\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udddd\" src=\"1f5dd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  couch_and_lamp: {\n    keywords: [ \"read\", \"chill\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udecb\" src=\"1f6cb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  sleeping_bed: {\n    keywords: [ \"bed\", \"rest\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udecc\" src=\"1f6cc.png\"/>',\n    fitzpatrick_scale: true,\n    category: \"objects\"\n  },\n  bed: {\n    keywords: [ \"sleep\", \"rest\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udecf\" src=\"1f6cf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  door: {\n    keywords: [ \"house\", \"entry\", \"exit\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeaa\" src=\"1f6aa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bellhop_bell: {\n    keywords: [ \"service\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udece\" src=\"1f6ce.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  teddy_bear: {\n    keywords: [ \"plush\", \"stuffed\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf8\" src=\"1f9f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  framed_picture: {\n    keywords: [ \"photography\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddbc\" src=\"1f5bc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  world_map: {\n    keywords: [ \"location\", \"direction\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddfa\" src=\"1f5fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  parasol_on_ground: {\n    keywords: [ \"weather\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26f1\" src=\"26f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  moyai: {\n    keywords: [ \"rock\", \"easter island\", \"moai\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddff\" src=\"1f5ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shopping: {\n    keywords: [ \"mall\", \"buy\", \"purchase\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udecd\" src=\"1f6cd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shopping_cart: {\n    keywords: [ \"trolley\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uded2\" src=\"1f6d2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  balloon: {\n    keywords: [ \"party\", \"celebration\", \"birthday\", \"circus\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf88\" src=\"1f388.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  flags: {\n    keywords: [ \"fish\", \"japanese\", \"koinobori\", \"carp\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf8f\" src=\"1f38f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  ribbon: {\n    keywords: [ \"decoration\", \"pink\", \"girl\", \"bowtie\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf80\" src=\"1f380.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gift: {\n    keywords: [ \"present\", \"birthday\", \"christmas\", \"xmas\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf81\" src=\"1f381.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  confetti_ball: {\n    keywords: [ \"festival\", \"party\", \"birthday\", \"circus\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf8a\" src=\"1f38a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  tada: {\n    keywords: [ \"party\", \"congratulations\", \"birthday\", \"magic\", \"circus\", \"celebration\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf89\" src=\"1f389.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dolls: {\n    keywords: [ \"japanese\", \"toy\", \"kimono\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf8e\" src=\"1f38e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  wind_chime: {\n    keywords: [ \"nature\", \"ding\", \"spring\", \"bell\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf90\" src=\"1f390.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crossed_flags: {\n    keywords: [ \"japanese\", \"nation\", \"country\", \"border\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf8c\" src=\"1f38c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  izakaya_lantern: {\n    keywords: [ \"light\", \"paper\", \"halloween\", \"spooky\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfee\" src=\"1f3ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  red_envelope: {\n    keywords: [ \"gift\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde7\" src=\"1f9e7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  email: {\n    keywords: [ \"letter\", \"postal\", \"inbox\", \"communication\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2709\\ufe0f\" src=\"2709.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  envelope_with_arrow: {\n    keywords: [ \"email\", \"communication\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce9\" src=\"1f4e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  incoming_envelope: {\n    keywords: [ \"email\", \"inbox\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce8\" src=\"1f4e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  \"e-mail\": {\n    keywords: [ \"communication\", \"inbox\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce7\" src=\"1f4e7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  love_letter: {\n    keywords: [ \"email\", \"like\", \"affection\", \"envelope\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc8c\" src=\"1f48c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  postbox: {\n    keywords: [ \"email\", \"letter\", \"envelope\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcee\" src=\"1f4ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox_closed: {\n    keywords: [ \"email\", \"communication\", \"inbox\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcea\" src=\"1f4ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox: {\n    keywords: [ \"email\", \"inbox\", \"communication\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udceb\" src=\"1f4eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox_with_mail: {\n    keywords: [ \"email\", \"inbox\", \"communication\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcec\" src=\"1f4ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox_with_no_mail: {\n    keywords: [ \"email\", \"inbox\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udced\" src=\"1f4ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  package: {\n    keywords: [ \"mail\", \"gift\", \"cardboard\", \"box\", \"moving\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce6\" src=\"1f4e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  postal_horn: {\n    keywords: [ \"instrument\", \"music\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcef\" src=\"1f4ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  inbox_tray: {\n    keywords: [ \"email\", \"documents\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce5\" src=\"1f4e5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  outbox_tray: {\n    keywords: [ \"inbox\", \"email\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce4\" src=\"1f4e4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  scroll: {\n    keywords: [ \"documents\", \"ancient\", \"history\", \"paper\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcdc\" src=\"1f4dc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  page_with_curl: {\n    keywords: [ \"documents\", \"office\", \"paper\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc3\" src=\"1f4c3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bookmark_tabs: {\n    keywords: [ \"favorite\", \"save\", \"order\", \"tidy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd1\" src=\"1f4d1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  receipt: {\n    keywords: [ \"accounting\", \"expenses\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddfe\" src=\"1f9fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bar_chart: {\n    keywords: [ \"graph\", \"presentation\", \"stats\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcca\" src=\"1f4ca.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  chart_with_upwards_trend: {\n    keywords: [ \"graph\", \"presentation\", \"stats\", \"recovery\", \"business\", \"economics\", \"money\", \"sales\", \"good\", \"success\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc8\" src=\"1f4c8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  chart_with_downwards_trend: {\n    keywords: [ \"graph\", \"presentation\", \"stats\", \"recession\", \"business\", \"economics\", \"money\", \"sales\", \"bad\", \"failure\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc9\" src=\"1f4c9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  page_facing_up: {\n    keywords: [ \"documents\", \"office\", \"paper\", \"information\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc4\" src=\"1f4c4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  date: {\n    keywords: [ \"calendar\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc5\" src=\"1f4c5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  calendar: {\n    keywords: [ \"schedule\", \"date\", \"planning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc6\" src=\"1f4c6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  spiral_calendar: {\n    keywords: [ \"date\", \"schedule\", \"planning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddd3\" src=\"1f5d3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  card_index: {\n    keywords: [ \"business\", \"stationery\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc7\" src=\"1f4c7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  card_file_box: {\n    keywords: [ \"business\", \"stationery\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddc3\" src=\"1f5c3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  ballot_box: {\n    keywords: [ \"election\", \"vote\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddf3\" src=\"1f5f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  file_cabinet: {\n    keywords: [ \"filing\", \"organizing\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddc4\" src=\"1f5c4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  clipboard: {\n    keywords: [ \"stationery\", \"documents\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udccb\" src=\"1f4cb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  spiral_notepad: {\n    keywords: [ \"memo\", \"stationery\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddd2\" src=\"1f5d2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  file_folder: {\n    keywords: [ \"documents\", \"business\", \"office\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc1\" src=\"1f4c1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  open_file_folder: {\n    keywords: [ \"documents\", \"load\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcc2\" src=\"1f4c2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  card_index_dividers: {\n    keywords: [ \"organizing\", \"business\", \"stationery\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddc2\" src=\"1f5c2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  newspaper_roll: {\n    keywords: [ \"press\", \"headline\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddde\" src=\"1f5de.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  newspaper: {\n    keywords: [ \"press\", \"headline\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf0\" src=\"1f4f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  notebook: {\n    keywords: [ \"stationery\", \"record\", \"notes\", \"paper\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd3\" src=\"1f4d3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  closed_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"textbook\", \"learn\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd5\" src=\"1f4d5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  green_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd7\" src=\"1f4d7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  blue_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"learn\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd8\" src=\"1f4d8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  orange_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"textbook\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd9\" src=\"1f4d9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  notebook_with_decorative_cover: {\n    keywords: [ \"classroom\", \"notes\", \"record\", \"paper\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd4\" src=\"1f4d4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  ledger: {\n    keywords: [ \"notes\", \"paper\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd2\" src=\"1f4d2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  books: {\n    keywords: [ \"literature\", \"library\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcda\" src=\"1f4da.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  open_book: {\n    keywords: [ \"book\", \"read\", \"library\", \"knowledge\", \"literature\", \"learn\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd6\" src=\"1f4d6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  safety_pin: {\n    keywords: [ \"diaper\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddf7\" src=\"1f9f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  link: {\n    keywords: [ \"rings\", \"url\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd17\" src=\"1f517.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  paperclip: {\n    keywords: [ \"documents\", \"stationery\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcce\" src=\"1f4ce.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  paperclips: {\n    keywords: [ \"documents\", \"stationery\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd87\" src=\"1f587.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  scissors: {\n    keywords: [ \"stationery\", \"cut\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2702\\ufe0f\" src=\"2702.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  triangular_ruler: {\n    keywords: [ \"stationery\", \"math\", \"architect\", \"sketch\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcd0\" src=\"1f4d0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  straight_ruler: {\n    keywords: [ \"stationery\", \"calculate\", \"length\", \"math\", \"school\", \"drawing\", \"architect\", \"sketch\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udccf\" src=\"1f4cf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  abacus: {\n    keywords: [ \"calculation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\uddee\" src=\"1f9ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pushpin: {\n    keywords: [ \"stationery\", \"mark\", \"here\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udccc\" src=\"1f4cc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  round_pushpin: {\n    keywords: [ \"stationery\", \"location\", \"map\", \"here\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udccd\" src=\"1f4cd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  triangular_flag_on_post: {\n    keywords: [ \"mark\", \"milestone\", \"place\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udea9\" src=\"1f6a9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  white_flag: {\n    keywords: [ \"losing\", \"loser\", \"lost\", \"surrender\", \"give up\", \"fail\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff3\" src=\"1f3f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  black_flag: {\n    keywords: [ \"pirate\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff4\" src=\"1f3f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  rainbow_flag: {\n    keywords: [ \"flag\", \"rainbow\", \"pride\", \"gay\", \"lgbt\", \"glbt\", \"queer\", \"homosexual\", \"lesbian\", \"bisexual\", \"transgender\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff3\\ufe0f\\u200d\\ud83c\\udf08\" src=\"1f3f3-fe0f-200d-1f308.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  closed_lock_with_key: {\n    keywords: [ \"security\", \"privacy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd10\" src=\"1f510.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  lock: {\n    keywords: [ \"security\", \"password\", \"padlock\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd12\" src=\"1f512.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  unlock: {\n    keywords: [ \"privacy\", \"security\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd13\" src=\"1f513.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  lock_with_ink_pen: {\n    keywords: [ \"security\", \"secret\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd0f\" src=\"1f50f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pen: {\n    keywords: [ \"stationery\", \"writing\", \"write\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd8a\" src=\"1f58a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  fountain_pen: {\n    keywords: [ \"stationery\", \"writing\", \"write\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd8b\" src=\"1f58b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  black_nib: {\n    keywords: [ \"pen\", \"stationery\", \"writing\", \"write\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2712\\ufe0f\" src=\"2712.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  memo: {\n    keywords: [ \"write\", \"documents\", \"stationery\", \"pencil\", \"paper\", \"writing\", \"legal\", \"exam\", \"quiz\", \"test\", \"study\", \"compose\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcdd\" src=\"1f4dd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pencil2: {\n    keywords: [ \"stationery\", \"write\", \"paper\", \"writing\", \"school\", \"study\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u270f\\ufe0f\" src=\"270f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crayon: {\n    keywords: [ \"drawing\", \"creativity\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd8d\" src=\"1f58d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  paintbrush: {\n    keywords: [ \"drawing\", \"creativity\", \"art\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd8c\" src=\"1f58c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mag: {\n    keywords: [ \"search\", \"zoom\", \"find\", \"detective\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd0d\" src=\"1f50d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mag_right: {\n    keywords: [ \"search\", \"zoom\", \"find\", \"detective\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd0e\" src=\"1f50e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  heart: {\n    keywords: [ \"love\", \"like\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2764\\ufe0f\" src=\"2764.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  orange_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83e\\udde1\" src=\"1f9e1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  yellow_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc9b\" src=\"1f49b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  green_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc9a\" src=\"1f49a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  blue_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc99\" src=\"1f499.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  purple_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc9c\" src=\"1f49c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_heart: {\n    keywords: [ \"evil\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udda4\" src=\"1f5a4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  broken_heart: {\n    keywords: [ \"sad\", \"sorry\", \"break\", \"heart\", \"heartbreak\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc94\" src=\"1f494.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_heart_exclamation: {\n    keywords: [ \"decoration\", \"love\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2763\" src=\"2763.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  two_hearts: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\", \"heart\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc95\" src=\"1f495.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  revolving_hearts: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc9e\" src=\"1f49e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heartbeat: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\", \"pink\", \"heart\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc93\" src=\"1f493.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heartpulse: {\n    keywords: [ \"like\", \"love\", \"affection\", \"valentines\", \"pink\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc97\" src=\"1f497.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sparkling_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc96\" src=\"1f496.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cupid: {\n    keywords: [ \"love\", \"like\", \"heart\", \"affection\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc98\" src=\"1f498.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  gift_heart: {\n    keywords: [ \"love\", \"valentines\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc9d\" src=\"1f49d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heart_decoration: {\n    keywords: [ \"purple-square\", \"love\", \"like\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udc9f\" src=\"1f49f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  peace_symbol: {\n    keywords: [ \"hippie\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u262e\" src=\"262e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  latin_cross: {\n    keywords: [ \"christianity\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u271d\" src=\"271d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  star_and_crescent: {\n    keywords: [ \"islam\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u262a\" src=\"262a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  om: {\n    keywords: [ \"hinduism\", \"buddhism\", \"sikhism\", \"jainism\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd49\" src=\"1f549.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wheel_of_dharma: {\n    keywords: [ \"hinduism\", \"buddhism\", \"sikhism\", \"jainism\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2638\" src=\"2638.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  star_of_david: {\n    keywords: [ \"judaism\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2721\" src=\"2721.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  six_pointed_star: {\n    keywords: [ \"purple-square\", \"religion\", \"jewish\", \"hexagram\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd2f\" src=\"1f52f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  menorah: {\n    keywords: [ \"hanukkah\", \"candles\", \"jewish\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd4e\" src=\"1f54e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  yin_yang: {\n    keywords: [ \"balance\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u262f\" src=\"262f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  orthodox_cross: {\n    keywords: [ \"suppedaneum\", \"religion\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2626\" src=\"2626.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  place_of_worship: {\n    keywords: [ \"religion\", \"church\", \"temple\", \"prayer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uded0\" src=\"1f6d0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ophiuchus: {\n    keywords: [ \"sign\", \"purple-square\", \"constellation\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26ce\" src=\"26ce.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  aries: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2648\" src=\"2648.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  taurus: {\n    keywords: [ \"purple-square\", \"sign\", \"zodiac\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2649\" src=\"2649.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  gemini: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u264a\" src=\"264a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cancer: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u264b\" src=\"264b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  leo: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u264c\" src=\"264c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  virgo: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u264d\" src=\"264d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  libra: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u264e\" src=\"264e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  scorpius: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\", \"scorpio\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u264f\" src=\"264f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sagittarius: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2650\" src=\"2650.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  capricorn: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2651\" src=\"2651.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  aquarius: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2652\" src=\"2652.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  pisces: {\n    keywords: [ \"purple-square\", \"sign\", \"zodiac\", \"astrology\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2653\" src=\"2653.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  id: {\n    keywords: [ \"purple-square\", \"words\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd94\" src=\"1f194.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  atom_symbol: {\n    keywords: [ \"science\", \"physics\", \"chemistry\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u269b\" src=\"269b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7a7a: {\n    keywords: [ \"kanji\", \"japanese\", \"chinese\", \"empty\", \"sky\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude33\" src=\"1f233.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u5272: {\n    keywords: [ \"cut\", \"divide\", \"chinese\", \"kanji\", \"pink-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude39\" src=\"1f239.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  radioactive: {\n    keywords: [ \"nuclear\", \"danger\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2622\" src=\"2622.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  biohazard: {\n    keywords: [ \"danger\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2623\" src=\"2623.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mobile_phone_off: {\n    keywords: [ \"mute\", \"orange-square\", \"silence\", \"quiet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf4\" src=\"1f4f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  vibration_mode: {\n    keywords: [ \"orange-square\", \"phone\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf3\" src=\"1f4f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6709: {\n    keywords: [ \"orange-square\", \"chinese\", \"have\", \"kanji\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude36\" src=\"1f236.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7121: {\n    keywords: [ \"nothing\", \"chinese\", \"kanji\", \"japanese\", \"orange-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude1a\" src=\"1f21a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7533: {\n    keywords: [ \"chinese\", \"japanese\", \"kanji\", \"orange-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude38\" src=\"1f238.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u55b6: {\n    keywords: [ \"japanese\", \"opening hours\", \"orange-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude3a\" src=\"1f23a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6708: {\n    keywords: [ \"chinese\", \"month\", \"moon\", \"japanese\", \"orange-square\", \"kanji\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude37\\ufe0f\" src=\"1f237.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eight_pointed_black_star: {\n    keywords: [ \"orange-square\", \"shape\", \"polygon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2734\\ufe0f\" src=\"2734.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  vs: {\n    keywords: [ \"words\", \"orange-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd9a\" src=\"1f19a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  accept: {\n    keywords: [ \"ok\", \"good\", \"chinese\", \"kanji\", \"agree\", \"yes\", \"orange-circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude51\" src=\"1f251.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_flower: {\n    keywords: [ \"japanese\", \"spring\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcae\" src=\"1f4ae.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ideograph_advantage: {\n    keywords: [ \"chinese\", \"kanji\", \"obtain\", \"get\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude50\" src=\"1f250.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  secret: {\n    keywords: [ \"privacy\", \"chinese\", \"sshh\", \"kanji\", \"red-circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u3299\\ufe0f\" src=\"3299.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  congratulations: {\n    keywords: [ \"chinese\", \"kanji\", \"japanese\", \"red-circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u3297\\ufe0f\" src=\"3297.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u5408: {\n    keywords: [ \"japanese\", \"chinese\", \"join\", \"kanji\", \"red-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude34\" src=\"1f234.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6e80: {\n    keywords: [ \"full\", \"chinese\", \"japanese\", \"red-square\", \"kanji\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude35\" src=\"1f235.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7981: {\n    keywords: [ \"kanji\", \"japanese\", \"chinese\", \"forbidden\", \"limit\", \"restricted\", \"red-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude32\" src=\"1f232.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  a: {\n    keywords: [ \"red-square\", \"alphabet\", \"letter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd70\\ufe0f\" src=\"1f170.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  b: {\n    keywords: [ \"red-square\", \"alphabet\", \"letter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd71\\ufe0f\" src=\"1f171.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ab: {\n    keywords: [ \"red-square\", \"alphabet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd8e\" src=\"1f18e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cl: {\n    keywords: [ \"alphabet\", \"words\", \"red-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd91\" src=\"1f191.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  o2: {\n    keywords: [ \"alphabet\", \"red-square\", \"letter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd7e\\ufe0f\" src=\"1f17e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sos: {\n    keywords: [ \"help\", \"red-square\", \"words\", \"emergency\", \"911\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd98\" src=\"1f198.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_entry: {\n    keywords: [ \"limit\", \"security\", \"privacy\", \"bad\", \"denied\", \"stop\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26d4\" src=\"26d4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  name_badge: {\n    keywords: [ \"fire\", \"forbid\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcdb\" src=\"1f4db.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_entry_sign: {\n    keywords: [ \"forbid\", \"stop\", \"limit\", \"denied\", \"disallow\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeab\" src=\"1f6ab.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  x: {\n    keywords: [ \"no\", \"delete\", \"remove\", \"cancel\", \"red\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u274c\" src=\"274c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  o: {\n    keywords: [ \"circle\", \"round\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2b55\" src=\"2b55.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  stop_sign: {\n    keywords: [ \"stop\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uded1\" src=\"1f6d1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  anger: {\n    keywords: [ \"angry\", \"mad\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca2\" src=\"1f4a2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  hotsprings: {\n    keywords: [ \"bath\", \"warm\", \"relax\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2668\\ufe0f\" src=\"2668.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_pedestrians: {\n    keywords: [ \"rules\", \"crossing\", \"walking\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb7\" src=\"1f6b7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  do_not_litter: {\n    keywords: [ \"trash\", \"bin\", \"garbage\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeaf\" src=\"1f6af.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_bicycles: {\n    keywords: [ \"cyclist\", \"prohibited\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb3\" src=\"1f6b3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  \"non-potable_water\": {\n    keywords: [ \"drink\", \"faucet\", \"tap\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb1\" src=\"1f6b1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  underage: {\n    keywords: [ \"18\", \"drink\", \"pub\", \"night\", \"minor\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd1e\" src=\"1f51e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_mobile_phones: {\n    keywords: [ \"iphone\", \"mute\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf5\" src=\"1f4f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  exclamation: {\n    keywords: [ \"heavy_exclamation_mark\", \"danger\", \"surprise\", \"punctuation\", \"wow\", \"warning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2757\" src=\"2757.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  grey_exclamation: {\n    keywords: [ \"surprise\", \"punctuation\", \"gray\", \"wow\", \"warning\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2755\" src=\"2755.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  question: {\n    keywords: [ \"doubt\", \"confused\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2753\" src=\"2753.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  grey_question: {\n    keywords: [ \"doubts\", \"gray\", \"huh\", \"confused\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2754\" src=\"2754.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  bangbang: {\n    keywords: [ \"exclamation\", \"surprise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u203c\\ufe0f\" src=\"203c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  interrobang: {\n    keywords: [ \"wat\", \"punctuation\", \"surprise\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2049\\ufe0f\" src=\"2049.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  low_brightness: {\n    keywords: [ \"sun\", \"afternoon\", \"warm\", \"summer\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd05\" src=\"1f505.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  high_brightness: {\n    keywords: [ \"sun\", \"light\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd06\" src=\"1f506.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  trident: {\n    keywords: [ \"weapon\", \"spear\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd31\" src=\"1f531.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  fleur_de_lis: {\n    keywords: [ \"decorative\", \"scout\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u269c\" src=\"269c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  part_alternation_mark: {\n    keywords: [ \"graph\", \"presentation\", \"stats\", \"business\", \"economics\", \"bad\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u303d\\ufe0f\" src=\"303d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  warning: {\n    keywords: [ \"exclamation\", \"wip\", \"alert\", \"error\", \"problem\", \"issue\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26a0\\ufe0f\" src=\"26a0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  children_crossing: {\n    keywords: [ \"school\", \"warning\", \"danger\", \"sign\", \"driving\", \"yellow-diamond\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb8\" src=\"1f6b8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  beginner: {\n    keywords: [ \"badge\", \"shield\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd30\" src=\"1f530.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  recycle: {\n    keywords: [ \"arrow\", \"environment\", \"garbage\", \"trash\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u267b\\ufe0f\" src=\"267b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6307: {\n    keywords: [ \"chinese\", \"point\", \"green-square\", \"kanji\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude2f\" src=\"1f22f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  chart: {\n    keywords: [ \"green-square\", \"graph\", \"presentation\", \"stats\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb9\" src=\"1f4b9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sparkle: {\n    keywords: [ \"stars\", \"green-square\", \"awesome\", \"good\", \"fireworks\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2747\\ufe0f\" src=\"2747.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eight_spoked_asterisk: {\n    keywords: [ \"star\", \"sparkle\", \"green-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2733\\ufe0f\" src=\"2733.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  negative_squared_cross_mark: {\n    keywords: [ \"x\", \"green-square\", \"no\", \"deny\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u274e\" src=\"274e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_check_mark: {\n    keywords: [ \"green-square\", \"ok\", \"agree\", \"vote\", \"election\", \"answer\", \"tick\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2705\" src=\"2705.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  diamond_shape_with_a_dot_inside: {\n    keywords: [ \"jewel\", \"blue\", \"gem\", \"crystal\", \"fancy\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udca0\" src=\"1f4a0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cyclone: {\n    keywords: [ \"weather\", \"swirl\", \"blue\", \"cloud\", \"vortex\", \"spiral\", \"whirlpool\", \"spin\", \"tornado\", \"hurricane\", \"typhoon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf00\" src=\"1f300.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  loop: {\n    keywords: [ \"tape\", \"cassette\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u27bf\" src=\"27bf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  globe_with_meridians: {\n    keywords: [ \"earth\", \"international\", \"world\", \"internet\", \"interweb\", \"i18n\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udf10\" src=\"1f310.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  m: {\n    keywords: [ \"alphabet\", \"blue-circle\", \"letter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u24c2\\ufe0f\" src=\"24c2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  atm: {\n    keywords: [ \"money\", \"sales\", \"cash\", \"blue-square\", \"payment\", \"bank\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfe7\" src=\"1f3e7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sa: {\n    keywords: [ \"japanese\", \"blue-square\", \"katakana\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude02\\ufe0f\" src=\"1f202.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  passport_control: {\n    keywords: [ \"custom\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udec2\" src=\"1f6c2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  customs: {\n    keywords: [ \"passport\", \"border\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udec3\" src=\"1f6c3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  baggage_claim: {\n    keywords: [ \"blue-square\", \"airport\", \"transport\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udec4\" src=\"1f6c4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  left_luggage: {\n    keywords: [ \"blue-square\", \"travel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udec5\" src=\"1f6c5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wheelchair: {\n    keywords: [ \"blue-square\", \"disabled\", \"a11y\", \"accessibility\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u267f\" src=\"267f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_smoking: {\n    keywords: [ \"cigarette\", \"blue-square\", \"smell\", \"smoke\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udead\" src=\"1f6ad.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wc: {\n    keywords: [ \"toilet\", \"restroom\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udebe\" src=\"1f6be.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  parking: {\n    keywords: [ \"cars\", \"blue-square\", \"alphabet\", \"letter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd7f\\ufe0f\" src=\"1f17f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  potable_water: {\n    keywords: [ \"blue-square\", \"liquid\", \"restroom\", \"cleaning\", \"faucet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb0\" src=\"1f6b0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mens: {\n    keywords: [ \"toilet\", \"restroom\", \"wc\", \"blue-square\", \"gender\", \"male\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeb9\" src=\"1f6b9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  womens: {\n    keywords: [ \"purple-square\", \"woman\", \"female\", \"toilet\", \"loo\", \"restroom\", \"gender\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeba\" src=\"1f6ba.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  baby_symbol: {\n    keywords: [ \"orange-square\", \"child\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udebc\" src=\"1f6bc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  restroom: {\n    keywords: [ \"blue-square\", \"toilet\", \"refresh\", \"wc\", \"gender\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udebb\" src=\"1f6bb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  put_litter_in_its_place: {\n    keywords: [ \"blue-square\", \"sign\", \"human\", \"info\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udeae\" src=\"1f6ae.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cinema: {\n    keywords: [ \"blue-square\", \"record\", \"film\", \"movie\", \"curtain\", \"stage\", \"theater\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfa6\" src=\"1f3a6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  signal_strength: {\n    keywords: [ \"blue-square\", \"reception\", \"phone\", \"internet\", \"connection\", \"wifi\", \"bluetooth\", \"bars\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcf6\" src=\"1f4f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  koko: {\n    keywords: [ \"blue-square\", \"here\", \"katakana\", \"japanese\", \"destination\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\ude01\" src=\"1f201.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ng: {\n    keywords: [ \"blue-square\", \"words\", \"shape\", \"icon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd96\" src=\"1f196.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ok: {\n    keywords: [ \"good\", \"agree\", \"yes\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd97\" src=\"1f197.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  up: {\n    keywords: [ \"blue-square\", \"above\", \"high\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd99\" src=\"1f199.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cool: {\n    keywords: [ \"words\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd92\" src=\"1f192.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  new: {\n    keywords: [ \"blue-square\", \"words\", \"start\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd95\" src=\"1f195.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  free: {\n    keywords: [ \"blue-square\", \"words\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udd93\" src=\"1f193.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  zero: {\n    keywords: [ \"0\", \"numbers\", \"blue-square\", \"null\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"0\\ufe0f\\u20e3\" src=\"30-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  one: {\n    keywords: [ \"blue-square\", \"numbers\", \"1\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"1\\ufe0f\\u20e3\" src=\"31-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  two: {\n    keywords: [ \"numbers\", \"2\", \"prime\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"2\\ufe0f\\u20e3\" src=\"32-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  three: {\n    keywords: [ \"3\", \"numbers\", \"prime\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"3\\ufe0f\\u20e3\" src=\"33-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  four: {\n    keywords: [ \"4\", \"numbers\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"4\\ufe0f\\u20e3\" src=\"34-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  five: {\n    keywords: [ \"5\", \"numbers\", \"blue-square\", \"prime\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"5\\ufe0f\\u20e3\" src=\"35-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  six: {\n    keywords: [ \"6\", \"numbers\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"6\\ufe0f\\u20e3\" src=\"36-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  seven: {\n    keywords: [ \"7\", \"numbers\", \"blue-square\", \"prime\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"7\\ufe0f\\u20e3\" src=\"37-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eight: {\n    keywords: [ \"8\", \"blue-square\", \"numbers\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"8\\ufe0f\\u20e3\" src=\"38-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  nine: {\n    keywords: [ \"blue-square\", \"numbers\", \"9\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"9\\ufe0f\\u20e3\" src=\"39-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  keycap_ten: {\n    keywords: [ \"numbers\", \"10\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd1f\" src=\"1f51f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  asterisk: {\n    keywords: [ \"star\", \"keycap\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"*\\u20e3\" src=\"2a-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eject_button: {\n    keywords: [ \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23cf\\ufe0f\" src=\"23cf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_forward: {\n    keywords: [ \"blue-square\", \"right\", \"direction\", \"play\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25b6\\ufe0f\" src=\"25b6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  pause_button: {\n    keywords: [ \"pause\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23f8\" src=\"23f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  next_track_button: {\n    keywords: [ \"forward\", \"next\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23ed\" src=\"23ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  stop_button: {\n    keywords: [ \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23f9\" src=\"23f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  record_button: {\n    keywords: [ \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23fa\" src=\"23fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  play_or_pause_button: {\n    keywords: [ \"blue-square\", \"play\", \"pause\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23ef\" src=\"23ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  previous_track_button: {\n    keywords: [ \"backward\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23ee\" src=\"23ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  fast_forward: {\n    keywords: [ \"blue-square\", \"play\", \"speed\", \"continue\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23e9\" src=\"23e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  rewind: {\n    keywords: [ \"play\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23ea\" src=\"23ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  twisted_rightwards_arrows: {\n    keywords: [ \"blue-square\", \"shuffle\", \"music\", \"random\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd00\" src=\"1f500.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  repeat: {\n    keywords: [ \"loop\", \"record\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd01\" src=\"1f501.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  repeat_one: {\n    keywords: [ \"blue-square\", \"loop\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd02\" src=\"1f502.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_backward: {\n    keywords: [ \"blue-square\", \"left\", \"direction\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25c0\\ufe0f\" src=\"25c0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_up_small: {\n    keywords: [ \"blue-square\", \"triangle\", \"direction\", \"point\", \"forward\", \"top\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd3c\" src=\"1f53c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_down_small: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd3d\" src=\"1f53d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_double_up: {\n    keywords: [ \"blue-square\", \"direction\", \"top\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23eb\" src=\"23eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_double_down: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u23ec\" src=\"23ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_right: {\n    keywords: [ \"blue-square\", \"next\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u27a1\\ufe0f\" src=\"27a1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_left: {\n    keywords: [ \"blue-square\", \"previous\", \"back\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2b05\\ufe0f\" src=\"2b05.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_up: {\n    keywords: [ \"blue-square\", \"continue\", \"top\", \"direction\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2b06\\ufe0f\" src=\"2b06.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_down: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2b07\\ufe0f\" src=\"2b07.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_upper_right: {\n    keywords: [ \"blue-square\", \"point\", \"direction\", \"diagonal\", \"northeast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2197\\ufe0f\" src=\"2197.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_lower_right: {\n    keywords: [ \"blue-square\", \"direction\", \"diagonal\", \"southeast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2198\\ufe0f\" src=\"2198.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_lower_left: {\n    keywords: [ \"blue-square\", \"direction\", \"diagonal\", \"southwest\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2199\\ufe0f\" src=\"2199.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_upper_left: {\n    keywords: [ \"blue-square\", \"point\", \"direction\", \"diagonal\", \"northwest\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2196\\ufe0f\" src=\"2196.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_up_down: {\n    keywords: [ \"blue-square\", \"direction\", \"way\", \"vertical\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2195\\ufe0f\" src=\"2195.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  left_right_arrow: {\n    keywords: [ \"shape\", \"direction\", \"horizontal\", \"sideways\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2194\\ufe0f\" src=\"2194.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrows_counterclockwise: {\n    keywords: [ \"blue-square\", \"sync\", \"cycle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd04\" src=\"1f504.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_right_hook: {\n    keywords: [ \"blue-square\", \"return\", \"rotate\", \"direction\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u21aa\\ufe0f\" src=\"21aa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  leftwards_arrow_with_hook: {\n    keywords: [ \"back\", \"return\", \"blue-square\", \"undo\", \"enter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u21a9\\ufe0f\" src=\"21a9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_heading_up: {\n    keywords: [ \"blue-square\", \"direction\", \"top\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2934\\ufe0f\" src=\"2934.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_heading_down: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2935\\ufe0f\" src=\"2935.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  hash: {\n    keywords: [ \"symbol\", \"blue-square\", \"twitter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"#\\ufe0f\\u20e3\" src=\"23-20e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  information_source: {\n    keywords: [ \"blue-square\", \"alphabet\", \"letter\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2139\\ufe0f\" src=\"2139.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  abc: {\n    keywords: [ \"blue-square\", \"alphabet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd24\" src=\"1f524.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  abcd: {\n    keywords: [ \"blue-square\", \"alphabet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd21\" src=\"1f521.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  capital_abcd: {\n    keywords: [ \"alphabet\", \"words\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd20\" src=\"1f520.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  symbols: {\n    keywords: [ \"blue-square\", \"music\", \"note\", \"ampersand\", \"percent\", \"glyphs\", \"characters\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd23\" src=\"1f523.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  musical_note: {\n    keywords: [ \"score\", \"tone\", \"sound\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb5\" src=\"1f3b5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  notes: {\n    keywords: [ \"music\", \"score\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb6\" src=\"1f3b6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wavy_dash: {\n    keywords: [ \"draw\", \"line\", \"moustache\", \"mustache\", \"squiggle\", \"scribble\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u3030\\ufe0f\" src=\"3030.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  curly_loop: {\n    keywords: [ \"scribble\", \"draw\", \"shape\", \"squiggle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u27b0\" src=\"27b0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_check_mark: {\n    keywords: [ \"ok\", \"nike\", \"answer\", \"yes\", \"tick\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2714\\ufe0f\" src=\"2714.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrows_clockwise: {\n    keywords: [ \"sync\", \"cycle\", \"round\", \"repeat\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd03\" src=\"1f503.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_plus_sign: {\n    keywords: [ \"math\", \"calculation\", \"addition\", \"more\", \"increase\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2795\" src=\"2795.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_minus_sign: {\n    keywords: [ \"math\", \"calculation\", \"subtract\", \"less\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2796\" src=\"2796.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_division_sign: {\n    keywords: [ \"divide\", \"math\", \"calculation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2797\" src=\"2797.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_multiplication_x: {\n    keywords: [ \"math\", \"calculation\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2716\\ufe0f\" src=\"2716.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  infinity: {\n    keywords: [ \"forever\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u267e\" src=\"267e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_dollar_sign: {\n    keywords: [ \"money\", \"sales\", \"payment\", \"currency\", \"buck\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb2\" src=\"1f4b2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  currency_exchange: {\n    keywords: [ \"money\", \"sales\", \"dollar\", \"travel\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcb1\" src=\"1f4b1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  copyright: {\n    keywords: [ \"ip\", \"license\", \"circle\", \"law\", \"legal\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\xa9\\ufe0f\" src=\"a9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  registered: {\n    keywords: [ \"alphabet\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\xae\\ufe0f\" src=\"ae.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  tm: {\n    keywords: [ \"trademark\", \"brand\", \"law\", \"legal\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2122\\ufe0f\" src=\"2122.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  end: {\n    keywords: [ \"words\", \"arrow\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd1a\" src=\"1f51a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  back: {\n    keywords: [ \"arrow\", \"words\", \"return\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd19\" src=\"1f519.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  on: {\n    keywords: [ \"arrow\", \"words\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd1b\" src=\"1f51b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  top: {\n    keywords: [ \"words\", \"blue-square\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd1d\" src=\"1f51d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  soon: {\n    keywords: [ \"arrow\", \"words\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd1c\" src=\"1f51c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ballot_box_with_check: {\n    keywords: [ \"ok\", \"agree\", \"confirm\", \"black-square\", \"vote\", \"election\", \"yes\", \"tick\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2611\\ufe0f\" src=\"2611.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  radio_button: {\n    keywords: [ \"input\", \"old\", \"music\", \"circle\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd18\" src=\"1f518.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_circle: {\n    keywords: [ \"shape\", \"round\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26aa\" src=\"26aa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_circle: {\n    keywords: [ \"shape\", \"button\", \"round\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u26ab\" src=\"26ab.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  red_circle: {\n    keywords: [ \"shape\", \"error\", \"danger\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd34\" src=\"1f534.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  large_blue_circle: {\n    keywords: [ \"shape\", \"icon\", \"button\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd35\" src=\"1f535.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_orange_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd38\" src=\"1f538.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_blue_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd39\" src=\"1f539.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  large_orange_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd36\" src=\"1f536.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  large_blue_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd37\" src=\"1f537.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_red_triangle: {\n    keywords: [ \"shape\", \"direction\", \"up\", \"top\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd3a\" src=\"1f53a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_small_square: {\n    keywords: [ \"shape\", \"icon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25aa\\ufe0f\" src=\"25aa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_small_square: {\n    keywords: [ \"shape\", \"icon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25ab\\ufe0f\" src=\"25ab.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_large_square: {\n    keywords: [ \"shape\", \"icon\", \"button\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2b1b\" src=\"2b1b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_large_square: {\n    keywords: [ \"shape\", \"icon\", \"stone\", \"button\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2b1c\" src=\"2b1c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_red_triangle_down: {\n    keywords: [ \"shape\", \"direction\", \"bottom\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd3b\" src=\"1f53b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_medium_square: {\n    keywords: [ \"shape\", \"button\", \"icon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25fc\\ufe0f\" src=\"25fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_medium_square: {\n    keywords: [ \"shape\", \"stone\", \"icon\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25fb\\ufe0f\" src=\"25fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_medium_small_square: {\n    keywords: [ \"icon\", \"shape\", \"button\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25fe\" src=\"25fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_medium_small_square: {\n    keywords: [ \"shape\", \"stone\", \"icon\", \"button\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u25fd\" src=\"25fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_square_button: {\n    keywords: [ \"shape\", \"input\", \"frame\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd32\" src=\"1f532.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_square_button: {\n    keywords: [ \"shape\", \"input\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd33\" src=\"1f533.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  speaker: {\n    keywords: [ \"sound\", \"volume\", \"silence\", \"broadcast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd08\" src=\"1f508.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sound: {\n    keywords: [ \"volume\", \"speaker\", \"broadcast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd09\" src=\"1f509.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  loud_sound: {\n    keywords: [ \"volume\", \"noise\", \"noisy\", \"speaker\", \"broadcast\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd0a\" src=\"1f50a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mute: {\n    keywords: [ \"sound\", \"volume\", \"silence\", \"quiet\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd07\" src=\"1f507.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mega: {\n    keywords: [ \"sound\", \"speaker\", \"volume\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce3\" src=\"1f4e3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  loudspeaker: {\n    keywords: [ \"volume\", \"sound\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udce2\" src=\"1f4e2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  bell: {\n    keywords: [ \"sound\", \"notification\", \"christmas\", \"xmas\", \"chime\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd14\" src=\"1f514.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_bell: {\n    keywords: [ \"sound\", \"volume\", \"mute\", \"quiet\", \"silent\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd15\" src=\"1f515.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_joker: {\n    keywords: [ \"poker\", \"cards\", \"game\", \"play\", \"magic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udccf\" src=\"1f0cf.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mahjong: {\n    keywords: [ \"game\", \"play\", \"chinese\", \"kanji\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udc04\" src=\"1f004.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  spades: {\n    keywords: [ \"poker\", \"cards\", \"suits\", \"magic\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2660\\ufe0f\" src=\"2660.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clubs: {\n    keywords: [ \"poker\", \"cards\", \"magic\", \"suits\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2663\\ufe0f\" src=\"2663.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  hearts: {\n    keywords: [ \"poker\", \"cards\", \"magic\", \"suits\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2665\\ufe0f\" src=\"2665.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  diamonds: {\n    keywords: [ \"poker\", \"cards\", \"magic\", \"suits\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\u2666\\ufe0f\" src=\"2666.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  flower_playing_cards: {\n    keywords: [ \"game\", \"sunset\", \"red\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udfb4\" src=\"1f3b4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  thought_balloon: {\n    keywords: [ \"bubble\", \"cloud\", \"speech\", \"thinking\", \"dream\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcad\" src=\"1f4ad.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  right_anger_bubble: {\n    keywords: [ \"caption\", \"speech\", \"thinking\", \"mad\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\uddef\" src=\"1f5ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  speech_balloon: {\n    keywords: [ \"bubble\", \"words\", \"message\", \"talk\", \"chatting\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udcac\" src=\"1f4ac.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  left_speech_bubble: {\n    keywords: [ \"words\", \"message\", \"talk\", \"chatting\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udde8\" src=\"1f5e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd50\" src=\"1f550.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock2: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd51\" src=\"1f551.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock3: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd52\" src=\"1f552.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock4: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd53\" src=\"1f553.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock5: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd54\" src=\"1f554.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock6: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\", \"dawn\", \"dusk\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd55\" src=\"1f555.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock7: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd56\" src=\"1f556.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock8: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd57\" src=\"1f557.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock9: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd58\" src=\"1f558.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock10: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd59\" src=\"1f559.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock11: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd5a\" src=\"1f55a.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock12: {\n    keywords: [ \"time\", \"noon\", \"midnight\", \"midday\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd5b\" src=\"1f55b.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock130: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd5c\" src=\"1f55c.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock230: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd5d\" src=\"1f55d.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock330: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd5e\" src=\"1f55e.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock430: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd5f\" src=\"1f55f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock530: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd60\" src=\"1f560.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock630: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd61\" src=\"1f561.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock730: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd62\" src=\"1f562.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock830: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd63\" src=\"1f563.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock930: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd64\" src=\"1f564.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1030: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd65\" src=\"1f565.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1130: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd66\" src=\"1f566.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1230: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83d\\udd67\" src=\"1f567.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  afghanistan: {\n    keywords: [ \"af\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddeb\" src=\"1f1e6-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  aland_islands: {\n    keywords: [ \"\\xc5land\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddfd\" src=\"1f1e6-1f1fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  albania: {\n    keywords: [ \"al\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddf1\" src=\"1f1e6-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  algeria: {\n    keywords: [ \"dz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde9\\ud83c\\uddff\" src=\"1f1e9-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  american_samoa: {\n    keywords: [ \"american\", \"ws\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddf8\" src=\"1f1e6-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  andorra: {\n    keywords: [ \"ad\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\udde9\" src=\"1f1e6-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  angola: {\n    keywords: [ \"ao\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddf4\" src=\"1f1e6-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  anguilla: {\n    keywords: [ \"ai\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddee\" src=\"1f1e6-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  antarctica: {\n    keywords: [ \"aq\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddf6\" src=\"1f1e6-1f1f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  antigua_barbuda: {\n    keywords: [ \"antigua\", \"barbuda\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddec\" src=\"1f1e6-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  argentina: {\n    keywords: [ \"ar\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddf7\" src=\"1f1e6-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  armenia: {\n    keywords: [ \"am\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddf2\" src=\"1f1e6-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  aruba: {\n    keywords: [ \"aw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddfc\" src=\"1f1e6-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  australia: {\n    keywords: [ \"au\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddfa\" src=\"1f1e6-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  austria: {\n    keywords: [ \"at\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddf9\" src=\"1f1e6-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  azerbaijan: {\n    keywords: [ \"az\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddff\" src=\"1f1e6-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bahamas: {\n    keywords: [ \"bs\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf8\" src=\"1f1e7-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bahrain: {\n    keywords: [ \"bh\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\udded\" src=\"1f1e7-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bangladesh: {\n    keywords: [ \"bd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\udde9\" src=\"1f1e7-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  barbados: {\n    keywords: [ \"bb\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\udde7\" src=\"1f1e7-1f1e7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  belarus: {\n    keywords: [ \"by\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddfe\" src=\"1f1e7-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  belgium: {\n    keywords: [ \"be\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddea\" src=\"1f1e7-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  belize: {\n    keywords: [ \"bz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddff\" src=\"1f1e7-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  benin: {\n    keywords: [ \"bj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddef\" src=\"1f1e7-1f1ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bermuda: {\n    keywords: [ \"bm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf2\" src=\"1f1e7-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bhutan: {\n    keywords: [ \"bt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf9\" src=\"1f1e7-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bolivia: {\n    keywords: [ \"bo\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf4\" src=\"1f1e7-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  caribbean_netherlands: {\n    keywords: [ \"bonaire\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf6\" src=\"1f1e7-1f1f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bosnia_herzegovina: {\n    keywords: [ \"bosnia\", \"herzegovina\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\udde6\" src=\"1f1e7-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  botswana: {\n    keywords: [ \"bw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddfc\" src=\"1f1e7-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  brazil: {\n    keywords: [ \"br\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf7\" src=\"1f1e7-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  british_indian_ocean_territory: {\n    keywords: [ \"british\", \"indian\", \"ocean\", \"territory\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf4\" src=\"1f1ee-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  british_virgin_islands: {\n    keywords: [ \"british\", \"virgin\", \"islands\", \"bvi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfb\\ud83c\\uddec\" src=\"1f1fb-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  brunei: {\n    keywords: [ \"bn\", \"darussalam\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf3\" src=\"1f1e7-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bulgaria: {\n    keywords: [ \"bg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddec\" src=\"1f1e7-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  burkina_faso: {\n    keywords: [ \"burkina\", \"faso\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddeb\" src=\"1f1e7-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  burundi: {\n    keywords: [ \"bi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddee\" src=\"1f1e7-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cape_verde: {\n    keywords: [ \"cabo\", \"verde\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddfb\" src=\"1f1e8-1f1fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cambodia: {\n    keywords: [ \"kh\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\udded\" src=\"1f1f0-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cameroon: {\n    keywords: [ \"cm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddf2\" src=\"1f1e8-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  canada: {\n    keywords: [ \"ca\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\udde6\" src=\"1f1e8-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  canary_islands: {\n    keywords: [ \"canary\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\udde8\" src=\"1f1ee-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cayman_islands: {\n    keywords: [ \"cayman\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddfe\" src=\"1f1f0-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  central_african_republic: {\n    keywords: [ \"central\", \"african\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddeb\" src=\"1f1e8-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  chad: {\n    keywords: [ \"td\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\udde9\" src=\"1f1f9-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  chile: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddf1\" src=\"1f1e8-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cn: {\n    keywords: [ \"china\", \"chinese\", \"prc\", \"flag\", \"country\", \"nation\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddf3\" src=\"1f1e8-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  christmas_island: {\n    keywords: [ \"christmas\", \"island\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddfd\" src=\"1f1e8-1f1fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cocos_islands: {\n    keywords: [ \"cocos\", \"keeling\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\udde8\" src=\"1f1e8-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  colombia: {\n    keywords: [ \"co\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddf4\" src=\"1f1e8-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  comoros: {\n    keywords: [ \"km\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddf2\" src=\"1f1f0-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  congo_brazzaville: {\n    keywords: [ \"congo\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddec\" src=\"1f1e8-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  congo_kinshasa: {\n    keywords: [ \"congo\", \"democratic\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\udde9\" src=\"1f1e8-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cook_islands: {\n    keywords: [ \"cook\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddf0\" src=\"1f1e8-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  costa_rica: {\n    keywords: [ \"costa\", \"rica\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddf7\" src=\"1f1e8-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  croatia: {\n    keywords: [ \"hr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udded\\ud83c\\uddf7\" src=\"1f1ed-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cuba: {\n    keywords: [ \"cu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddfa\" src=\"1f1e8-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  curacao: {\n    keywords: [ \"cura\\xe7ao\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddfc\" src=\"1f1e8-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cyprus: {\n    keywords: [ \"cy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddfe\" src=\"1f1e8-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  czech_republic: {\n    keywords: [ \"cz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddff\" src=\"1f1e8-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  denmark: {\n    keywords: [ \"dk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde9\\ud83c\\uddf0\" src=\"1f1e9-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  djibouti: {\n    keywords: [ \"dj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde9\\ud83c\\uddef\" src=\"1f1e9-1f1ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  dominica: {\n    keywords: [ \"dm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde9\\ud83c\\uddf2\" src=\"1f1e9-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  dominican_republic: {\n    keywords: [ \"dominican\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde9\\ud83c\\uddf4\" src=\"1f1e9-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ecuador: {\n    keywords: [ \"ec\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\udde8\" src=\"1f1ea-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  egypt: {\n    keywords: [ \"eg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\uddec\" src=\"1f1ea-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  el_salvador: {\n    keywords: [ \"el\", \"salvador\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddfb\" src=\"1f1f8-1f1fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  equatorial_guinea: {\n    keywords: [ \"equatorial\", \"gn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf6\" src=\"1f1ec-1f1f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  eritrea: {\n    keywords: [ \"er\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\uddf7\" src=\"1f1ea-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  estonia: {\n    keywords: [ \"ee\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\uddea\" src=\"1f1ea-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ethiopia: {\n    keywords: [ \"et\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\uddf9\" src=\"1f1ea-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  eu: {\n    keywords: [ \"european\", \"union\", \"flag\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\uddfa\" src=\"1f1ea-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  falkland_islands: {\n    keywords: [ \"falkland\", \"islands\", \"malvinas\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddeb\\ud83c\\uddf0\" src=\"1f1eb-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  faroe_islands: {\n    keywords: [ \"faroe\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddeb\\ud83c\\uddf4\" src=\"1f1eb-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  fiji: {\n    keywords: [ \"fj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddeb\\ud83c\\uddef\" src=\"1f1eb-1f1ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  finland: {\n    keywords: [ \"fi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddeb\\ud83c\\uddee\" src=\"1f1eb-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  fr: {\n    keywords: [ \"banner\", \"flag\", \"nation\", \"france\", \"french\", \"country\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddeb\\ud83c\\uddf7\" src=\"1f1eb-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  french_guiana: {\n    keywords: [ \"french\", \"guiana\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddeb\" src=\"1f1ec-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  french_polynesia: {\n    keywords: [ \"french\", \"polynesia\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddeb\" src=\"1f1f5-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  french_southern_territories: {\n    keywords: [ \"french\", \"southern\", \"territories\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddeb\" src=\"1f1f9-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  gabon: {\n    keywords: [ \"ga\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\udde6\" src=\"1f1ec-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  gambia: {\n    keywords: [ \"gm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf2\" src=\"1f1ec-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  georgia: {\n    keywords: [ \"ge\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddea\" src=\"1f1ec-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  de: {\n    keywords: [ \"german\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde9\\ud83c\\uddea\" src=\"1f1e9-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ghana: {\n    keywords: [ \"gh\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\udded\" src=\"1f1ec-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  gibraltar: {\n    keywords: [ \"gi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddee\" src=\"1f1ec-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  greece: {\n    keywords: [ \"gr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf7\" src=\"1f1ec-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  greenland: {\n    keywords: [ \"gl\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf1\" src=\"1f1ec-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  grenada: {\n    keywords: [ \"gd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\udde9\" src=\"1f1ec-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guadeloupe: {\n    keywords: [ \"gp\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf5\" src=\"1f1ec-1f1f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guam: {\n    keywords: [ \"gu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddfa\" src=\"1f1ec-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guatemala: {\n    keywords: [ \"gt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf9\" src=\"1f1ec-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guernsey: {\n    keywords: [ \"gg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddec\" src=\"1f1ec-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guinea: {\n    keywords: [ \"gn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf3\" src=\"1f1ec-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guinea_bissau: {\n    keywords: [ \"gw\", \"bissau\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddfc\" src=\"1f1ec-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guyana: {\n    keywords: [ \"gy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddfe\" src=\"1f1ec-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  haiti: {\n    keywords: [ \"ht\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udded\\ud83c\\uddf9\" src=\"1f1ed-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  honduras: {\n    keywords: [ \"hn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udded\\ud83c\\uddf3\" src=\"1f1ed-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  hong_kong: {\n    keywords: [ \"hong\", \"kong\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udded\\ud83c\\uddf0\" src=\"1f1ed-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  hungary: {\n    keywords: [ \"hu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udded\\ud83c\\uddfa\" src=\"1f1ed-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  iceland: {\n    keywords: [ \"is\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf8\" src=\"1f1ee-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  india: {\n    keywords: [ \"in\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf3\" src=\"1f1ee-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  indonesia: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\udde9\" src=\"1f1ee-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  iran: {\n    keywords: [ \"iran,\", \"islamic\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf7\" src=\"1f1ee-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  iraq: {\n    keywords: [ \"iq\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf6\" src=\"1f1ee-1f1f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ireland: {\n    keywords: [ \"ie\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddea\" src=\"1f1ee-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  isle_of_man: {\n    keywords: [ \"isle\", \"man\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf2\" src=\"1f1ee-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  israel: {\n    keywords: [ \"il\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf1\" src=\"1f1ee-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  it: {\n    keywords: [ \"italy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddee\\ud83c\\uddf9\" src=\"1f1ee-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cote_divoire: {\n    keywords: [ \"ivory\", \"coast\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\uddee\" src=\"1f1e8-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jamaica: {\n    keywords: [ \"jm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddef\\ud83c\\uddf2\" src=\"1f1ef-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jp: {\n    keywords: [ \"japanese\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddef\\ud83c\\uddf5\" src=\"1f1ef-1f1f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jersey: {\n    keywords: [ \"je\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddef\\ud83c\\uddea\" src=\"1f1ef-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jordan: {\n    keywords: [ \"jo\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddef\\ud83c\\uddf4\" src=\"1f1ef-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kazakhstan: {\n    keywords: [ \"kz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddff\" src=\"1f1f0-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kenya: {\n    keywords: [ \"ke\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddea\" src=\"1f1f0-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kiribati: {\n    keywords: [ \"ki\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddee\" src=\"1f1f0-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kosovo: {\n    keywords: [ \"xk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfd\\ud83c\\uddf0\" src=\"1f1fd-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kuwait: {\n    keywords: [ \"kw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddfc\" src=\"1f1f0-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kyrgyzstan: {\n    keywords: [ \"kg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddec\" src=\"1f1f0-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  laos: {\n    keywords: [ \"lao\", \"democratic\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\udde6\" src=\"1f1f1-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  latvia: {\n    keywords: [ \"lv\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddfb\" src=\"1f1f1-1f1fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  lebanon: {\n    keywords: [ \"lb\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\udde7\" src=\"1f1f1-1f1e7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  lesotho: {\n    keywords: [ \"ls\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddf8\" src=\"1f1f1-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  liberia: {\n    keywords: [ \"lr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddf7\" src=\"1f1f1-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  libya: {\n    keywords: [ \"ly\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddfe\" src=\"1f1f1-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  liechtenstein: {\n    keywords: [ \"li\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddee\" src=\"1f1f1-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  lithuania: {\n    keywords: [ \"lt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddf9\" src=\"1f1f1-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  luxembourg: {\n    keywords: [ \"lu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddfa\" src=\"1f1f1-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  macau: {\n    keywords: [ \"macao\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf4\" src=\"1f1f2-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  macedonia: {\n    keywords: [ \"macedonia,\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf0\" src=\"1f1f2-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  madagascar: {\n    keywords: [ \"mg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddec\" src=\"1f1f2-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  malawi: {\n    keywords: [ \"mw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddfc\" src=\"1f1f2-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  malaysia: {\n    keywords: [ \"my\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddfe\" src=\"1f1f2-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  maldives: {\n    keywords: [ \"mv\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddfb\" src=\"1f1f2-1f1fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mali: {\n    keywords: [ \"ml\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf1\" src=\"1f1f2-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  malta: {\n    keywords: [ \"mt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf9\" src=\"1f1f2-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  marshall_islands: {\n    keywords: [ \"marshall\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\udded\" src=\"1f1f2-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  martinique: {\n    keywords: [ \"mq\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf6\" src=\"1f1f2-1f1f6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mauritania: {\n    keywords: [ \"mr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf7\" src=\"1f1f2-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mauritius: {\n    keywords: [ \"mu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddfa\" src=\"1f1f2-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mayotte: {\n    keywords: [ \"yt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfe\\ud83c\\uddf9\" src=\"1f1fe-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mexico: {\n    keywords: [ \"mx\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddfd\" src=\"1f1f2-1f1fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  micronesia: {\n    keywords: [ \"micronesia,\", \"federated\", \"states\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddeb\\ud83c\\uddf2\" src=\"1f1eb-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  moldova: {\n    keywords: [ \"moldova,\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\udde9\" src=\"1f1f2-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  monaco: {\n    keywords: [ \"mc\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\udde8\" src=\"1f1f2-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mongolia: {\n    keywords: [ \"mn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf3\" src=\"1f1f2-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  montenegro: {\n    keywords: [ \"me\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddea\" src=\"1f1f2-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  montserrat: {\n    keywords: [ \"ms\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf8\" src=\"1f1f2-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  morocco: {\n    keywords: [ \"ma\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\udde6\" src=\"1f1f2-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mozambique: {\n    keywords: [ \"mz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddff\" src=\"1f1f2-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  myanmar: {\n    keywords: [ \"mm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf2\" src=\"1f1f2-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  namibia: {\n    keywords: [ \"na\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\udde6\" src=\"1f1f3-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nauru: {\n    keywords: [ \"nr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddf7\" src=\"1f1f3-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nepal: {\n    keywords: [ \"np\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddf5\" src=\"1f1f3-1f1f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  netherlands: {\n    keywords: [ \"nl\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddf1\" src=\"1f1f3-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  new_caledonia: {\n    keywords: [ \"new\", \"caledonia\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\udde8\" src=\"1f1f3-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  new_zealand: {\n    keywords: [ \"new\", \"zealand\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddff\" src=\"1f1f3-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nicaragua: {\n    keywords: [ \"ni\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddee\" src=\"1f1f3-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  niger: {\n    keywords: [ \"ne\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddea\" src=\"1f1f3-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nigeria: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddec\" src=\"1f1f3-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  niue: {\n    keywords: [ \"nu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddfa\" src=\"1f1f3-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  norfolk_island: {\n    keywords: [ \"norfolk\", \"island\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddeb\" src=\"1f1f3-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  northern_mariana_islands: {\n    keywords: [ \"northern\", \"mariana\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf2\\ud83c\\uddf5\" src=\"1f1f2-1f1f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  north_korea: {\n    keywords: [ \"north\", \"korea\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddf5\" src=\"1f1f0-1f1f5.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  norway: {\n    keywords: [ \"no\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf3\\ud83c\\uddf4\" src=\"1f1f3-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  oman: {\n    keywords: [ \"om_symbol\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf4\\ud83c\\uddf2\" src=\"1f1f4-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  pakistan: {\n    keywords: [ \"pk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddf0\" src=\"1f1f5-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  palau: {\n    keywords: [ \"pw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddfc\" src=\"1f1f5-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  palestinian_territories: {\n    keywords: [ \"palestine\", \"palestinian\", \"territories\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddf8\" src=\"1f1f5-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  panama: {\n    keywords: [ \"pa\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\udde6\" src=\"1f1f5-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  papua_new_guinea: {\n    keywords: [ \"papua\", \"new\", \"guinea\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddec\" src=\"1f1f5-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  paraguay: {\n    keywords: [ \"py\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddfe\" src=\"1f1f5-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  peru: {\n    keywords: [ \"pe\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddea\" src=\"1f1f5-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  philippines: {\n    keywords: [ \"ph\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\udded\" src=\"1f1f5-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  pitcairn_islands: {\n    keywords: [ \"pitcairn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddf3\" src=\"1f1f5-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  poland: {\n    keywords: [ \"pl\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddf1\" src=\"1f1f5-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  portugal: {\n    keywords: [ \"pt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddf9\" src=\"1f1f5-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  puerto_rico: {\n    keywords: [ \"puerto\", \"rico\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddf7\" src=\"1f1f5-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  qatar: {\n    keywords: [ \"qa\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf6\\ud83c\\udde6\" src=\"1f1f6-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  reunion: {\n    keywords: [ \"r\\xe9union\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf7\\ud83c\\uddea\" src=\"1f1f7-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  romania: {\n    keywords: [ \"ro\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf7\\ud83c\\uddf4\" src=\"1f1f7-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ru: {\n    keywords: [ \"russian\", \"federation\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf7\\ud83c\\uddfa\" src=\"1f1f7-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  rwanda: {\n    keywords: [ \"rw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf7\\ud83c\\uddfc\" src=\"1f1f7-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_barthelemy: {\n    keywords: [ \"saint\", \"barth\\xe9lemy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde7\\ud83c\\uddf1\" src=\"1f1e7-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_helena: {\n    keywords: [ \"saint\", \"helena\", \"ascension\", \"tristan\", \"cunha\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\udded\" src=\"1f1f8-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_kitts_nevis: {\n    keywords: [ \"saint\", \"kitts\", \"nevis\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddf3\" src=\"1f1f0-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_lucia: {\n    keywords: [ \"saint\", \"lucia\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\udde8\" src=\"1f1f1-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_pierre_miquelon: {\n    keywords: [ \"saint\", \"pierre\", \"miquelon\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf5\\ud83c\\uddf2\" src=\"1f1f5-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_vincent_grenadines: {\n    keywords: [ \"saint\", \"vincent\", \"grenadines\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfb\\ud83c\\udde8\" src=\"1f1fb-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  samoa: {\n    keywords: [ \"ws\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfc\\ud83c\\uddf8\" src=\"1f1fc-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  san_marino: {\n    keywords: [ \"san\", \"marino\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf2\" src=\"1f1f8-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sao_tome_principe: {\n    keywords: [ \"sao\", \"tome\", \"principe\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf9\" src=\"1f1f8-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  saudi_arabia: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\udde6\" src=\"1f1f8-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  senegal: {\n    keywords: [ \"sn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf3\" src=\"1f1f8-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  serbia: {\n    keywords: [ \"rs\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf7\\ud83c\\uddf8\" src=\"1f1f7-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  seychelles: {\n    keywords: [ \"sc\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\udde8\" src=\"1f1f8-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sierra_leone: {\n    keywords: [ \"sierra\", \"leone\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf1\" src=\"1f1f8-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  singapore: {\n    keywords: [ \"sg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddec\" src=\"1f1f8-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sint_maarten: {\n    keywords: [ \"sint\", \"maarten\", \"dutch\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddfd\" src=\"1f1f8-1f1fd.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  slovakia: {\n    keywords: [ \"sk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf0\" src=\"1f1f8-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  slovenia: {\n    keywords: [ \"si\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddee\" src=\"1f1f8-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  solomon_islands: {\n    keywords: [ \"solomon\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\udde7\" src=\"1f1f8-1f1e7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  somalia: {\n    keywords: [ \"so\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf4\" src=\"1f1f8-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  south_africa: {\n    keywords: [ \"south\", \"africa\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddff\\ud83c\\udde6\" src=\"1f1ff-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  south_georgia_south_sandwich_islands: {\n    keywords: [ \"south\", \"georgia\", \"sandwich\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\uddf8\" src=\"1f1ec-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kr: {\n    keywords: [ \"south\", \"korea\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf0\\ud83c\\uddf7\" src=\"1f1f0-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  south_sudan: {\n    keywords: [ \"south\", \"sd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf8\" src=\"1f1f8-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  es: {\n    keywords: [ \"spain\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\uddf8\" src=\"1f1ea-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sri_lanka: {\n    keywords: [ \"sri\", \"lanka\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf1\\ud83c\\uddf0\" src=\"1f1f1-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sudan: {\n    keywords: [ \"sd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\udde9\" src=\"1f1f8-1f1e9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  suriname: {\n    keywords: [ \"sr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddf7\" src=\"1f1f8-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  swaziland: {\n    keywords: [ \"sz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddff\" src=\"1f1f8-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sweden: {\n    keywords: [ \"se\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddea\" src=\"1f1f8-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  switzerland: {\n    keywords: [ \"ch\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde8\\ud83c\\udded\" src=\"1f1e8-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  syria: {\n    keywords: [ \"syrian\", \"arab\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf8\\ud83c\\uddfe\" src=\"1f1f8-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  taiwan: {\n    keywords: [ \"tw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddfc\" src=\"1f1f9-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tajikistan: {\n    keywords: [ \"tj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddef\" src=\"1f1f9-1f1ef.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tanzania: {\n    keywords: [ \"tanzania,\", \"united\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddff\" src=\"1f1f9-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  thailand: {\n    keywords: [ \"th\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\udded\" src=\"1f1f9-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  timor_leste: {\n    keywords: [ \"timor\", \"leste\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddf1\" src=\"1f1f9-1f1f1.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  togo: {\n    keywords: [ \"tg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddec\" src=\"1f1f9-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tokelau: {\n    keywords: [ \"tk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddf0\" src=\"1f1f9-1f1f0.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tonga: {\n    keywords: [ \"to\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddf4\" src=\"1f1f9-1f1f4.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  trinidad_tobago: {\n    keywords: [ \"trinidad\", \"tobago\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddf9\" src=\"1f1f9-1f1f9.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tunisia: {\n    keywords: [ \"tn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddf3\" src=\"1f1f9-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tr: {\n    keywords: [ \"turkey\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddf7\" src=\"1f1f9-1f1f7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  turkmenistan: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddf2\" src=\"1f1f9-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  turks_caicos_islands: {\n    keywords: [ \"turks\", \"caicos\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\udde8\" src=\"1f1f9-1f1e8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tuvalu: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddf9\\ud83c\\uddfb\" src=\"1f1f9-1f1fb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uganda: {\n    keywords: [ \"ug\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfa\\ud83c\\uddec\" src=\"1f1fa-1f1ec.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ukraine: {\n    keywords: [ \"ua\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfa\\ud83c\\udde6\" src=\"1f1fa-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  united_arab_emirates: {\n    keywords: [ \"united\", \"arab\", \"emirates\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udde6\\ud83c\\uddea\" src=\"1f1e6-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uk: {\n    keywords: [ \"united\", \"kingdom\", \"great\", \"britain\", \"northern\", \"ireland\", \"flag\", \"nation\", \"country\", \"banner\", \"british\", \"UK\", \"english\", \"england\", \"union jack\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddec\\ud83c\\udde7\" src=\"1f1ec-1f1e7.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  england: {\n    keywords: [ \"flag\", \"english\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40\\udc65\\udb40\\udc6e\\udb40\\udc67\\udb40\\udc7f\" src=\"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  scotland: {\n    keywords: [ \"flag\", \"scottish\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40\\udc73\\udb40\\udc63\\udb40\\udc74\\udb40\\udc7f\" src=\"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  wales: {\n    keywords: [ \"flag\", \"welsh\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40\\udc77\\udb40\\udc6c\\udb40\\udc73\\udb40\\udc7f\" src=\"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  us: {\n    keywords: [ \"united\", \"states\", \"america\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfa\\ud83c\\uddf8\" src=\"1f1fa-1f1f8.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  us_virgin_islands: {\n    keywords: [ \"virgin\", \"islands\", \"us\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfb\\ud83c\\uddee\" src=\"1f1fb-1f1ee.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uruguay: {\n    keywords: [ \"uy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfa\\ud83c\\uddfe\" src=\"1f1fa-1f1fe.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uzbekistan: {\n    keywords: [ \"uz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfa\\ud83c\\uddff\" src=\"1f1fa-1f1ff.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  vanuatu: {\n    keywords: [ \"vu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfb\\ud83c\\uddfa\" src=\"1f1fb-1f1fa.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  vatican_city: {\n    keywords: [ \"vatican\", \"city\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfb\\ud83c\\udde6\" src=\"1f1fb-1f1e6.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  venezuela: {\n    keywords: [ \"ve\", \"bolivarian\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfb\\ud83c\\uddea\" src=\"1f1fb-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  vietnam: {\n    keywords: [ \"viet\", \"nam\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfb\\ud83c\\uddf3\" src=\"1f1fb-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  wallis_futuna: {\n    keywords: [ \"wallis\", \"futuna\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfc\\ud83c\\uddeb\" src=\"1f1fc-1f1eb.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  western_sahara: {\n    keywords: [ \"western\", \"sahara\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddea\\ud83c\\udded\" src=\"1f1ea-1f1ed.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  yemen: {\n    keywords: [ \"ye\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfe\\ud83c\\uddea\" src=\"1f1fe-1f1ea.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  zambia: {\n    keywords: [ \"zm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddff\\ud83c\\uddf2\" src=\"1f1ff-1f1f2.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  zimbabwe: {\n    keywords: [ \"zw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddff\\ud83c\\uddfc\" src=\"1f1ff-1f1fc.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  united_nations: {\n    keywords: [ \"un\", \"flag\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\uddfa\\ud83c\\uddf3\" src=\"1f1fa-1f1f3.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  pirate_flag: {\n    keywords: [ \"skull\", \"crossbones\", \"flag\", \"banner\" ],\n    char: '<img data-emoticon=\"true\" style=\"width:1em;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em\" draggable=\"false\" alt=\"\\ud83c\\udff4\\u200d\\u2620\\ufe0f\" src=\"1f3f4-200d-2620-fe0f.png\"/>',\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  }\n});"
  },
  {
    "path": "app/src/helpers/vendor/tinymce/plugins/emoticons/js/emojis.js",
    "content": "// Source: npm package: emojilib, file:emojis.json\nwindow.tinymce.Resource.add(\"tinymce.plugins.emoticons\", {\n  grinning: {\n    keywords: [ \"face\", \"smile\", \"happy\", \"joy\", \":D\", \"grin\" ],\n    char: \"\\ud83d\\ude00\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  grimacing: {\n    keywords: [ \"face\", \"grimace\", \"teeth\" ],\n    char: \"\\ud83d\\ude2c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  grin: {\n    keywords: [ \"face\", \"happy\", \"smile\", \"joy\", \"kawaii\" ],\n    char: \"\\ud83d\\ude01\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  joy: {\n    keywords: [ \"face\", \"cry\", \"tears\", \"weep\", \"happy\", \"happytears\", \"haha\" ],\n    char: \"\\ud83d\\ude02\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  rofl: {\n    keywords: [ \"face\", \"rolling\", \"floor\", \"laughing\", \"lol\", \"haha\" ],\n    char: \"\\ud83e\\udd23\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  partying: {\n    keywords: [ \"face\", \"celebration\", \"woohoo\" ],\n    char: \"\\ud83e\\udd73\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiley: {\n    keywords: [ \"face\", \"happy\", \"joy\", \"haha\", \":D\", \":)\", \"smile\", \"funny\" ],\n    char: \"\\ud83d\\ude03\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smile: {\n    keywords: [ \"face\", \"happy\", \"joy\", \"funny\", \"haha\", \"laugh\", \"like\", \":D\", \":)\" ],\n    char: \"\\ud83d\\ude04\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sweat_smile: {\n    keywords: [ \"face\", \"hot\", \"happy\", \"laugh\", \"sweat\", \"smile\", \"relief\" ],\n    char: \"\\ud83d\\ude05\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  laughing: {\n    keywords: [ \"happy\", \"joy\", \"lol\", \"satisfied\", \"haha\", \"face\", \"glad\", \"XD\", \"laugh\" ],\n    char: \"\\ud83d\\ude06\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  innocent: {\n    keywords: [ \"face\", \"angel\", \"heaven\", \"halo\" ],\n    char: \"\\ud83d\\ude07\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  wink: {\n    keywords: [ \"face\", \"happy\", \"mischievous\", \"secret\", \";)\", \"smile\", \"eye\" ],\n    char: \"\\ud83d\\ude09\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  blush: {\n    keywords: [ \"face\", \"smile\", \"happy\", \"flushed\", \"crush\", \"embarrassed\", \"shy\", \"joy\" ],\n    char: \"\\ud83d\\ude0a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  slightly_smiling_face: {\n    keywords: [ \"face\", \"smile\" ],\n    char: \"\\ud83d\\ude42\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  upside_down_face: {\n    keywords: [ \"face\", \"flipped\", \"silly\", \"smile\" ],\n    char: \"\\ud83d\\ude43\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  relaxed: {\n    keywords: [ \"face\", \"blush\", \"massage\", \"happiness\" ],\n    char: \"\\u263a\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  yum: {\n    keywords: [ \"happy\", \"joy\", \"tongue\", \"smile\", \"face\", \"silly\", \"yummy\", \"nom\", \"delicious\", \"savouring\" ],\n    char: \"\\ud83d\\ude0b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  relieved: {\n    keywords: [ \"face\", \"relaxed\", \"phew\", \"massage\", \"happiness\" ],\n    char: \"\\ud83d\\ude0c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  heart_eyes: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"crush\", \"heart\" ],\n    char: \"\\ud83d\\ude0d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiling_face_with_three_hearts: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"crush\", \"hearts\", \"adore\" ],\n    char: \"\\ud83e\\udd70\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_heart: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: \"\\ud83d\\ude18\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing: {\n    keywords: [ \"love\", \"like\", \"face\", \"3\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: \"\\ud83d\\ude17\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_smiling_eyes: {\n    keywords: [ \"face\", \"affection\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: \"\\ud83d\\ude19\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_closed_eyes: {\n    keywords: [ \"face\", \"love\", \"like\", \"affection\", \"valentines\", \"infatuation\", \"kiss\" ],\n    char: \"\\ud83d\\ude1a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  stuck_out_tongue_winking_eye: {\n    keywords: [ \"face\", \"prank\", \"childish\", \"playful\", \"mischievous\", \"smile\", \"wink\", \"tongue\" ],\n    char: \"\\ud83d\\ude1c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  zany: {\n    keywords: [ \"face\", \"goofy\", \"crazy\" ],\n    char: \"\\ud83e\\udd2a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  raised_eyebrow: {\n    keywords: [ \"face\", \"distrust\", \"scepticism\", \"disapproval\", \"disbelief\", \"surprise\" ],\n    char: \"\\ud83e\\udd28\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  monocle: {\n    keywords: [ \"face\", \"stuffy\", \"wealthy\" ],\n    char: \"\\ud83e\\uddd0\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  stuck_out_tongue_closed_eyes: {\n    keywords: [ \"face\", \"prank\", \"playful\", \"mischievous\", \"smile\", \"tongue\" ],\n    char: \"\\ud83d\\ude1d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  stuck_out_tongue: {\n    keywords: [ \"face\", \"prank\", \"childish\", \"playful\", \"mischievous\", \"smile\", \"tongue\" ],\n    char: \"\\ud83d\\ude1b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  money_mouth_face: {\n    keywords: [ \"face\", \"rich\", \"dollar\", \"money\" ],\n    char: \"\\ud83e\\udd11\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  nerd_face: {\n    keywords: [ \"face\", \"nerdy\", \"geek\", \"dork\" ],\n    char: \"\\ud83e\\udd13\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sunglasses: {\n    keywords: [ \"face\", \"cool\", \"smile\", \"summer\", \"beach\", \"sunglass\" ],\n    char: \"\\ud83d\\ude0e\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  star_struck: {\n    keywords: [ \"face\", \"smile\", \"starry\", \"eyes\", \"grinning\" ],\n    char: \"\\ud83e\\udd29\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  clown_face: {\n    keywords: [ \"face\" ],\n    char: \"\\ud83e\\udd21\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cowboy_hat_face: {\n    keywords: [ \"face\", \"cowgirl\", \"hat\" ],\n    char: \"\\ud83e\\udd20\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hugs: {\n    keywords: [ \"face\", \"smile\", \"hug\" ],\n    char: \"\\ud83e\\udd17\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smirk: {\n    keywords: [ \"face\", \"smile\", \"mean\", \"prank\", \"smug\", \"sarcasm\" ],\n    char: \"\\ud83d\\ude0f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  no_mouth: {\n    keywords: [ \"face\", \"hellokitty\" ],\n    char: \"\\ud83d\\ude36\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  neutral_face: {\n    keywords: [ \"indifference\", \"meh\", \":|\", \"neutral\" ],\n    char: \"\\ud83d\\ude10\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  expressionless: {\n    keywords: [ \"face\", \"indifferent\", \"-_-\", \"meh\", \"deadpan\" ],\n    char: \"\\ud83d\\ude11\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  unamused: {\n    keywords: [ \"indifference\", \"bored\", \"straight face\", \"serious\", \"sarcasm\", \"unimpressed\", \"skeptical\", \"dubious\", \"side_eye\" ],\n    char: \"\\ud83d\\ude12\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  roll_eyes: {\n    keywords: [ \"face\", \"eyeroll\", \"frustrated\" ],\n    char: \"\\ud83d\\ude44\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  thinking: {\n    keywords: [ \"face\", \"hmmm\", \"think\", \"consider\" ],\n    char: \"\\ud83e\\udd14\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  lying_face: {\n    keywords: [ \"face\", \"lie\", \"pinocchio\" ],\n    char: \"\\ud83e\\udd25\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hand_over_mouth: {\n    keywords: [ \"face\", \"whoops\", \"shock\", \"surprise\" ],\n    char: \"\\ud83e\\udd2d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  shushing: {\n    keywords: [ \"face\", \"quiet\", \"shhh\" ],\n    char: \"\\ud83e\\udd2b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  symbols_over_mouth: {\n    keywords: [ \"face\", \"swearing\", \"cursing\", \"cussing\", \"profanity\", \"expletive\" ],\n    char: \"\\ud83e\\udd2c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  exploding_head: {\n    keywords: [ \"face\", \"shocked\", \"mind\", \"blown\" ],\n    char: \"\\ud83e\\udd2f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  flushed: {\n    keywords: [ \"face\", \"blush\", \"shy\", \"flattered\" ],\n    char: \"\\ud83d\\ude33\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  disappointed: {\n    keywords: [ \"face\", \"sad\", \"upset\", \"depressed\", \":(\" ],\n    char: \"\\ud83d\\ude1e\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  worried: {\n    keywords: [ \"face\", \"concern\", \"nervous\", \":(\" ],\n    char: \"\\ud83d\\ude1f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  angry: {\n    keywords: [ \"mad\", \"face\", \"annoyed\", \"frustrated\" ],\n    char: \"\\ud83d\\ude20\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  rage: {\n    keywords: [ \"angry\", \"mad\", \"hate\", \"despise\" ],\n    char: \"\\ud83d\\ude21\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pensive: {\n    keywords: [ \"face\", \"sad\", \"depressed\", \"upset\" ],\n    char: \"\\ud83d\\ude14\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  confused: {\n    keywords: [ \"face\", \"indifference\", \"huh\", \"weird\", \"hmmm\", \":/\" ],\n    char: \"\\ud83d\\ude15\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  slightly_frowning_face: {\n    keywords: [ \"face\", \"frowning\", \"disappointed\", \"sad\", \"upset\" ],\n    char: \"\\ud83d\\ude41\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  frowning_face: {\n    keywords: [ \"face\", \"sad\", \"upset\", \"frown\" ],\n    char: \"\\u2639\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  persevere: {\n    keywords: [ \"face\", \"sick\", \"no\", \"upset\", \"oops\" ],\n    char: \"\\ud83d\\ude23\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  confounded: {\n    keywords: [ \"face\", \"confused\", \"sick\", \"unwell\", \"oops\", \":S\" ],\n    char: \"\\ud83d\\ude16\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tired_face: {\n    keywords: [ \"sick\", \"whine\", \"upset\", \"frustrated\" ],\n    char: \"\\ud83d\\ude2b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  weary: {\n    keywords: [ \"face\", \"tired\", \"sleepy\", \"sad\", \"frustrated\", \"upset\" ],\n    char: \"\\ud83d\\ude29\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pleading: {\n    keywords: [ \"face\", \"begging\", \"mercy\" ],\n    char: \"\\ud83e\\udd7a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  triumph: {\n    keywords: [ \"face\", \"gas\", \"phew\", \"proud\", \"pride\" ],\n    char: \"\\ud83d\\ude24\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  open_mouth: {\n    keywords: [ \"face\", \"surprise\", \"impressed\", \"wow\", \"whoa\", \":O\" ],\n    char: \"\\ud83d\\ude2e\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  scream: {\n    keywords: [ \"face\", \"munch\", \"scared\", \"omg\" ],\n    char: \"\\ud83d\\ude31\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  fearful: {\n    keywords: [ \"face\", \"scared\", \"terrified\", \"nervous\", \"oops\", \"huh\" ],\n    char: \"\\ud83d\\ude28\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cold_sweat: {\n    keywords: [ \"face\", \"nervous\", \"sweat\" ],\n    char: \"\\ud83d\\ude30\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hushed: {\n    keywords: [ \"face\", \"woo\", \"shh\" ],\n    char: \"\\ud83d\\ude2f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  frowning: {\n    keywords: [ \"face\", \"aw\", \"what\" ],\n    char: \"\\ud83d\\ude26\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  anguished: {\n    keywords: [ \"face\", \"stunned\", \"nervous\" ],\n    char: \"\\ud83d\\ude27\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cry: {\n    keywords: [ \"face\", \"tears\", \"sad\", \"depressed\", \"upset\", \":'(\" ],\n    char: \"\\ud83d\\ude22\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  disappointed_relieved: {\n    keywords: [ \"face\", \"phew\", \"sweat\", \"nervous\" ],\n    char: \"\\ud83d\\ude25\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  drooling_face: {\n    keywords: [ \"face\" ],\n    char: \"\\ud83e\\udd24\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sleepy: {\n    keywords: [ \"face\", \"tired\", \"rest\", \"nap\" ],\n    char: \"\\ud83d\\ude2a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sweat: {\n    keywords: [ \"face\", \"hot\", \"sad\", \"tired\", \"exercise\" ],\n    char: \"\\ud83d\\ude13\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hot: {\n    keywords: [ \"face\", \"feverish\", \"heat\", \"red\", \"sweating\" ],\n    char: \"\\ud83e\\udd75\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  cold: {\n    keywords: [ \"face\", \"blue\", \"freezing\", \"frozen\", \"frostbite\", \"icicles\" ],\n    char: \"\\ud83e\\udd76\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sob: {\n    keywords: [ \"face\", \"cry\", \"tears\", \"sad\", \"upset\", \"depressed\" ],\n    char: \"\\ud83d\\ude2d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dizzy_face: {\n    keywords: [ \"spent\", \"unconscious\", \"xox\", \"dizzy\" ],\n    char: \"\\ud83d\\ude35\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  astonished: {\n    keywords: [ \"face\", \"xox\", \"surprised\", \"poisoned\" ],\n    char: \"\\ud83d\\ude32\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  zipper_mouth_face: {\n    keywords: [ \"face\", \"sealed\", \"zipper\", \"secret\" ],\n    char: \"\\ud83e\\udd10\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  nauseated_face: {\n    keywords: [ \"face\", \"vomit\", \"gross\", \"green\", \"sick\", \"throw up\", \"ill\" ],\n    char: \"\\ud83e\\udd22\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sneezing_face: {\n    keywords: [ \"face\", \"gesundheit\", \"sneeze\", \"sick\", \"allergy\" ],\n    char: \"\\ud83e\\udd27\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  vomiting: {\n    keywords: [ \"face\", \"sick\" ],\n    char: \"\\ud83e\\udd2e\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mask: {\n    keywords: [ \"face\", \"sick\", \"ill\", \"disease\" ],\n    char: \"\\ud83d\\ude37\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  face_with_thermometer: {\n    keywords: [ \"sick\", \"temperature\", \"thermometer\", \"cold\", \"fever\" ],\n    char: \"\\ud83e\\udd12\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  face_with_head_bandage: {\n    keywords: [ \"injured\", \"clumsy\", \"bandage\", \"hurt\" ],\n    char: \"\\ud83e\\udd15\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  woozy: {\n    keywords: [ \"face\", \"dizzy\", \"intoxicated\", \"tipsy\", \"wavy\" ],\n    char: \"\\ud83e\\udd74\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sleeping: {\n    keywords: [ \"face\", \"tired\", \"sleepy\", \"night\", \"zzz\" ],\n    char: \"\\ud83d\\ude34\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  zzz: {\n    keywords: [ \"sleepy\", \"tired\", \"dream\" ],\n    char: \"\\ud83d\\udca4\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  poop: {\n    keywords: [ \"hankey\", \"shitface\", \"fail\", \"turd\", \"shit\" ],\n    char: \"\\ud83d\\udca9\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiling_imp: {\n    keywords: [ \"devil\", \"horns\" ],\n    char: \"\\ud83d\\ude08\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  imp: {\n    keywords: [ \"devil\", \"angry\", \"horns\" ],\n    char: \"\\ud83d\\udc7f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  japanese_ogre: {\n    keywords: [ \"monster\", \"red\", \"mask\", \"halloween\", \"scary\", \"creepy\", \"devil\", \"demon\", \"japanese\", \"ogre\" ],\n    char: \"\\ud83d\\udc79\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  japanese_goblin: {\n    keywords: [ \"red\", \"evil\", \"mask\", \"monster\", \"scary\", \"creepy\", \"japanese\", \"goblin\" ],\n    char: \"\\ud83d\\udc7a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  skull: {\n    keywords: [ \"dead\", \"skeleton\", \"creepy\", \"death\" ],\n    char: \"\\ud83d\\udc80\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  ghost: {\n    keywords: [ \"halloween\", \"spooky\", \"scary\" ],\n    char: \"\\ud83d\\udc7b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  alien: {\n    keywords: [ \"UFO\", \"paul\", \"weird\", \"outer_space\" ],\n    char: \"\\ud83d\\udc7d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  robot: {\n    keywords: [ \"computer\", \"machine\", \"bot\" ],\n    char: \"\\ud83e\\udd16\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smiley_cat: {\n    keywords: [ \"animal\", \"cats\", \"happy\", \"smile\" ],\n    char: \"\\ud83d\\ude3a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smile_cat: {\n    keywords: [ \"animal\", \"cats\", \"smile\" ],\n    char: \"\\ud83d\\ude38\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  joy_cat: {\n    keywords: [ \"animal\", \"cats\", \"haha\", \"happy\", \"tears\" ],\n    char: \"\\ud83d\\ude39\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  heart_eyes_cat: {\n    keywords: [ \"animal\", \"love\", \"like\", \"affection\", \"cats\", \"valentines\", \"heart\" ],\n    char: \"\\ud83d\\ude3b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  smirk_cat: {\n    keywords: [ \"animal\", \"cats\", \"smirk\" ],\n    char: \"\\ud83d\\ude3c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kissing_cat: {\n    keywords: [ \"animal\", \"cats\", \"kiss\" ],\n    char: \"\\ud83d\\ude3d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  scream_cat: {\n    keywords: [ \"animal\", \"cats\", \"munch\", \"scared\", \"scream\" ],\n    char: \"\\ud83d\\ude40\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  crying_cat_face: {\n    keywords: [ \"animal\", \"tears\", \"weep\", \"sad\", \"cats\", \"upset\", \"cry\" ],\n    char: \"\\ud83d\\ude3f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pouting_cat: {\n    keywords: [ \"animal\", \"cats\" ],\n    char: \"\\ud83d\\ude3e\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  palms_up: {\n    keywords: [ \"hands\", \"gesture\", \"cupped\", \"prayer\" ],\n    char: \"\\ud83e\\udd32\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_hands: {\n    keywords: [ \"gesture\", \"hooray\", \"yea\", \"celebration\", \"hands\" ],\n    char: \"\\ud83d\\ude4c\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  clap: {\n    keywords: [ \"hands\", \"praise\", \"applause\", \"congrats\", \"yay\" ],\n    char: \"\\ud83d\\udc4f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  wave: {\n    keywords: [ \"hands\", \"gesture\", \"goodbye\", \"solong\", \"farewell\", \"hello\", \"hi\", \"palm\" ],\n    char: \"\\ud83d\\udc4b\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  call_me_hand: {\n    keywords: [ \"hands\", \"gesture\" ],\n    char: \"\\ud83e\\udd19\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  \"+1\": {\n    keywords: [ \"thumbsup\", \"yes\", \"awesome\", \"good\", \"agree\", \"accept\", \"cool\", \"hand\", \"like\" ],\n    char: \"\\ud83d\\udc4d\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  \"-1\": {\n    keywords: [ \"thumbsdown\", \"no\", \"dislike\", \"hand\" ],\n    char: \"\\ud83d\\udc4e\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  facepunch: {\n    keywords: [ \"angry\", \"violence\", \"fist\", \"hit\", \"attack\", \"hand\" ],\n    char: \"\\ud83d\\udc4a\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fist: {\n    keywords: [ \"fingers\", \"hand\", \"grasp\" ],\n    char: \"\\u270a\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fist_left: {\n    keywords: [ \"hand\", \"fistbump\" ],\n    char: \"\\ud83e\\udd1b\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fist_right: {\n    keywords: [ \"hand\", \"fistbump\" ],\n    char: \"\\ud83e\\udd1c\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  v: {\n    keywords: [ \"fingers\", \"ohyeah\", \"hand\", \"peace\", \"victory\", \"two\" ],\n    char: \"\\u270c\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  ok_hand: {\n    keywords: [ \"fingers\", \"limbs\", \"perfect\", \"ok\", \"okay\" ],\n    char: \"\\ud83d\\udc4c\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_hand: {\n    keywords: [ \"fingers\", \"stop\", \"highfive\", \"palm\", \"ban\" ],\n    char: \"\\u270b\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_back_of_hand: {\n    keywords: [ \"fingers\", \"raised\", \"backhand\" ],\n    char: \"\\ud83e\\udd1a\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  open_hands: {\n    keywords: [ \"fingers\", \"butterfly\", \"hands\", \"open\" ],\n    char: \"\\ud83d\\udc50\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  muscle: {\n    keywords: [ \"arm\", \"flex\", \"hand\", \"summer\", \"strong\", \"biceps\" ],\n    char: \"\\ud83d\\udcaa\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pray: {\n    keywords: [ \"please\", \"hope\", \"wish\", \"namaste\", \"highfive\" ],\n    char: \"\\ud83d\\ude4f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  foot: {\n    keywords: [ \"kick\", \"stomp\" ],\n    char: \"\\ud83e\\uddb6\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  leg: {\n    keywords: [ \"kick\", \"limb\" ],\n    char: \"\\ud83e\\uddb5\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  handshake: {\n    keywords: [ \"agreement\", \"shake\" ],\n    char: \"\\ud83e\\udd1d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  point_up: {\n    keywords: [ \"hand\", \"fingers\", \"direction\", \"up\" ],\n    char: \"\\u261d\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_up_2: {\n    keywords: [ \"fingers\", \"hand\", \"direction\", \"up\" ],\n    char: \"\\ud83d\\udc46\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_down: {\n    keywords: [ \"fingers\", \"hand\", \"direction\", \"down\" ],\n    char: \"\\ud83d\\udc47\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_left: {\n    keywords: [ \"direction\", \"fingers\", \"hand\", \"left\" ],\n    char: \"\\ud83d\\udc48\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  point_right: {\n    keywords: [ \"fingers\", \"hand\", \"direction\", \"right\" ],\n    char: \"\\ud83d\\udc49\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  fu: {\n    keywords: [ \"hand\", \"fingers\", \"rude\", \"middle\", \"flipping\" ],\n    char: \"\\ud83d\\udd95\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raised_hand_with_fingers_splayed: {\n    keywords: [ \"hand\", \"fingers\", \"palm\" ],\n    char: \"\\ud83d\\udd90\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  love_you: {\n    keywords: [ \"hand\", \"fingers\", \"gesture\" ],\n    char: \"\\ud83e\\udd1f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  metal: {\n    keywords: [ \"hand\", \"fingers\", \"evil_eye\", \"sign_of_horns\", \"rock_on\" ],\n    char: \"\\ud83e\\udd18\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  crossed_fingers: {\n    keywords: [ \"good\", \"lucky\" ],\n    char: \"\\ud83e\\udd1e\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  vulcan_salute: {\n    keywords: [ \"hand\", \"fingers\", \"spock\", \"star trek\" ],\n    char: \"\\ud83d\\udd96\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  writing_hand: {\n    keywords: [ \"lower_left_ballpoint_pen\", \"stationery\", \"write\", \"compose\" ],\n    char: \"\\u270d\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  selfie: {\n    keywords: [ \"camera\", \"phone\" ],\n    char: \"\\ud83e\\udd33\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  nail_care: {\n    keywords: [ \"beauty\", \"manicure\", \"finger\", \"fashion\", \"nail\" ],\n    char: \"\\ud83d\\udc85\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  lips: {\n    keywords: [ \"mouth\", \"kiss\" ],\n    char: \"\\ud83d\\udc44\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tooth: {\n    keywords: [ \"teeth\", \"dentist\" ],\n    char: \"\\ud83e\\uddb7\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tongue: {\n    keywords: [ \"mouth\", \"playful\" ],\n    char: \"\\ud83d\\udc45\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  ear: {\n    keywords: [ \"face\", \"hear\", \"sound\", \"listen\" ],\n    char: \"\\ud83d\\udc42\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  nose: {\n    keywords: [ \"smell\", \"sniff\" ],\n    char: \"\\ud83d\\udc43\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  eye: {\n    keywords: [ \"face\", \"look\", \"see\", \"watch\", \"stare\" ],\n    char: \"\\ud83d\\udc41\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  eyes: {\n    keywords: [ \"look\", \"watch\", \"stalk\", \"peek\", \"see\" ],\n    char: \"\\ud83d\\udc40\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  brain: {\n    keywords: [ \"smart\", \"intelligent\" ],\n    char: \"\\ud83e\\udde0\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  bust_in_silhouette: {\n    keywords: [ \"user\", \"person\", \"human\" ],\n    char: \"\\ud83d\\udc64\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  busts_in_silhouette: {\n    keywords: [ \"user\", \"person\", \"human\", \"group\", \"team\" ],\n    char: \"\\ud83d\\udc65\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  speaking_head: {\n    keywords: [ \"user\", \"person\", \"human\", \"sing\", \"say\", \"talk\" ],\n    char: \"\\ud83d\\udde3\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  baby: {\n    keywords: [ \"child\", \"boy\", \"girl\", \"toddler\" ],\n    char: \"\\ud83d\\udc76\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  child: {\n    keywords: [ \"gender-neutral\", \"young\" ],\n    char: \"\\ud83e\\uddd2\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  boy: {\n    keywords: [ \"man\", \"male\", \"guy\", \"teenager\" ],\n    char: \"\\ud83d\\udc66\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  girl: {\n    keywords: [ \"female\", \"woman\", \"teenager\" ],\n    char: \"\\ud83d\\udc67\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  adult: {\n    keywords: [ \"gender-neutral\", \"person\" ],\n    char: \"\\ud83e\\uddd1\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man: {\n    keywords: [ \"mustache\", \"father\", \"dad\", \"guy\", \"classy\", \"sir\", \"moustache\" ],\n    char: \"\\ud83d\\udc68\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman: {\n    keywords: [ \"female\", \"girls\", \"lady\" ],\n    char: \"\\ud83d\\udc69\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  blonde_woman: {\n    keywords: [ \"woman\", \"female\", \"girl\", \"blonde\", \"person\" ],\n    char: \"\\ud83d\\udc71\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  blonde_man: {\n    keywords: [ \"man\", \"male\", \"boy\", \"blonde\", \"guy\", \"person\" ],\n    char: \"\\ud83d\\udc71\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  bearded_person: {\n    keywords: [ \"person\", \"bewhiskered\" ],\n    char: \"\\ud83e\\uddd4\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  older_adult: {\n    keywords: [ \"human\", \"elder\", \"senior\", \"gender-neutral\" ],\n    char: \"\\ud83e\\uddd3\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  older_man: {\n    keywords: [ \"human\", \"male\", \"men\", \"old\", \"elder\", \"senior\" ],\n    char: \"\\ud83d\\udc74\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  older_woman: {\n    keywords: [ \"human\", \"female\", \"women\", \"lady\", \"old\", \"elder\", \"senior\" ],\n    char: \"\\ud83d\\udc75\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_with_gua_pi_mao: {\n    keywords: [ \"male\", \"boy\", \"chinese\" ],\n    char: \"\\ud83d\\udc72\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_with_headscarf: {\n    keywords: [ \"female\", \"hijab\", \"mantilla\", \"tichel\" ],\n    char: \"\\ud83e\\uddd5\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_with_turban: {\n    keywords: [ \"female\", \"indian\", \"hinduism\", \"arabs\", \"woman\" ],\n    char: \"\\ud83d\\udc73\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_with_turban: {\n    keywords: [ \"male\", \"indian\", \"hinduism\", \"arabs\" ],\n    char: \"\\ud83d\\udc73\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  policewoman: {\n    keywords: [ \"woman\", \"police\", \"law\", \"legal\", \"enforcement\", \"arrest\", \"911\", \"female\" ],\n    char: \"\\ud83d\\udc6e\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  policeman: {\n    keywords: [ \"man\", \"police\", \"law\", \"legal\", \"enforcement\", \"arrest\", \"911\" ],\n    char: \"\\ud83d\\udc6e\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  construction_worker_woman: {\n    keywords: [ \"female\", \"human\", \"wip\", \"build\", \"construction\", \"worker\", \"labor\", \"woman\" ],\n    char: \"\\ud83d\\udc77\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  construction_worker_man: {\n    keywords: [ \"male\", \"human\", \"wip\", \"guy\", \"build\", \"construction\", \"worker\", \"labor\" ],\n    char: \"\\ud83d\\udc77\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  guardswoman: {\n    keywords: [ \"uk\", \"gb\", \"british\", \"female\", \"royal\", \"woman\" ],\n    char: \"\\ud83d\\udc82\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  guardsman: {\n    keywords: [ \"uk\", \"gb\", \"british\", \"male\", \"guy\", \"royal\" ],\n    char: \"\\ud83d\\udc82\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  female_detective: {\n    keywords: [ \"human\", \"spy\", \"detective\", \"female\", \"woman\" ],\n    char: \"\\ud83d\\udd75\\ufe0f\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  male_detective: {\n    keywords: [ \"human\", \"spy\", \"detective\" ],\n    char: \"\\ud83d\\udd75\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_health_worker: {\n    keywords: [ \"doctor\", \"nurse\", \"therapist\", \"healthcare\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\u2695\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_health_worker: {\n    keywords: [ \"doctor\", \"nurse\", \"therapist\", \"healthcare\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\u2695\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_farmer: {\n    keywords: [ \"rancher\", \"gardener\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83c\\udf3e\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_farmer: {\n    keywords: [ \"rancher\", \"gardener\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83c\\udf3e\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_cook: {\n    keywords: [ \"chef\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83c\\udf73\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_cook: {\n    keywords: [ \"chef\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83c\\udf73\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_student: {\n    keywords: [ \"graduate\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83c\\udf93\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_student: {\n    keywords: [ \"graduate\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83c\\udf93\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_singer: {\n    keywords: [ \"rockstar\", \"entertainer\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83c\\udfa4\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_singer: {\n    keywords: [ \"rockstar\", \"entertainer\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83c\\udfa4\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_teacher: {\n    keywords: [ \"instructor\", \"professor\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83c\\udfeb\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_teacher: {\n    keywords: [ \"instructor\", \"professor\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83c\\udfeb\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_factory_worker: {\n    keywords: [ \"assembly\", \"industrial\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83c\\udfed\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_factory_worker: {\n    keywords: [ \"assembly\", \"industrial\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83c\\udfed\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_technologist: {\n    keywords: [ \"coder\", \"developer\", \"engineer\", \"programmer\", \"software\", \"woman\", \"human\", \"laptop\", \"computer\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udcbb\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_technologist: {\n    keywords: [ \"coder\", \"developer\", \"engineer\", \"programmer\", \"software\", \"man\", \"human\", \"laptop\", \"computer\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udcbb\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_office_worker: {\n    keywords: [ \"business\", \"manager\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udcbc\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_office_worker: {\n    keywords: [ \"business\", \"manager\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udcbc\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_mechanic: {\n    keywords: [ \"plumber\", \"woman\", \"human\", \"wrench\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udd27\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_mechanic: {\n    keywords: [ \"plumber\", \"man\", \"human\", \"wrench\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udd27\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_scientist: {\n    keywords: [ \"biologist\", \"chemist\", \"engineer\", \"physicist\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udd2c\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_scientist: {\n    keywords: [ \"biologist\", \"chemist\", \"engineer\", \"physicist\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udd2c\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_artist: {\n    keywords: [ \"painter\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83c\\udfa8\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_artist: {\n    keywords: [ \"painter\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83c\\udfa8\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_firefighter: {\n    keywords: [ \"fireman\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\ude92\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_firefighter: {\n    keywords: [ \"fireman\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\ude92\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_pilot: {\n    keywords: [ \"aviator\", \"plane\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\u2708\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_pilot: {\n    keywords: [ \"aviator\", \"plane\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\u2708\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_astronaut: {\n    keywords: [ \"space\", \"rocket\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\ude80\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_astronaut: {\n    keywords: [ \"space\", \"rocket\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\ude80\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_judge: {\n    keywords: [ \"justice\", \"court\", \"woman\", \"human\" ],\n    char: \"\\ud83d\\udc69\\u200d\\u2696\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_judge: {\n    keywords: [ \"justice\", \"court\", \"man\", \"human\" ],\n    char: \"\\ud83d\\udc68\\u200d\\u2696\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_superhero: {\n    keywords: [ \"woman\", \"female\", \"good\", \"heroine\", \"superpowers\" ],\n    char: \"\\ud83e\\uddb8\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_superhero: {\n    keywords: [ \"man\", \"male\", \"good\", \"hero\", \"superpowers\" ],\n    char: \"\\ud83e\\uddb8\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_supervillain: {\n    keywords: [ \"woman\", \"female\", \"evil\", \"bad\", \"criminal\", \"heroine\", \"superpowers\" ],\n    char: \"\\ud83e\\uddb9\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_supervillain: {\n    keywords: [ \"man\", \"male\", \"evil\", \"bad\", \"criminal\", \"hero\", \"superpowers\" ],\n    char: \"\\ud83e\\uddb9\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  mrs_claus: {\n    keywords: [ \"woman\", \"female\", \"xmas\", \"mother christmas\" ],\n    char: \"\\ud83e\\udd36\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  santa: {\n    keywords: [ \"festival\", \"man\", \"male\", \"xmas\", \"father christmas\" ],\n    char: \"\\ud83c\\udf85\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  sorceress: {\n    keywords: [ \"woman\", \"female\", \"mage\", \"witch\" ],\n    char: \"\\ud83e\\uddd9\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  wizard: {\n    keywords: [ \"man\", \"male\", \"mage\", \"sorcerer\" ],\n    char: \"\\ud83e\\uddd9\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_elf: {\n    keywords: [ \"woman\", \"female\" ],\n    char: \"\\ud83e\\udddd\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_elf: {\n    keywords: [ \"man\", \"male\" ],\n    char: \"\\ud83e\\udddd\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_vampire: {\n    keywords: [ \"woman\", \"female\" ],\n    char: \"\\ud83e\\udddb\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_vampire: {\n    keywords: [ \"man\", \"male\", \"dracula\" ],\n    char: \"\\ud83e\\udddb\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_zombie: {\n    keywords: [ \"woman\", \"female\", \"undead\", \"walking dead\" ],\n    char: \"\\ud83e\\udddf\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  man_zombie: {\n    keywords: [ \"man\", \"male\", \"dracula\", \"undead\", \"walking dead\" ],\n    char: \"\\ud83e\\udddf\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  woman_genie: {\n    keywords: [ \"woman\", \"female\" ],\n    char: \"\\ud83e\\uddde\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  man_genie: {\n    keywords: [ \"man\", \"male\" ],\n    char: \"\\ud83e\\uddde\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mermaid: {\n    keywords: [ \"woman\", \"female\", \"merwoman\", \"ariel\" ],\n    char: \"\\ud83e\\udddc\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  merman: {\n    keywords: [ \"man\", \"male\", \"triton\" ],\n    char: \"\\ud83e\\udddc\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_fairy: {\n    keywords: [ \"woman\", \"female\" ],\n    char: \"\\ud83e\\uddda\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_fairy: {\n    keywords: [ \"man\", \"male\" ],\n    char: \"\\ud83e\\uddda\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  angel: {\n    keywords: [ \"heaven\", \"wings\", \"halo\" ],\n    char: \"\\ud83d\\udc7c\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pregnant_woman: {\n    keywords: [ \"baby\" ],\n    char: \"\\ud83e\\udd30\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  breastfeeding: {\n    keywords: [ \"nursing\", \"baby\" ],\n    char: \"\\ud83e\\udd31\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  princess: {\n    keywords: [ \"girl\", \"woman\", \"female\", \"blond\", \"crown\", \"royal\", \"queen\" ],\n    char: \"\\ud83d\\udc78\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  prince: {\n    keywords: [ \"boy\", \"man\", \"male\", \"crown\", \"royal\", \"king\" ],\n    char: \"\\ud83e\\udd34\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  bride_with_veil: {\n    keywords: [ \"couple\", \"marriage\", \"wedding\", \"woman\", \"bride\" ],\n    char: \"\\ud83d\\udc70\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_in_tuxedo: {\n    keywords: [ \"couple\", \"marriage\", \"wedding\", \"groom\" ],\n    char: \"\\ud83e\\udd35\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  running_woman: {\n    keywords: [ \"woman\", \"walking\", \"exercise\", \"race\", \"running\", \"female\" ],\n    char: \"\\ud83c\\udfc3\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  running_man: {\n    keywords: [ \"man\", \"walking\", \"exercise\", \"race\", \"running\" ],\n    char: \"\\ud83c\\udfc3\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  walking_woman: {\n    keywords: [ \"human\", \"feet\", \"steps\", \"woman\", \"female\" ],\n    char: \"\\ud83d\\udeb6\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  walking_man: {\n    keywords: [ \"human\", \"feet\", \"steps\" ],\n    char: \"\\ud83d\\udeb6\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  dancer: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"fun\" ],\n    char: \"\\ud83d\\udc83\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_dancing: {\n    keywords: [ \"male\", \"boy\", \"fun\", \"dancer\" ],\n    char: \"\\ud83d\\udd7a\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  dancing_women: {\n    keywords: [ \"female\", \"bunny\", \"women\", \"girls\" ],\n    char: \"\\ud83d\\udc6f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dancing_men: {\n    keywords: [ \"male\", \"bunny\", \"men\", \"boys\" ],\n    char: \"\\ud83d\\udc6f\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couple: {\n    keywords: [ \"pair\", \"people\", \"human\", \"love\", \"date\", \"dating\", \"like\", \"affection\", \"valentines\", \"marriage\" ],\n    char: \"\\ud83d\\udc6b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  two_men_holding_hands: {\n    keywords: [ \"pair\", \"couple\", \"love\", \"like\", \"bromance\", \"friendship\", \"people\", \"human\" ],\n    char: \"\\ud83d\\udc6c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  two_women_holding_hands: {\n    keywords: [ \"pair\", \"friendship\", \"couple\", \"love\", \"like\", \"female\", \"people\", \"human\" ],\n    char: \"\\ud83d\\udc6d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  bowing_woman: {\n    keywords: [ \"woman\", \"female\", \"girl\" ],\n    char: \"\\ud83d\\ude47\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  bowing_man: {\n    keywords: [ \"man\", \"male\", \"boy\" ],\n    char: \"\\ud83d\\ude47\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_facepalming: {\n    keywords: [ \"man\", \"male\", \"boy\", \"disbelief\" ],\n    char: \"\\ud83e\\udd26\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_facepalming: {\n    keywords: [ \"woman\", \"female\", \"girl\", \"disbelief\" ],\n    char: \"\\ud83e\\udd26\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_shrugging: {\n    keywords: [ \"woman\", \"female\", \"girl\", \"confused\", \"indifferent\", \"doubt\" ],\n    char: \"\\ud83e\\udd37\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_shrugging: {\n    keywords: [ \"man\", \"male\", \"boy\", \"confused\", \"indifferent\", \"doubt\" ],\n    char: \"\\ud83e\\udd37\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  tipping_hand_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"human\", \"information\" ],\n    char: \"\\ud83d\\udc81\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  tipping_hand_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"human\", \"information\" ],\n    char: \"\\ud83d\\udc81\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  no_good_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"nope\" ],\n    char: \"\\ud83d\\ude45\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  no_good_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"nope\" ],\n    char: \"\\ud83d\\ude45\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  ok_woman: {\n    keywords: [ \"women\", \"girl\", \"female\", \"pink\", \"human\", \"woman\" ],\n    char: \"\\ud83d\\ude46\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  ok_man: {\n    keywords: [ \"men\", \"boy\", \"male\", \"blue\", \"human\", \"man\" ],\n    char: \"\\ud83d\\ude46\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raising_hand_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\" ],\n    char: \"\\ud83d\\ude4b\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  raising_hand_man: {\n    keywords: [ \"male\", \"boy\", \"man\" ],\n    char: \"\\ud83d\\ude4b\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pouting_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\" ],\n    char: \"\\ud83d\\ude4e\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  pouting_man: {\n    keywords: [ \"male\", \"boy\", \"man\" ],\n    char: \"\\ud83d\\ude4e\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  frowning_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"sad\", \"depressed\", \"discouraged\", \"unhappy\" ],\n    char: \"\\ud83d\\ude4d\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  frowning_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"sad\", \"depressed\", \"discouraged\", \"unhappy\" ],\n    char: \"\\ud83d\\ude4d\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  haircut_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\" ],\n    char: \"\\ud83d\\udc87\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  haircut_man: {\n    keywords: [ \"male\", \"boy\", \"man\" ],\n    char: \"\\ud83d\\udc87\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  massage_woman: {\n    keywords: [ \"female\", \"girl\", \"woman\", \"head\" ],\n    char: \"\\ud83d\\udc86\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  massage_man: {\n    keywords: [ \"male\", \"boy\", \"man\", \"head\" ],\n    char: \"\\ud83d\\udc86\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  woman_in_steamy_room: {\n    keywords: [ \"female\", \"woman\", \"spa\", \"steamroom\", \"sauna\" ],\n    char: \"\\ud83e\\uddd6\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  man_in_steamy_room: {\n    keywords: [ \"male\", \"man\", \"spa\", \"steamroom\", \"sauna\" ],\n    char: \"\\ud83e\\uddd6\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"people\"\n  },\n  couple_with_heart_woman_man: {\n    keywords: [ \"pair\", \"love\", \"like\", \"affection\", \"human\", \"dating\", \"valentines\", \"marriage\" ],\n    char: \"\\ud83d\\udc91\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couple_with_heart_woman_woman: {\n    keywords: [ \"pair\", \"love\", \"like\", \"affection\", \"human\", \"dating\", \"valentines\", \"marriage\" ],\n    char: \"\\ud83d\\udc69\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc69\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couple_with_heart_man_man: {\n    keywords: [ \"pair\", \"love\", \"like\", \"affection\", \"human\", \"dating\", \"valentines\", \"marriage\" ],\n    char: \"\\ud83d\\udc68\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc68\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couplekiss_man_woman: {\n    keywords: [ \"pair\", \"valentines\", \"love\", \"like\", \"dating\", \"marriage\" ],\n    char: \"\\ud83d\\udc8f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couplekiss_woman_woman: {\n    keywords: [ \"pair\", \"valentines\", \"love\", \"like\", \"dating\", \"marriage\" ],\n    char: \"\\ud83d\\udc69\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc8b\\u200d\\ud83d\\udc69\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  couplekiss_man_man: {\n    keywords: [ \"pair\", \"valentines\", \"love\", \"like\", \"dating\", \"marriage\" ],\n    char: \"\\ud83d\\udc68\\u200d\\u2764\\ufe0f\\u200d\\ud83d\\udc8b\\u200d\\ud83d\\udc68\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_boy: {\n    keywords: [ \"home\", \"parents\", \"child\", \"mom\", \"dad\", \"father\", \"mother\", \"people\", \"human\" ],\n    char: \"\\ud83d\\udc6a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"child\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_girl_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_boy_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_woman_girl_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_girl_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_boy_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_woman_girl_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_girl_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_boy_boy: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_man_girl_girl: {\n    keywords: [ \"home\", \"parents\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_girl_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_boy_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_woman_girl_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc69\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"child\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_girl_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_boy_boy: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc66\\u200d\\ud83d\\udc66\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  family_man_girl_girl: {\n    keywords: [ \"home\", \"parent\", \"people\", \"human\", \"children\" ],\n    char: \"\\ud83d\\udc68\\u200d\\ud83d\\udc67\\u200d\\ud83d\\udc67\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  yarn: {\n    keywords: [ \"ball\", \"crochet\", \"knit\" ],\n    char: \"\\ud83e\\uddf6\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  thread: {\n    keywords: [ \"needle\", \"sewing\", \"spool\", \"string\" ],\n    char: \"\\ud83e\\uddf5\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  coat: {\n    keywords: [ \"jacket\" ],\n    char: \"\\ud83e\\udde5\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  labcoat: {\n    keywords: [ \"doctor\", \"experiment\", \"scientist\", \"chemist\" ],\n    char: \"\\ud83e\\udd7c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  womans_clothes: {\n    keywords: [ \"fashion\", \"shopping_bags\", \"female\" ],\n    char: \"\\ud83d\\udc5a\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tshirt: {\n    keywords: [ \"fashion\", \"cloth\", \"casual\", \"shirt\", \"tee\" ],\n    char: \"\\ud83d\\udc55\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  jeans: {\n    keywords: [ \"fashion\", \"shopping\" ],\n    char: \"\\ud83d\\udc56\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  necktie: {\n    keywords: [ \"shirt\", \"suitup\", \"formal\", \"fashion\", \"cloth\", \"business\" ],\n    char: \"\\ud83d\\udc54\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dress: {\n    keywords: [ \"clothes\", \"fashion\", \"shopping\" ],\n    char: \"\\ud83d\\udc57\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  bikini: {\n    keywords: [ \"swimming\", \"female\", \"woman\", \"girl\", \"fashion\", \"beach\", \"summer\" ],\n    char: \"\\ud83d\\udc59\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kimono: {\n    keywords: [ \"dress\", \"fashion\", \"women\", \"female\", \"japanese\" ],\n    char: \"\\ud83d\\udc58\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  lipstick: {\n    keywords: [ \"female\", \"girl\", \"fashion\", \"woman\" ],\n    char: \"\\ud83d\\udc84\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  kiss: {\n    keywords: [ \"face\", \"lips\", \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc8b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  footprints: {\n    keywords: [ \"feet\", \"tracking\", \"walking\", \"beach\" ],\n    char: \"\\ud83d\\udc63\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  flat_shoe: {\n    keywords: [ \"ballet\", \"slip-on\", \"slipper\" ],\n    char: \"\\ud83e\\udd7f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  high_heel: {\n    keywords: [ \"fashion\", \"shoes\", \"female\", \"pumps\", \"stiletto\" ],\n    char: \"\\ud83d\\udc60\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  sandal: {\n    keywords: [ \"shoes\", \"fashion\", \"flip flops\" ],\n    char: \"\\ud83d\\udc61\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  boot: {\n    keywords: [ \"shoes\", \"fashion\" ],\n    char: \"\\ud83d\\udc62\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mans_shoe: {\n    keywords: [ \"fashion\", \"male\" ],\n    char: \"\\ud83d\\udc5e\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  athletic_shoe: {\n    keywords: [ \"shoes\", \"sports\", \"sneakers\" ],\n    char: \"\\ud83d\\udc5f\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  hiking_boot: {\n    keywords: [ \"backpacking\", \"camping\", \"hiking\" ],\n    char: \"\\ud83e\\udd7e\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  socks: {\n    keywords: [ \"stockings\", \"clothes\" ],\n    char: \"\\ud83e\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  gloves: {\n    keywords: [ \"hands\", \"winter\", \"clothes\" ],\n    char: \"\\ud83e\\udde4\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  scarf: {\n    keywords: [ \"neck\", \"winter\", \"clothes\" ],\n    char: \"\\ud83e\\udde3\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  womans_hat: {\n    keywords: [ \"fashion\", \"accessories\", \"female\", \"lady\", \"spring\" ],\n    char: \"\\ud83d\\udc52\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  tophat: {\n    keywords: [ \"magic\", \"gentleman\", \"classy\", \"circus\" ],\n    char: \"\\ud83c\\udfa9\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  billed_hat: {\n    keywords: [ \"cap\", \"baseball\" ],\n    char: \"\\ud83e\\udde2\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  rescue_worker_helmet: {\n    keywords: [ \"construction\", \"build\" ],\n    char: \"\\u26d1\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  mortar_board: {\n    keywords: [ \"school\", \"college\", \"degree\", \"university\", \"graduation\", \"cap\", \"hat\", \"legal\", \"learn\", \"education\" ],\n    char: \"\\ud83c\\udf93\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  crown: {\n    keywords: [ \"king\", \"kod\", \"leader\", \"royalty\", \"lord\" ],\n    char: \"\\ud83d\\udc51\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  school_satchel: {\n    keywords: [ \"student\", \"education\", \"bag\", \"backpack\" ],\n    char: \"\\ud83c\\udf92\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  luggage: {\n    keywords: [ \"packing\", \"travel\" ],\n    char: \"\\ud83e\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  pouch: {\n    keywords: [ \"bag\", \"accessories\", \"shopping\" ],\n    char: \"\\ud83d\\udc5d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  purse: {\n    keywords: [ \"fashion\", \"accessories\", \"money\", \"sales\", \"shopping\" ],\n    char: \"\\ud83d\\udc5b\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  handbag: {\n    keywords: [ \"fashion\", \"accessory\", \"accessories\", \"shopping\" ],\n    char: \"\\ud83d\\udc5c\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  briefcase: {\n    keywords: [ \"business\", \"documents\", \"work\", \"law\", \"legal\", \"job\", \"career\" ],\n    char: \"\\ud83d\\udcbc\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  eyeglasses: {\n    keywords: [ \"fashion\", \"accessories\", \"eyesight\", \"nerdy\", \"dork\", \"geek\" ],\n    char: \"\\ud83d\\udc53\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dark_sunglasses: {\n    keywords: [ \"face\", \"cool\", \"accessories\" ],\n    char: \"\\ud83d\\udd76\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  goggles: {\n    keywords: [ \"eyes\", \"protection\", \"safety\" ],\n    char: \"\\ud83e\\udd7d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  ring: {\n    keywords: [ \"wedding\", \"propose\", \"marriage\", \"valentines\", \"diamond\", \"fashion\", \"jewelry\", \"gem\", \"engagement\" ],\n    char: \"\\ud83d\\udc8d\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  closed_umbrella: {\n    keywords: [ \"weather\", \"rain\", \"drizzle\" ],\n    char: \"\\ud83c\\udf02\",\n    fitzpatrick_scale: false,\n    category: \"people\"\n  },\n  dog: {\n    keywords: [ \"animal\", \"friend\", \"nature\", \"woof\", \"puppy\", \"pet\", \"faithful\" ],\n    char: \"\\ud83d\\udc36\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cat: {\n    keywords: [ \"animal\", \"meow\", \"nature\", \"pet\", \"kitten\" ],\n    char: \"\\ud83d\\udc31\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mouse: {\n    keywords: [ \"animal\", \"nature\", \"cheese_wedge\", \"rodent\" ],\n    char: \"\\ud83d\\udc2d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hamster: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83d\\udc39\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rabbit: {\n    keywords: [ \"animal\", \"nature\", \"pet\", \"spring\", \"magic\", \"bunny\" ],\n    char: \"\\ud83d\\udc30\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fox_face: {\n    keywords: [ \"animal\", \"nature\", \"face\" ],\n    char: \"\\ud83e\\udd8a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bear: {\n    keywords: [ \"animal\", \"nature\", \"wild\" ],\n    char: \"\\ud83d\\udc3b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  panda_face: {\n    keywords: [ \"animal\", \"nature\", \"panda\" ],\n    char: \"\\ud83d\\udc3c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  koala: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83d\\udc28\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tiger: {\n    keywords: [ \"animal\", \"cat\", \"danger\", \"wild\", \"nature\", \"roar\" ],\n    char: \"\\ud83d\\udc2f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  lion: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83e\\udd81\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cow: {\n    keywords: [ \"beef\", \"ox\", \"animal\", \"nature\", \"moo\", \"milk\" ],\n    char: \"\\ud83d\\udc2e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  pig: {\n    keywords: [ \"animal\", \"oink\", \"nature\" ],\n    char: \"\\ud83d\\udc37\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  pig_nose: {\n    keywords: [ \"animal\", \"oink\" ],\n    char: \"\\ud83d\\udc3d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  frog: {\n    keywords: [ \"animal\", \"nature\", \"croak\", \"toad\" ],\n    char: \"\\ud83d\\udc38\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  squid: {\n    keywords: [ \"animal\", \"nature\", \"ocean\", \"sea\" ],\n    char: \"\\ud83e\\udd91\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  octopus: {\n    keywords: [ \"animal\", \"creature\", \"ocean\", \"sea\", \"nature\", \"beach\" ],\n    char: \"\\ud83d\\udc19\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shrimp: {\n    keywords: [ \"animal\", \"ocean\", \"nature\", \"seafood\" ],\n    char: \"\\ud83e\\udd90\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  monkey_face: {\n    keywords: [ \"animal\", \"nature\", \"circus\" ],\n    char: \"\\ud83d\\udc35\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  gorilla: {\n    keywords: [ \"animal\", \"nature\", \"circus\" ],\n    char: \"\\ud83e\\udd8d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  see_no_evil: {\n    keywords: [ \"monkey\", \"animal\", \"nature\", \"haha\" ],\n    char: \"\\ud83d\\ude48\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hear_no_evil: {\n    keywords: [ \"animal\", \"monkey\", \"nature\" ],\n    char: \"\\ud83d\\ude49\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  speak_no_evil: {\n    keywords: [ \"monkey\", \"animal\", \"nature\", \"omg\" ],\n    char: \"\\ud83d\\ude4a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  monkey: {\n    keywords: [ \"animal\", \"nature\", \"banana\", \"circus\" ],\n    char: \"\\ud83d\\udc12\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  chicken: {\n    keywords: [ \"animal\", \"cluck\", \"nature\", \"bird\" ],\n    char: \"\\ud83d\\udc14\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  penguin: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83d\\udc27\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bird: {\n    keywords: [ \"animal\", \"nature\", \"fly\", \"tweet\", \"spring\" ],\n    char: \"\\ud83d\\udc26\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  baby_chick: {\n    keywords: [ \"animal\", \"chicken\", \"bird\" ],\n    char: \"\\ud83d\\udc24\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hatching_chick: {\n    keywords: [ \"animal\", \"chicken\", \"egg\", \"born\", \"baby\", \"bird\" ],\n    char: \"\\ud83d\\udc23\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hatched_chick: {\n    keywords: [ \"animal\", \"chicken\", \"baby\", \"bird\" ],\n    char: \"\\ud83d\\udc25\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  duck: {\n    keywords: [ \"animal\", \"nature\", \"bird\", \"mallard\" ],\n    char: \"\\ud83e\\udd86\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  eagle: {\n    keywords: [ \"animal\", \"nature\", \"bird\" ],\n    char: \"\\ud83e\\udd85\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  owl: {\n    keywords: [ \"animal\", \"nature\", \"bird\", \"hoot\" ],\n    char: \"\\ud83e\\udd89\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bat: {\n    keywords: [ \"animal\", \"nature\", \"blind\", \"vampire\" ],\n    char: \"\\ud83e\\udd87\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  wolf: {\n    keywords: [ \"animal\", \"nature\", \"wild\" ],\n    char: \"\\ud83d\\udc3a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  boar: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83d\\udc17\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  horse: {\n    keywords: [ \"animal\", \"brown\", \"nature\" ],\n    char: \"\\ud83d\\udc34\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  unicorn: {\n    keywords: [ \"animal\", \"nature\", \"mystical\" ],\n    char: \"\\ud83e\\udd84\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  honeybee: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"bug\", \"spring\", \"honey\" ],\n    char: \"\\ud83d\\udc1d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bug: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"worm\" ],\n    char: \"\\ud83d\\udc1b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  butterfly: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"caterpillar\" ],\n    char: \"\\ud83e\\udd8b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snail: {\n    keywords: [ \"slow\", \"animal\", \"shell\" ],\n    char: \"\\ud83d\\udc0c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  beetle: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"ladybug\" ],\n    char: \"\\ud83d\\udc1e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ant: {\n    keywords: [ \"animal\", \"insect\", \"nature\", \"bug\" ],\n    char: \"\\ud83d\\udc1c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  grasshopper: {\n    keywords: [ \"animal\", \"cricket\", \"chirp\" ],\n    char: \"\\ud83e\\udd97\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  spider: {\n    keywords: [ \"animal\", \"arachnid\" ],\n    char: \"\\ud83d\\udd77\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  scorpion: {\n    keywords: [ \"animal\", \"arachnid\" ],\n    char: \"\\ud83e\\udd82\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  crab: {\n    keywords: [ \"animal\", \"crustacean\" ],\n    char: \"\\ud83e\\udd80\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snake: {\n    keywords: [ \"animal\", \"evil\", \"nature\", \"hiss\", \"python\" ],\n    char: \"\\ud83d\\udc0d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  lizard: {\n    keywords: [ \"animal\", \"nature\", \"reptile\" ],\n    char: \"\\ud83e\\udd8e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  \"t-rex\": {\n    keywords: [ \"animal\", \"nature\", \"dinosaur\", \"tyrannosaurus\", \"extinct\" ],\n    char: \"\\ud83e\\udd96\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sauropod: {\n    keywords: [ \"animal\", \"nature\", \"dinosaur\", \"brachiosaurus\", \"brontosaurus\", \"diplodocus\", \"extinct\" ],\n    char: \"\\ud83e\\udd95\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  turtle: {\n    keywords: [ \"animal\", \"slow\", \"nature\", \"tortoise\" ],\n    char: \"\\ud83d\\udc22\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tropical_fish: {\n    keywords: [ \"animal\", \"swim\", \"ocean\", \"beach\", \"nemo\" ],\n    char: \"\\ud83d\\udc20\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fish: {\n    keywords: [ \"animal\", \"food\", \"nature\" ],\n    char: \"\\ud83d\\udc1f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  blowfish: {\n    keywords: [ \"animal\", \"nature\", \"food\", \"sea\", \"ocean\" ],\n    char: \"\\ud83d\\udc21\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dolphin: {\n    keywords: [ \"animal\", \"nature\", \"fish\", \"sea\", \"ocean\", \"flipper\", \"fins\", \"beach\" ],\n    char: \"\\ud83d\\udc2c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shark: {\n    keywords: [ \"animal\", \"nature\", \"fish\", \"sea\", \"ocean\", \"jaws\", \"fins\", \"beach\" ],\n    char: \"\\ud83e\\udd88\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  whale: {\n    keywords: [ \"animal\", \"nature\", \"sea\", \"ocean\" ],\n    char: \"\\ud83d\\udc33\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  whale2: {\n    keywords: [ \"animal\", \"nature\", \"sea\", \"ocean\" ],\n    char: \"\\ud83d\\udc0b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  crocodile: {\n    keywords: [ \"animal\", \"nature\", \"reptile\", \"lizard\", \"alligator\" ],\n    char: \"\\ud83d\\udc0a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  leopard: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83d\\udc06\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  zebra: {\n    keywords: [ \"animal\", \"nature\", \"stripes\", \"safari\" ],\n    char: \"\\ud83e\\udd93\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tiger2: {\n    keywords: [ \"animal\", \"nature\", \"roar\" ],\n    char: \"\\ud83d\\udc05\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  water_buffalo: {\n    keywords: [ \"animal\", \"nature\", \"ox\", \"cow\" ],\n    char: \"\\ud83d\\udc03\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ox: {\n    keywords: [ \"animal\", \"cow\", \"beef\" ],\n    char: \"\\ud83d\\udc02\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cow2: {\n    keywords: [ \"beef\", \"ox\", \"animal\", \"nature\", \"moo\", \"milk\" ],\n    char: \"\\ud83d\\udc04\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  deer: {\n    keywords: [ \"animal\", \"nature\", \"horns\", \"venison\" ],\n    char: \"\\ud83e\\udd8c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dromedary_camel: {\n    keywords: [ \"animal\", \"hot\", \"desert\", \"hump\" ],\n    char: \"\\ud83d\\udc2a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  camel: {\n    keywords: [ \"animal\", \"nature\", \"hot\", \"desert\", \"hump\" ],\n    char: \"\\ud83d\\udc2b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  giraffe: {\n    keywords: [ \"animal\", \"nature\", \"spots\", \"safari\" ],\n    char: \"\\ud83e\\udd92\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  elephant: {\n    keywords: [ \"animal\", \"nature\", \"nose\", \"th\", \"circus\" ],\n    char: \"\\ud83d\\udc18\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rhinoceros: {\n    keywords: [ \"animal\", \"nature\", \"horn\" ],\n    char: \"\\ud83e\\udd8f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  goat: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83d\\udc10\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ram: {\n    keywords: [ \"animal\", \"sheep\", \"nature\" ],\n    char: \"\\ud83d\\udc0f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sheep: {\n    keywords: [ \"animal\", \"nature\", \"wool\", \"shipit\" ],\n    char: \"\\ud83d\\udc11\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  racehorse: {\n    keywords: [ \"animal\", \"gamble\", \"luck\" ],\n    char: \"\\ud83d\\udc0e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  pig2: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83d\\udc16\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rat: {\n    keywords: [ \"animal\", \"mouse\", \"rodent\" ],\n    char: \"\\ud83d\\udc00\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mouse2: {\n    keywords: [ \"animal\", \"nature\", \"rodent\" ],\n    char: \"\\ud83d\\udc01\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rooster: {\n    keywords: [ \"animal\", \"nature\", \"chicken\" ],\n    char: \"\\ud83d\\udc13\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  turkey: {\n    keywords: [ \"animal\", \"bird\" ],\n    char: \"\\ud83e\\udd83\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dove: {\n    keywords: [ \"animal\", \"bird\" ],\n    char: \"\\ud83d\\udd4a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dog2: {\n    keywords: [ \"animal\", \"nature\", \"friend\", \"doge\", \"pet\", \"faithful\" ],\n    char: \"\\ud83d\\udc15\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  poodle: {\n    keywords: [ \"dog\", \"animal\", \"101\", \"nature\", \"pet\" ],\n    char: \"\\ud83d\\udc29\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cat2: {\n    keywords: [ \"animal\", \"meow\", \"pet\", \"cats\" ],\n    char: \"\\ud83d\\udc08\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rabbit2: {\n    keywords: [ \"animal\", \"nature\", \"pet\", \"magic\", \"spring\" ],\n    char: \"\\ud83d\\udc07\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  chipmunk: {\n    keywords: [ \"animal\", \"nature\", \"rodent\", \"squirrel\" ],\n    char: \"\\ud83d\\udc3f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hedgehog: {\n    keywords: [ \"animal\", \"nature\", \"spiny\" ],\n    char: \"\\ud83e\\udd94\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  raccoon: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83e\\udd9d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  llama: {\n    keywords: [ \"animal\", \"nature\", \"alpaca\" ],\n    char: \"\\ud83e\\udd99\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hippopotamus: {\n    keywords: [ \"animal\", \"nature\" ],\n    char: \"\\ud83e\\udd9b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  kangaroo: {\n    keywords: [ \"animal\", \"nature\", \"australia\", \"joey\", \"hop\", \"marsupial\" ],\n    char: \"\\ud83e\\udd98\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  badger: {\n    keywords: [ \"animal\", \"nature\", \"honey\" ],\n    char: \"\\ud83e\\udda1\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  swan: {\n    keywords: [ \"animal\", \"nature\", \"bird\" ],\n    char: \"\\ud83e\\udda2\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  peacock: {\n    keywords: [ \"animal\", \"nature\", \"peahen\", \"bird\" ],\n    char: \"\\ud83e\\udd9a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  parrot: {\n    keywords: [ \"animal\", \"nature\", \"bird\", \"pirate\", \"talk\" ],\n    char: \"\\ud83e\\udd9c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  lobster: {\n    keywords: [ \"animal\", \"nature\", \"bisque\", \"claws\", \"seafood\" ],\n    char: \"\\ud83e\\udd9e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mosquito: {\n    keywords: [ \"animal\", \"nature\", \"insect\", \"malaria\" ],\n    char: \"\\ud83e\\udd9f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  paw_prints: {\n    keywords: [ \"animal\", \"tracking\", \"footprints\", \"dog\", \"cat\", \"pet\", \"feet\" ],\n    char: \"\\ud83d\\udc3e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dragon: {\n    keywords: [ \"animal\", \"myth\", \"nature\", \"chinese\", \"green\" ],\n    char: \"\\ud83d\\udc09\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dragon_face: {\n    keywords: [ \"animal\", \"myth\", \"nature\", \"chinese\", \"green\" ],\n    char: \"\\ud83d\\udc32\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cactus: {\n    keywords: [ \"vegetable\", \"plant\", \"nature\" ],\n    char: \"\\ud83c\\udf35\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  christmas_tree: {\n    keywords: [ \"festival\", \"vacation\", \"december\", \"xmas\", \"celebration\" ],\n    char: \"\\ud83c\\udf84\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  evergreen_tree: {\n    keywords: [ \"plant\", \"nature\" ],\n    char: \"\\ud83c\\udf32\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  deciduous_tree: {\n    keywords: [ \"plant\", \"nature\" ],\n    char: \"\\ud83c\\udf33\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  palm_tree: {\n    keywords: [ \"plant\", \"vegetable\", \"nature\", \"summer\", \"beach\", \"mojito\", \"tropical\" ],\n    char: \"\\ud83c\\udf34\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  seedling: {\n    keywords: [ \"plant\", \"nature\", \"grass\", \"lawn\", \"spring\" ],\n    char: \"\\ud83c\\udf31\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  herb: {\n    keywords: [ \"vegetable\", \"plant\", \"medicine\", \"weed\", \"grass\", \"lawn\" ],\n    char: \"\\ud83c\\udf3f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shamrock: {\n    keywords: [ \"vegetable\", \"plant\", \"nature\", \"irish\", \"clover\" ],\n    char: \"\\u2618\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  four_leaf_clover: {\n    keywords: [ \"vegetable\", \"plant\", \"nature\", \"lucky\", \"irish\" ],\n    char: \"\\ud83c\\udf40\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bamboo: {\n    keywords: [ \"plant\", \"nature\", \"vegetable\", \"panda\", \"pine_decoration\" ],\n    char: \"\\ud83c\\udf8d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tanabata_tree: {\n    keywords: [ \"plant\", \"nature\", \"branch\", \"summer\" ],\n    char: \"\\ud83c\\udf8b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  leaves: {\n    keywords: [ \"nature\", \"plant\", \"tree\", \"vegetable\", \"grass\", \"lawn\", \"spring\" ],\n    char: \"\\ud83c\\udf43\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fallen_leaf: {\n    keywords: [ \"nature\", \"plant\", \"vegetable\", \"leaves\" ],\n    char: \"\\ud83c\\udf42\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  maple_leaf: {\n    keywords: [ \"nature\", \"plant\", \"vegetable\", \"ca\", \"fall\" ],\n    char: \"\\ud83c\\udf41\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ear_of_rice: {\n    keywords: [ \"nature\", \"plant\" ],\n    char: \"\\ud83c\\udf3e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  hibiscus: {\n    keywords: [ \"plant\", \"vegetable\", \"flowers\", \"beach\" ],\n    char: \"\\ud83c\\udf3a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sunflower: {\n    keywords: [ \"nature\", \"plant\", \"fall\" ],\n    char: \"\\ud83c\\udf3b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  rose: {\n    keywords: [ \"flowers\", \"valentines\", \"love\", \"spring\" ],\n    char: \"\\ud83c\\udf39\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  wilted_flower: {\n    keywords: [ \"plant\", \"nature\", \"flower\" ],\n    char: \"\\ud83e\\udd40\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tulip: {\n    keywords: [ \"flowers\", \"plant\", \"nature\", \"summer\", \"spring\" ],\n    char: \"\\ud83c\\udf37\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  blossom: {\n    keywords: [ \"nature\", \"flowers\", \"yellow\" ],\n    char: \"\\ud83c\\udf3c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cherry_blossom: {\n    keywords: [ \"nature\", \"plant\", \"spring\", \"flower\" ],\n    char: \"\\ud83c\\udf38\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  bouquet: {\n    keywords: [ \"flowers\", \"nature\", \"spring\" ],\n    char: \"\\ud83d\\udc90\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  mushroom: {\n    keywords: [ \"plant\", \"vegetable\" ],\n    char: \"\\ud83c\\udf44\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  chestnut: {\n    keywords: [ \"food\", \"squirrel\" ],\n    char: \"\\ud83c\\udf30\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  jack_o_lantern: {\n    keywords: [ \"halloween\", \"light\", \"pumpkin\", \"creepy\", \"fall\" ],\n    char: \"\\ud83c\\udf83\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  shell: {\n    keywords: [ \"nature\", \"sea\", \"beach\" ],\n    char: \"\\ud83d\\udc1a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  spider_web: {\n    keywords: [ \"animal\", \"insect\", \"arachnid\", \"silk\" ],\n    char: \"\\ud83d\\udd78\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  earth_americas: {\n    keywords: [ \"globe\", \"world\", \"USA\", \"international\" ],\n    char: \"\\ud83c\\udf0e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  earth_africa: {\n    keywords: [ \"globe\", \"world\", \"international\" ],\n    char: \"\\ud83c\\udf0d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  earth_asia: {\n    keywords: [ \"globe\", \"world\", \"east\", \"international\" ],\n    char: \"\\ud83c\\udf0f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  full_moon: {\n    keywords: [ \"nature\", \"yellow\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf15\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waning_gibbous_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\", \"waxing_gibbous_moon\" ],\n    char: \"\\ud83c\\udf16\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  last_quarter_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf17\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waning_crescent_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf18\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  new_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf11\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waxing_crescent_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf12\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  first_quarter_moon: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf13\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  waxing_gibbous_moon: {\n    keywords: [ \"nature\", \"night\", \"sky\", \"gray\", \"twilight\", \"planet\", \"space\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf14\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  new_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf1a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  full_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf1d\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  first_quarter_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf1b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  last_quarter_moon_with_face: {\n    keywords: [ \"nature\", \"twilight\", \"planet\", \"space\", \"night\", \"evening\", \"sleep\" ],\n    char: \"\\ud83c\\udf1c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_with_face: {\n    keywords: [ \"nature\", \"morning\", \"sky\" ],\n    char: \"\\ud83c\\udf1e\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  crescent_moon: {\n    keywords: [ \"night\", \"sleep\", \"sky\", \"evening\", \"magic\" ],\n    char: \"\\ud83c\\udf19\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  star: {\n    keywords: [ \"night\", \"yellow\" ],\n    char: \"\\u2b50\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  star2: {\n    keywords: [ \"night\", \"sparkle\", \"awesome\", \"good\", \"magic\" ],\n    char: \"\\ud83c\\udf1f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dizzy: {\n    keywords: [ \"star\", \"sparkle\", \"shoot\", \"magic\" ],\n    char: \"\\ud83d\\udcab\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sparkles: {\n    keywords: [ \"stars\", \"shine\", \"shiny\", \"cool\", \"awesome\", \"good\", \"magic\" ],\n    char: \"\\u2728\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  comet: {\n    keywords: [ \"space\" ],\n    char: \"\\u2604\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sunny: {\n    keywords: [ \"weather\", \"nature\", \"brightness\", \"summer\", \"beach\", \"spring\" ],\n    char: \"\\u2600\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_behind_small_cloud: {\n    keywords: [ \"weather\" ],\n    char: \"\\ud83c\\udf24\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  partly_sunny: {\n    keywords: [ \"weather\", \"nature\", \"cloudy\", \"morning\", \"fall\", \"spring\" ],\n    char: \"\\u26c5\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_behind_large_cloud: {\n    keywords: [ \"weather\" ],\n    char: \"\\ud83c\\udf25\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sun_behind_rain_cloud: {\n    keywords: [ \"weather\" ],\n    char: \"\\ud83c\\udf26\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud: {\n    keywords: [ \"weather\", \"sky\" ],\n    char: \"\\u2601\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_rain: {\n    keywords: [ \"weather\" ],\n    char: \"\\ud83c\\udf27\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_lightning_and_rain: {\n    keywords: [ \"weather\", \"lightning\" ],\n    char: \"\\u26c8\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_lightning: {\n    keywords: [ \"weather\", \"thunder\" ],\n    char: \"\\ud83c\\udf29\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  zap: {\n    keywords: [ \"thunder\", \"weather\", \"lightning bolt\", \"fast\" ],\n    char: \"\\u26a1\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fire: {\n    keywords: [ \"hot\", \"cook\", \"flame\" ],\n    char: \"\\ud83d\\udd25\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  boom: {\n    keywords: [ \"bomb\", \"explode\", \"explosion\", \"collision\", \"blown\" ],\n    char: \"\\ud83d\\udca5\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snowflake: {\n    keywords: [ \"winter\", \"season\", \"cold\", \"weather\", \"christmas\", \"xmas\" ],\n    char: \"\\u2744\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  cloud_with_snow: {\n    keywords: [ \"weather\" ],\n    char: \"\\ud83c\\udf28\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snowman: {\n    keywords: [ \"winter\", \"season\", \"cold\", \"weather\", \"christmas\", \"xmas\", \"frozen\", \"without_snow\" ],\n    char: \"\\u26c4\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  snowman_with_snow: {\n    keywords: [ \"winter\", \"season\", \"cold\", \"weather\", \"christmas\", \"xmas\", \"frozen\" ],\n    char: \"\\u2603\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  wind_face: {\n    keywords: [ \"gust\", \"air\" ],\n    char: \"\\ud83c\\udf2c\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  dash: {\n    keywords: [ \"wind\", \"air\", \"fast\", \"shoo\", \"fart\", \"smoke\", \"puff\" ],\n    char: \"\\ud83d\\udca8\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  tornado: {\n    keywords: [ \"weather\", \"cyclone\", \"twister\" ],\n    char: \"\\ud83c\\udf2a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  fog: {\n    keywords: [ \"weather\" ],\n    char: \"\\ud83c\\udf2b\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  open_umbrella: {\n    keywords: [ \"weather\", \"spring\" ],\n    char: \"\\u2602\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  umbrella: {\n    keywords: [ \"rainy\", \"weather\", \"spring\" ],\n    char: \"\\u2614\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  droplet: {\n    keywords: [ \"water\", \"drip\", \"faucet\", \"spring\" ],\n    char: \"\\ud83d\\udca7\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  sweat_drops: {\n    keywords: [ \"water\", \"drip\", \"oops\" ],\n    char: \"\\ud83d\\udca6\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  ocean: {\n    keywords: [ \"sea\", \"water\", \"wave\", \"nature\", \"tsunami\", \"disaster\" ],\n    char: \"\\ud83c\\udf0a\",\n    fitzpatrick_scale: false,\n    category: \"animals_and_nature\"\n  },\n  green_apple: {\n    keywords: [ \"fruit\", \"nature\" ],\n    char: \"\\ud83c\\udf4f\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  apple: {\n    keywords: [ \"fruit\", \"mac\", \"school\" ],\n    char: \"\\ud83c\\udf4e\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pear: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: \"\\ud83c\\udf50\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tangerine: {\n    keywords: [ \"food\", \"fruit\", \"nature\", \"orange\" ],\n    char: \"\\ud83c\\udf4a\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  lemon: {\n    keywords: [ \"fruit\", \"nature\" ],\n    char: \"\\ud83c\\udf4b\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  banana: {\n    keywords: [ \"fruit\", \"food\", \"monkey\" ],\n    char: \"\\ud83c\\udf4c\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  watermelon: {\n    keywords: [ \"fruit\", \"food\", \"picnic\", \"summer\" ],\n    char: \"\\ud83c\\udf49\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  grapes: {\n    keywords: [ \"fruit\", \"food\", \"wine\" ],\n    char: \"\\ud83c\\udf47\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  strawberry: {\n    keywords: [ \"fruit\", \"food\", \"nature\" ],\n    char: \"\\ud83c\\udf53\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  melon: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: \"\\ud83c\\udf48\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cherries: {\n    keywords: [ \"food\", \"fruit\" ],\n    char: \"\\ud83c\\udf52\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  peach: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: \"\\ud83c\\udf51\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pineapple: {\n    keywords: [ \"fruit\", \"nature\", \"food\" ],\n    char: \"\\ud83c\\udf4d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  coconut: {\n    keywords: [ \"fruit\", \"nature\", \"food\", \"palm\" ],\n    char: \"\\ud83e\\udd65\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  kiwi_fruit: {\n    keywords: [ \"fruit\", \"food\" ],\n    char: \"\\ud83e\\udd5d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  mango: {\n    keywords: [ \"fruit\", \"food\", \"tropical\" ],\n    char: \"\\ud83e\\udd6d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  avocado: {\n    keywords: [ \"fruit\", \"food\" ],\n    char: \"\\ud83e\\udd51\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  broccoli: {\n    keywords: [ \"fruit\", \"food\", \"vegetable\" ],\n    char: \"\\ud83e\\udd66\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tomato: {\n    keywords: [ \"fruit\", \"vegetable\", \"nature\", \"food\" ],\n    char: \"\\ud83c\\udf45\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  eggplant: {\n    keywords: [ \"vegetable\", \"nature\", \"food\", \"aubergine\" ],\n    char: \"\\ud83c\\udf46\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cucumber: {\n    keywords: [ \"fruit\", \"food\", \"pickle\" ],\n    char: \"\\ud83e\\udd52\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  carrot: {\n    keywords: [ \"vegetable\", \"food\", \"orange\" ],\n    char: \"\\ud83e\\udd55\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  hot_pepper: {\n    keywords: [ \"food\", \"spicy\", \"chilli\", \"chili\" ],\n    char: \"\\ud83c\\udf36\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  potato: {\n    keywords: [ \"food\", \"tuber\", \"vegatable\", \"starch\" ],\n    char: \"\\ud83e\\udd54\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  corn: {\n    keywords: [ \"food\", \"vegetable\", \"plant\" ],\n    char: \"\\ud83c\\udf3d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  leafy_greens: {\n    keywords: [ \"food\", \"vegetable\", \"plant\", \"bok choy\", \"cabbage\", \"kale\", \"lettuce\" ],\n    char: \"\\ud83e\\udd6c\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sweet_potato: {\n    keywords: [ \"food\", \"nature\" ],\n    char: \"\\ud83c\\udf60\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  peanuts: {\n    keywords: [ \"food\", \"nut\" ],\n    char: \"\\ud83e\\udd5c\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  honey_pot: {\n    keywords: [ \"bees\", \"sweet\", \"kitchen\" ],\n    char: \"\\ud83c\\udf6f\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  croissant: {\n    keywords: [ \"food\", \"bread\", \"french\" ],\n    char: \"\\ud83e\\udd50\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bread: {\n    keywords: [ \"food\", \"wheat\", \"breakfast\", \"toast\" ],\n    char: \"\\ud83c\\udf5e\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  baguette_bread: {\n    keywords: [ \"food\", \"bread\", \"french\" ],\n    char: \"\\ud83e\\udd56\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bagel: {\n    keywords: [ \"food\", \"bread\", \"bakery\", \"schmear\" ],\n    char: \"\\ud83e\\udd6f\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pretzel: {\n    keywords: [ \"food\", \"bread\", \"twisted\" ],\n    char: \"\\ud83e\\udd68\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cheese: {\n    keywords: [ \"food\", \"chadder\" ],\n    char: \"\\ud83e\\uddc0\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  egg: {\n    keywords: [ \"food\", \"chicken\", \"breakfast\" ],\n    char: \"\\ud83e\\udd5a\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bacon: {\n    keywords: [ \"food\", \"breakfast\", \"pork\", \"pig\", \"meat\" ],\n    char: \"\\ud83e\\udd53\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  steak: {\n    keywords: [ \"food\", \"cow\", \"meat\", \"cut\", \"chop\", \"lambchop\", \"porkchop\" ],\n    char: \"\\ud83e\\udd69\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pancakes: {\n    keywords: [ \"food\", \"breakfast\", \"flapjacks\", \"hotcakes\" ],\n    char: \"\\ud83e\\udd5e\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  poultry_leg: {\n    keywords: [ \"food\", \"meat\", \"drumstick\", \"bird\", \"chicken\", \"turkey\" ],\n    char: \"\\ud83c\\udf57\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  meat_on_bone: {\n    keywords: [ \"good\", \"food\", \"drumstick\" ],\n    char: \"\\ud83c\\udf56\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bone: {\n    keywords: [ \"skeleton\" ],\n    char: \"\\ud83e\\uddb4\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fried_shrimp: {\n    keywords: [ \"food\", \"animal\", \"appetizer\", \"summer\" ],\n    char: \"\\ud83c\\udf64\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fried_egg: {\n    keywords: [ \"food\", \"breakfast\", \"kitchen\", \"egg\" ],\n    char: \"\\ud83c\\udf73\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  hamburger: {\n    keywords: [ \"meat\", \"fast food\", \"beef\", \"cheeseburger\", \"mcdonalds\", \"burger king\" ],\n    char: \"\\ud83c\\udf54\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fries: {\n    keywords: [ \"chips\", \"snack\", \"fast food\" ],\n    char: \"\\ud83c\\udf5f\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  stuffed_flatbread: {\n    keywords: [ \"food\", \"flatbread\", \"stuffed\", \"gyro\" ],\n    char: \"\\ud83e\\udd59\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  hotdog: {\n    keywords: [ \"food\", \"frankfurter\" ],\n    char: \"\\ud83c\\udf2d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pizza: {\n    keywords: [ \"food\", \"party\" ],\n    char: \"\\ud83c\\udf55\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sandwich: {\n    keywords: [ \"food\", \"lunch\", \"bread\" ],\n    char: \"\\ud83e\\udd6a\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  canned_food: {\n    keywords: [ \"food\", \"soup\" ],\n    char: \"\\ud83e\\udd6b\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  spaghetti: {\n    keywords: [ \"food\", \"italian\", \"noodle\" ],\n    char: \"\\ud83c\\udf5d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  taco: {\n    keywords: [ \"food\", \"mexican\" ],\n    char: \"\\ud83c\\udf2e\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  burrito: {\n    keywords: [ \"food\", \"mexican\" ],\n    char: \"\\ud83c\\udf2f\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  green_salad: {\n    keywords: [ \"food\", \"healthy\", \"lettuce\" ],\n    char: \"\\ud83e\\udd57\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  shallow_pan_of_food: {\n    keywords: [ \"food\", \"cooking\", \"casserole\", \"paella\" ],\n    char: \"\\ud83e\\udd58\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  ramen: {\n    keywords: [ \"food\", \"japanese\", \"noodle\", \"chopsticks\" ],\n    char: \"\\ud83c\\udf5c\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  stew: {\n    keywords: [ \"food\", \"meat\", \"soup\" ],\n    char: \"\\ud83c\\udf72\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fish_cake: {\n    keywords: [ \"food\", \"japan\", \"sea\", \"beach\", \"narutomaki\", \"pink\", \"swirl\", \"kamaboko\", \"surimi\", \"ramen\" ],\n    char: \"\\ud83c\\udf65\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fortune_cookie: {\n    keywords: [ \"food\", \"prophecy\" ],\n    char: \"\\ud83e\\udd60\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sushi: {\n    keywords: [ \"food\", \"fish\", \"japanese\", \"rice\" ],\n    char: \"\\ud83c\\udf63\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bento: {\n    keywords: [ \"food\", \"japanese\", \"box\" ],\n    char: \"\\ud83c\\udf71\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  curry: {\n    keywords: [ \"food\", \"spicy\", \"hot\", \"indian\" ],\n    char: \"\\ud83c\\udf5b\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  rice_ball: {\n    keywords: [ \"food\", \"japanese\" ],\n    char: \"\\ud83c\\udf59\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  rice: {\n    keywords: [ \"food\", \"china\", \"asian\" ],\n    char: \"\\ud83c\\udf5a\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  rice_cracker: {\n    keywords: [ \"food\", \"japanese\" ],\n    char: \"\\ud83c\\udf58\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  oden: {\n    keywords: [ \"food\", \"japanese\" ],\n    char: \"\\ud83c\\udf62\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  dango: {\n    keywords: [ \"food\", \"dessert\", \"sweet\", \"japanese\", \"barbecue\", \"meat\" ],\n    char: \"\\ud83c\\udf61\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  shaved_ice: {\n    keywords: [ \"hot\", \"dessert\", \"summer\" ],\n    char: \"\\ud83c\\udf67\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  ice_cream: {\n    keywords: [ \"food\", \"hot\", \"dessert\" ],\n    char: \"\\ud83c\\udf68\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  icecream: {\n    keywords: [ \"food\", \"hot\", \"dessert\", \"summer\" ],\n    char: \"\\ud83c\\udf66\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  pie: {\n    keywords: [ \"food\", \"dessert\", \"pastry\" ],\n    char: \"\\ud83e\\udd67\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cake: {\n    keywords: [ \"food\", \"dessert\" ],\n    char: \"\\ud83c\\udf70\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cupcake: {\n    keywords: [ \"food\", \"dessert\", \"bakery\", \"sweet\" ],\n    char: \"\\ud83e\\uddc1\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  moon_cake: {\n    keywords: [ \"food\", \"autumn\" ],\n    char: \"\\ud83e\\udd6e\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  birthday: {\n    keywords: [ \"food\", \"dessert\", \"cake\" ],\n    char: \"\\ud83c\\udf82\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  custard: {\n    keywords: [ \"dessert\", \"food\" ],\n    char: \"\\ud83c\\udf6e\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  candy: {\n    keywords: [ \"snack\", \"dessert\", \"sweet\", \"lolly\" ],\n    char: \"\\ud83c\\udf6c\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  lollipop: {\n    keywords: [ \"food\", \"snack\", \"candy\", \"sweet\" ],\n    char: \"\\ud83c\\udf6d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  chocolate_bar: {\n    keywords: [ \"food\", \"snack\", \"dessert\", \"sweet\" ],\n    char: \"\\ud83c\\udf6b\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  popcorn: {\n    keywords: [ \"food\", \"movie theater\", \"films\", \"snack\" ],\n    char: \"\\ud83c\\udf7f\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  dumpling: {\n    keywords: [ \"food\", \"empanada\", \"pierogi\", \"potsticker\" ],\n    char: \"\\ud83e\\udd5f\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  doughnut: {\n    keywords: [ \"food\", \"dessert\", \"snack\", \"sweet\", \"donut\" ],\n    char: \"\\ud83c\\udf69\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cookie: {\n    keywords: [ \"food\", \"snack\", \"oreo\", \"chocolate\", \"sweet\", \"dessert\" ],\n    char: \"\\ud83c\\udf6a\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  milk_glass: {\n    keywords: [ \"beverage\", \"drink\", \"cow\" ],\n    char: \"\\ud83e\\udd5b\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  beer: {\n    keywords: [ \"relax\", \"beverage\", \"drink\", \"drunk\", \"party\", \"pub\", \"summer\", \"alcohol\", \"booze\" ],\n    char: \"\\ud83c\\udf7a\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  beers: {\n    keywords: [ \"relax\", \"beverage\", \"drink\", \"drunk\", \"party\", \"pub\", \"summer\", \"alcohol\", \"booze\" ],\n    char: \"\\ud83c\\udf7b\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  clinking_glasses: {\n    keywords: [ \"beverage\", \"drink\", \"party\", \"alcohol\", \"celebrate\", \"cheers\", \"wine\", \"champagne\", \"toast\" ],\n    char: \"\\ud83e\\udd42\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  wine_glass: {\n    keywords: [ \"drink\", \"beverage\", \"drunk\", \"alcohol\", \"booze\" ],\n    char: \"\\ud83c\\udf77\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tumbler_glass: {\n    keywords: [ \"drink\", \"beverage\", \"drunk\", \"alcohol\", \"liquor\", \"booze\", \"bourbon\", \"scotch\", \"whisky\", \"glass\", \"shot\" ],\n    char: \"\\ud83e\\udd43\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cocktail: {\n    keywords: [ \"drink\", \"drunk\", \"alcohol\", \"beverage\", \"booze\", \"mojito\" ],\n    char: \"\\ud83c\\udf78\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tropical_drink: {\n    keywords: [ \"beverage\", \"cocktail\", \"summer\", \"beach\", \"alcohol\", \"booze\", \"mojito\" ],\n    char: \"\\ud83c\\udf79\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  champagne: {\n    keywords: [ \"drink\", \"wine\", \"bottle\", \"celebration\" ],\n    char: \"\\ud83c\\udf7e\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  sake: {\n    keywords: [ \"wine\", \"drink\", \"drunk\", \"beverage\", \"japanese\", \"alcohol\", \"booze\" ],\n    char: \"\\ud83c\\udf76\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  tea: {\n    keywords: [ \"drink\", \"bowl\", \"breakfast\", \"green\", \"british\" ],\n    char: \"\\ud83c\\udf75\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  cup_with_straw: {\n    keywords: [ \"drink\", \"soda\" ],\n    char: \"\\ud83e\\udd64\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  coffee: {\n    keywords: [ \"beverage\", \"caffeine\", \"latte\", \"espresso\" ],\n    char: \"\\u2615\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  baby_bottle: {\n    keywords: [ \"food\", \"container\", \"milk\" ],\n    char: \"\\ud83c\\udf7c\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  salt: {\n    keywords: [ \"condiment\", \"shaker\" ],\n    char: \"\\ud83e\\uddc2\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  spoon: {\n    keywords: [ \"cutlery\", \"kitchen\", \"tableware\" ],\n    char: \"\\ud83e\\udd44\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  fork_and_knife: {\n    keywords: [ \"cutlery\", \"kitchen\" ],\n    char: \"\\ud83c\\udf74\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  plate_with_cutlery: {\n    keywords: [ \"food\", \"eat\", \"meal\", \"lunch\", \"dinner\", \"restaurant\" ],\n    char: \"\\ud83c\\udf7d\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  bowl_with_spoon: {\n    keywords: [ \"food\", \"breakfast\", \"cereal\", \"oatmeal\", \"porridge\" ],\n    char: \"\\ud83e\\udd63\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  takeout_box: {\n    keywords: [ \"food\", \"leftovers\" ],\n    char: \"\\ud83e\\udd61\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  chopsticks: {\n    keywords: [ \"food\" ],\n    char: \"\\ud83e\\udd62\",\n    fitzpatrick_scale: false,\n    category: \"food_and_drink\"\n  },\n  soccer: {\n    keywords: [ \"sports\", \"football\" ],\n    char: \"\\u26bd\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  basketball: {\n    keywords: [ \"sports\", \"balls\", \"NBA\" ],\n    char: \"\\ud83c\\udfc0\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  football: {\n    keywords: [ \"sports\", \"balls\", \"NFL\" ],\n    char: \"\\ud83c\\udfc8\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  baseball: {\n    keywords: [ \"sports\", \"balls\" ],\n    char: \"\\u26be\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  softball: {\n    keywords: [ \"sports\", \"balls\" ],\n    char: \"\\ud83e\\udd4e\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  tennis: {\n    keywords: [ \"sports\", \"balls\", \"green\" ],\n    char: \"\\ud83c\\udfbe\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  volleyball: {\n    keywords: [ \"sports\", \"balls\" ],\n    char: \"\\ud83c\\udfd0\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  rugby_football: {\n    keywords: [ \"sports\", \"team\" ],\n    char: \"\\ud83c\\udfc9\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  flying_disc: {\n    keywords: [ \"sports\", \"frisbee\", \"ultimate\" ],\n    char: \"\\ud83e\\udd4f\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"8ball\": {\n    keywords: [ \"pool\", \"hobby\", \"game\", \"luck\", \"magic\" ],\n    char: \"\\ud83c\\udfb1\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  golf: {\n    keywords: [ \"sports\", \"business\", \"flag\", \"hole\", \"summer\" ],\n    char: \"\\u26f3\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  golfing_woman: {\n    keywords: [ \"sports\", \"business\", \"woman\", \"female\" ],\n    char: \"\\ud83c\\udfcc\\ufe0f\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  golfing_man: {\n    keywords: [ \"sports\", \"business\" ],\n    char: \"\\ud83c\\udfcc\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  ping_pong: {\n    keywords: [ \"sports\", \"pingpong\" ],\n    char: \"\\ud83c\\udfd3\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  badminton: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83c\\udff8\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  goal_net: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83e\\udd45\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  ice_hockey: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83c\\udfd2\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  field_hockey: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83c\\udfd1\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  lacrosse: {\n    keywords: [ \"sports\", \"ball\", \"stick\" ],\n    char: \"\\ud83e\\udd4d\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  cricket: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83c\\udfcf\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  ski: {\n    keywords: [ \"sports\", \"winter\", \"cold\", \"snow\" ],\n    char: \"\\ud83c\\udfbf\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  skier: {\n    keywords: [ \"sports\", \"winter\", \"snow\" ],\n    char: \"\\u26f7\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  snowboarder: {\n    keywords: [ \"sports\", \"winter\" ],\n    char: \"\\ud83c\\udfc2\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  person_fencing: {\n    keywords: [ \"sports\", \"fencing\", \"sword\" ],\n    char: \"\\ud83e\\udd3a\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  women_wrestling: {\n    keywords: [ \"sports\", \"wrestlers\" ],\n    char: \"\\ud83e\\udd3c\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  men_wrestling: {\n    keywords: [ \"sports\", \"wrestlers\" ],\n    char: \"\\ud83e\\udd3c\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  woman_cartwheeling: {\n    keywords: [ \"gymnastics\" ],\n    char: \"\\ud83e\\udd38\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_cartwheeling: {\n    keywords: [ \"gymnastics\" ],\n    char: \"\\ud83e\\udd38\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  woman_playing_handball: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83e\\udd3e\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_playing_handball: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83e\\udd3e\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  ice_skate: {\n    keywords: [ \"sports\" ],\n    char: \"\\u26f8\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  curling_stone: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83e\\udd4c\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  skateboard: {\n    keywords: [ \"board\" ],\n    char: \"\\ud83d\\udef9\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  sled: {\n    keywords: [ \"sleigh\", \"luge\", \"toboggan\" ],\n    char: \"\\ud83d\\udef7\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  bow_and_arrow: {\n    keywords: [ \"sports\" ],\n    char: \"\\ud83c\\udff9\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  fishing_pole_and_fish: {\n    keywords: [ \"food\", \"hobby\", \"summer\" ],\n    char: \"\\ud83c\\udfa3\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  boxing_glove: {\n    keywords: [ \"sports\", \"fighting\" ],\n    char: \"\\ud83e\\udd4a\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  martial_arts_uniform: {\n    keywords: [ \"judo\", \"karate\", \"taekwondo\" ],\n    char: \"\\ud83e\\udd4b\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  rowing_woman: {\n    keywords: [ \"sports\", \"hobby\", \"water\", \"ship\", \"woman\", \"female\" ],\n    char: \"\\ud83d\\udea3\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  rowing_man: {\n    keywords: [ \"sports\", \"hobby\", \"water\", \"ship\" ],\n    char: \"\\ud83d\\udea3\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  climbing_woman: {\n    keywords: [ \"sports\", \"hobby\", \"woman\", \"female\", \"rock\" ],\n    char: \"\\ud83e\\uddd7\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  climbing_man: {\n    keywords: [ \"sports\", \"hobby\", \"man\", \"male\", \"rock\" ],\n    char: \"\\ud83e\\uddd7\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  swimming_woman: {\n    keywords: [ \"sports\", \"exercise\", \"human\", \"athlete\", \"water\", \"summer\", \"woman\", \"female\" ],\n    char: \"\\ud83c\\udfca\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  swimming_man: {\n    keywords: [ \"sports\", \"exercise\", \"human\", \"athlete\", \"water\", \"summer\" ],\n    char: \"\\ud83c\\udfca\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  woman_playing_water_polo: {\n    keywords: [ \"sports\", \"pool\" ],\n    char: \"\\ud83e\\udd3d\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_playing_water_polo: {\n    keywords: [ \"sports\", \"pool\" ],\n    char: \"\\ud83e\\udd3d\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  woman_in_lotus_position: {\n    keywords: [ \"woman\", \"female\", \"meditation\", \"yoga\", \"serenity\", \"zen\", \"mindfulness\" ],\n    char: \"\\ud83e\\uddd8\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_in_lotus_position: {\n    keywords: [ \"man\", \"male\", \"meditation\", \"yoga\", \"serenity\", \"zen\", \"mindfulness\" ],\n    char: \"\\ud83e\\uddd8\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  surfing_woman: {\n    keywords: [ \"sports\", \"ocean\", \"sea\", \"summer\", \"beach\", \"woman\", \"female\" ],\n    char: \"\\ud83c\\udfc4\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  surfing_man: {\n    keywords: [ \"sports\", \"ocean\", \"sea\", \"summer\", \"beach\" ],\n    char: \"\\ud83c\\udfc4\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  bath: {\n    keywords: [ \"clean\", \"shower\", \"bathroom\" ],\n    char: \"\\ud83d\\udec0\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  basketball_woman: {\n    keywords: [ \"sports\", \"human\", \"woman\", \"female\" ],\n    char: \"\\u26f9\\ufe0f\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  basketball_man: {\n    keywords: [ \"sports\", \"human\" ],\n    char: \"\\u26f9\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  weight_lifting_woman: {\n    keywords: [ \"sports\", \"training\", \"exercise\", \"woman\", \"female\" ],\n    char: \"\\ud83c\\udfcb\\ufe0f\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  weight_lifting_man: {\n    keywords: [ \"sports\", \"training\", \"exercise\" ],\n    char: \"\\ud83c\\udfcb\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  biking_woman: {\n    keywords: [ \"sports\", \"bike\", \"exercise\", \"hipster\", \"woman\", \"female\" ],\n    char: \"\\ud83d\\udeb4\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  biking_man: {\n    keywords: [ \"sports\", \"bike\", \"exercise\", \"hipster\" ],\n    char: \"\\ud83d\\udeb4\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  mountain_biking_woman: {\n    keywords: [ \"transportation\", \"sports\", \"human\", \"race\", \"bike\", \"woman\", \"female\" ],\n    char: \"\\ud83d\\udeb5\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  mountain_biking_man: {\n    keywords: [ \"transportation\", \"sports\", \"human\", \"race\", \"bike\" ],\n    char: \"\\ud83d\\udeb5\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  horse_racing: {\n    keywords: [ \"animal\", \"betting\", \"competition\", \"gambling\", \"luck\" ],\n    char: \"\\ud83c\\udfc7\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  business_suit_levitating: {\n    keywords: [ \"suit\", \"business\", \"levitate\", \"hover\", \"jump\" ],\n    char: \"\\ud83d\\udd74\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  trophy: {\n    keywords: [ \"win\", \"award\", \"contest\", \"place\", \"ftw\", \"ceremony\" ],\n    char: \"\\ud83c\\udfc6\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  running_shirt_with_sash: {\n    keywords: [ \"play\", \"pageant\" ],\n    char: \"\\ud83c\\udfbd\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  medal_sports: {\n    keywords: [ \"award\", \"winning\" ],\n    char: \"\\ud83c\\udfc5\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  medal_military: {\n    keywords: [ \"award\", \"winning\", \"army\" ],\n    char: \"\\ud83c\\udf96\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"1st_place_medal\": {\n    keywords: [ \"award\", \"winning\", \"first\" ],\n    char: \"\\ud83e\\udd47\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"2nd_place_medal\": {\n    keywords: [ \"award\", \"second\" ],\n    char: \"\\ud83e\\udd48\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  \"3rd_place_medal\": {\n    keywords: [ \"award\", \"third\" ],\n    char: \"\\ud83e\\udd49\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  reminder_ribbon: {\n    keywords: [ \"sports\", \"cause\", \"support\", \"awareness\" ],\n    char: \"\\ud83c\\udf97\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  rosette: {\n    keywords: [ \"flower\", \"decoration\", \"military\" ],\n    char: \"\\ud83c\\udff5\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  ticket: {\n    keywords: [ \"event\", \"concert\", \"pass\" ],\n    char: \"\\ud83c\\udfab\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  tickets: {\n    keywords: [ \"sports\", \"concert\", \"entrance\" ],\n    char: \"\\ud83c\\udf9f\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  performing_arts: {\n    keywords: [ \"acting\", \"theater\", \"drama\" ],\n    char: \"\\ud83c\\udfad\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  art: {\n    keywords: [ \"design\", \"paint\", \"draw\", \"colors\" ],\n    char: \"\\ud83c\\udfa8\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  circus_tent: {\n    keywords: [ \"festival\", \"carnival\", \"party\" ],\n    char: \"\\ud83c\\udfaa\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  woman_juggling: {\n    keywords: [ \"juggle\", \"balance\", \"skill\", \"multitask\" ],\n    char: \"\\ud83e\\udd39\\u200d\\u2640\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  man_juggling: {\n    keywords: [ \"juggle\", \"balance\", \"skill\", \"multitask\" ],\n    char: \"\\ud83e\\udd39\\u200d\\u2642\\ufe0f\",\n    fitzpatrick_scale: true,\n    category: \"activity\"\n  },\n  microphone: {\n    keywords: [ \"sound\", \"music\", \"PA\", \"sing\", \"talkshow\" ],\n    char: \"\\ud83c\\udfa4\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  headphones: {\n    keywords: [ \"music\", \"score\", \"gadgets\" ],\n    char: \"\\ud83c\\udfa7\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  musical_score: {\n    keywords: [ \"treble\", \"clef\", \"compose\" ],\n    char: \"\\ud83c\\udfbc\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  musical_keyboard: {\n    keywords: [ \"piano\", \"instrument\", \"compose\" ],\n    char: \"\\ud83c\\udfb9\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  drum: {\n    keywords: [ \"music\", \"instrument\", \"drumsticks\", \"snare\" ],\n    char: \"\\ud83e\\udd41\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  saxophone: {\n    keywords: [ \"music\", \"instrument\", \"jazz\", \"blues\" ],\n    char: \"\\ud83c\\udfb7\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  trumpet: {\n    keywords: [ \"music\", \"brass\" ],\n    char: \"\\ud83c\\udfba\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  guitar: {\n    keywords: [ \"music\", \"instrument\" ],\n    char: \"\\ud83c\\udfb8\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  violin: {\n    keywords: [ \"music\", \"instrument\", \"orchestra\", \"symphony\" ],\n    char: \"\\ud83c\\udfbb\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  clapper: {\n    keywords: [ \"movie\", \"film\", \"record\" ],\n    char: \"\\ud83c\\udfac\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  video_game: {\n    keywords: [ \"play\", \"console\", \"PS4\", \"controller\" ],\n    char: \"\\ud83c\\udfae\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  space_invader: {\n    keywords: [ \"game\", \"arcade\", \"play\" ],\n    char: \"\\ud83d\\udc7e\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  dart: {\n    keywords: [ \"game\", \"play\", \"bar\", \"target\", \"bullseye\" ],\n    char: \"\\ud83c\\udfaf\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  game_die: {\n    keywords: [ \"dice\", \"random\", \"tabletop\", \"play\", \"luck\" ],\n    char: \"\\ud83c\\udfb2\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  chess_pawn: {\n    keywords: [ \"expendable\" ],\n    char: \"\\u265f\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  slot_machine: {\n    keywords: [ \"bet\", \"gamble\", \"vegas\", \"fruit machine\", \"luck\", \"casino\" ],\n    char: \"\\ud83c\\udfb0\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  jigsaw: {\n    keywords: [ \"interlocking\", \"puzzle\", \"piece\" ],\n    char: \"\\ud83e\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  bowling: {\n    keywords: [ \"sports\", \"fun\", \"play\" ],\n    char: \"\\ud83c\\udfb3\",\n    fitzpatrick_scale: false,\n    category: \"activity\"\n  },\n  red_car: {\n    keywords: [ \"red\", \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude97\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  taxi: {\n    keywords: [ \"uber\", \"vehicle\", \"cars\", \"transportation\" ],\n    char: \"\\ud83d\\ude95\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  blue_car: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude99\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bus: {\n    keywords: [ \"car\", \"vehicle\", \"transportation\" ],\n    char: \"\\ud83d\\ude8c\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  trolleybus: {\n    keywords: [ \"bart\", \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude8e\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  racing_car: {\n    keywords: [ \"sports\", \"race\", \"fast\", \"formula\", \"f1\" ],\n    char: \"\\ud83c\\udfce\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  police_car: {\n    keywords: [ \"vehicle\", \"cars\", \"transportation\", \"law\", \"legal\", \"enforcement\" ],\n    char: \"\\ud83d\\ude93\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ambulance: {\n    keywords: [ \"health\", \"911\", \"hospital\" ],\n    char: \"\\ud83d\\ude91\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fire_engine: {\n    keywords: [ \"transportation\", \"cars\", \"vehicle\" ],\n    char: \"\\ud83d\\ude92\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  minibus: {\n    keywords: [ \"vehicle\", \"car\", \"transportation\" ],\n    char: \"\\ud83d\\ude90\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  truck: {\n    keywords: [ \"cars\", \"transportation\" ],\n    char: \"\\ud83d\\ude9a\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  articulated_lorry: {\n    keywords: [ \"vehicle\", \"cars\", \"transportation\", \"express\" ],\n    char: \"\\ud83d\\ude9b\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tractor: {\n    keywords: [ \"vehicle\", \"car\", \"farming\", \"agriculture\" ],\n    char: \"\\ud83d\\ude9c\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  kick_scooter: {\n    keywords: [ \"vehicle\", \"kick\", \"razor\" ],\n    char: \"\\ud83d\\udef4\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motorcycle: {\n    keywords: [ \"race\", \"sports\", \"fast\" ],\n    char: \"\\ud83c\\udfcd\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bike: {\n    keywords: [ \"sports\", \"bicycle\", \"exercise\", \"hipster\" ],\n    char: \"\\ud83d\\udeb2\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motor_scooter: {\n    keywords: [ \"vehicle\", \"vespa\", \"sasha\" ],\n    char: \"\\ud83d\\udef5\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rotating_light: {\n    keywords: [ \"police\", \"ambulance\", \"911\", \"emergency\", \"alert\", \"error\", \"pinged\", \"law\", \"legal\" ],\n    char: \"\\ud83d\\udea8\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_police_car: {\n    keywords: [ \"vehicle\", \"law\", \"legal\", \"enforcement\", \"911\" ],\n    char: \"\\ud83d\\ude94\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_bus: {\n    keywords: [ \"vehicle\", \"transportation\" ],\n    char: \"\\ud83d\\ude8d\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_automobile: {\n    keywords: [ \"car\", \"vehicle\", \"transportation\" ],\n    char: \"\\ud83d\\ude98\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  oncoming_taxi: {\n    keywords: [ \"vehicle\", \"cars\", \"uber\" ],\n    char: \"\\ud83d\\ude96\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  aerial_tramway: {\n    keywords: [ \"transportation\", \"vehicle\", \"ski\" ],\n    char: \"\\ud83d\\udea1\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain_cableway: {\n    keywords: [ \"transportation\", \"vehicle\", \"ski\" ],\n    char: \"\\ud83d\\udea0\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  suspension_railway: {\n    keywords: [ \"vehicle\", \"transportation\" ],\n    char: \"\\ud83d\\ude9f\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  railway_car: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude83\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  train: {\n    keywords: [ \"transportation\", \"vehicle\", \"carriage\", \"public\", \"travel\" ],\n    char: \"\\ud83d\\ude8b\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  monorail: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude9d\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bullettrain_side: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude84\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bullettrain_front: {\n    keywords: [ \"transportation\", \"vehicle\", \"speed\", \"fast\", \"public\", \"travel\" ],\n    char: \"\\ud83d\\ude85\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  light_rail: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude88\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain_railway: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude9e\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  steam_locomotive: {\n    keywords: [ \"transportation\", \"vehicle\", \"train\" ],\n    char: \"\\ud83d\\ude82\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  train2: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude86\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  metro: {\n    keywords: [ \"transportation\", \"blue-square\", \"mrt\", \"underground\", \"tube\" ],\n    char: \"\\ud83d\\ude87\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tram: {\n    keywords: [ \"transportation\", \"vehicle\" ],\n    char: \"\\ud83d\\ude8a\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  station: {\n    keywords: [ \"transportation\", \"vehicle\", \"public\" ],\n    char: \"\\ud83d\\ude89\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  flying_saucer: {\n    keywords: [ \"transportation\", \"vehicle\", \"ufo\" ],\n    char: \"\\ud83d\\udef8\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  helicopter: {\n    keywords: [ \"transportation\", \"vehicle\", \"fly\" ],\n    char: \"\\ud83d\\ude81\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  small_airplane: {\n    keywords: [ \"flight\", \"transportation\", \"fly\", \"vehicle\" ],\n    char: \"\\ud83d\\udee9\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  airplane: {\n    keywords: [ \"vehicle\", \"transportation\", \"flight\", \"fly\" ],\n    char: \"\\u2708\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  flight_departure: {\n    keywords: [ \"airport\", \"flight\", \"landing\" ],\n    char: \"\\ud83d\\udeeb\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  flight_arrival: {\n    keywords: [ \"airport\", \"flight\", \"boarding\" ],\n    char: \"\\ud83d\\udeec\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sailboat: {\n    keywords: [ \"ship\", \"summer\", \"transportation\", \"water\", \"sailing\" ],\n    char: \"\\u26f5\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motor_boat: {\n    keywords: [ \"ship\" ],\n    char: \"\\ud83d\\udee5\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  speedboat: {\n    keywords: [ \"ship\", \"transportation\", \"vehicle\", \"summer\" ],\n    char: \"\\ud83d\\udea4\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ferry: {\n    keywords: [ \"boat\", \"ship\", \"yacht\" ],\n    char: \"\\u26f4\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  passenger_ship: {\n    keywords: [ \"yacht\", \"cruise\", \"ferry\" ],\n    char: \"\\ud83d\\udef3\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rocket: {\n    keywords: [ \"launch\", \"ship\", \"staffmode\", \"NASA\", \"outer space\", \"outer_space\", \"fly\" ],\n    char: \"\\ud83d\\ude80\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  artificial_satellite: {\n    keywords: [ \"communication\", \"gps\", \"orbit\", \"spaceflight\", \"NASA\", \"ISS\" ],\n    char: \"\\ud83d\\udef0\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  seat: {\n    keywords: [ \"sit\", \"airplane\", \"transport\", \"bus\", \"flight\", \"fly\" ],\n    char: \"\\ud83d\\udcba\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  canoe: {\n    keywords: [ \"boat\", \"paddle\", \"water\", \"ship\" ],\n    char: \"\\ud83d\\udef6\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  anchor: {\n    keywords: [ \"ship\", \"ferry\", \"sea\", \"boat\" ],\n    char: \"\\u2693\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  construction: {\n    keywords: [ \"wip\", \"progress\", \"caution\", \"warning\" ],\n    char: \"\\ud83d\\udea7\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fuelpump: {\n    keywords: [ \"gas station\", \"petroleum\" ],\n    char: \"\\u26fd\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  busstop: {\n    keywords: [ \"transportation\", \"wait\" ],\n    char: \"\\ud83d\\ude8f\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  vertical_traffic_light: {\n    keywords: [ \"transportation\", \"driving\" ],\n    char: \"\\ud83d\\udea6\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  traffic_light: {\n    keywords: [ \"transportation\", \"signal\" ],\n    char: \"\\ud83d\\udea5\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  checkered_flag: {\n    keywords: [ \"contest\", \"finishline\", \"race\", \"gokart\" ],\n    char: \"\\ud83c\\udfc1\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ship: {\n    keywords: [ \"transportation\", \"titanic\", \"deploy\" ],\n    char: \"\\ud83d\\udea2\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  ferris_wheel: {\n    keywords: [ \"photo\", \"carnival\", \"londoneye\" ],\n    char: \"\\ud83c\\udfa1\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  roller_coaster: {\n    keywords: [ \"carnival\", \"playground\", \"photo\", \"fun\" ],\n    char: \"\\ud83c\\udfa2\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  carousel_horse: {\n    keywords: [ \"photo\", \"carnival\" ],\n    char: \"\\ud83c\\udfa0\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  building_construction: {\n    keywords: [ \"wip\", \"working\", \"progress\" ],\n    char: \"\\ud83c\\udfd7\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  foggy: {\n    keywords: [ \"photo\", \"mountain\" ],\n    char: \"\\ud83c\\udf01\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tokyo_tower: {\n    keywords: [ \"photo\", \"japanese\" ],\n    char: \"\\ud83d\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  factory: {\n    keywords: [ \"building\", \"industry\", \"pollution\", \"smoke\" ],\n    char: \"\\ud83c\\udfed\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fountain: {\n    keywords: [ \"photo\", \"summer\", \"water\", \"fresh\" ],\n    char: \"\\u26f2\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rice_scene: {\n    keywords: [ \"photo\", \"japan\", \"asia\", \"tsukimi\" ],\n    char: \"\\ud83c\\udf91\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain: {\n    keywords: [ \"photo\", \"nature\", \"environment\" ],\n    char: \"\\u26f0\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mountain_snow: {\n    keywords: [ \"photo\", \"nature\", \"environment\", \"winter\", \"cold\" ],\n    char: \"\\ud83c\\udfd4\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mount_fuji: {\n    keywords: [ \"photo\", \"mountain\", \"nature\", \"japanese\" ],\n    char: \"\\ud83d\\uddfb\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  volcano: {\n    keywords: [ \"photo\", \"nature\", \"disaster\" ],\n    char: \"\\ud83c\\udf0b\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  japan: {\n    keywords: [ \"nation\", \"country\", \"japanese\", \"asia\" ],\n    char: \"\\ud83d\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  camping: {\n    keywords: [ \"photo\", \"outdoors\", \"tent\" ],\n    char: \"\\ud83c\\udfd5\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  tent: {\n    keywords: [ \"photo\", \"camping\", \"outdoors\" ],\n    char: \"\\u26fa\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  national_park: {\n    keywords: [ \"photo\", \"environment\", \"nature\" ],\n    char: \"\\ud83c\\udfde\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  motorway: {\n    keywords: [ \"road\", \"cupertino\", \"interstate\", \"highway\" ],\n    char: \"\\ud83d\\udee3\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  railway_track: {\n    keywords: [ \"train\", \"transportation\" ],\n    char: \"\\ud83d\\udee4\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sunrise: {\n    keywords: [ \"morning\", \"view\", \"vacation\", \"photo\" ],\n    char: \"\\ud83c\\udf05\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sunrise_over_mountains: {\n    keywords: [ \"view\", \"vacation\", \"photo\" ],\n    char: \"\\ud83c\\udf04\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  desert: {\n    keywords: [ \"photo\", \"warm\", \"saharah\" ],\n    char: \"\\ud83c\\udfdc\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  beach_umbrella: {\n    keywords: [ \"weather\", \"summer\", \"sunny\", \"sand\", \"mojito\" ],\n    char: \"\\ud83c\\udfd6\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  desert_island: {\n    keywords: [ \"photo\", \"tropical\", \"mojito\" ],\n    char: \"\\ud83c\\udfdd\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  city_sunrise: {\n    keywords: [ \"photo\", \"good morning\", \"dawn\" ],\n    char: \"\\ud83c\\udf07\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  city_sunset: {\n    keywords: [ \"photo\", \"evening\", \"sky\", \"buildings\" ],\n    char: \"\\ud83c\\udf06\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  cityscape: {\n    keywords: [ \"photo\", \"night life\", \"urban\" ],\n    char: \"\\ud83c\\udfd9\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  night_with_stars: {\n    keywords: [ \"evening\", \"city\", \"downtown\" ],\n    char: \"\\ud83c\\udf03\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bridge_at_night: {\n    keywords: [ \"photo\", \"sanfrancisco\" ],\n    char: \"\\ud83c\\udf09\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  milky_way: {\n    keywords: [ \"photo\", \"space\", \"stars\" ],\n    char: \"\\ud83c\\udf0c\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  stars: {\n    keywords: [ \"night\", \"photo\" ],\n    char: \"\\ud83c\\udf20\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  sparkler: {\n    keywords: [ \"stars\", \"night\", \"shine\" ],\n    char: \"\\ud83c\\udf87\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  fireworks: {\n    keywords: [ \"photo\", \"festival\", \"carnival\", \"congratulations\" ],\n    char: \"\\ud83c\\udf86\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  rainbow: {\n    keywords: [ \"nature\", \"happy\", \"unicorn_face\", \"photo\", \"sky\", \"spring\" ],\n    char: \"\\ud83c\\udf08\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  houses: {\n    keywords: [ \"buildings\", \"photo\" ],\n    char: \"\\ud83c\\udfd8\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  european_castle: {\n    keywords: [ \"building\", \"royalty\", \"history\" ],\n    char: \"\\ud83c\\udff0\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  japanese_castle: {\n    keywords: [ \"photo\", \"building\" ],\n    char: \"\\ud83c\\udfef\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  stadium: {\n    keywords: [ \"photo\", \"place\", \"sports\", \"concert\", \"venue\" ],\n    char: \"\\ud83c\\udfdf\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  statue_of_liberty: {\n    keywords: [ \"american\", \"newyork\" ],\n    char: \"\\ud83d\\uddfd\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  house: {\n    keywords: [ \"building\", \"home\" ],\n    char: \"\\ud83c\\udfe0\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  house_with_garden: {\n    keywords: [ \"home\", \"plant\", \"nature\" ],\n    char: \"\\ud83c\\udfe1\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  derelict_house: {\n    keywords: [ \"abandon\", \"evict\", \"broken\", \"building\" ],\n    char: \"\\ud83c\\udfda\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  office: {\n    keywords: [ \"building\", \"bureau\", \"work\" ],\n    char: \"\\ud83c\\udfe2\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  department_store: {\n    keywords: [ \"building\", \"shopping\", \"mall\" ],\n    char: \"\\ud83c\\udfec\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  post_office: {\n    keywords: [ \"building\", \"envelope\", \"communication\" ],\n    char: \"\\ud83c\\udfe3\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  european_post_office: {\n    keywords: [ \"building\", \"email\" ],\n    char: \"\\ud83c\\udfe4\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  hospital: {\n    keywords: [ \"building\", \"health\", \"surgery\", \"doctor\" ],\n    char: \"\\ud83c\\udfe5\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  bank: {\n    keywords: [ \"building\", \"money\", \"sales\", \"cash\", \"business\", \"enterprise\" ],\n    char: \"\\ud83c\\udfe6\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  hotel: {\n    keywords: [ \"building\", \"accomodation\", \"checkin\" ],\n    char: \"\\ud83c\\udfe8\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  convenience_store: {\n    keywords: [ \"building\", \"shopping\", \"groceries\" ],\n    char: \"\\ud83c\\udfea\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  school: {\n    keywords: [ \"building\", \"student\", \"education\", \"learn\", \"teach\" ],\n    char: \"\\ud83c\\udfeb\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  love_hotel: {\n    keywords: [ \"like\", \"affection\", \"dating\" ],\n    char: \"\\ud83c\\udfe9\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  wedding: {\n    keywords: [ \"love\", \"like\", \"affection\", \"couple\", \"marriage\", \"bride\", \"groom\" ],\n    char: \"\\ud83d\\udc92\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  classical_building: {\n    keywords: [ \"art\", \"culture\", \"history\" ],\n    char: \"\\ud83c\\udfdb\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  church: {\n    keywords: [ \"building\", \"religion\", \"christ\" ],\n    char: \"\\u26ea\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  mosque: {\n    keywords: [ \"islam\", \"worship\", \"minaret\" ],\n    char: \"\\ud83d\\udd4c\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  synagogue: {\n    keywords: [ \"judaism\", \"worship\", \"temple\", \"jewish\" ],\n    char: \"\\ud83d\\udd4d\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  kaaba: {\n    keywords: [ \"mecca\", \"mosque\", \"islam\" ],\n    char: \"\\ud83d\\udd4b\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  shinto_shrine: {\n    keywords: [ \"temple\", \"japan\", \"kyoto\" ],\n    char: \"\\u26e9\",\n    fitzpatrick_scale: false,\n    category: \"travel_and_places\"\n  },\n  watch: {\n    keywords: [ \"time\", \"accessories\" ],\n    char: \"\\u231a\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  iphone: {\n    keywords: [ \"technology\", \"apple\", \"gadgets\", \"dial\" ],\n    char: \"\\ud83d\\udcf1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  calling: {\n    keywords: [ \"iphone\", \"incoming\" ],\n    char: \"\\ud83d\\udcf2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  computer: {\n    keywords: [ \"technology\", \"laptop\", \"screen\", \"display\", \"monitor\" ],\n    char: \"\\ud83d\\udcbb\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  keyboard: {\n    keywords: [ \"technology\", \"computer\", \"type\", \"input\", \"text\" ],\n    char: \"\\u2328\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  desktop_computer: {\n    keywords: [ \"technology\", \"computing\", \"screen\" ],\n    char: \"\\ud83d\\udda5\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  printer: {\n    keywords: [ \"paper\", \"ink\" ],\n    char: \"\\ud83d\\udda8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  computer_mouse: {\n    keywords: [ \"click\" ],\n    char: \"\\ud83d\\uddb1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  trackball: {\n    keywords: [ \"technology\", \"trackpad\" ],\n    char: \"\\ud83d\\uddb2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  joystick: {\n    keywords: [ \"game\", \"play\" ],\n    char: \"\\ud83d\\udd79\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  clamp: {\n    keywords: [ \"tool\" ],\n    char: \"\\ud83d\\udddc\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  minidisc: {\n    keywords: [ \"technology\", \"record\", \"data\", \"disk\", \"90s\" ],\n    char: \"\\ud83d\\udcbd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  floppy_disk: {\n    keywords: [ \"oldschool\", \"technology\", \"save\", \"90s\", \"80s\" ],\n    char: \"\\ud83d\\udcbe\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  cd: {\n    keywords: [ \"technology\", \"dvd\", \"disk\", \"disc\", \"90s\" ],\n    char: \"\\ud83d\\udcbf\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dvd: {\n    keywords: [ \"cd\", \"disk\", \"disc\" ],\n    char: \"\\ud83d\\udcc0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  vhs: {\n    keywords: [ \"record\", \"video\", \"oldschool\", \"90s\", \"80s\" ],\n    char: \"\\ud83d\\udcfc\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  camera: {\n    keywords: [ \"gadgets\", \"photography\" ],\n    char: \"\\ud83d\\udcf7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  camera_flash: {\n    keywords: [ \"photography\", \"gadgets\" ],\n    char: \"\\ud83d\\udcf8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  video_camera: {\n    keywords: [ \"film\", \"record\" ],\n    char: \"\\ud83d\\udcf9\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  movie_camera: {\n    keywords: [ \"film\", \"record\" ],\n    char: \"\\ud83c\\udfa5\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  film_projector: {\n    keywords: [ \"video\", \"tape\", \"record\", \"movie\" ],\n    char: \"\\ud83d\\udcfd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  film_strip: {\n    keywords: [ \"movie\" ],\n    char: \"\\ud83c\\udf9e\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  telephone_receiver: {\n    keywords: [ \"technology\", \"communication\", \"dial\" ],\n    char: \"\\ud83d\\udcde\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  phone: {\n    keywords: [ \"technology\", \"communication\", \"dial\", \"telephone\" ],\n    char: \"\\u260e\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pager: {\n    keywords: [ \"bbcall\", \"oldschool\", \"90s\" ],\n    char: \"\\ud83d\\udcdf\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  fax: {\n    keywords: [ \"communication\", \"technology\" ],\n    char: \"\\ud83d\\udce0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  tv: {\n    keywords: [ \"technology\", \"program\", \"oldschool\", \"show\", \"television\" ],\n    char: \"\\ud83d\\udcfa\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  radio: {\n    keywords: [ \"communication\", \"music\", \"podcast\", \"program\" ],\n    char: \"\\ud83d\\udcfb\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  studio_microphone: {\n    keywords: [ \"sing\", \"recording\", \"artist\", \"talkshow\" ],\n    char: \"\\ud83c\\udf99\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  level_slider: {\n    keywords: [ \"scale\" ],\n    char: \"\\ud83c\\udf9a\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  control_knobs: {\n    keywords: [ \"dial\" ],\n    char: \"\\ud83c\\udf9b\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  compass: {\n    keywords: [ \"magnetic\", \"navigation\", \"orienteering\" ],\n    char: \"\\ud83e\\udded\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  stopwatch: {\n    keywords: [ \"time\", \"deadline\" ],\n    char: \"\\u23f1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  timer_clock: {\n    keywords: [ \"alarm\" ],\n    char: \"\\u23f2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  alarm_clock: {\n    keywords: [ \"time\", \"wake\" ],\n    char: \"\\u23f0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mantelpiece_clock: {\n    keywords: [ \"time\" ],\n    char: \"\\ud83d\\udd70\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hourglass_flowing_sand: {\n    keywords: [ \"oldschool\", \"time\", \"countdown\" ],\n    char: \"\\u23f3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hourglass: {\n    keywords: [ \"time\", \"clock\", \"oldschool\", \"limit\", \"exam\", \"quiz\", \"test\" ],\n    char: \"\\u231b\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  satellite: {\n    keywords: [ \"communication\", \"future\", \"radio\", \"space\" ],\n    char: \"\\ud83d\\udce1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  battery: {\n    keywords: [ \"power\", \"energy\", \"sustain\" ],\n    char: \"\\ud83d\\udd0b\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  electric_plug: {\n    keywords: [ \"charger\", \"power\" ],\n    char: \"\\ud83d\\udd0c\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bulb: {\n    keywords: [ \"light\", \"electricity\", \"idea\" ],\n    char: \"\\ud83d\\udca1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  flashlight: {\n    keywords: [ \"dark\", \"camping\", \"sight\", \"night\" ],\n    char: \"\\ud83d\\udd26\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  candle: {\n    keywords: [ \"fire\", \"wax\" ],\n    char: \"\\ud83d\\udd6f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  fire_extinguisher: {\n    keywords: [ \"quench\" ],\n    char: \"\\ud83e\\uddef\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  wastebasket: {\n    keywords: [ \"bin\", \"trash\", \"rubbish\", \"garbage\", \"toss\" ],\n    char: \"\\ud83d\\uddd1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  oil_drum: {\n    keywords: [ \"barrell\" ],\n    char: \"\\ud83d\\udee2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  money_with_wings: {\n    keywords: [ \"dollar\", \"bills\", \"payment\", \"sale\" ],\n    char: \"\\ud83d\\udcb8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dollar: {\n    keywords: [ \"money\", \"sales\", \"bill\", \"currency\" ],\n    char: \"\\ud83d\\udcb5\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  yen: {\n    keywords: [ \"money\", \"sales\", \"japanese\", \"dollar\", \"currency\" ],\n    char: \"\\ud83d\\udcb4\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  euro: {\n    keywords: [ \"money\", \"sales\", \"dollar\", \"currency\" ],\n    char: \"\\ud83d\\udcb6\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pound: {\n    keywords: [ \"british\", \"sterling\", \"money\", \"sales\", \"bills\", \"uk\", \"england\", \"currency\" ],\n    char: \"\\ud83d\\udcb7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  moneybag: {\n    keywords: [ \"dollar\", \"payment\", \"coins\", \"sale\" ],\n    char: \"\\ud83d\\udcb0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  credit_card: {\n    keywords: [ \"money\", \"sales\", \"dollar\", \"bill\", \"payment\", \"shopping\" ],\n    char: \"\\ud83d\\udcb3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gem: {\n    keywords: [ \"blue\", \"ruby\", \"diamond\", \"jewelry\" ],\n    char: \"\\ud83d\\udc8e\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  balance_scale: {\n    keywords: [ \"law\", \"fairness\", \"weight\" ],\n    char: \"\\u2696\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  toolbox: {\n    keywords: [ \"tools\", \"diy\", \"fix\", \"maintainer\", \"mechanic\" ],\n    char: \"\\ud83e\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  wrench: {\n    keywords: [ \"tools\", \"diy\", \"ikea\", \"fix\", \"maintainer\" ],\n    char: \"\\ud83d\\udd27\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hammer: {\n    keywords: [ \"tools\", \"build\", \"create\" ],\n    char: \"\\ud83d\\udd28\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hammer_and_pick: {\n    keywords: [ \"tools\", \"build\", \"create\" ],\n    char: \"\\u2692\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hammer_and_wrench: {\n    keywords: [ \"tools\", \"build\", \"create\" ],\n    char: \"\\ud83d\\udee0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pick: {\n    keywords: [ \"tools\", \"dig\" ],\n    char: \"\\u26cf\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  nut_and_bolt: {\n    keywords: [ \"handy\", \"tools\", \"fix\" ],\n    char: \"\\ud83d\\udd29\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gear: {\n    keywords: [ \"cog\" ],\n    char: \"\\u2699\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  brick: {\n    keywords: [ \"bricks\" ],\n    char: \"\\ud83e\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  chains: {\n    keywords: [ \"lock\", \"arrest\" ],\n    char: \"\\u26d3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  magnet: {\n    keywords: [ \"attraction\", \"magnetic\" ],\n    char: \"\\ud83e\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gun: {\n    keywords: [ \"violence\", \"weapon\", \"pistol\", \"revolver\" ],\n    char: \"\\ud83d\\udd2b\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bomb: {\n    keywords: [ \"boom\", \"explode\", \"explosion\", \"terrorism\" ],\n    char: \"\\ud83d\\udca3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  firecracker: {\n    keywords: [ \"dynamite\", \"boom\", \"explode\", \"explosion\", \"explosive\" ],\n    char: \"\\ud83e\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hocho: {\n    keywords: [ \"knife\", \"blade\", \"cutlery\", \"kitchen\", \"weapon\" ],\n    char: \"\\ud83d\\udd2a\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dagger: {\n    keywords: [ \"weapon\" ],\n    char: \"\\ud83d\\udde1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crossed_swords: {\n    keywords: [ \"weapon\" ],\n    char: \"\\u2694\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shield: {\n    keywords: [ \"protection\", \"security\" ],\n    char: \"\\ud83d\\udee1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  smoking: {\n    keywords: [ \"kills\", \"tobacco\", \"cigarette\", \"joint\", \"smoke\" ],\n    char: \"\\ud83d\\udeac\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  skull_and_crossbones: {\n    keywords: [ \"poison\", \"danger\", \"deadly\", \"scary\", \"death\", \"pirate\", \"evil\" ],\n    char: \"\\u2620\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  coffin: {\n    keywords: [ \"vampire\", \"dead\", \"die\", \"death\", \"rip\", \"graveyard\", \"cemetery\", \"casket\", \"funeral\", \"box\" ],\n    char: \"\\u26b0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  funeral_urn: {\n    keywords: [ \"dead\", \"die\", \"death\", \"rip\", \"ashes\" ],\n    char: \"\\u26b1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  amphora: {\n    keywords: [ \"vase\", \"jar\" ],\n    char: \"\\ud83c\\udffa\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crystal_ball: {\n    keywords: [ \"disco\", \"party\", \"magic\", \"circus\", \"fortune_teller\" ],\n    char: \"\\ud83d\\udd2e\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  prayer_beads: {\n    keywords: [ \"dhikr\", \"religious\" ],\n    char: \"\\ud83d\\udcff\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  nazar_amulet: {\n    keywords: [ \"bead\", \"charm\" ],\n    char: \"\\ud83e\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  barber: {\n    keywords: [ \"hair\", \"salon\", \"style\" ],\n    char: \"\\ud83d\\udc88\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  alembic: {\n    keywords: [ \"distilling\", \"science\", \"experiment\", \"chemistry\" ],\n    char: \"\\u2697\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  telescope: {\n    keywords: [ \"stars\", \"space\", \"zoom\", \"science\", \"astronomy\" ],\n    char: \"\\ud83d\\udd2d\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  microscope: {\n    keywords: [ \"laboratory\", \"experiment\", \"zoomin\", \"science\", \"study\" ],\n    char: \"\\ud83d\\udd2c\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  hole: {\n    keywords: [ \"embarrassing\" ],\n    char: \"\\ud83d\\udd73\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pill: {\n    keywords: [ \"health\", \"medicine\", \"doctor\", \"pharmacy\", \"drug\" ],\n    char: \"\\ud83d\\udc8a\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  syringe: {\n    keywords: [ \"health\", \"hospital\", \"drugs\", \"blood\", \"medicine\", \"needle\", \"doctor\", \"nurse\" ],\n    char: \"\\ud83d\\udc89\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dna: {\n    keywords: [ \"biologist\", \"genetics\", \"life\" ],\n    char: \"\\ud83e\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  microbe: {\n    keywords: [ \"amoeba\", \"bacteria\", \"germs\" ],\n    char: \"\\ud83e\\udda0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  petri_dish: {\n    keywords: [ \"bacteria\", \"biology\", \"culture\", \"lab\" ],\n    char: \"\\ud83e\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  test_tube: {\n    keywords: [ \"chemistry\", \"experiment\", \"lab\", \"science\" ],\n    char: \"\\ud83e\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  thermometer: {\n    keywords: [ \"weather\", \"temperature\", \"hot\", \"cold\" ],\n    char: \"\\ud83c\\udf21\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  broom: {\n    keywords: [ \"cleaning\", \"sweeping\", \"witch\" ],\n    char: \"\\ud83e\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  basket: {\n    keywords: [ \"laundry\" ],\n    char: \"\\ud83e\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  toilet_paper: {\n    keywords: [ \"roll\" ],\n    char: \"\\ud83e\\uddfb\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  label: {\n    keywords: [ \"sale\", \"tag\" ],\n    char: \"\\ud83c\\udff7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bookmark: {\n    keywords: [ \"favorite\", \"label\", \"save\" ],\n    char: \"\\ud83d\\udd16\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  toilet: {\n    keywords: [ \"restroom\", \"wc\", \"washroom\", \"bathroom\", \"potty\" ],\n    char: \"\\ud83d\\udebd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shower: {\n    keywords: [ \"clean\", \"water\", \"bathroom\" ],\n    char: \"\\ud83d\\udebf\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bathtub: {\n    keywords: [ \"clean\", \"shower\", \"bathroom\" ],\n    char: \"\\ud83d\\udec1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  soap: {\n    keywords: [ \"bar\", \"bathing\", \"cleaning\", \"lather\" ],\n    char: \"\\ud83e\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  sponge: {\n    keywords: [ \"absorbing\", \"cleaning\", \"porous\" ],\n    char: \"\\ud83e\\uddfd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  lotion_bottle: {\n    keywords: [ \"moisturizer\", \"sunscreen\" ],\n    char: \"\\ud83e\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  key: {\n    keywords: [ \"lock\", \"door\", \"password\" ],\n    char: \"\\ud83d\\udd11\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  old_key: {\n    keywords: [ \"lock\", \"door\", \"password\" ],\n    char: \"\\ud83d\\udddd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  couch_and_lamp: {\n    keywords: [ \"read\", \"chill\" ],\n    char: \"\\ud83d\\udecb\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  sleeping_bed: {\n    keywords: [ \"bed\", \"rest\" ],\n    char: \"\\ud83d\\udecc\",\n    fitzpatrick_scale: true,\n    category: \"objects\"\n  },\n  bed: {\n    keywords: [ \"sleep\", \"rest\" ],\n    char: \"\\ud83d\\udecf\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  door: {\n    keywords: [ \"house\", \"entry\", \"exit\" ],\n    char: \"\\ud83d\\udeaa\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bellhop_bell: {\n    keywords: [ \"service\" ],\n    char: \"\\ud83d\\udece\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  teddy_bear: {\n    keywords: [ \"plush\", \"stuffed\" ],\n    char: \"\\ud83e\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  framed_picture: {\n    keywords: [ \"photography\" ],\n    char: \"\\ud83d\\uddbc\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  world_map: {\n    keywords: [ \"location\", \"direction\" ],\n    char: \"\\ud83d\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  parasol_on_ground: {\n    keywords: [ \"weather\", \"summer\" ],\n    char: \"\\u26f1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  moyai: {\n    keywords: [ \"rock\", \"easter island\", \"moai\" ],\n    char: \"\\ud83d\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shopping: {\n    keywords: [ \"mall\", \"buy\", \"purchase\" ],\n    char: \"\\ud83d\\udecd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  shopping_cart: {\n    keywords: [ \"trolley\" ],\n    char: \"\\ud83d\\uded2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  balloon: {\n    keywords: [ \"party\", \"celebration\", \"birthday\", \"circus\" ],\n    char: \"\\ud83c\\udf88\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  flags: {\n    keywords: [ \"fish\", \"japanese\", \"koinobori\", \"carp\", \"banner\" ],\n    char: \"\\ud83c\\udf8f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  ribbon: {\n    keywords: [ \"decoration\", \"pink\", \"girl\", \"bowtie\" ],\n    char: \"\\ud83c\\udf80\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  gift: {\n    keywords: [ \"present\", \"birthday\", \"christmas\", \"xmas\" ],\n    char: \"\\ud83c\\udf81\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  confetti_ball: {\n    keywords: [ \"festival\", \"party\", \"birthday\", \"circus\" ],\n    char: \"\\ud83c\\udf8a\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  tada: {\n    keywords: [ \"party\", \"congratulations\", \"birthday\", \"magic\", \"circus\", \"celebration\" ],\n    char: \"\\ud83c\\udf89\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  dolls: {\n    keywords: [ \"japanese\", \"toy\", \"kimono\" ],\n    char: \"\\ud83c\\udf8e\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  wind_chime: {\n    keywords: [ \"nature\", \"ding\", \"spring\", \"bell\" ],\n    char: \"\\ud83c\\udf90\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crossed_flags: {\n    keywords: [ \"japanese\", \"nation\", \"country\", \"border\" ],\n    char: \"\\ud83c\\udf8c\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  izakaya_lantern: {\n    keywords: [ \"light\", \"paper\", \"halloween\", \"spooky\" ],\n    char: \"\\ud83c\\udfee\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  red_envelope: {\n    keywords: [ \"gift\" ],\n    char: \"\\ud83e\\udde7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  email: {\n    keywords: [ \"letter\", \"postal\", \"inbox\", \"communication\" ],\n    char: \"\\u2709\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  envelope_with_arrow: {\n    keywords: [ \"email\", \"communication\" ],\n    char: \"\\ud83d\\udce9\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  incoming_envelope: {\n    keywords: [ \"email\", \"inbox\" ],\n    char: \"\\ud83d\\udce8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  \"e-mail\": {\n    keywords: [ \"communication\", \"inbox\" ],\n    char: \"\\ud83d\\udce7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  love_letter: {\n    keywords: [ \"email\", \"like\", \"affection\", \"envelope\", \"valentines\" ],\n    char: \"\\ud83d\\udc8c\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  postbox: {\n    keywords: [ \"email\", \"letter\", \"envelope\" ],\n    char: \"\\ud83d\\udcee\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox_closed: {\n    keywords: [ \"email\", \"communication\", \"inbox\" ],\n    char: \"\\ud83d\\udcea\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox: {\n    keywords: [ \"email\", \"inbox\", \"communication\" ],\n    char: \"\\ud83d\\udceb\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox_with_mail: {\n    keywords: [ \"email\", \"inbox\", \"communication\" ],\n    char: \"\\ud83d\\udcec\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mailbox_with_no_mail: {\n    keywords: [ \"email\", \"inbox\" ],\n    char: \"\\ud83d\\udced\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  package: {\n    keywords: [ \"mail\", \"gift\", \"cardboard\", \"box\", \"moving\" ],\n    char: \"\\ud83d\\udce6\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  postal_horn: {\n    keywords: [ \"instrument\", \"music\" ],\n    char: \"\\ud83d\\udcef\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  inbox_tray: {\n    keywords: [ \"email\", \"documents\" ],\n    char: \"\\ud83d\\udce5\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  outbox_tray: {\n    keywords: [ \"inbox\", \"email\" ],\n    char: \"\\ud83d\\udce4\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  scroll: {\n    keywords: [ \"documents\", \"ancient\", \"history\", \"paper\" ],\n    char: \"\\ud83d\\udcdc\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  page_with_curl: {\n    keywords: [ \"documents\", \"office\", \"paper\" ],\n    char: \"\\ud83d\\udcc3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bookmark_tabs: {\n    keywords: [ \"favorite\", \"save\", \"order\", \"tidy\" ],\n    char: \"\\ud83d\\udcd1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  receipt: {\n    keywords: [ \"accounting\", \"expenses\" ],\n    char: \"\\ud83e\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  bar_chart: {\n    keywords: [ \"graph\", \"presentation\", \"stats\" ],\n    char: \"\\ud83d\\udcca\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  chart_with_upwards_trend: {\n    keywords: [ \"graph\", \"presentation\", \"stats\", \"recovery\", \"business\", \"economics\", \"money\", \"sales\", \"good\", \"success\" ],\n    char: \"\\ud83d\\udcc8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  chart_with_downwards_trend: {\n    keywords: [ \"graph\", \"presentation\", \"stats\", \"recession\", \"business\", \"economics\", \"money\", \"sales\", \"bad\", \"failure\" ],\n    char: \"\\ud83d\\udcc9\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  page_facing_up: {\n    keywords: [ \"documents\", \"office\", \"paper\", \"information\" ],\n    char: \"\\ud83d\\udcc4\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  date: {\n    keywords: [ \"calendar\", \"schedule\" ],\n    char: \"\\ud83d\\udcc5\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  calendar: {\n    keywords: [ \"schedule\", \"date\", \"planning\" ],\n    char: \"\\ud83d\\udcc6\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  spiral_calendar: {\n    keywords: [ \"date\", \"schedule\", \"planning\" ],\n    char: \"\\ud83d\\uddd3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  card_index: {\n    keywords: [ \"business\", \"stationery\" ],\n    char: \"\\ud83d\\udcc7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  card_file_box: {\n    keywords: [ \"business\", \"stationery\" ],\n    char: \"\\ud83d\\uddc3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  ballot_box: {\n    keywords: [ \"election\", \"vote\" ],\n    char: \"\\ud83d\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  file_cabinet: {\n    keywords: [ \"filing\", \"organizing\" ],\n    char: \"\\ud83d\\uddc4\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  clipboard: {\n    keywords: [ \"stationery\", \"documents\" ],\n    char: \"\\ud83d\\udccb\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  spiral_notepad: {\n    keywords: [ \"memo\", \"stationery\" ],\n    char: \"\\ud83d\\uddd2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  file_folder: {\n    keywords: [ \"documents\", \"business\", \"office\" ],\n    char: \"\\ud83d\\udcc1\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  open_file_folder: {\n    keywords: [ \"documents\", \"load\" ],\n    char: \"\\ud83d\\udcc2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  card_index_dividers: {\n    keywords: [ \"organizing\", \"business\", \"stationery\" ],\n    char: \"\\ud83d\\uddc2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  newspaper_roll: {\n    keywords: [ \"press\", \"headline\" ],\n    char: \"\\ud83d\\uddde\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  newspaper: {\n    keywords: [ \"press\", \"headline\" ],\n    char: \"\\ud83d\\udcf0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  notebook: {\n    keywords: [ \"stationery\", \"record\", \"notes\", \"paper\", \"study\" ],\n    char: \"\\ud83d\\udcd3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  closed_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"textbook\", \"learn\" ],\n    char: \"\\ud83d\\udcd5\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  green_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"study\" ],\n    char: \"\\ud83d\\udcd7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  blue_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"learn\", \"study\" ],\n    char: \"\\ud83d\\udcd8\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  orange_book: {\n    keywords: [ \"read\", \"library\", \"knowledge\", \"textbook\", \"study\" ],\n    char: \"\\ud83d\\udcd9\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  notebook_with_decorative_cover: {\n    keywords: [ \"classroom\", \"notes\", \"record\", \"paper\", \"study\" ],\n    char: \"\\ud83d\\udcd4\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  ledger: {\n    keywords: [ \"notes\", \"paper\" ],\n    char: \"\\ud83d\\udcd2\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  books: {\n    keywords: [ \"literature\", \"library\", \"study\" ],\n    char: \"\\ud83d\\udcda\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  open_book: {\n    keywords: [ \"book\", \"read\", \"library\", \"knowledge\", \"literature\", \"learn\", \"study\" ],\n    char: \"\\ud83d\\udcd6\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  safety_pin: {\n    keywords: [ \"diaper\" ],\n    char: \"\\ud83e\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  link: {\n    keywords: [ \"rings\", \"url\" ],\n    char: \"\\ud83d\\udd17\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  paperclip: {\n    keywords: [ \"documents\", \"stationery\" ],\n    char: \"\\ud83d\\udcce\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  paperclips: {\n    keywords: [ \"documents\", \"stationery\" ],\n    char: \"\\ud83d\\udd87\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  scissors: {\n    keywords: [ \"stationery\", \"cut\" ],\n    char: \"\\u2702\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  triangular_ruler: {\n    keywords: [ \"stationery\", \"math\", \"architect\", \"sketch\" ],\n    char: \"\\ud83d\\udcd0\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  straight_ruler: {\n    keywords: [ \"stationery\", \"calculate\", \"length\", \"math\", \"school\", \"drawing\", \"architect\", \"sketch\" ],\n    char: \"\\ud83d\\udccf\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  abacus: {\n    keywords: [ \"calculation\" ],\n    char: \"\\ud83e\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pushpin: {\n    keywords: [ \"stationery\", \"mark\", \"here\" ],\n    char: \"\\ud83d\\udccc\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  round_pushpin: {\n    keywords: [ \"stationery\", \"location\", \"map\", \"here\" ],\n    char: \"\\ud83d\\udccd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  triangular_flag_on_post: {\n    keywords: [ \"mark\", \"milestone\", \"place\" ],\n    char: \"\\ud83d\\udea9\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  white_flag: {\n    keywords: [ \"losing\", \"loser\", \"lost\", \"surrender\", \"give up\", \"fail\" ],\n    char: \"\\ud83c\\udff3\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  black_flag: {\n    keywords: [ \"pirate\" ],\n    char: \"\\ud83c\\udff4\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  rainbow_flag: {\n    keywords: [ \"flag\", \"rainbow\", \"pride\", \"gay\", \"lgbt\", \"glbt\", \"queer\", \"homosexual\", \"lesbian\", \"bisexual\", \"transgender\" ],\n    char: \"\\ud83c\\udff3\\ufe0f\\u200d\\ud83c\\udf08\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  closed_lock_with_key: {\n    keywords: [ \"security\", \"privacy\" ],\n    char: \"\\ud83d\\udd10\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  lock: {\n    keywords: [ \"security\", \"password\", \"padlock\" ],\n    char: \"\\ud83d\\udd12\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  unlock: {\n    keywords: [ \"privacy\", \"security\" ],\n    char: \"\\ud83d\\udd13\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  lock_with_ink_pen: {\n    keywords: [ \"security\", \"secret\" ],\n    char: \"\\ud83d\\udd0f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pen: {\n    keywords: [ \"stationery\", \"writing\", \"write\" ],\n    char: \"\\ud83d\\udd8a\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  fountain_pen: {\n    keywords: [ \"stationery\", \"writing\", \"write\" ],\n    char: \"\\ud83d\\udd8b\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  black_nib: {\n    keywords: [ \"pen\", \"stationery\", \"writing\", \"write\" ],\n    char: \"\\u2712\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  memo: {\n    keywords: [ \"write\", \"documents\", \"stationery\", \"pencil\", \"paper\", \"writing\", \"legal\", \"exam\", \"quiz\", \"test\", \"study\", \"compose\" ],\n    char: \"\\ud83d\\udcdd\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  pencil2: {\n    keywords: [ \"stationery\", \"write\", \"paper\", \"writing\", \"school\", \"study\" ],\n    char: \"\\u270f\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  crayon: {\n    keywords: [ \"drawing\", \"creativity\" ],\n    char: \"\\ud83d\\udd8d\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  paintbrush: {\n    keywords: [ \"drawing\", \"creativity\", \"art\" ],\n    char: \"\\ud83d\\udd8c\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mag: {\n    keywords: [ \"search\", \"zoom\", \"find\", \"detective\" ],\n    char: \"\\ud83d\\udd0d\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  mag_right: {\n    keywords: [ \"search\", \"zoom\", \"find\", \"detective\" ],\n    char: \"\\ud83d\\udd0e\",\n    fitzpatrick_scale: false,\n    category: \"objects\"\n  },\n  heart: {\n    keywords: [ \"love\", \"like\", \"valentines\" ],\n    char: \"\\u2764\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  orange_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83e\\udde1\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  yellow_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc9b\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  green_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc9a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  blue_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc99\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  purple_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc9c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_heart: {\n    keywords: [ \"evil\" ],\n    char: \"\\ud83d\\udda4\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  broken_heart: {\n    keywords: [ \"sad\", \"sorry\", \"break\", \"heart\", \"heartbreak\" ],\n    char: \"\\ud83d\\udc94\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_heart_exclamation: {\n    keywords: [ \"decoration\", \"love\" ],\n    char: \"\\u2763\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  two_hearts: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\", \"heart\" ],\n    char: \"\\ud83d\\udc95\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  revolving_hearts: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc9e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heartbeat: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\", \"pink\", \"heart\" ],\n    char: \"\\ud83d\\udc93\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heartpulse: {\n    keywords: [ \"like\", \"love\", \"affection\", \"valentines\", \"pink\" ],\n    char: \"\\ud83d\\udc97\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sparkling_heart: {\n    keywords: [ \"love\", \"like\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc96\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cupid: {\n    keywords: [ \"love\", \"like\", \"heart\", \"affection\", \"valentines\" ],\n    char: \"\\ud83d\\udc98\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  gift_heart: {\n    keywords: [ \"love\", \"valentines\" ],\n    char: \"\\ud83d\\udc9d\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heart_decoration: {\n    keywords: [ \"purple-square\", \"love\", \"like\" ],\n    char: \"\\ud83d\\udc9f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  peace_symbol: {\n    keywords: [ \"hippie\" ],\n    char: \"\\u262e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  latin_cross: {\n    keywords: [ \"christianity\" ],\n    char: \"\\u271d\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  star_and_crescent: {\n    keywords: [ \"islam\" ],\n    char: \"\\u262a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  om: {\n    keywords: [ \"hinduism\", \"buddhism\", \"sikhism\", \"jainism\" ],\n    char: \"\\ud83d\\udd49\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wheel_of_dharma: {\n    keywords: [ \"hinduism\", \"buddhism\", \"sikhism\", \"jainism\" ],\n    char: \"\\u2638\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  star_of_david: {\n    keywords: [ \"judaism\" ],\n    char: \"\\u2721\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  six_pointed_star: {\n    keywords: [ \"purple-square\", \"religion\", \"jewish\", \"hexagram\" ],\n    char: \"\\ud83d\\udd2f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  menorah: {\n    keywords: [ \"hanukkah\", \"candles\", \"jewish\" ],\n    char: \"\\ud83d\\udd4e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  yin_yang: {\n    keywords: [ \"balance\" ],\n    char: \"\\u262f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  orthodox_cross: {\n    keywords: [ \"suppedaneum\", \"religion\" ],\n    char: \"\\u2626\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  place_of_worship: {\n    keywords: [ \"religion\", \"church\", \"temple\", \"prayer\" ],\n    char: \"\\ud83d\\uded0\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ophiuchus: {\n    keywords: [ \"sign\", \"purple-square\", \"constellation\", \"astrology\" ],\n    char: \"\\u26ce\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  aries: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: \"\\u2648\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  taurus: {\n    keywords: [ \"purple-square\", \"sign\", \"zodiac\", \"astrology\" ],\n    char: \"\\u2649\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  gemini: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: \"\\u264a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cancer: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: \"\\u264b\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  leo: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: \"\\u264c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  virgo: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: \"\\u264d\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  libra: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: \"\\u264e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  scorpius: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\", \"scorpio\" ],\n    char: \"\\u264f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sagittarius: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: \"\\u2650\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  capricorn: {\n    keywords: [ \"sign\", \"zodiac\", \"purple-square\", \"astrology\" ],\n    char: \"\\u2651\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  aquarius: {\n    keywords: [ \"sign\", \"purple-square\", \"zodiac\", \"astrology\" ],\n    char: \"\\u2652\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  pisces: {\n    keywords: [ \"purple-square\", \"sign\", \"zodiac\", \"astrology\" ],\n    char: \"\\u2653\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  id: {\n    keywords: [ \"purple-square\", \"words\" ],\n    char: \"\\ud83c\\udd94\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  atom_symbol: {\n    keywords: [ \"science\", \"physics\", \"chemistry\" ],\n    char: \"\\u269b\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7a7a: {\n    keywords: [ \"kanji\", \"japanese\", \"chinese\", \"empty\", \"sky\", \"blue-square\" ],\n    char: \"\\ud83c\\ude33\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u5272: {\n    keywords: [ \"cut\", \"divide\", \"chinese\", \"kanji\", \"pink-square\" ],\n    char: \"\\ud83c\\ude39\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  radioactive: {\n    keywords: [ \"nuclear\", \"danger\" ],\n    char: \"\\u2622\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  biohazard: {\n    keywords: [ \"danger\" ],\n    char: \"\\u2623\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mobile_phone_off: {\n    keywords: [ \"mute\", \"orange-square\", \"silence\", \"quiet\" ],\n    char: \"\\ud83d\\udcf4\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  vibration_mode: {\n    keywords: [ \"orange-square\", \"phone\" ],\n    char: \"\\ud83d\\udcf3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6709: {\n    keywords: [ \"orange-square\", \"chinese\", \"have\", \"kanji\" ],\n    char: \"\\ud83c\\ude36\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7121: {\n    keywords: [ \"nothing\", \"chinese\", \"kanji\", \"japanese\", \"orange-square\" ],\n    char: \"\\ud83c\\ude1a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7533: {\n    keywords: [ \"chinese\", \"japanese\", \"kanji\", \"orange-square\" ],\n    char: \"\\ud83c\\ude38\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u55b6: {\n    keywords: [ \"japanese\", \"opening hours\", \"orange-square\" ],\n    char: \"\\ud83c\\ude3a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6708: {\n    keywords: [ \"chinese\", \"month\", \"moon\", \"japanese\", \"orange-square\", \"kanji\" ],\n    char: \"\\ud83c\\ude37\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eight_pointed_black_star: {\n    keywords: [ \"orange-square\", \"shape\", \"polygon\" ],\n    char: \"\\u2734\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  vs: {\n    keywords: [ \"words\", \"orange-square\" ],\n    char: \"\\ud83c\\udd9a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  accept: {\n    keywords: [ \"ok\", \"good\", \"chinese\", \"kanji\", \"agree\", \"yes\", \"orange-circle\" ],\n    char: \"\\ud83c\\ude51\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_flower: {\n    keywords: [ \"japanese\", \"spring\" ],\n    char: \"\\ud83d\\udcae\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ideograph_advantage: {\n    keywords: [ \"chinese\", \"kanji\", \"obtain\", \"get\", \"circle\" ],\n    char: \"\\ud83c\\ude50\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  secret: {\n    keywords: [ \"privacy\", \"chinese\", \"sshh\", \"kanji\", \"red-circle\" ],\n    char: \"\\u3299\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  congratulations: {\n    keywords: [ \"chinese\", \"kanji\", \"japanese\", \"red-circle\" ],\n    char: \"\\u3297\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u5408: {\n    keywords: [ \"japanese\", \"chinese\", \"join\", \"kanji\", \"red-square\" ],\n    char: \"\\ud83c\\ude34\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6e80: {\n    keywords: [ \"full\", \"chinese\", \"japanese\", \"red-square\", \"kanji\" ],\n    char: \"\\ud83c\\ude35\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u7981: {\n    keywords: [ \"kanji\", \"japanese\", \"chinese\", \"forbidden\", \"limit\", \"restricted\", \"red-square\" ],\n    char: \"\\ud83c\\ude32\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  a: {\n    keywords: [ \"red-square\", \"alphabet\", \"letter\" ],\n    char: \"\\ud83c\\udd70\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  b: {\n    keywords: [ \"red-square\", \"alphabet\", \"letter\" ],\n    char: \"\\ud83c\\udd71\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ab: {\n    keywords: [ \"red-square\", \"alphabet\" ],\n    char: \"\\ud83c\\udd8e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cl: {\n    keywords: [ \"alphabet\", \"words\", \"red-square\" ],\n    char: \"\\ud83c\\udd91\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  o2: {\n    keywords: [ \"alphabet\", \"red-square\", \"letter\" ],\n    char: \"\\ud83c\\udd7e\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sos: {\n    keywords: [ \"help\", \"red-square\", \"words\", \"emergency\", \"911\" ],\n    char: \"\\ud83c\\udd98\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_entry: {\n    keywords: [ \"limit\", \"security\", \"privacy\", \"bad\", \"denied\", \"stop\", \"circle\" ],\n    char: \"\\u26d4\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  name_badge: {\n    keywords: [ \"fire\", \"forbid\" ],\n    char: \"\\ud83d\\udcdb\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_entry_sign: {\n    keywords: [ \"forbid\", \"stop\", \"limit\", \"denied\", \"disallow\", \"circle\" ],\n    char: \"\\ud83d\\udeab\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  x: {\n    keywords: [ \"no\", \"delete\", \"remove\", \"cancel\", \"red\" ],\n    char: \"\\u274c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  o: {\n    keywords: [ \"circle\", \"round\" ],\n    char: \"\\u2b55\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  stop_sign: {\n    keywords: [ \"stop\" ],\n    char: \"\\ud83d\\uded1\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  anger: {\n    keywords: [ \"angry\", \"mad\" ],\n    char: \"\\ud83d\\udca2\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  hotsprings: {\n    keywords: [ \"bath\", \"warm\", \"relax\" ],\n    char: \"\\u2668\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_pedestrians: {\n    keywords: [ \"rules\", \"crossing\", \"walking\", \"circle\" ],\n    char: \"\\ud83d\\udeb7\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  do_not_litter: {\n    keywords: [ \"trash\", \"bin\", \"garbage\", \"circle\" ],\n    char: \"\\ud83d\\udeaf\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_bicycles: {\n    keywords: [ \"cyclist\", \"prohibited\", \"circle\" ],\n    char: \"\\ud83d\\udeb3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  \"non-potable_water\": {\n    keywords: [ \"drink\", \"faucet\", \"tap\", \"circle\" ],\n    char: \"\\ud83d\\udeb1\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  underage: {\n    keywords: [ \"18\", \"drink\", \"pub\", \"night\", \"minor\", \"circle\" ],\n    char: \"\\ud83d\\udd1e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_mobile_phones: {\n    keywords: [ \"iphone\", \"mute\", \"circle\" ],\n    char: \"\\ud83d\\udcf5\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  exclamation: {\n    keywords: [ \"heavy_exclamation_mark\", \"danger\", \"surprise\", \"punctuation\", \"wow\", \"warning\" ],\n    char: \"\\u2757\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  grey_exclamation: {\n    keywords: [ \"surprise\", \"punctuation\", \"gray\", \"wow\", \"warning\" ],\n    char: \"\\u2755\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  question: {\n    keywords: [ \"doubt\", \"confused\" ],\n    char: \"\\u2753\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  grey_question: {\n    keywords: [ \"doubts\", \"gray\", \"huh\", \"confused\" ],\n    char: \"\\u2754\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  bangbang: {\n    keywords: [ \"exclamation\", \"surprise\" ],\n    char: \"\\u203c\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  interrobang: {\n    keywords: [ \"wat\", \"punctuation\", \"surprise\" ],\n    char: \"\\u2049\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  100: {\n    keywords: [ \"score\", \"perfect\", \"numbers\", \"century\", \"exam\", \"quiz\", \"test\", \"pass\", \"hundred\" ],\n    char: \"\\ud83d\\udcaf\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  low_brightness: {\n    keywords: [ \"sun\", \"afternoon\", \"warm\", \"summer\" ],\n    char: \"\\ud83d\\udd05\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  high_brightness: {\n    keywords: [ \"sun\", \"light\" ],\n    char: \"\\ud83d\\udd06\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  trident: {\n    keywords: [ \"weapon\", \"spear\" ],\n    char: \"\\ud83d\\udd31\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  fleur_de_lis: {\n    keywords: [ \"decorative\", \"scout\" ],\n    char: \"\\u269c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  part_alternation_mark: {\n    keywords: [ \"graph\", \"presentation\", \"stats\", \"business\", \"economics\", \"bad\" ],\n    char: \"\\u303d\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  warning: {\n    keywords: [ \"exclamation\", \"wip\", \"alert\", \"error\", \"problem\", \"issue\" ],\n    char: \"\\u26a0\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  children_crossing: {\n    keywords: [ \"school\", \"warning\", \"danger\", \"sign\", \"driving\", \"yellow-diamond\" ],\n    char: \"\\ud83d\\udeb8\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  beginner: {\n    keywords: [ \"badge\", \"shield\" ],\n    char: \"\\ud83d\\udd30\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  recycle: {\n    keywords: [ \"arrow\", \"environment\", \"garbage\", \"trash\" ],\n    char: \"\\u267b\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  u6307: {\n    keywords: [ \"chinese\", \"point\", \"green-square\", \"kanji\" ],\n    char: \"\\ud83c\\ude2f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  chart: {\n    keywords: [ \"green-square\", \"graph\", \"presentation\", \"stats\" ],\n    char: \"\\ud83d\\udcb9\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sparkle: {\n    keywords: [ \"stars\", \"green-square\", \"awesome\", \"good\", \"fireworks\" ],\n    char: \"\\u2747\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eight_spoked_asterisk: {\n    keywords: [ \"star\", \"sparkle\", \"green-square\" ],\n    char: \"\\u2733\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  negative_squared_cross_mark: {\n    keywords: [ \"x\", \"green-square\", \"no\", \"deny\" ],\n    char: \"\\u274e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_check_mark: {\n    keywords: [ \"green-square\", \"ok\", \"agree\", \"vote\", \"election\", \"answer\", \"tick\" ],\n    char: \"\\u2705\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  diamond_shape_with_a_dot_inside: {\n    keywords: [ \"jewel\", \"blue\", \"gem\", \"crystal\", \"fancy\" ],\n    char: \"\\ud83d\\udca0\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cyclone: {\n    keywords: [ \"weather\", \"swirl\", \"blue\", \"cloud\", \"vortex\", \"spiral\", \"whirlpool\", \"spin\", \"tornado\", \"hurricane\", \"typhoon\" ],\n    char: \"\\ud83c\\udf00\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  loop: {\n    keywords: [ \"tape\", \"cassette\" ],\n    char: \"\\u27bf\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  globe_with_meridians: {\n    keywords: [ \"earth\", \"international\", \"world\", \"internet\", \"interweb\", \"i18n\" ],\n    char: \"\\ud83c\\udf10\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  m: {\n    keywords: [ \"alphabet\", \"blue-circle\", \"letter\" ],\n    char: \"\\u24c2\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  atm: {\n    keywords: [ \"money\", \"sales\", \"cash\", \"blue-square\", \"payment\", \"bank\" ],\n    char: \"\\ud83c\\udfe7\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sa: {\n    keywords: [ \"japanese\", \"blue-square\", \"katakana\" ],\n    char: \"\\ud83c\\ude02\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  passport_control: {\n    keywords: [ \"custom\", \"blue-square\" ],\n    char: \"\\ud83d\\udec2\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  customs: {\n    keywords: [ \"passport\", \"border\", \"blue-square\" ],\n    char: \"\\ud83d\\udec3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  baggage_claim: {\n    keywords: [ \"blue-square\", \"airport\", \"transport\" ],\n    char: \"\\ud83d\\udec4\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  left_luggage: {\n    keywords: [ \"blue-square\", \"travel\" ],\n    char: \"\\ud83d\\udec5\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wheelchair: {\n    keywords: [ \"blue-square\", \"disabled\", \"a11y\", \"accessibility\" ],\n    char: \"\\u267f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_smoking: {\n    keywords: [ \"cigarette\", \"blue-square\", \"smell\", \"smoke\" ],\n    char: \"\\ud83d\\udead\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wc: {\n    keywords: [ \"toilet\", \"restroom\", \"blue-square\" ],\n    char: \"\\ud83d\\udebe\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  parking: {\n    keywords: [ \"cars\", \"blue-square\", \"alphabet\", \"letter\" ],\n    char: \"\\ud83c\\udd7f\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  potable_water: {\n    keywords: [ \"blue-square\", \"liquid\", \"restroom\", \"cleaning\", \"faucet\" ],\n    char: \"\\ud83d\\udeb0\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mens: {\n    keywords: [ \"toilet\", \"restroom\", \"wc\", \"blue-square\", \"gender\", \"male\" ],\n    char: \"\\ud83d\\udeb9\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  womens: {\n    keywords: [ \"purple-square\", \"woman\", \"female\", \"toilet\", \"loo\", \"restroom\", \"gender\" ],\n    char: \"\\ud83d\\udeba\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  baby_symbol: {\n    keywords: [ \"orange-square\", \"child\" ],\n    char: \"\\ud83d\\udebc\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  restroom: {\n    keywords: [ \"blue-square\", \"toilet\", \"refresh\", \"wc\", \"gender\" ],\n    char: \"\\ud83d\\udebb\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  put_litter_in_its_place: {\n    keywords: [ \"blue-square\", \"sign\", \"human\", \"info\" ],\n    char: \"\\ud83d\\udeae\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cinema: {\n    keywords: [ \"blue-square\", \"record\", \"film\", \"movie\", \"curtain\", \"stage\", \"theater\" ],\n    char: \"\\ud83c\\udfa6\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  signal_strength: {\n    keywords: [ \"blue-square\", \"reception\", \"phone\", \"internet\", \"connection\", \"wifi\", \"bluetooth\", \"bars\" ],\n    char: \"\\ud83d\\udcf6\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  koko: {\n    keywords: [ \"blue-square\", \"here\", \"katakana\", \"japanese\", \"destination\" ],\n    char: \"\\ud83c\\ude01\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ng: {\n    keywords: [ \"blue-square\", \"words\", \"shape\", \"icon\" ],\n    char: \"\\ud83c\\udd96\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ok: {\n    keywords: [ \"good\", \"agree\", \"yes\", \"blue-square\" ],\n    char: \"\\ud83c\\udd97\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  up: {\n    keywords: [ \"blue-square\", \"above\", \"high\" ],\n    char: \"\\ud83c\\udd99\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  cool: {\n    keywords: [ \"words\", \"blue-square\" ],\n    char: \"\\ud83c\\udd92\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  new: {\n    keywords: [ \"blue-square\", \"words\", \"start\" ],\n    char: \"\\ud83c\\udd95\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  free: {\n    keywords: [ \"blue-square\", \"words\" ],\n    char: \"\\ud83c\\udd93\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  zero: {\n    keywords: [ \"0\", \"numbers\", \"blue-square\", \"null\" ],\n    char: \"0\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  one: {\n    keywords: [ \"blue-square\", \"numbers\", \"1\" ],\n    char: \"1\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  two: {\n    keywords: [ \"numbers\", \"2\", \"prime\", \"blue-square\" ],\n    char: \"2\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  three: {\n    keywords: [ \"3\", \"numbers\", \"prime\", \"blue-square\" ],\n    char: \"3\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  four: {\n    keywords: [ \"4\", \"numbers\", \"blue-square\" ],\n    char: \"4\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  five: {\n    keywords: [ \"5\", \"numbers\", \"blue-square\", \"prime\" ],\n    char: \"5\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  six: {\n    keywords: [ \"6\", \"numbers\", \"blue-square\" ],\n    char: \"6\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  seven: {\n    keywords: [ \"7\", \"numbers\", \"blue-square\", \"prime\" ],\n    char: \"7\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eight: {\n    keywords: [ \"8\", \"blue-square\", \"numbers\" ],\n    char: \"8\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  nine: {\n    keywords: [ \"blue-square\", \"numbers\", \"9\" ],\n    char: \"9\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  keycap_ten: {\n    keywords: [ \"numbers\", \"10\", \"blue-square\" ],\n    char: \"\\ud83d\\udd1f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  asterisk: {\n    keywords: [ \"star\", \"keycap\" ],\n    char: \"*\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  1234: {\n    keywords: [ \"numbers\", \"blue-square\" ],\n    char: \"\\ud83d\\udd22\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  eject_button: {\n    keywords: [ \"blue-square\" ],\n    char: \"\\u23cf\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_forward: {\n    keywords: [ \"blue-square\", \"right\", \"direction\", \"play\" ],\n    char: \"\\u25b6\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  pause_button: {\n    keywords: [ \"pause\", \"blue-square\" ],\n    char: \"\\u23f8\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  next_track_button: {\n    keywords: [ \"forward\", \"next\", \"blue-square\" ],\n    char: \"\\u23ed\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  stop_button: {\n    keywords: [ \"blue-square\" ],\n    char: \"\\u23f9\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  record_button: {\n    keywords: [ \"blue-square\" ],\n    char: \"\\u23fa\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  play_or_pause_button: {\n    keywords: [ \"blue-square\", \"play\", \"pause\" ],\n    char: \"\\u23ef\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  previous_track_button: {\n    keywords: [ \"backward\" ],\n    char: \"\\u23ee\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  fast_forward: {\n    keywords: [ \"blue-square\", \"play\", \"speed\", \"continue\" ],\n    char: \"\\u23e9\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  rewind: {\n    keywords: [ \"play\", \"blue-square\" ],\n    char: \"\\u23ea\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  twisted_rightwards_arrows: {\n    keywords: [ \"blue-square\", \"shuffle\", \"music\", \"random\" ],\n    char: \"\\ud83d\\udd00\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  repeat: {\n    keywords: [ \"loop\", \"record\" ],\n    char: \"\\ud83d\\udd01\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  repeat_one: {\n    keywords: [ \"blue-square\", \"loop\" ],\n    char: \"\\ud83d\\udd02\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_backward: {\n    keywords: [ \"blue-square\", \"left\", \"direction\" ],\n    char: \"\\u25c0\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_up_small: {\n    keywords: [ \"blue-square\", \"triangle\", \"direction\", \"point\", \"forward\", \"top\" ],\n    char: \"\\ud83d\\udd3c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_down_small: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: \"\\ud83d\\udd3d\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_double_up: {\n    keywords: [ \"blue-square\", \"direction\", \"top\" ],\n    char: \"\\u23eb\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_double_down: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: \"\\u23ec\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_right: {\n    keywords: [ \"blue-square\", \"next\" ],\n    char: \"\\u27a1\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_left: {\n    keywords: [ \"blue-square\", \"previous\", \"back\" ],\n    char: \"\\u2b05\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_up: {\n    keywords: [ \"blue-square\", \"continue\", \"top\", \"direction\" ],\n    char: \"\\u2b06\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_down: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: \"\\u2b07\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_upper_right: {\n    keywords: [ \"blue-square\", \"point\", \"direction\", \"diagonal\", \"northeast\" ],\n    char: \"\\u2197\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_lower_right: {\n    keywords: [ \"blue-square\", \"direction\", \"diagonal\", \"southeast\" ],\n    char: \"\\u2198\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_lower_left: {\n    keywords: [ \"blue-square\", \"direction\", \"diagonal\", \"southwest\" ],\n    char: \"\\u2199\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_upper_left: {\n    keywords: [ \"blue-square\", \"point\", \"direction\", \"diagonal\", \"northwest\" ],\n    char: \"\\u2196\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_up_down: {\n    keywords: [ \"blue-square\", \"direction\", \"way\", \"vertical\" ],\n    char: \"\\u2195\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  left_right_arrow: {\n    keywords: [ \"shape\", \"direction\", \"horizontal\", \"sideways\" ],\n    char: \"\\u2194\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrows_counterclockwise: {\n    keywords: [ \"blue-square\", \"sync\", \"cycle\" ],\n    char: \"\\ud83d\\udd04\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_right_hook: {\n    keywords: [ \"blue-square\", \"return\", \"rotate\", \"direction\" ],\n    char: \"\\u21aa\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  leftwards_arrow_with_hook: {\n    keywords: [ \"back\", \"return\", \"blue-square\", \"undo\", \"enter\" ],\n    char: \"\\u21a9\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_heading_up: {\n    keywords: [ \"blue-square\", \"direction\", \"top\" ],\n    char: \"\\u2934\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrow_heading_down: {\n    keywords: [ \"blue-square\", \"direction\", \"bottom\" ],\n    char: \"\\u2935\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  hash: {\n    keywords: [ \"symbol\", \"blue-square\", \"twitter\" ],\n    char: \"#\\ufe0f\\u20e3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  information_source: {\n    keywords: [ \"blue-square\", \"alphabet\", \"letter\" ],\n    char: \"\\u2139\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  abc: {\n    keywords: [ \"blue-square\", \"alphabet\" ],\n    char: \"\\ud83d\\udd24\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  abcd: {\n    keywords: [ \"blue-square\", \"alphabet\" ],\n    char: \"\\ud83d\\udd21\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  capital_abcd: {\n    keywords: [ \"alphabet\", \"words\", \"blue-square\" ],\n    char: \"\\ud83d\\udd20\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  symbols: {\n    keywords: [ \"blue-square\", \"music\", \"note\", \"ampersand\", \"percent\", \"glyphs\", \"characters\" ],\n    char: \"\\ud83d\\udd23\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  musical_note: {\n    keywords: [ \"score\", \"tone\", \"sound\" ],\n    char: \"\\ud83c\\udfb5\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  notes: {\n    keywords: [ \"music\", \"score\" ],\n    char: \"\\ud83c\\udfb6\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  wavy_dash: {\n    keywords: [ \"draw\", \"line\", \"moustache\", \"mustache\", \"squiggle\", \"scribble\" ],\n    char: \"\\u3030\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  curly_loop: {\n    keywords: [ \"scribble\", \"draw\", \"shape\", \"squiggle\" ],\n    char: \"\\u27b0\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_check_mark: {\n    keywords: [ \"ok\", \"nike\", \"answer\", \"yes\", \"tick\" ],\n    char: \"\\u2714\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  arrows_clockwise: {\n    keywords: [ \"sync\", \"cycle\", \"round\", \"repeat\" ],\n    char: \"\\ud83d\\udd03\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_plus_sign: {\n    keywords: [ \"math\", \"calculation\", \"addition\", \"more\", \"increase\" ],\n    char: \"\\u2795\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_minus_sign: {\n    keywords: [ \"math\", \"calculation\", \"subtract\", \"less\" ],\n    char: \"\\u2796\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_division_sign: {\n    keywords: [ \"divide\", \"math\", \"calculation\" ],\n    char: \"\\u2797\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_multiplication_x: {\n    keywords: [ \"math\", \"calculation\" ],\n    char: \"\\u2716\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  infinity: {\n    keywords: [ \"forever\" ],\n    char: \"\\u267e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  heavy_dollar_sign: {\n    keywords: [ \"money\", \"sales\", \"payment\", \"currency\", \"buck\" ],\n    char: \"\\ud83d\\udcb2\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  currency_exchange: {\n    keywords: [ \"money\", \"sales\", \"dollar\", \"travel\" ],\n    char: \"\\ud83d\\udcb1\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  copyright: {\n    keywords: [ \"ip\", \"license\", \"circle\", \"law\", \"legal\" ],\n    char: \"\\xa9\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  registered: {\n    keywords: [ \"alphabet\", \"circle\" ],\n    char: \"\\xae\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  tm: {\n    keywords: [ \"trademark\", \"brand\", \"law\", \"legal\" ],\n    char: \"\\u2122\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  end: {\n    keywords: [ \"words\", \"arrow\" ],\n    char: \"\\ud83d\\udd1a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  back: {\n    keywords: [ \"arrow\", \"words\", \"return\" ],\n    char: \"\\ud83d\\udd19\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  on: {\n    keywords: [ \"arrow\", \"words\" ],\n    char: \"\\ud83d\\udd1b\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  top: {\n    keywords: [ \"words\", \"blue-square\" ],\n    char: \"\\ud83d\\udd1d\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  soon: {\n    keywords: [ \"arrow\", \"words\" ],\n    char: \"\\ud83d\\udd1c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  ballot_box_with_check: {\n    keywords: [ \"ok\", \"agree\", \"confirm\", \"black-square\", \"vote\", \"election\", \"yes\", \"tick\" ],\n    char: \"\\u2611\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  radio_button: {\n    keywords: [ \"input\", \"old\", \"music\", \"circle\" ],\n    char: \"\\ud83d\\udd18\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_circle: {\n    keywords: [ \"shape\", \"round\" ],\n    char: \"\\u26aa\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_circle: {\n    keywords: [ \"shape\", \"button\", \"round\" ],\n    char: \"\\u26ab\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  red_circle: {\n    keywords: [ \"shape\", \"error\", \"danger\" ],\n    char: \"\\ud83d\\udd34\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  large_blue_circle: {\n    keywords: [ \"shape\", \"icon\", \"button\" ],\n    char: \"\\ud83d\\udd35\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_orange_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: \"\\ud83d\\udd38\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_blue_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: \"\\ud83d\\udd39\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  large_orange_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: \"\\ud83d\\udd36\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  large_blue_diamond: {\n    keywords: [ \"shape\", \"jewel\", \"gem\" ],\n    char: \"\\ud83d\\udd37\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_red_triangle: {\n    keywords: [ \"shape\", \"direction\", \"up\", \"top\" ],\n    char: \"\\ud83d\\udd3a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_small_square: {\n    keywords: [ \"shape\", \"icon\" ],\n    char: \"\\u25aa\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_small_square: {\n    keywords: [ \"shape\", \"icon\" ],\n    char: \"\\u25ab\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_large_square: {\n    keywords: [ \"shape\", \"icon\", \"button\" ],\n    char: \"\\u2b1b\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_large_square: {\n    keywords: [ \"shape\", \"icon\", \"stone\", \"button\" ],\n    char: \"\\u2b1c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  small_red_triangle_down: {\n    keywords: [ \"shape\", \"direction\", \"bottom\" ],\n    char: \"\\ud83d\\udd3b\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_medium_square: {\n    keywords: [ \"shape\", \"button\", \"icon\" ],\n    char: \"\\u25fc\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_medium_square: {\n    keywords: [ \"shape\", \"stone\", \"icon\" ],\n    char: \"\\u25fb\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_medium_small_square: {\n    keywords: [ \"icon\", \"shape\", \"button\" ],\n    char: \"\\u25fe\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_medium_small_square: {\n    keywords: [ \"shape\", \"stone\", \"icon\", \"button\" ],\n    char: \"\\u25fd\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_square_button: {\n    keywords: [ \"shape\", \"input\", \"frame\" ],\n    char: \"\\ud83d\\udd32\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  white_square_button: {\n    keywords: [ \"shape\", \"input\" ],\n    char: \"\\ud83d\\udd33\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  speaker: {\n    keywords: [ \"sound\", \"volume\", \"silence\", \"broadcast\" ],\n    char: \"\\ud83d\\udd08\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  sound: {\n    keywords: [ \"volume\", \"speaker\", \"broadcast\" ],\n    char: \"\\ud83d\\udd09\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  loud_sound: {\n    keywords: [ \"volume\", \"noise\", \"noisy\", \"speaker\", \"broadcast\" ],\n    char: \"\\ud83d\\udd0a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mute: {\n    keywords: [ \"sound\", \"volume\", \"silence\", \"quiet\" ],\n    char: \"\\ud83d\\udd07\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mega: {\n    keywords: [ \"sound\", \"speaker\", \"volume\" ],\n    char: \"\\ud83d\\udce3\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  loudspeaker: {\n    keywords: [ \"volume\", \"sound\" ],\n    char: \"\\ud83d\\udce2\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  bell: {\n    keywords: [ \"sound\", \"notification\", \"christmas\", \"xmas\", \"chime\" ],\n    char: \"\\ud83d\\udd14\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  no_bell: {\n    keywords: [ \"sound\", \"volume\", \"mute\", \"quiet\", \"silent\" ],\n    char: \"\\ud83d\\udd15\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  black_joker: {\n    keywords: [ \"poker\", \"cards\", \"game\", \"play\", \"magic\" ],\n    char: \"\\ud83c\\udccf\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  mahjong: {\n    keywords: [ \"game\", \"play\", \"chinese\", \"kanji\" ],\n    char: \"\\ud83c\\udc04\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  spades: {\n    keywords: [ \"poker\", \"cards\", \"suits\", \"magic\" ],\n    char: \"\\u2660\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clubs: {\n    keywords: [ \"poker\", \"cards\", \"magic\", \"suits\" ],\n    char: \"\\u2663\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  hearts: {\n    keywords: [ \"poker\", \"cards\", \"magic\", \"suits\" ],\n    char: \"\\u2665\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  diamonds: {\n    keywords: [ \"poker\", \"cards\", \"magic\", \"suits\" ],\n    char: \"\\u2666\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  flower_playing_cards: {\n    keywords: [ \"game\", \"sunset\", \"red\" ],\n    char: \"\\ud83c\\udfb4\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  thought_balloon: {\n    keywords: [ \"bubble\", \"cloud\", \"speech\", \"thinking\", \"dream\" ],\n    char: \"\\ud83d\\udcad\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  right_anger_bubble: {\n    keywords: [ \"caption\", \"speech\", \"thinking\", \"mad\" ],\n    char: \"\\ud83d\\uddef\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  speech_balloon: {\n    keywords: [ \"bubble\", \"words\", \"message\", \"talk\", \"chatting\" ],\n    char: \"\\ud83d\\udcac\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  left_speech_bubble: {\n    keywords: [ \"words\", \"message\", \"talk\", \"chatting\" ],\n    char: \"\\ud83d\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd50\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock2: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd51\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock3: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd52\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock4: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd53\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock5: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd54\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock6: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\", \"dawn\", \"dusk\" ],\n    char: \"\\ud83d\\udd55\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock7: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd56\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock8: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd57\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock9: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd58\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock10: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd59\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock11: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd5a\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock12: {\n    keywords: [ \"time\", \"noon\", \"midnight\", \"midday\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd5b\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock130: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd5c\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock230: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd5d\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock330: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd5e\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock430: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd5f\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock530: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd60\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock630: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd61\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock730: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd62\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock830: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd63\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock930: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd64\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1030: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd65\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1130: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd66\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  clock1230: {\n    keywords: [ \"time\", \"late\", \"early\", \"schedule\" ],\n    char: \"\\ud83d\\udd67\",\n    fitzpatrick_scale: false,\n    category: \"symbols\"\n  },\n  afghanistan: {\n    keywords: [ \"af\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  aland_islands: {\n    keywords: [ \"\\xc5land\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddfd\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  albania: {\n    keywords: [ \"al\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  algeria: {\n    keywords: [ \"dz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde9\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  american_samoa: {\n    keywords: [ \"american\", \"ws\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  andorra: {\n    keywords: [ \"ad\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  angola: {\n    keywords: [ \"ao\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  anguilla: {\n    keywords: [ \"ai\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  antarctica: {\n    keywords: [ \"aq\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddf6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  antigua_barbuda: {\n    keywords: [ \"antigua\", \"barbuda\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  argentina: {\n    keywords: [ \"ar\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  armenia: {\n    keywords: [ \"am\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  aruba: {\n    keywords: [ \"aw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  australia: {\n    keywords: [ \"au\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  austria: {\n    keywords: [ \"at\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  azerbaijan: {\n    keywords: [ \"az\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bahamas: {\n    keywords: [ \"bs\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bahrain: {\n    keywords: [ \"bh\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bangladesh: {\n    keywords: [ \"bd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  barbados: {\n    keywords: [ \"bb\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\udde7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  belarus: {\n    keywords: [ \"by\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  belgium: {\n    keywords: [ \"be\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  belize: {\n    keywords: [ \"bz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  benin: {\n    keywords: [ \"bj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddef\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bermuda: {\n    keywords: [ \"bm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bhutan: {\n    keywords: [ \"bt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bolivia: {\n    keywords: [ \"bo\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  caribbean_netherlands: {\n    keywords: [ \"bonaire\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bosnia_herzegovina: {\n    keywords: [ \"bosnia\", \"herzegovina\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  botswana: {\n    keywords: [ \"bw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  brazil: {\n    keywords: [ \"br\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  british_indian_ocean_territory: {\n    keywords: [ \"british\", \"indian\", \"ocean\", \"territory\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  british_virgin_islands: {\n    keywords: [ \"british\", \"virgin\", \"islands\", \"bvi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfb\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  brunei: {\n    keywords: [ \"bn\", \"darussalam\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  bulgaria: {\n    keywords: [ \"bg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  burkina_faso: {\n    keywords: [ \"burkina\", \"faso\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  burundi: {\n    keywords: [ \"bi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cape_verde: {\n    keywords: [ \"cabo\", \"verde\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddfb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cambodia: {\n    keywords: [ \"kh\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cameroon: {\n    keywords: [ \"cm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  canada: {\n    keywords: [ \"ca\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  canary_islands: {\n    keywords: [ \"canary\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cayman_islands: {\n    keywords: [ \"cayman\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  central_african_republic: {\n    keywords: [ \"central\", \"african\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  chad: {\n    keywords: [ \"td\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  chile: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cn: {\n    keywords: [ \"china\", \"chinese\", \"prc\", \"flag\", \"country\", \"nation\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  christmas_island: {\n    keywords: [ \"christmas\", \"island\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddfd\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cocos_islands: {\n    keywords: [ \"cocos\", \"keeling\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  colombia: {\n    keywords: [ \"co\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  comoros: {\n    keywords: [ \"km\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  congo_brazzaville: {\n    keywords: [ \"congo\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  congo_kinshasa: {\n    keywords: [ \"congo\", \"democratic\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cook_islands: {\n    keywords: [ \"cook\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  costa_rica: {\n    keywords: [ \"costa\", \"rica\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  croatia: {\n    keywords: [ \"hr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udded\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cuba: {\n    keywords: [ \"cu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  curacao: {\n    keywords: [ \"cura\\xe7ao\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cyprus: {\n    keywords: [ \"cy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  czech_republic: {\n    keywords: [ \"cz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  denmark: {\n    keywords: [ \"dk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde9\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  djibouti: {\n    keywords: [ \"dj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde9\\ud83c\\uddef\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  dominica: {\n    keywords: [ \"dm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde9\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  dominican_republic: {\n    keywords: [ \"dominican\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde9\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ecuador: {\n    keywords: [ \"ec\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  egypt: {\n    keywords: [ \"eg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  el_salvador: {\n    keywords: [ \"el\", \"salvador\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddfb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  equatorial_guinea: {\n    keywords: [ \"equatorial\", \"gn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  eritrea: {\n    keywords: [ \"er\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  estonia: {\n    keywords: [ \"ee\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ethiopia: {\n    keywords: [ \"et\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  eu: {\n    keywords: [ \"european\", \"union\", \"flag\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  falkland_islands: {\n    keywords: [ \"falkland\", \"islands\", \"malvinas\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddeb\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  faroe_islands: {\n    keywords: [ \"faroe\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddeb\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  fiji: {\n    keywords: [ \"fj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddeb\\ud83c\\uddef\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  finland: {\n    keywords: [ \"fi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddeb\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  fr: {\n    keywords: [ \"banner\", \"flag\", \"nation\", \"france\", \"french\", \"country\" ],\n    char: \"\\ud83c\\uddeb\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  french_guiana: {\n    keywords: [ \"french\", \"guiana\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  french_polynesia: {\n    keywords: [ \"french\", \"polynesia\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  french_southern_territories: {\n    keywords: [ \"french\", \"southern\", \"territories\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  gabon: {\n    keywords: [ \"ga\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  gambia: {\n    keywords: [ \"gm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  georgia: {\n    keywords: [ \"ge\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  de: {\n    keywords: [ \"german\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde9\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ghana: {\n    keywords: [ \"gh\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  gibraltar: {\n    keywords: [ \"gi\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  greece: {\n    keywords: [ \"gr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  greenland: {\n    keywords: [ \"gl\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  grenada: {\n    keywords: [ \"gd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guadeloupe: {\n    keywords: [ \"gp\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf5\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guam: {\n    keywords: [ \"gu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guatemala: {\n    keywords: [ \"gt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guernsey: {\n    keywords: [ \"gg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guinea: {\n    keywords: [ \"gn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guinea_bissau: {\n    keywords: [ \"gw\", \"bissau\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  guyana: {\n    keywords: [ \"gy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  haiti: {\n    keywords: [ \"ht\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udded\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  honduras: {\n    keywords: [ \"hn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udded\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  hong_kong: {\n    keywords: [ \"hong\", \"kong\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udded\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  hungary: {\n    keywords: [ \"hu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udded\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  iceland: {\n    keywords: [ \"is\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  india: {\n    keywords: [ \"in\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  indonesia: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  iran: {\n    keywords: [ \"iran,\", \"islamic\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  iraq: {\n    keywords: [ \"iq\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ireland: {\n    keywords: [ \"ie\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  isle_of_man: {\n    keywords: [ \"isle\", \"man\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  israel: {\n    keywords: [ \"il\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  it: {\n    keywords: [ \"italy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddee\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  cote_divoire: {\n    keywords: [ \"ivory\", \"coast\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jamaica: {\n    keywords: [ \"jm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddef\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jp: {\n    keywords: [ \"japanese\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddef\\ud83c\\uddf5\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jersey: {\n    keywords: [ \"je\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddef\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  jordan: {\n    keywords: [ \"jo\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddef\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kazakhstan: {\n    keywords: [ \"kz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kenya: {\n    keywords: [ \"ke\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kiribati: {\n    keywords: [ \"ki\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kosovo: {\n    keywords: [ \"xk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfd\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kuwait: {\n    keywords: [ \"kw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kyrgyzstan: {\n    keywords: [ \"kg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  laos: {\n    keywords: [ \"lao\", \"democratic\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  latvia: {\n    keywords: [ \"lv\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddfb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  lebanon: {\n    keywords: [ \"lb\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\udde7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  lesotho: {\n    keywords: [ \"ls\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  liberia: {\n    keywords: [ \"lr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  libya: {\n    keywords: [ \"ly\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  liechtenstein: {\n    keywords: [ \"li\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  lithuania: {\n    keywords: [ \"lt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  luxembourg: {\n    keywords: [ \"lu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  macau: {\n    keywords: [ \"macao\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  macedonia: {\n    keywords: [ \"macedonia,\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  madagascar: {\n    keywords: [ \"mg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  malawi: {\n    keywords: [ \"mw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  malaysia: {\n    keywords: [ \"my\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  maldives: {\n    keywords: [ \"mv\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddfb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mali: {\n    keywords: [ \"ml\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  malta: {\n    keywords: [ \"mt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  marshall_islands: {\n    keywords: [ \"marshall\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  martinique: {\n    keywords: [ \"mq\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mauritania: {\n    keywords: [ \"mr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mauritius: {\n    keywords: [ \"mu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mayotte: {\n    keywords: [ \"yt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfe\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mexico: {\n    keywords: [ \"mx\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddfd\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  micronesia: {\n    keywords: [ \"micronesia,\", \"federated\", \"states\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddeb\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  moldova: {\n    keywords: [ \"moldova,\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  monaco: {\n    keywords: [ \"mc\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mongolia: {\n    keywords: [ \"mn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  montenegro: {\n    keywords: [ \"me\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  montserrat: {\n    keywords: [ \"ms\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  morocco: {\n    keywords: [ \"ma\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  mozambique: {\n    keywords: [ \"mz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  myanmar: {\n    keywords: [ \"mm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  namibia: {\n    keywords: [ \"na\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nauru: {\n    keywords: [ \"nr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nepal: {\n    keywords: [ \"np\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddf5\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  netherlands: {\n    keywords: [ \"nl\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  new_caledonia: {\n    keywords: [ \"new\", \"caledonia\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  new_zealand: {\n    keywords: [ \"new\", \"zealand\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nicaragua: {\n    keywords: [ \"ni\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  niger: {\n    keywords: [ \"ne\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  nigeria: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  niue: {\n    keywords: [ \"nu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  norfolk_island: {\n    keywords: [ \"norfolk\", \"island\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  northern_mariana_islands: {\n    keywords: [ \"northern\", \"mariana\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf2\\ud83c\\uddf5\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  north_korea: {\n    keywords: [ \"north\", \"korea\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddf5\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  norway: {\n    keywords: [ \"no\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf3\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  oman: {\n    keywords: [ \"om_symbol\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf4\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  pakistan: {\n    keywords: [ \"pk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  palau: {\n    keywords: [ \"pw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  palestinian_territories: {\n    keywords: [ \"palestine\", \"palestinian\", \"territories\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  panama: {\n    keywords: [ \"pa\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  papua_new_guinea: {\n    keywords: [ \"papua\", \"new\", \"guinea\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  paraguay: {\n    keywords: [ \"py\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  peru: {\n    keywords: [ \"pe\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  philippines: {\n    keywords: [ \"ph\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  pitcairn_islands: {\n    keywords: [ \"pitcairn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  poland: {\n    keywords: [ \"pl\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  portugal: {\n    keywords: [ \"pt\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  puerto_rico: {\n    keywords: [ \"puerto\", \"rico\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  qatar: {\n    keywords: [ \"qa\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf6\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  reunion: {\n    keywords: [ \"r\\xe9union\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf7\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  romania: {\n    keywords: [ \"ro\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf7\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ru: {\n    keywords: [ \"russian\", \"federation\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf7\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  rwanda: {\n    keywords: [ \"rw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf7\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_barthelemy: {\n    keywords: [ \"saint\", \"barth\\xe9lemy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde7\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_helena: {\n    keywords: [ \"saint\", \"helena\", \"ascension\", \"tristan\", \"cunha\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_kitts_nevis: {\n    keywords: [ \"saint\", \"kitts\", \"nevis\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_lucia: {\n    keywords: [ \"saint\", \"lucia\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_pierre_miquelon: {\n    keywords: [ \"saint\", \"pierre\", \"miquelon\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf5\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  st_vincent_grenadines: {\n    keywords: [ \"saint\", \"vincent\", \"grenadines\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfb\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  samoa: {\n    keywords: [ \"ws\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfc\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  san_marino: {\n    keywords: [ \"san\", \"marino\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sao_tome_principe: {\n    keywords: [ \"sao\", \"tome\", \"principe\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  saudi_arabia: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  senegal: {\n    keywords: [ \"sn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  serbia: {\n    keywords: [ \"rs\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf7\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  seychelles: {\n    keywords: [ \"sc\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sierra_leone: {\n    keywords: [ \"sierra\", \"leone\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  singapore: {\n    keywords: [ \"sg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sint_maarten: {\n    keywords: [ \"sint\", \"maarten\", \"dutch\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddfd\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  slovakia: {\n    keywords: [ \"sk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  slovenia: {\n    keywords: [ \"si\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  solomon_islands: {\n    keywords: [ \"solomon\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\udde7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  somalia: {\n    keywords: [ \"so\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  south_africa: {\n    keywords: [ \"south\", \"africa\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddff\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  south_georgia_south_sandwich_islands: {\n    keywords: [ \"south\", \"georgia\", \"sandwich\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  kr: {\n    keywords: [ \"south\", \"korea\", \"nation\", \"flag\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf0\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  south_sudan: {\n    keywords: [ \"south\", \"sd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  es: {\n    keywords: [ \"spain\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sri_lanka: {\n    keywords: [ \"sri\", \"lanka\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf1\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sudan: {\n    keywords: [ \"sd\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\udde9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  suriname: {\n    keywords: [ \"sr\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  swaziland: {\n    keywords: [ \"sz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  sweden: {\n    keywords: [ \"se\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  switzerland: {\n    keywords: [ \"ch\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde8\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  syria: {\n    keywords: [ \"syrian\", \"arab\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf8\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  taiwan: {\n    keywords: [ \"tw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tajikistan: {\n    keywords: [ \"tj\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddef\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tanzania: {\n    keywords: [ \"tanzania,\", \"united\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  thailand: {\n    keywords: [ \"th\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  timor_leste: {\n    keywords: [ \"timor\", \"leste\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddf1\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  togo: {\n    keywords: [ \"tg\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tokelau: {\n    keywords: [ \"tk\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddf0\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tonga: {\n    keywords: [ \"to\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddf4\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  trinidad_tobago: {\n    keywords: [ \"trinidad\", \"tobago\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddf9\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tunisia: {\n    keywords: [ \"tn\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tr: {\n    keywords: [ \"turkey\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddf7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  turkmenistan: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  turks_caicos_islands: {\n    keywords: [ \"turks\", \"caicos\", \"islands\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\udde8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  tuvalu: {\n    keywords: [ \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddf9\\ud83c\\uddfb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uganda: {\n    keywords: [ \"ug\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfa\\ud83c\\uddec\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  ukraine: {\n    keywords: [ \"ua\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfa\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  united_arab_emirates: {\n    keywords: [ \"united\", \"arab\", \"emirates\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\udde6\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uk: {\n    keywords: [ \"united\", \"kingdom\", \"great\", \"britain\", \"northern\", \"ireland\", \"flag\", \"nation\", \"country\", \"banner\", \"british\", \"UK\", \"english\", \"england\", \"union jack\" ],\n    char: \"\\ud83c\\uddec\\ud83c\\udde7\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  england: {\n    keywords: [ \"flag\", \"english\" ],\n    char: \"\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40\\udc65\\udb40\\udc6e\\udb40\\udc67\\udb40\\udc7f\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  scotland: {\n    keywords: [ \"flag\", \"scottish\" ],\n    char: \"\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40\\udc73\\udb40\\udc63\\udb40\\udc74\\udb40\\udc7f\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  wales: {\n    keywords: [ \"flag\", \"welsh\" ],\n    char: \"\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40\\udc77\\udb40\\udc6c\\udb40\\udc73\\udb40\\udc7f\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  us: {\n    keywords: [ \"united\", \"states\", \"america\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfa\\ud83c\\uddf8\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  us_virgin_islands: {\n    keywords: [ \"virgin\", \"islands\", \"us\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfb\\ud83c\\uddee\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uruguay: {\n    keywords: [ \"uy\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfa\\ud83c\\uddfe\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  uzbekistan: {\n    keywords: [ \"uz\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfa\\ud83c\\uddff\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  vanuatu: {\n    keywords: [ \"vu\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfb\\ud83c\\uddfa\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  vatican_city: {\n    keywords: [ \"vatican\", \"city\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfb\\ud83c\\udde6\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  venezuela: {\n    keywords: [ \"ve\", \"bolivarian\", \"republic\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfb\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  vietnam: {\n    keywords: [ \"viet\", \"nam\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfb\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  wallis_futuna: {\n    keywords: [ \"wallis\", \"futuna\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfc\\ud83c\\uddeb\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  western_sahara: {\n    keywords: [ \"western\", \"sahara\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddea\\ud83c\\udded\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  yemen: {\n    keywords: [ \"ye\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddfe\\ud83c\\uddea\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  zambia: {\n    keywords: [ \"zm\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddff\\ud83c\\uddf2\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  zimbabwe: {\n    keywords: [ \"zw\", \"flag\", \"nation\", \"country\", \"banner\" ],\n    char: \"\\ud83c\\uddff\\ud83c\\uddfc\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  united_nations: {\n    keywords: [ \"un\", \"flag\", \"banner\" ],\n    char: \"\\ud83c\\uddfa\\ud83c\\uddf3\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  },\n  pirate_flag: {\n    keywords: [ \"skull\", \"crossbones\", \"flag\", \"banner\" ],\n    char: \"\\ud83c\\udff4\\u200d\\u2620\\ufe0f\",\n    fitzpatrick_scale: false,\n    category: \"flags\"\n  }\n});"
  },
  {
    "path": "app/src/helpers/vendor/tinymce/tinymce.d.ts",
    "content": "interface StringPathBookmark {\n    start: string;\n    end?: string;\n}\ninterface RangeBookmark {\n    rng: Range;\n}\ninterface IdBookmark {\n    id: string;\n    keep?: boolean;\n}\ninterface IndexBookmark {\n    name: string;\n    index: number;\n}\ninterface PathBookmark {\n    start: number[];\n    end?: number[];\n    isFakeCaret?: boolean;\n}\ndeclare type Bookmark = StringPathBookmark | RangeBookmark | IdBookmark | IndexBookmark | PathBookmark;\ndeclare type NormalizedEvent<E, T = any> = E & {\n    readonly type: string;\n    readonly target: T;\n    readonly isDefaultPrevented: () => boolean;\n    readonly preventDefault: () => void;\n    readonly isPropagationStopped: () => boolean;\n    readonly stopPropagation: () => void;\n    readonly isImmediatePropagationStopped: () => boolean;\n    readonly stopImmediatePropagation: () => void;\n};\ndeclare type MappedEvent<T, K extends string> = K extends keyof T ? T[K] : any;\ninterface NativeEventMap {\n    'beforepaste': Event;\n    'blur': FocusEvent;\n    'beforeinput': InputEvent;\n    'click': MouseEvent;\n    'compositionend': Event;\n    'compositionstart': Event;\n    'compositionupdate': Event;\n    'contextmenu': PointerEvent;\n    'copy': ClipboardEvent;\n    'cut': ClipboardEvent;\n    'dblclick': MouseEvent;\n    'drag': DragEvent;\n    'dragdrop': DragEvent;\n    'dragend': DragEvent;\n    'draggesture': DragEvent;\n    'dragover': DragEvent;\n    'dragstart': DragEvent;\n    'drop': DragEvent;\n    'focus': FocusEvent;\n    'focusin': FocusEvent;\n    'focusout': FocusEvent;\n    'input': InputEvent;\n    'keydown': KeyboardEvent;\n    'keypress': KeyboardEvent;\n    'keyup': KeyboardEvent;\n    'mousedown': MouseEvent;\n    'mouseenter': MouseEvent;\n    'mouseleave': MouseEvent;\n    'mousemove': MouseEvent;\n    'mouseout': MouseEvent;\n    'mouseover': MouseEvent;\n    'mouseup': MouseEvent;\n    'paste': ClipboardEvent;\n    'selectionchange': Event;\n    'submit': Event;\n    'touchend': TouchEvent;\n    'touchmove': TouchEvent;\n    'touchstart': TouchEvent;\n    'touchcancel': TouchEvent;\n    'wheel': WheelEvent;\n}\ndeclare type EditorEvent<T> = NormalizedEvent<T>;\ninterface EventDispatcherSettings {\n    scope?: any;\n    toggleEvent?: (name: string, state: boolean) => void | boolean;\n    beforeFire?: <T>(args: EditorEvent<T>) => void;\n}\ninterface EventDispatcherConstructor<T extends NativeEventMap> {\n    readonly prototype: EventDispatcher<T>;\n    new (settings?: EventDispatcherSettings): EventDispatcher<T>;\n    isNative: (name: string) => boolean;\n}\ndeclare class EventDispatcher<T> {\n    static isNative(name: string): boolean;\n    private readonly settings;\n    private readonly scope;\n    private readonly toggleEvent;\n    private bindings;\n    constructor(settings?: Record<string, any>);\n    fire<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U): EditorEvent<U>;\n    on<K extends string>(name: K, callback: false | ((event: EditorEvent<MappedEvent<T, K>>) => void), prepend?: boolean, extra?: {}): this;\n    off<K extends string>(name?: K, callback?: (event: EditorEvent<MappedEvent<T, K>>) => void): this;\n    once<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void, prepend?: boolean): this;\n    has(name: string): boolean;\n}\ndeclare const enum UndoLevelType {\n    Fragmented = \"fragmented\",\n    Complete = \"complete\"\n}\ninterface UndoLevel {\n    type: UndoLevelType;\n    fragments: string[];\n    content: string;\n    bookmark: Bookmark;\n    beforeBookmark: Bookmark;\n}\ninterface UndoManager {\n    data: UndoLevel[];\n    typing: boolean;\n    add: (level?: UndoLevel, event?: EditorEvent<any>) => UndoLevel;\n    beforeChange: () => void;\n    undo: () => UndoLevel;\n    redo: () => UndoLevel;\n    clear: () => void;\n    reset: () => void;\n    hasUndo: () => boolean;\n    hasRedo: () => boolean;\n    transact: (callback: () => void) => UndoLevel;\n    ignore: (callback: () => void) => void;\n    extra: (callback1: () => void, callback2: () => void) => void;\n}\ndeclare type ArrayCallback$1<T, R> = (x: T, i: number, xs: ReadonlyArray<T>) => R;\ndeclare type ObjCallback$1<T, R> = (value: T, key: string, obj: Record<string, T>) => R;\ndeclare type ArrayCallback<T, R> = ArrayCallback$1<T, R>;\ndeclare type ObjCallback<T, R> = ObjCallback$1<T, R>;\ninterface Tools {\n    is: (obj: any, type: string) => boolean;\n    isArray: <T>(arr: any) => arr is Array<T>;\n    inArray: <T>(arr: ArrayLike<T>, value: T) => number;\n    grep: {\n        <T>(arr: ArrayLike<T> | null | undefined, pred?: ArrayCallback<T, boolean>): T[];\n        <T>(arr: Record<string, T> | null | undefined, pred?: ObjCallback<T, boolean>): T[];\n    };\n    trim: (str: string) => string;\n    toArray: <T>(obj: ArrayLike<T>) => T[];\n    hasOwn: (obj: any, name: string) => boolean;\n    makeMap: <T>(items: ArrayLike<T> | string, delim?: string | RegExp, map?: Record<string, T | string>) => Record<string, T | string>;\n    each: {\n        <T>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, void | boolean>, scope?: any): boolean;\n        <T>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, void | boolean>, scope?: any): boolean;\n    };\n    map: {\n        <T, R>(arr: ArrayLike<T> | null | undefined, cb: ArrayCallback<T, R>): R[];\n        <T, R>(obj: Record<string, T> | null | undefined, cb: ObjCallback<T, R>): R[];\n    };\n    extend: (obj: Object, ext: Object, ...objs: Object[]) => any;\n    create: (name: string, p: Object, root?: Object) => void;\n    walk: <T = any>(obj: T, f: Function, n?: keyof T, scope?: any) => void;\n    createNS: (name: string, o?: Object) => any;\n    resolve: (path: string, o?: Object) => any;\n    explode: (s: string, d?: string | RegExp) => string[];\n    _addCacheSuffix: (url: string) => string;\n}\ndeclare type EventUtilsCallback<T> = (event: EventUtilsEvent<T>) => void;\ndeclare type EventUtilsEvent<T> = NormalizedEvent<T> & {\n    metaKey: boolean;\n};\ninterface EventUtilsConstructor {\n    readonly prototype: EventUtils;\n    new (): EventUtils;\n    Event: EventUtils;\n}\ndeclare class EventUtils {\n    static Event: EventUtils;\n    domLoaded: boolean;\n    events: Record<string, any>;\n    private readonly expando;\n    private hasFocusIn;\n    private hasMouseEnterLeave;\n    private mouseEnterLeave;\n    private count;\n    constructor();\n    bind<K extends keyof HTMLElementEventMap>(target: any, name: K, callback: EventUtilsCallback<HTMLElementEventMap[K]>, scope?: any): EventUtilsCallback<HTMLElementEventMap[K]>;\n    bind<T = any>(target: any, names: string, callback: EventUtilsCallback<T>, scope?: any): EventUtilsCallback<T>;\n    unbind<K extends keyof HTMLElementEventMap>(target: any, name: K, callback?: EventUtilsCallback<HTMLElementEventMap[K]>): this;\n    unbind<T = any>(target: any, names: string, callback?: EventUtilsCallback<T>): this;\n    unbind(target: any): this;\n    fire(target: any, name: string, args?: {}): this;\n    clean(target: any): this;\n    destroy(): void;\n    cancel<T = any>(e: EventUtilsEvent<T>): boolean;\n    private executeHandlers;\n}\ndeclare type DomQuerySelector<T extends Node> = string | T | T[] | DomQuery<T>;\ndeclare type DomQueryInitSelector<T extends Node> = DomQuerySelector<T> | Window;\ninterface Hook {\n    get: <T extends Node>(elm: T) => string;\n    set: <T extends Node>($elm: DomQuery<T>, value: string | null) => void;\n}\ninterface DomQueryConstructor {\n    readonly prototype: DomQuery;\n    attrHooks: Record<string, Hook>;\n    cssHooks: Record<string, Hook>;\n    fn: DomQuery;\n    find: any;\n    expr: {\n        cacheLength: number;\n        createPseudo: Function;\n        match: Record<string, RegExp>;\n        attrHandle: {};\n        find: Record<string, Function>;\n        relative: Record<string, {\n            dir: string;\n            first?: boolean;\n        }>;\n        preFilter: Record<string, Function>;\n        filter: Record<string, Function>;\n        pseudos: Record<string, Function>;\n    };\n    extend: Tools['extend'];\n    isArray: Tools['isArray'];\n    new <T extends Node = Node>(selector?: DomQueryInitSelector<T>, context?: Node): DomQuery<T>;\n    <T extends Node = Node>(selector?: DomQueryInitSelector<T>, context?: Node): DomQuery<T>;\n    overrideDefaults(callback: () => {\n        context: Node;\n        element: Element;\n    }): DomQueryConstructor;\n    makeArray<T>(object: T): T[];\n    inArray<T>(item: {}, array: T[]): number;\n    each<T>(obj: T[], callback: (i: number, value: T) => void): void;\n    each<T>(obj: T, callback: (key: string, obj: T[keyof T]) => void): void;\n    trim(str: string): string;\n    grep<T>(array: T[], callback: (item: any, i: number) => boolean): T[];\n    unique<T>(results: T[]): T[];\n    text(elem: Node): string;\n    contains(context: any, elem: Node): boolean;\n    filter(expr: string, elems: Node[], not?: boolean): any;\n}\ninterface DomQuery<T extends Node = Node> extends ArrayLike<T> {\n    init: (selector?: DomQueryInitSelector<T>, context?: Node) => void;\n    context: T;\n    length: number;\n    selector: string;\n    add(items: Array<string | T> | DomQuery<T>, sort?: boolean): this;\n    addClass(className: string): this;\n    after(content: DomQuerySelector<T>): this;\n    append(content: DomQuerySelector<T>): this;\n    appendTo(val: DomQuerySelector<T>): this;\n    attr(name: string, value: string | boolean | number | null): this;\n    attr(attrs: Record<string, string | boolean | number | null>): this;\n    attr(name: string): string;\n    before(content: DomQuerySelector<T>): this;\n    children(selector?: string): DomQuery<Node & ChildNode>;\n    clone(): this;\n    closest(selector: DomQuerySelector<T>): this;\n    contents(selector?: string): DomQuery<Node & ChildNode>;\n    css(name: string, value: string | number | null): this;\n    css(styles: Record<string, string | number | null>): this;\n    css(name: string): string;\n    each(callback: (i: number, value: T) => void): this;\n    empty(): this;\n    eq(index: number): this;\n    filter(selector: string | ((i: number, item: any) => boolean)): this;\n    find<K extends keyof HTMLElementTagNameMap>(selector: K): DomQuery<HTMLElementTagNameMap[K]>;\n    find<T extends Node>(selector: string): DomQuery<T>;\n    first(): this;\n    hasClass(className: string): boolean;\n    hide(): this;\n    html(value: string): this;\n    html(): string;\n    is(selector: string | ((i: number, item: any) => boolean)): boolean;\n    last(): this;\n    next(selector?: string): DomQuery<Node & ChildNode>;\n    nextUntil(selector: DomQuerySelector<T>, until?: string): DomQuery<Node & ChildNode>;\n    off<K extends keyof HTMLElementEventMap>(name: K, callback?: EventUtilsCallback<HTMLElementEventMap[K]>): this;\n    off<U>(name?: string, callback?: EventUtilsCallback<U>): this;\n    offset(offset?: {}): {} | this;\n    on<K extends keyof HTMLElementEventMap>(name: K, callback: EventUtilsCallback<HTMLElementEventMap[K]>): this;\n    on<U>(name: string, callback: EventUtilsCallback<U>): this;\n    parent(selector?: string): DomQuery<Node>;\n    parents(selector?: string): DomQuery<Node>;\n    parentsUntil(selector: DomQuerySelector<T>, filter?: string): DomQuery<Node>;\n    prepend(content: DomQuerySelector<T>): this;\n    prependTo(val: DomQuerySelector<T>): this;\n    prev(selector?: string): DomQuery<Node & ChildNode>;\n    prevUntil(selector: DomQuerySelector<T>, filter?: string): DomQuery<Node & ChildNode>;\n    prop(name: string, value: string): this;\n    prop(props: Record<string, string | number>): this;\n    prop(name: string): string;\n    push(...items: T[]): number;\n    remove(): this;\n    removeAttr(name: string): this;\n    removeClass(className: string): this;\n    replaceWith(content: DomQuerySelector<T>): this;\n    show(): this;\n    slice(start: number, end?: number): this;\n    splice(start: number, deleteCount?: number): T[];\n    sort(compareFn?: (a: T, b: T) => number): T[];\n    text(value: string): DomQuery;\n    text(): string;\n    toArray(): T[];\n    toggleClass(className: string, state?: boolean): this;\n    trigger(name: string | {\n        type: string;\n    }): this;\n    unwrap(): this;\n    wrap(content: DomQuerySelector<T>): this;\n    wrapAll(content: DomQuerySelector<T>): this;\n    wrapInner(content: string): this;\n}\ndeclare type SchemaType = 'html4' | 'html5' | 'html5-strict';\ninterface SchemaSettings {\n    block_elements?: string;\n    boolean_attributes?: string;\n    custom_elements?: string;\n    extended_valid_elements?: string;\n    invalid_elements?: string;\n    invalid_styles?: string | Record<string, string>;\n    move_caret_before_on_enter_elements?: string;\n    non_empty_elements?: string;\n    schema?: SchemaType;\n    self_closing_elements?: string;\n    short_ended_elements?: string;\n    special?: string;\n    text_block_elements?: string;\n    text_inline_elements?: string;\n    valid_children?: string;\n    valid_classes?: string | Record<string, string>;\n    valid_elements?: string;\n    valid_styles?: string | Record<string, string>;\n    verify_html?: boolean;\n    whitespace_elements?: string;\n}\ninterface Attribute {\n    required?: boolean;\n    defaultValue?: string;\n    forcedValue?: string;\n    validValues?: any;\n}\ninterface DefaultAttribute {\n    name: string;\n    value: string;\n}\ninterface AttributePattern {\n    defaultValue?: string;\n    forcedValue?: string;\n    pattern: RegExp;\n    required?: boolean;\n    validValues?: Record<string, string>;\n}\ninterface ElementRule {\n    attributes: Record<string, Attribute>;\n    attributesDefault?: DefaultAttribute[];\n    attributesForced?: DefaultAttribute[];\n    attributesOrder: string[];\n    attributePatterns?: AttributePattern[];\n    attributesRequired?: string[];\n    paddEmpty?: boolean;\n    removeEmpty?: boolean;\n    removeEmptyAttrs?: boolean;\n}\ninterface SchemaElement extends ElementRule {\n    outputName?: string;\n    parentsRequired?: string[];\n    pattern?: RegExp;\n}\ninterface SchemaMap {\n    [name: string]: {};\n}\ninterface SchemaRegExpMap {\n    [name: string]: RegExp;\n}\ninterface Schema {\n    children: Record<string, SchemaMap>;\n    elements: Record<string, SchemaElement>;\n    getValidStyles: () => Record<string, string[]> | undefined;\n    getValidClasses: () => Record<string, SchemaMap> | undefined;\n    getBlockElements: () => SchemaMap;\n    getInvalidStyles: () => Record<string, SchemaMap> | undefined;\n    getShortEndedElements: () => SchemaMap;\n    getTextBlockElements: () => SchemaMap;\n    getTextInlineElements: () => SchemaMap;\n    getBoolAttrs: () => SchemaMap;\n    getElementRule: (name: string) => SchemaElement | undefined;\n    getSelfClosingElements: () => SchemaMap;\n    getNonEmptyElements: () => SchemaMap;\n    getMoveCaretBeforeOnEnterElements: () => SchemaMap;\n    getWhiteSpaceElements: () => SchemaMap;\n    getSpecialElements: () => SchemaRegExpMap;\n    isValidChild: (name: string, child: string) => boolean;\n    isValid: (name: string, attr?: string) => boolean;\n    getCustomElements: () => SchemaMap;\n    addValidElements: (validElements: string) => void;\n    setValidElements: (validElements: string) => void;\n    addCustomElements: (customElements: string) => void;\n    addValidChildren: (validChildren: any) => void;\n}\ndeclare type Attributes$1 = Array<{\n    name: string;\n    value: string;\n}> & {\n    map: Record<string, string>;\n};\ninterface AstNodeConstructor {\n    readonly prototype: AstNode;\n    new (name: string, type: number): AstNode;\n    create(name: string, attrs?: Record<string, string>): AstNode;\n}\ndeclare class AstNode {\n    static create(name: string, attrs?: Record<string, string>): AstNode;\n    name: string;\n    type: number;\n    attributes?: Attributes$1;\n    value?: string;\n    shortEnded?: boolean;\n    parent?: AstNode;\n    firstChild?: AstNode;\n    lastChild?: AstNode;\n    next?: AstNode;\n    prev?: AstNode;\n    raw?: boolean;\n    fixed?: boolean;\n    constructor(name: string, type: number);\n    replace(node: AstNode): AstNode;\n    attr(name: string, value: string | null): AstNode | undefined;\n    attr(name: Record<string, string | null>): AstNode | undefined;\n    attr(name: string): string | undefined;\n    clone(): AstNode;\n    wrap(wrapper: AstNode): AstNode;\n    unwrap(): void;\n    remove(): AstNode;\n    append(node: AstNode): AstNode;\n    insert(node: AstNode, refNode: AstNode, before?: boolean): AstNode;\n    getAll(name: string): AstNode[];\n    children(): AstNode[];\n    empty(): AstNode;\n    isEmpty(elements: SchemaMap, whitespace?: SchemaMap, predicate?: (node: AstNode) => boolean): boolean;\n    walk(prev?: boolean): AstNode;\n}\ndeclare type Content = string | AstNode;\ndeclare type ContentFormat = 'raw' | 'text' | 'html' | 'tree';\ninterface GetContentArgs {\n    format?: ContentFormat;\n    get?: boolean;\n    content?: string;\n    getInner?: boolean;\n    no_events?: boolean;\n    [key: string]: any;\n}\ninterface SetContentArgs {\n    format?: string;\n    set?: boolean;\n    content?: string;\n    no_events?: boolean;\n    no_selection?: boolean;\n}\ninterface BlobInfoData {\n    id?: string;\n    name?: string;\n    filename?: string;\n    blob: Blob;\n    base64: string;\n    blobUri?: string;\n    uri?: string;\n}\ninterface BlobInfo {\n    id: () => string;\n    name: () => string;\n    filename: () => string;\n    blob: () => Blob;\n    base64: () => string;\n    blobUri: () => string;\n    uri: () => string | undefined;\n}\ninterface BlobCache {\n    create: (o: string | BlobInfoData, blob?: Blob, base64?: string, name?: string, filename?: string) => BlobInfo;\n    add: (blobInfo: BlobInfo) => void;\n    get: (id: string) => BlobInfo | undefined;\n    getByUri: (blobUri: string) => BlobInfo | undefined;\n    getByData: (base64: string, type: string) => BlobInfo | undefined;\n    findFirst: (predicate: (blobInfo: BlobInfo) => boolean) => BlobInfo | undefined;\n    removeByUri: (blobUri: string) => void;\n    destroy: () => void;\n}\ninterface NotificationManagerImpl {\n    open: (spec: NotificationSpec, closeCallback?: () => void) => NotificationApi;\n    close: <T extends NotificationApi>(notification: T) => void;\n    reposition: <T extends NotificationApi>(notifications: T[]) => void;\n    getArgs: <T extends NotificationApi>(notification: T) => NotificationSpec;\n}\ninterface NotificationSpec {\n    type?: 'info' | 'warning' | 'error' | 'success';\n    text: string;\n    icon?: string;\n    progressBar?: boolean;\n    timeout?: number;\n    closeButton?: boolean;\n}\ninterface NotificationApi {\n    close: () => void;\n    progressBar: {\n        value: (percent: number) => void;\n    };\n    text: (text: string) => void;\n    moveTo: (x: number, y: number) => void;\n    moveRel: (element: Element, rel: 'tc-tc' | 'bc-bc' | 'bc-tc' | 'tc-bc' | 'banner') => void;\n    getEl: () => HTMLElement;\n    settings: NotificationSpec;\n}\ninterface NotificationManager {\n    open: (spec: NotificationSpec) => NotificationApi;\n    close: () => void;\n    getNotifications: () => NotificationApi[];\n}\ninterface UploadFailureOptions {\n    remove?: boolean;\n}\ndeclare type UploadHandler = (blobInfo: BlobInfo, success: (url: string) => void, failure: (err: string, options?: UploadFailureOptions) => void, progress?: (percent: number) => void) => void;\ninterface UploadResult$2 {\n    url: string;\n    blobInfo: BlobInfo;\n    status: boolean;\n    error?: {\n        options: UploadFailureOptions;\n        message: string;\n    };\n}\ninterface RangeLikeObject {\n    startContainer: Node;\n    startOffset: number;\n    endContainer: Node;\n    endOffset: number;\n}\ndeclare type ApplyFormat = BlockFormat | InlineFormat | SelectorFormat;\ndeclare type RemoveFormat = RemoveBlockFormat | RemoveInlineFormat | RemoveSelectorFormat;\ndeclare type Format = ApplyFormat | RemoveFormat;\ndeclare type Formats = Record<string, Format | Format[]>;\ndeclare type FormatAttrOrStyleValue = string | ((vars?: FormatVars) => string);\ndeclare type FormatVars = Record<string, string | null>;\ninterface BaseFormat<T> {\n    ceFalseOverride?: boolean;\n    classes?: string | string[];\n    collapsed?: boolean;\n    exact?: boolean;\n    expand?: boolean;\n    links?: boolean;\n    mixed?: boolean;\n    block_expand?: boolean;\n    onmatch?: (node: Node, fmt: T, itemName: string) => boolean;\n    remove?: 'none' | 'empty' | 'all';\n    remove_similar?: boolean;\n    split?: boolean;\n    deep?: boolean;\n    preserve_attributes?: string[];\n}\ninterface Block {\n    block: string;\n    list_block?: string;\n    wrapper?: boolean;\n}\ninterface Inline {\n    inline: string;\n}\ninterface Selector {\n    selector: string;\n    inherit?: boolean;\n}\ninterface CommonFormat<T> extends BaseFormat<T> {\n    attributes?: Record<string, FormatAttrOrStyleValue>;\n    styles?: Record<string, FormatAttrOrStyleValue>;\n    toggle?: boolean;\n    preview?: string | false;\n    onformat?: (elm: Node, fmt: T, vars?: FormatVars, node?: Node | RangeLikeObject) => void;\n    clear_child_styles?: boolean;\n    merge_siblings?: boolean;\n    merge_with_parents?: boolean;\n    defaultBlock?: string;\n}\ninterface BlockFormat extends Block, CommonFormat<BlockFormat> {\n}\ninterface InlineFormat extends Inline, CommonFormat<InlineFormat> {\n}\ninterface SelectorFormat extends Selector, CommonFormat<SelectorFormat> {\n}\ninterface CommonRemoveFormat<T> extends BaseFormat<T> {\n    attributes?: string[] | Record<string, FormatAttrOrStyleValue>;\n    styles?: string[] | Record<string, FormatAttrOrStyleValue>;\n}\ninterface RemoveBlockFormat extends Block, CommonRemoveFormat<RemoveBlockFormat> {\n}\ninterface RemoveInlineFormat extends Inline, CommonRemoveFormat<RemoveInlineFormat> {\n}\ninterface RemoveSelectorFormat extends Selector, CommonRemoveFormat<RemoveSelectorFormat> {\n}\ntype Format_d_Formats = Formats;\ntype Format_d_Format = Format;\ntype Format_d_ApplyFormat = ApplyFormat;\ntype Format_d_BlockFormat = BlockFormat;\ntype Format_d_InlineFormat = InlineFormat;\ntype Format_d_SelectorFormat = SelectorFormat;\ntype Format_d_RemoveFormat = RemoveFormat;\ntype Format_d_RemoveBlockFormat = RemoveBlockFormat;\ntype Format_d_RemoveInlineFormat = RemoveInlineFormat;\ntype Format_d_RemoveSelectorFormat = RemoveSelectorFormat;\ndeclare namespace Format_d {\n    export { Format_d_Formats as Formats, Format_d_Format as Format, Format_d_ApplyFormat as ApplyFormat, Format_d_BlockFormat as BlockFormat, Format_d_InlineFormat as InlineFormat, Format_d_SelectorFormat as SelectorFormat, Format_d_RemoveFormat as RemoveFormat, Format_d_RemoveBlockFormat as RemoveBlockFormat, Format_d_RemoveInlineFormat as RemoveInlineFormat, Format_d_RemoveSelectorFormat as RemoveSelectorFormat, };\n}\ndeclare type StyleFormat = BlockStyleFormat | InlineStyleFormat | SelectorStyleFormat;\ndeclare type AllowedFormat = Separator | FormatReference | StyleFormat | NestedFormatting;\ninterface Separator {\n    title: string;\n}\ninterface FormatReference {\n    title: string;\n    format: string;\n    icon?: string;\n}\ninterface NestedFormatting {\n    title: string;\n    items: Array<FormatReference | StyleFormat>;\n}\ninterface CommonStyleFormat {\n    name?: string;\n    title: string;\n    icon?: string;\n}\ninterface BlockStyleFormat extends BlockFormat, CommonStyleFormat {\n}\ninterface InlineStyleFormat extends InlineFormat, CommonStyleFormat {\n}\ninterface SelectorStyleFormat extends SelectorFormat, CommonStyleFormat {\n}\ninterface AlertBannerSpec {\n    type: 'alertbanner';\n    level: 'info' | 'warn' | 'error' | 'success';\n    text: string;\n    icon: string;\n    url?: string;\n}\ninterface ButtonSpec {\n    type: 'button';\n    text: string;\n    disabled?: boolean;\n    primary?: boolean;\n    name?: string;\n    icon?: string;\n    borderless?: boolean;\n}\ninterface CheckboxSpec {\n    name: string;\n    type: 'checkbox';\n    label: string;\n    disabled?: boolean;\n}\ninterface FormComponentSpec {\n    type: string;\n    name: string;\n}\ninterface FormComponentWithLabelSpec extends FormComponentSpec {\n    label?: string;\n}\ninterface CollectionSpec extends FormComponentWithLabelSpec {\n    type: 'collection';\n}\ninterface ColorInputSpec extends FormComponentWithLabelSpec {\n    type: 'colorinput';\n}\ninterface ColorPickerSpec extends FormComponentWithLabelSpec {\n    type: 'colorpicker';\n}\ninterface CustomEditorInit {\n    setValue: (value: string) => void;\n    getValue: () => string;\n    destroy: () => void;\n}\ndeclare type CustomEditorInitFn = (elm: HTMLElement, settings: any) => Promise<CustomEditorInit>;\ninterface CustomEditorOldSpec extends FormComponentSpec {\n    type: 'customeditor';\n    tag?: string;\n    init: (e: HTMLElement) => Promise<CustomEditorInit>;\n}\ninterface CustomEditorNewSpec extends FormComponentSpec {\n    type: 'customeditor';\n    tag?: string;\n    scriptId: string;\n    scriptUrl: string;\n    settings?: any;\n}\ndeclare type CustomEditorSpec = CustomEditorOldSpec | CustomEditorNewSpec;\ninterface DropZoneSpec extends FormComponentWithLabelSpec {\n    type: 'dropzone';\n}\ninterface GridSpec {\n    type: 'grid';\n    columns: number;\n    items: BodyComponentSpec[];\n}\ninterface HtmlPanelSpec {\n    type: 'htmlpanel';\n    html: string;\n    presets?: 'presentation' | 'document';\n}\ninterface IframeSpec extends FormComponentWithLabelSpec {\n    type: 'iframe';\n    sandboxed?: boolean;\n}\ninterface ImageToolsState {\n    blob: Blob;\n    url: string;\n}\ninterface ImageToolsSpec extends FormComponentWithLabelSpec {\n    type: 'imagetools';\n    currentState: ImageToolsState;\n}\ninterface InputSpec extends FormComponentWithLabelSpec {\n    type: 'input';\n    inputMode?: string;\n    placeholder?: string;\n    maximized?: boolean;\n    disabled?: boolean;\n}\ninterface LabelSpec {\n    type: 'label';\n    label: string;\n    items: BodyComponentSpec[];\n}\ninterface ListBoxSingleItemSpec {\n    text: string;\n    value: string;\n}\ninterface ListBoxNestedItemSpec {\n    text: string;\n    items: ListBoxItemSpec[];\n}\ndeclare type ListBoxItemSpec = ListBoxNestedItemSpec | ListBoxSingleItemSpec;\ninterface ListBoxSpec extends FormComponentWithLabelSpec {\n    type: 'listbox';\n    items: ListBoxItemSpec[];\n    disabled?: boolean;\n}\ninterface PanelSpec {\n    type: 'panel';\n    classes?: string[];\n    items: BodyComponentSpec[];\n}\ninterface SelectBoxItemSpec {\n    text: string;\n    value: string;\n}\ninterface SelectBoxSpec extends FormComponentWithLabelSpec {\n    type: 'selectbox';\n    items: SelectBoxItemSpec[];\n    size?: number;\n    disabled?: boolean;\n}\ninterface SizeInputSpec extends FormComponentWithLabelSpec {\n    type: 'sizeinput';\n    constrain?: boolean;\n    disabled?: boolean;\n}\ninterface TableSpec {\n    type: 'table';\n    header: string[];\n    cells: string[][];\n}\ninterface TextAreaSpec extends FormComponentWithLabelSpec {\n    type: 'textarea';\n    placeholder?: string;\n    maximized?: boolean;\n    disabled?: boolean;\n}\ninterface UrlInputSpec extends FormComponentWithLabelSpec {\n    type: 'urlinput';\n    filetype?: 'image' | 'media' | 'file';\n    disabled?: boolean;\n}\ndeclare type BodyComponentSpec = BarSpec | ButtonSpec | CheckboxSpec | TextAreaSpec | InputSpec | ListBoxSpec | SelectBoxSpec | SizeInputSpec | IframeSpec | HtmlPanelSpec | UrlInputSpec | DropZoneSpec | ColorInputSpec | GridSpec | ColorPickerSpec | ImageToolsSpec | AlertBannerSpec | CollectionSpec | LabelSpec | TableSpec | PanelSpec | CustomEditorSpec;\ninterface BarSpec {\n    type: 'bar';\n    items: BodyComponentSpec[];\n}\ninterface CommonMenuItemSpec {\n    disabled?: boolean;\n    text?: string;\n    value?: string;\n    meta?: Record<string, any>;\n    shortcut?: string;\n}\ninterface CommonMenuItemInstanceApi {\n    isDisabled: () => boolean;\n    setDisabled: (state: boolean) => void;\n}\ninterface DialogToggleMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'togglemenuitem';\n    name: string;\n}\ndeclare type DialogFooterMenuButtonItemSpec = DialogToggleMenuItemSpec;\ninterface BaseDialogFooterButtonSpec {\n    name?: string;\n    align?: 'start' | 'end';\n    primary?: boolean;\n    disabled?: boolean;\n    icon?: string;\n}\ninterface DialogFooterNormalButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'submit' | 'cancel' | 'custom';\n    text: string;\n}\ninterface DialogFooterMenuButtonSpec extends BaseDialogFooterButtonSpec {\n    type: 'menu';\n    text?: string;\n    tooltip?: string;\n    icon?: string;\n    items: DialogFooterMenuButtonItemSpec[];\n}\ndeclare type DialogFooterButtonSpec = DialogFooterNormalButtonSpec | DialogFooterMenuButtonSpec;\ninterface TabSpec {\n    name?: string;\n    title: string;\n    items: BodyComponentSpec[];\n}\ninterface TabPanelSpec {\n    type: 'tabpanel';\n    tabs: TabSpec[];\n}\ndeclare type DialogDataItem = any;\ndeclare type DialogData = Record<string, DialogDataItem>;\ninterface DialogInstanceApi<T extends DialogData> {\n    getData: () => T;\n    setData: (data: Partial<T>) => void;\n    disable: (name: string) => void;\n    focus: (name: string) => void;\n    showTab: (name: string) => void;\n    redial: (nu: DialogSpec<T>) => void;\n    enable: (name: string) => void;\n    block: (msg: string) => void;\n    unblock: () => void;\n    close: () => void;\n}\ninterface DialogActionDetails {\n    name: string;\n    value?: any;\n}\ninterface DialogChangeDetails<T> {\n    name: keyof T;\n}\ninterface DialogTabChangeDetails {\n    newTabName: string;\n    oldTabName: string;\n}\ndeclare type DialogActionHandler<T> = (api: DialogInstanceApi<T>, details: DialogActionDetails) => void;\ndeclare type DialogChangeHandler<T> = (api: DialogInstanceApi<T>, details: DialogChangeDetails<T>) => void;\ndeclare type DialogSubmitHandler<T> = (api: DialogInstanceApi<T>) => void;\ndeclare type DialogCloseHandler = () => void;\ndeclare type DialogCancelHandler<T> = (api: DialogInstanceApi<T>) => void;\ndeclare type DialogTabChangeHandler<T> = (api: DialogInstanceApi<T>, details: DialogTabChangeDetails) => void;\ndeclare type DialogSize = 'normal' | 'medium' | 'large';\ninterface DialogSpec<T extends DialogData> {\n    title: string;\n    size?: DialogSize;\n    body: TabPanelSpec | PanelSpec;\n    buttons: DialogFooterButtonSpec[];\n    initialData?: T;\n    onAction?: DialogActionHandler<T>;\n    onChange?: DialogChangeHandler<T>;\n    onSubmit?: DialogSubmitHandler<T>;\n    onClose?: DialogCloseHandler;\n    onCancel?: DialogCancelHandler<T>;\n    onTabChange?: DialogTabChangeHandler<T>;\n}\ninterface UrlDialogInstanceApi {\n    block: (msg: string) => void;\n    unblock: () => void;\n    close: () => void;\n    sendMessage: (msg: any) => void;\n}\ninterface UrlDialogActionDetails {\n    name: string;\n    value?: any;\n}\ninterface UrlDialogMessage {\n    mceAction: string;\n    [key: string]: any;\n}\ndeclare type UrlDialogActionHandler = (api: UrlDialogInstanceApi, actions: UrlDialogActionDetails) => void;\ndeclare type UrlDialogCloseHandler = () => void;\ndeclare type UrlDialogCancelHandler = (api: UrlDialogInstanceApi) => void;\ndeclare type UrlDialogMessageHandler = (api: UrlDialogInstanceApi, message: UrlDialogMessage) => void;\ninterface UrlDialogFooterButtonSpec extends DialogFooterNormalButtonSpec {\n    type: 'cancel' | 'custom';\n}\ninterface UrlDialogSpec {\n    title: string;\n    url: string;\n    height?: number;\n    width?: number;\n    buttons?: UrlDialogFooterButtonSpec[];\n    onAction?: UrlDialogActionHandler;\n    onClose?: UrlDialogCloseHandler;\n    onCancel?: UrlDialogCancelHandler;\n    onMessage?: UrlDialogMessageHandler;\n}\ndeclare type CardContainerDirection = 'vertical' | 'horizontal';\ndeclare type CardContainerAlign = 'left' | 'right';\ndeclare type CardContainerValign = 'top' | 'middle' | 'bottom';\ninterface CardContainerSpec {\n    type: 'cardcontainer';\n    items: CardItemSpec[];\n    direction?: CardContainerDirection;\n    align?: CardContainerAlign;\n    valign?: CardContainerValign;\n}\ninterface CardImageSpec {\n    type: 'cardimage';\n    src: string;\n    alt?: string;\n    classes?: string[];\n}\ninterface CardTextSpec {\n    type: 'cardtext';\n    text: string;\n    name?: string;\n    classes?: string[];\n}\ndeclare type CardItemSpec = CardContainerSpec | CardImageSpec | CardTextSpec;\ninterface CardMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n}\ninterface CardMenuItemSpec extends Omit<CommonMenuItemSpec, 'text' | 'shortcut'> {\n    type: 'cardmenuitem';\n    label?: string;\n    items: CardItemSpec[];\n    onSetup?: (api: CardMenuItemInstanceApi) => (api: CardMenuItemInstanceApi) => void;\n    onAction?: (api: CardMenuItemInstanceApi) => void;\n}\ninterface SeparatorMenuItemSpec {\n    type?: 'separator';\n    text?: string;\n}\ndeclare type ColumnTypes$1 = number | 'auto';\ndeclare type SeparatorItemSpec = SeparatorMenuItemSpec;\ninterface AutocompleterItemSpec {\n    type?: 'autocompleteitem';\n    value: string;\n    text?: string;\n    icon?: string;\n    meta?: Record<string, any>;\n}\ndeclare type AutocompleterContents = SeparatorItemSpec | AutocompleterItemSpec | CardMenuItemSpec;\ninterface AutocompleterSpec {\n    type?: 'autocompleter';\n    ch: string;\n    minChars?: number;\n    columns?: ColumnTypes$1;\n    matches?: (rng: Range, text: string, pattern: string) => boolean;\n    fetch: (pattern: string, maxResults: number, fetchOptions: Record<string, any>) => Promise<AutocompleterContents[]>;\n    onAction: (autocompleterApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record<string, any>) => void;\n    maxResults?: number;\n    highlightOn?: string[];\n}\ninterface AutocompleterInstanceApi {\n    hide: () => void;\n    reload: (fetchOptions: Record<string, any>) => void;\n}\ndeclare type ContextPosition = 'node' | 'selection' | 'line';\ndeclare type ContextScope = 'node' | 'editor';\ninterface ContextBarSpec {\n    predicate?: (elem: Element) => boolean;\n    position?: ContextPosition;\n    scope?: ContextScope;\n}\ninterface BaseToolbarButtonSpec<I extends BaseToolbarButtonInstanceApi> {\n    disabled?: boolean;\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    onSetup?: (api: I) => (api: I) => void;\n}\ninterface BaseToolbarButtonInstanceApi {\n    isDisabled: () => boolean;\n    setDisabled: (state: boolean) => void;\n}\ninterface ToolbarButtonSpec extends BaseToolbarButtonSpec<ToolbarButtonInstanceApi> {\n    type?: 'button';\n    onAction: (api: ToolbarButtonInstanceApi) => void;\n}\ninterface ToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface BaseToolbarToggleButtonSpec<I extends BaseToolbarButtonInstanceApi> extends BaseToolbarButtonSpec<I> {\n    active?: boolean;\n}\ninterface BaseToolbarToggleButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface ToolbarToggleButtonSpec extends BaseToolbarToggleButtonSpec<ToolbarToggleButtonInstanceApi> {\n    type?: 'togglebutton';\n    onAction: (api: ToolbarToggleButtonInstanceApi) => void;\n}\ninterface ToolbarToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {\n}\ninterface ContextFormLaunchButtonApi extends BaseToolbarButtonSpec<BaseToolbarButtonInstanceApi> {\n    type: 'contextformbutton';\n}\ninterface ContextFormLaunchToggleButtonSpec extends BaseToolbarToggleButtonSpec<BaseToolbarToggleButtonInstanceApi> {\n    type: 'contextformtogglebutton';\n}\ninterface ContextFormButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface ContextFormToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {\n}\ninterface ContextFormButtonSpec extends BaseToolbarButtonSpec<ContextFormButtonInstanceApi> {\n    type?: 'contextformbutton';\n    primary?: boolean;\n    onAction: (formApi: ContextFormInstanceApi, api: ContextFormButtonInstanceApi) => void;\n}\ninterface ContextFormToggleButtonSpec extends BaseToolbarToggleButtonSpec<ContextFormToggleButtonInstanceApi> {\n    type?: 'contextformtogglebutton';\n    onAction: (formApi: ContextFormInstanceApi, buttonApi: ContextFormToggleButtonInstanceApi) => void;\n    primary?: boolean;\n}\ninterface ContextFormInstanceApi {\n    hide: () => void;\n    getValue: () => string;\n}\ninterface ContextFormSpec extends ContextBarSpec {\n    type?: 'contextform';\n    initValue?: () => string;\n    label?: string;\n    launch?: ContextFormLaunchButtonApi | ContextFormLaunchToggleButtonSpec;\n    commands: Array<ContextFormToggleButtonSpec | ContextFormButtonSpec>;\n}\ninterface ContextToolbarSpec extends ContextBarSpec {\n    type?: 'contexttoolbar';\n    items: string;\n}\ninterface ChoiceMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'choiceitem';\n    icon?: string;\n}\ninterface ChoiceMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface ContextMenuItem extends CommonMenuItemSpec {\n    text: string;\n    icon?: string;\n    type?: 'item';\n    onAction: () => void;\n}\ninterface ContextSubMenu extends CommonMenuItemSpec {\n    type: 'submenu';\n    text: string;\n    icon?: string;\n    getSubmenuItems: () => string | Array<ContextMenuContents>;\n}\ndeclare type ContextMenuContents = string | ContextMenuItem | SeparatorMenuItemSpec | ContextSubMenu;\ninterface ContextMenuApi {\n    update: (element: Element) => string | Array<ContextMenuContents>;\n}\ninterface FancyActionArgsMap {\n    'inserttable': {\n        numRows: number;\n        numColumns: number;\n    };\n    'colorswatch': {\n        value: string;\n    };\n}\ninterface BaseFancyMenuItemSpec<T extends keyof FancyActionArgsMap> {\n    type: 'fancymenuitem';\n    fancytype: T;\n    initData?: Record<string, unknown>;\n    onAction?: (data: FancyActionArgsMap[T]) => void;\n}\ninterface InsertTableMenuItemSpec extends BaseFancyMenuItemSpec<'inserttable'> {\n    fancytype: 'inserttable';\n    initData?: {};\n}\ninterface ColorSwatchMenuItemSpec extends BaseFancyMenuItemSpec<'colorswatch'> {\n    fancytype: 'colorswatch';\n    initData?: {\n        allowCustomColors?: boolean;\n        colors: ChoiceMenuItemSpec[];\n    };\n}\ndeclare type FancyMenuItemSpec = InsertTableMenuItemSpec | ColorSwatchMenuItemSpec;\ninterface MenuItemSpec extends CommonMenuItemSpec {\n    type?: 'menuitem';\n    icon?: string;\n    onSetup?: (api: MenuItemInstanceApi) => (api: MenuItemInstanceApi) => void;\n    onAction?: (api: MenuItemInstanceApi) => void;\n}\ninterface MenuItemInstanceApi extends CommonMenuItemInstanceApi {\n}\ndeclare type NestedMenuItemContents = string | MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec | SeparatorMenuItemSpec | FancyMenuItemSpec;\ninterface NestedMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'nestedmenuitem';\n    icon?: string;\n    getSubmenuItems: () => string | Array<NestedMenuItemContents>;\n    onSetup?: (api: NestedMenuItemInstanceApi) => (api: NestedMenuItemInstanceApi) => void;\n}\ninterface NestedMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n}\ninterface ToggleMenuItemSpec extends CommonMenuItemSpec {\n    type?: 'togglemenuitem';\n    icon?: string;\n    active?: boolean;\n    onSetup?: (api: ToggleMenuItemInstanceApi) => void;\n    onAction: (api: ToggleMenuItemInstanceApi) => void;\n}\ninterface ToggleMenuItemInstanceApi extends CommonMenuItemInstanceApi {\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ntype PublicDialog_d_AlertBannerSpec = AlertBannerSpec;\ntype PublicDialog_d_BarSpec = BarSpec;\ntype PublicDialog_d_BodyComponentSpec = BodyComponentSpec;\ntype PublicDialog_d_ButtonSpec = ButtonSpec;\ntype PublicDialog_d_CheckboxSpec = CheckboxSpec;\ntype PublicDialog_d_CollectionSpec = CollectionSpec;\ntype PublicDialog_d_ColorInputSpec = ColorInputSpec;\ntype PublicDialog_d_ColorPickerSpec = ColorPickerSpec;\ntype PublicDialog_d_CustomEditorSpec = CustomEditorSpec;\ntype PublicDialog_d_CustomEditorInit = CustomEditorInit;\ntype PublicDialog_d_CustomEditorInitFn = CustomEditorInitFn;\ntype PublicDialog_d_DialogData = DialogData;\ntype PublicDialog_d_DialogSize = DialogSize;\ntype PublicDialog_d_DialogSpec<_0> = DialogSpec<_0>;\ntype PublicDialog_d_DialogInstanceApi<_0> = DialogInstanceApi<_0>;\ntype PublicDialog_d_DialogFooterButtonSpec = DialogFooterButtonSpec;\ntype PublicDialog_d_DialogActionDetails = DialogActionDetails;\ntype PublicDialog_d_DialogChangeDetails<_0> = DialogChangeDetails<_0>;\ntype PublicDialog_d_DialogTabChangeDetails = DialogTabChangeDetails;\ntype PublicDialog_d_DropZoneSpec = DropZoneSpec;\ntype PublicDialog_d_GridSpec = GridSpec;\ntype PublicDialog_d_HtmlPanelSpec = HtmlPanelSpec;\ntype PublicDialog_d_IframeSpec = IframeSpec;\ntype PublicDialog_d_ImageToolsSpec = ImageToolsSpec;\ntype PublicDialog_d_InputSpec = InputSpec;\ntype PublicDialog_d_LabelSpec = LabelSpec;\ntype PublicDialog_d_ListBoxSpec = ListBoxSpec;\ntype PublicDialog_d_ListBoxItemSpec = ListBoxItemSpec;\ntype PublicDialog_d_ListBoxNestedItemSpec = ListBoxNestedItemSpec;\ntype PublicDialog_d_ListBoxSingleItemSpec = ListBoxSingleItemSpec;\ntype PublicDialog_d_PanelSpec = PanelSpec;\ntype PublicDialog_d_SelectBoxSpec = SelectBoxSpec;\ntype PublicDialog_d_SelectBoxItemSpec = SelectBoxItemSpec;\ntype PublicDialog_d_SizeInputSpec = SizeInputSpec;\ntype PublicDialog_d_TableSpec = TableSpec;\ntype PublicDialog_d_TabSpec = TabSpec;\ntype PublicDialog_d_TabPanelSpec = TabPanelSpec;\ntype PublicDialog_d_TextAreaSpec = TextAreaSpec;\ntype PublicDialog_d_UrlInputSpec = UrlInputSpec;\ntype PublicDialog_d_UrlDialogSpec = UrlDialogSpec;\ntype PublicDialog_d_UrlDialogFooterButtonSpec = UrlDialogFooterButtonSpec;\ntype PublicDialog_d_UrlDialogInstanceApi = UrlDialogInstanceApi;\ntype PublicDialog_d_UrlDialogActionDetails = UrlDialogActionDetails;\ntype PublicDialog_d_UrlDialogMessage = UrlDialogMessage;\ndeclare namespace PublicDialog_d {\n    export { PublicDialog_d_AlertBannerSpec as AlertBannerSpec, PublicDialog_d_BarSpec as BarSpec, PublicDialog_d_BodyComponentSpec as BodyComponentSpec, PublicDialog_d_ButtonSpec as ButtonSpec, PublicDialog_d_CheckboxSpec as CheckboxSpec, PublicDialog_d_CollectionSpec as CollectionSpec, PublicDialog_d_ColorInputSpec as ColorInputSpec, PublicDialog_d_ColorPickerSpec as ColorPickerSpec, PublicDialog_d_CustomEditorSpec as CustomEditorSpec, PublicDialog_d_CustomEditorInit as CustomEditorInit, PublicDialog_d_CustomEditorInitFn as CustomEditorInitFn, PublicDialog_d_DialogData as DialogData, PublicDialog_d_DialogSize as DialogSize, PublicDialog_d_DialogSpec as DialogSpec, PublicDialog_d_DialogInstanceApi as DialogInstanceApi, PublicDialog_d_DialogFooterButtonSpec as DialogFooterButtonSpec, PublicDialog_d_DialogActionDetails as DialogActionDetails, PublicDialog_d_DialogChangeDetails as DialogChangeDetails, PublicDialog_d_DialogTabChangeDetails as DialogTabChangeDetails, PublicDialog_d_DropZoneSpec as DropZoneSpec, PublicDialog_d_GridSpec as GridSpec, PublicDialog_d_HtmlPanelSpec as HtmlPanelSpec, PublicDialog_d_IframeSpec as IframeSpec, PublicDialog_d_ImageToolsSpec as ImageToolsSpec, PublicDialog_d_InputSpec as InputSpec, PublicDialog_d_LabelSpec as LabelSpec, PublicDialog_d_ListBoxSpec as ListBoxSpec, PublicDialog_d_ListBoxItemSpec as ListBoxItemSpec, PublicDialog_d_ListBoxNestedItemSpec as ListBoxNestedItemSpec, PublicDialog_d_ListBoxSingleItemSpec as ListBoxSingleItemSpec, PublicDialog_d_PanelSpec as PanelSpec, PublicDialog_d_SelectBoxSpec as SelectBoxSpec, PublicDialog_d_SelectBoxItemSpec as SelectBoxItemSpec, PublicDialog_d_SizeInputSpec as SizeInputSpec, PublicDialog_d_TableSpec as TableSpec, PublicDialog_d_TabSpec as TabSpec, PublicDialog_d_TabPanelSpec as TabPanelSpec, PublicDialog_d_TextAreaSpec as TextAreaSpec, PublicDialog_d_UrlInputSpec as UrlInputSpec, PublicDialog_d_UrlDialogSpec as UrlDialogSpec, PublicDialog_d_UrlDialogFooterButtonSpec as UrlDialogFooterButtonSpec, PublicDialog_d_UrlDialogInstanceApi as UrlDialogInstanceApi, PublicDialog_d_UrlDialogActionDetails as UrlDialogActionDetails, PublicDialog_d_UrlDialogMessage as UrlDialogMessage, };\n}\ntype PublicInlineContent_d_AutocompleterSpec = AutocompleterSpec;\ntype PublicInlineContent_d_AutocompleterItemSpec = AutocompleterItemSpec;\ntype PublicInlineContent_d_AutocompleterContents = AutocompleterContents;\ntype PublicInlineContent_d_AutocompleterInstanceApi = AutocompleterInstanceApi;\ntype PublicInlineContent_d_ContextPosition = ContextPosition;\ntype PublicInlineContent_d_ContextScope = ContextScope;\ntype PublicInlineContent_d_ContextFormSpec = ContextFormSpec;\ntype PublicInlineContent_d_ContextFormInstanceApi = ContextFormInstanceApi;\ntype PublicInlineContent_d_ContextFormButtonSpec = ContextFormButtonSpec;\ntype PublicInlineContent_d_ContextFormButtonInstanceApi = ContextFormButtonInstanceApi;\ntype PublicInlineContent_d_ContextFormToggleButtonSpec = ContextFormToggleButtonSpec;\ntype PublicInlineContent_d_ContextFormToggleButtonInstanceApi = ContextFormToggleButtonInstanceApi;\ntype PublicInlineContent_d_ContextToolbarSpec = ContextToolbarSpec;\ntype PublicInlineContent_d_SeparatorItemSpec = SeparatorItemSpec;\ndeclare namespace PublicInlineContent_d {\n    export { PublicInlineContent_d_AutocompleterSpec as AutocompleterSpec, PublicInlineContent_d_AutocompleterItemSpec as AutocompleterItemSpec, PublicInlineContent_d_AutocompleterContents as AutocompleterContents, PublicInlineContent_d_AutocompleterInstanceApi as AutocompleterInstanceApi, PublicInlineContent_d_ContextPosition as ContextPosition, PublicInlineContent_d_ContextScope as ContextScope, PublicInlineContent_d_ContextFormSpec as ContextFormSpec, PublicInlineContent_d_ContextFormInstanceApi as ContextFormInstanceApi, PublicInlineContent_d_ContextFormButtonSpec as ContextFormButtonSpec, PublicInlineContent_d_ContextFormButtonInstanceApi as ContextFormButtonInstanceApi, PublicInlineContent_d_ContextFormToggleButtonSpec as ContextFormToggleButtonSpec, PublicInlineContent_d_ContextFormToggleButtonInstanceApi as ContextFormToggleButtonInstanceApi, PublicInlineContent_d_ContextToolbarSpec as ContextToolbarSpec, PublicInlineContent_d_SeparatorItemSpec as SeparatorItemSpec, };\n}\ntype PublicMenu_d_MenuItemSpec = MenuItemSpec;\ntype PublicMenu_d_MenuItemInstanceApi = MenuItemInstanceApi;\ntype PublicMenu_d_NestedMenuItemContents = NestedMenuItemContents;\ntype PublicMenu_d_NestedMenuItemSpec = NestedMenuItemSpec;\ntype PublicMenu_d_NestedMenuItemInstanceApi = NestedMenuItemInstanceApi;\ntype PublicMenu_d_FancyMenuItemSpec = FancyMenuItemSpec;\ntype PublicMenu_d_ColorSwatchMenuItemSpec = ColorSwatchMenuItemSpec;\ntype PublicMenu_d_InsertTableMenuItemSpec = InsertTableMenuItemSpec;\ntype PublicMenu_d_ToggleMenuItemSpec = ToggleMenuItemSpec;\ntype PublicMenu_d_ToggleMenuItemInstanceApi = ToggleMenuItemInstanceApi;\ntype PublicMenu_d_ChoiceMenuItemSpec = ChoiceMenuItemSpec;\ntype PublicMenu_d_ChoiceMenuItemInstanceApi = ChoiceMenuItemInstanceApi;\ntype PublicMenu_d_SeparatorMenuItemSpec = SeparatorMenuItemSpec;\ntype PublicMenu_d_ContextMenuApi = ContextMenuApi;\ntype PublicMenu_d_ContextMenuContents = ContextMenuContents;\ntype PublicMenu_d_ContextMenuItem = ContextMenuItem;\ntype PublicMenu_d_ContextSubMenu = ContextSubMenu;\ntype PublicMenu_d_CardMenuItemSpec = CardMenuItemSpec;\ntype PublicMenu_d_CardMenuItemInstanceApi = CardMenuItemInstanceApi;\ntype PublicMenu_d_CardItemSpec = CardItemSpec;\ntype PublicMenu_d_CardContainerSpec = CardContainerSpec;\ntype PublicMenu_d_CardImageSpec = CardImageSpec;\ntype PublicMenu_d_CardTextSpec = CardTextSpec;\ndeclare namespace PublicMenu_d {\n    export { PublicMenu_d_MenuItemSpec as MenuItemSpec, PublicMenu_d_MenuItemInstanceApi as MenuItemInstanceApi, PublicMenu_d_NestedMenuItemContents as NestedMenuItemContents, PublicMenu_d_NestedMenuItemSpec as NestedMenuItemSpec, PublicMenu_d_NestedMenuItemInstanceApi as NestedMenuItemInstanceApi, PublicMenu_d_FancyMenuItemSpec as FancyMenuItemSpec, PublicMenu_d_ColorSwatchMenuItemSpec as ColorSwatchMenuItemSpec, PublicMenu_d_InsertTableMenuItemSpec as InsertTableMenuItemSpec, PublicMenu_d_ToggleMenuItemSpec as ToggleMenuItemSpec, PublicMenu_d_ToggleMenuItemInstanceApi as ToggleMenuItemInstanceApi, PublicMenu_d_ChoiceMenuItemSpec as ChoiceMenuItemSpec, PublicMenu_d_ChoiceMenuItemInstanceApi as ChoiceMenuItemInstanceApi, PublicMenu_d_SeparatorMenuItemSpec as SeparatorMenuItemSpec, PublicMenu_d_ContextMenuApi as ContextMenuApi, PublicMenu_d_ContextMenuContents as ContextMenuContents, PublicMenu_d_ContextMenuItem as ContextMenuItem, PublicMenu_d_ContextSubMenu as ContextSubMenu, PublicMenu_d_CardMenuItemSpec as CardMenuItemSpec, PublicMenu_d_CardMenuItemInstanceApi as CardMenuItemInstanceApi, PublicMenu_d_CardItemSpec as CardItemSpec, PublicMenu_d_CardContainerSpec as CardContainerSpec, PublicMenu_d_CardImageSpec as CardImageSpec, PublicMenu_d_CardTextSpec as CardTextSpec, };\n}\ninterface SidebarInstanceApi {\n    element: () => HTMLElement;\n}\ninterface SidebarSpec {\n    icon?: string;\n    tooltip?: string;\n    onShow?: (api: SidebarInstanceApi) => void;\n    onSetup?: (api: SidebarInstanceApi) => (api: SidebarInstanceApi) => void;\n    onHide?: (api: SidebarInstanceApi) => void;\n}\ntype PublicSidebar_d_SidebarSpec = SidebarSpec;\ntype PublicSidebar_d_SidebarInstanceApi = SidebarInstanceApi;\ndeclare namespace PublicSidebar_d {\n    export { PublicSidebar_d_SidebarSpec as SidebarSpec, PublicSidebar_d_SidebarInstanceApi as SidebarInstanceApi, };\n}\ninterface ToolbarGroupSetting {\n    name: string;\n    items: string[];\n}\ndeclare type ToolbarConfig = string | ToolbarGroupSetting[];\ninterface GroupToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {\n}\ninterface GroupToolbarButtonSpec extends BaseToolbarButtonSpec<GroupToolbarButtonInstanceApi> {\n    type?: 'grouptoolbarbutton';\n    items?: ToolbarConfig;\n}\ndeclare type MenuButtonItemTypes = NestedMenuItemContents;\ndeclare type SuccessCallback$1 = (menu: string | MenuButtonItemTypes[]) => void;\ninterface BaseMenuButtonSpec {\n    text?: string;\n    tooltip?: string;\n    icon?: string;\n    fetch: (success: SuccessCallback$1) => void;\n    onSetup?: (api: BaseMenuButtonInstanceApi) => (api: BaseMenuButtonInstanceApi) => void;\n}\ninterface BaseMenuButtonInstanceApi {\n    isDisabled: () => boolean;\n    setDisabled: (state: boolean) => void;\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ninterface ToolbarMenuButtonSpec extends BaseMenuButtonSpec {\n    type?: 'menubutton';\n    onSetup?: (api: ToolbarMenuButtonInstanceApi) => (api: ToolbarMenuButtonInstanceApi) => void;\n}\ninterface ToolbarMenuButtonInstanceApi extends BaseMenuButtonInstanceApi {\n}\ndeclare type ToolbarSplitButtonItemTypes = ChoiceMenuItemSpec | SeparatorMenuItemSpec;\ndeclare type SuccessCallback = (menu: ToolbarSplitButtonItemTypes[]) => void;\ndeclare type SelectPredicate = (value: string) => boolean;\ndeclare type PresetTypes = 'color' | 'normal' | 'listpreview';\ndeclare type ColumnTypes = number | 'auto';\ninterface ToolbarSplitButtonSpec {\n    type?: 'splitbutton';\n    tooltip?: string;\n    icon?: string;\n    text?: string;\n    select?: SelectPredicate;\n    presets?: PresetTypes;\n    columns?: ColumnTypes;\n    fetch: (success: SuccessCallback) => void;\n    onSetup?: (api: ToolbarSplitButtonInstanceApi) => (api: ToolbarSplitButtonInstanceApi) => void;\n    onAction: (api: ToolbarSplitButtonInstanceApi) => void;\n    onItemAction: (api: ToolbarSplitButtonInstanceApi, value: string) => void;\n}\ninterface ToolbarSplitButtonInstanceApi {\n    isDisabled: () => boolean;\n    setDisabled: (state: boolean) => void;\n    setIconFill: (id: string, value: string) => void;\n    setIconStroke: (id: string, value: string) => void;\n    isActive: () => boolean;\n    setActive: (state: boolean) => void;\n}\ntype PublicToolbar_d_ToolbarButtonSpec = ToolbarButtonSpec;\ntype PublicToolbar_d_ToolbarButtonInstanceApi = ToolbarButtonInstanceApi;\ntype PublicToolbar_d_ToolbarSplitButtonSpec = ToolbarSplitButtonSpec;\ntype PublicToolbar_d_ToolbarSplitButtonInstanceApi = ToolbarSplitButtonInstanceApi;\ntype PublicToolbar_d_ToolbarMenuButtonSpec = ToolbarMenuButtonSpec;\ntype PublicToolbar_d_ToolbarMenuButtonInstanceApi = ToolbarMenuButtonInstanceApi;\ntype PublicToolbar_d_ToolbarToggleButtonSpec = ToolbarToggleButtonSpec;\ntype PublicToolbar_d_ToolbarToggleButtonInstanceApi = ToolbarToggleButtonInstanceApi;\ntype PublicToolbar_d_GroupToolbarButtonSpec = GroupToolbarButtonSpec;\ntype PublicToolbar_d_GroupToolbarButtonInstanceApi = GroupToolbarButtonInstanceApi;\ndeclare namespace PublicToolbar_d {\n    export { PublicToolbar_d_ToolbarButtonSpec as ToolbarButtonSpec, PublicToolbar_d_ToolbarButtonInstanceApi as ToolbarButtonInstanceApi, PublicToolbar_d_ToolbarSplitButtonSpec as ToolbarSplitButtonSpec, PublicToolbar_d_ToolbarSplitButtonInstanceApi as ToolbarSplitButtonInstanceApi, PublicToolbar_d_ToolbarMenuButtonSpec as ToolbarMenuButtonSpec, PublicToolbar_d_ToolbarMenuButtonInstanceApi as ToolbarMenuButtonInstanceApi, PublicToolbar_d_ToolbarToggleButtonSpec as ToolbarToggleButtonSpec, PublicToolbar_d_ToolbarToggleButtonInstanceApi as ToolbarToggleButtonInstanceApi, PublicToolbar_d_GroupToolbarButtonSpec as GroupToolbarButtonSpec, PublicToolbar_d_GroupToolbarButtonInstanceApi as GroupToolbarButtonInstanceApi, };\n}\ninterface Registry$1 {\n    addButton: (name: string, spec: ToolbarButtonSpec) => void;\n    addGroupToolbarButton: (name: string, spec: GroupToolbarButtonSpec) => void;\n    addToggleButton: (name: string, spec: ToolbarToggleButtonSpec) => void;\n    addMenuButton: (name: string, spec: ToolbarMenuButtonSpec) => void;\n    addSplitButton: (name: string, spec: ToolbarSplitButtonSpec) => void;\n    addMenuItem: (name: string, spec: MenuItemSpec) => void;\n    addNestedMenuItem: (name: string, spec: NestedMenuItemSpec) => void;\n    addToggleMenuItem: (name: string, spec: ToggleMenuItemSpec) => void;\n    addContextMenu: (name: string, spec: ContextMenuApi) => void;\n    addContextToolbar: (name: string, spec: ContextToolbarSpec) => void;\n    addContextForm: (name: string, spec: ContextFormSpec) => void;\n    addIcon: (name: string, svgData: string) => void;\n    addAutocompleter: (name: string, spec: AutocompleterSpec) => void;\n    addSidebar: (name: string, spec: SidebarSpec) => void;\n    getAll: () => {\n        buttons: Record<string, ToolbarButtonSpec | GroupToolbarButtonSpec | ToolbarMenuButtonSpec | ToolbarSplitButtonSpec | ToolbarToggleButtonSpec>;\n        menuItems: Record<string, MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec>;\n        popups: Record<string, AutocompleterSpec>;\n        contextMenus: Record<string, ContextMenuApi>;\n        contextToolbars: Record<string, ContextToolbarSpec | ContextFormSpec>;\n        icons: Record<string, string>;\n        sidebars: Record<string, SidebarSpec>;\n    };\n}\ninterface StyleSheetLoaderSettings {\n    maxLoadTime?: number;\n    contentCssCors?: boolean;\n    referrerPolicy?: ReferrerPolicy;\n}\ninterface StyleSheetLoader {\n    load: (url: string, success: () => void, failure?: () => void) => void;\n    loadAll: (urls: string[], success: (urls: string[]) => void, failure: (urls: string[]) => void) => void;\n    unload: (url: string) => void;\n    unloadAll: (urls: string[]) => void;\n    _setReferrerPolicy: (referrerPolicy: ReferrerPolicy) => void;\n}\ndeclare type Registry = Registry$1;\ninterface EditorUiApi {\n    show: () => void;\n    hide: () => void;\n    enable: () => void;\n    disable: () => void;\n    isDisabled: () => boolean;\n}\ninterface EditorUi extends EditorUiApi {\n    registry: Registry;\n    styleSheetLoader: StyleSheetLoader;\n}\ntype Ui_d_Registry = Registry;\ntype Ui_d_EditorUiApi = EditorUiApi;\ntype Ui_d_EditorUi = EditorUi;\ndeclare namespace Ui_d {\n    export { Ui_d_Registry as Registry, PublicDialog_d as Dialog, PublicInlineContent_d as InlineContent, PublicMenu_d as Menu, PublicSidebar_d as Sidebar, PublicToolbar_d as Toolbar, Ui_d_EditorUiApi as EditorUiApi, Ui_d_EditorUi as EditorUi, };\n}\ndeclare type EntityEncoding = 'named' | 'numeric' | 'raw' | 'named,numeric' | 'named+numeric' | 'numeric,named' | 'numeric+named';\ninterface ContentLanguage {\n    readonly title: string;\n    readonly code: string;\n    readonly customCode?: string;\n}\ndeclare type ThemeInitFunc = (editor: Editor, elm: HTMLElement) => {\n    editorContainer: HTMLElement;\n    iframeContainer: HTMLElement;\n    height?: number;\n    iframeHeight?: number;\n    api?: EditorUiApi;\n};\ndeclare type SetupCallback = (editor: Editor) => void;\ndeclare type FilePickerCallback = (callback: Function, value: any, meta: Record<string, any>) => void;\ndeclare type FilePickerValidationStatus = 'valid' | 'unknown' | 'invalid' | 'none';\ndeclare type FilePickerValidationCallback = (info: {\n    type: string;\n    url: string;\n}, callback: (validation: {\n    status: FilePickerValidationStatus;\n    message: string;\n}) => void) => void;\ndeclare type URLConverter = (url: string, name: string, elm?: HTMLElement) => string;\ndeclare type URLConverterCallback = (url: string, node: Node, on_save: boolean, name: string) => void;\ninterface ToolbarGroup {\n    name?: string;\n    items: string[];\n}\ndeclare type ToolbarMode = 'floating' | 'sliding' | 'scrolling' | 'wrap';\ninterface BaseEditorSettings {\n    add_form_submit_trigger?: boolean;\n    add_unload_trigger?: boolean;\n    allow_conditional_comments?: boolean;\n    allow_html_data_urls?: boolean;\n    allow_html_in_named_anchor?: boolean;\n    allow_script_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    allow_unsafe_link_target?: boolean;\n    anchor_bottom?: false | string;\n    anchor_top?: false | string;\n    auto_focus?: string | true;\n    automatic_uploads?: boolean;\n    base_url?: string;\n    block_formats?: string;\n    block_unsupported_drop?: boolean;\n    body_id?: string;\n    body_class?: string;\n    br_in_pre?: boolean;\n    br_newline_selector?: string;\n    browser_spellcheck?: boolean;\n    branding?: boolean;\n    cache_suffix?: string;\n    color_cols?: number;\n    color_map?: string[];\n    content_css?: boolean | string | string[];\n    content_css_cors?: boolean;\n    content_security_policy?: string;\n    content_style?: string;\n    deprecation_warnings?: boolean;\n    font_css?: string | string[];\n    content_langs?: ContentLanguage[];\n    contextmenu?: string | false;\n    contextmenu_never_use_native?: boolean;\n    convert_fonts_to_spans?: boolean;\n    convert_urls?: boolean;\n    custom_colors?: boolean;\n    custom_elements?: string;\n    custom_ui_selector?: string;\n    custom_undo_redo_levels?: number;\n    directionality?: 'ltr' | 'rtl';\n    doctype?: string;\n    document_base_url?: string;\n    element_format?: 'xhtml' | 'html';\n    elementpath?: boolean;\n    encoding?: string;\n    end_container_on_empty_block?: boolean;\n    entities?: string;\n    entity_encoding?: EntityEncoding;\n    extended_valid_elements?: string;\n    event_root?: string;\n    file_picker_callback?: FilePickerCallback;\n    file_picker_types?: string;\n    file_picker_validator_handler?: FilePickerValidationCallback;\n    fix_list_elements?: boolean;\n    fixed_toolbar_container?: string;\n    fixed_toolbar_container_target?: HTMLElement;\n    font_formats?: string;\n    font_size_classes?: string;\n    font_size_legacy_values?: string;\n    font_size_style_values?: string;\n    fontsize_formats?: string;\n    force_hex_style_colors?: boolean;\n    forced_root_block?: boolean | string;\n    forced_root_block_attrs?: Record<string, string>;\n    formats?: Formats;\n    gecko_spellcheck?: boolean;\n    height?: number | string;\n    hidden_input?: boolean;\n    icons?: string;\n    icons_url?: string;\n    id?: string;\n    iframe_aria_text?: string;\n    images_dataimg_filter?: (imgElm: HTMLImageElement) => boolean;\n    images_file_types?: string;\n    images_replace_blob_uris?: boolean;\n    images_reuse_filename?: boolean;\n    images_upload_base_path?: string;\n    images_upload_credentials?: boolean;\n    images_upload_handler?: UploadHandler;\n    images_upload_url?: string;\n    indent?: boolean;\n    indent_after?: string;\n    indent_before?: string;\n    indent_use_margin?: boolean;\n    indentation?: string;\n    init_instance_callback?: SetupCallback;\n    inline?: boolean;\n    inline_boundaries?: boolean;\n    inline_boundaries_selector?: string;\n    inline_styles?: boolean;\n    invalid_elements?: string;\n    invalid_styles?: string | Record<string, string>;\n    keep_styles?: boolean;\n    language?: string;\n    language_load?: boolean;\n    language_url?: string;\n    lineheight_formats?: string;\n    max_height?: number;\n    max_width?: number;\n    menu?: Record<string, {\n        title: string;\n        items: string;\n    }>;\n    menubar?: boolean | string;\n    min_height?: number;\n    min_width?: number;\n    no_newline_selector?: string;\n    nowrap?: boolean;\n    object_resizing?: boolean | string;\n    padd_empty_with_br?: boolean;\n    placeholder?: string;\n    preserve_cdata?: boolean;\n    preview_styles?: boolean | string;\n    protect?: RegExp[];\n    readonly?: boolean;\n    referrer_policy?: ReferrerPolicy;\n    relative_urls?: boolean;\n    remove_script_host?: boolean;\n    remove_trailing_brs?: boolean;\n    removed_menuitems?: string;\n    resize?: boolean | 'both';\n    resize_img_proportional?: boolean;\n    root_name?: string;\n    schema?: SchemaType;\n    selector?: string;\n    setup?: SetupCallback;\n    skin?: boolean | string;\n    skin_url?: string;\n    statusbar?: boolean;\n    style_formats?: AllowedFormat[];\n    style_formats_autohide?: boolean;\n    style_formats_merge?: boolean;\n    submit_patch?: boolean;\n    suffix?: string;\n    target?: HTMLElement;\n    theme?: string | ThemeInitFunc;\n    theme_url?: string;\n    toolbar?: boolean | string | string[] | Array<ToolbarGroup>;\n    toolbar1?: string;\n    toolbar2?: string;\n    toolbar3?: string;\n    toolbar4?: string;\n    toolbar5?: string;\n    toolbar6?: string;\n    toolbar7?: string;\n    toolbar8?: string;\n    toolbar9?: string;\n    toolbar_mode?: ToolbarMode;\n    typeahead_urls?: boolean;\n    url_converter?: URLConverter;\n    url_converter_scope?: any;\n    urlconverter_callback?: string | URLConverterCallback;\n    valid_children?: string;\n    valid_classes?: string | Record<string, string>;\n    valid_elements?: string;\n    valid_styles?: string | Record<string, string>;\n    verify_html?: boolean;\n    visual?: boolean;\n    visual_anchor_class?: string;\n    visual_table_class?: string;\n    width?: number | string;\n    toolbar_drawer?: false | 'floating' | 'sliding' | 'scrolling';\n    editor_deselector?: string;\n    editor_selector?: string;\n    elements?: string;\n    filepicker_validator_handler?: FilePickerValidationCallback;\n    mode?: 'exact' | 'textareas' | 'specific_textareas';\n    types?: Record<string, any>[];\n    block_elements?: string;\n    boolean_attributes?: string;\n    move_caret_before_on_enter_elements?: string;\n    non_empty_elements?: string;\n    self_closing_elements?: string;\n    short_ended_elements?: string;\n    text_block_elements?: string;\n    text_inline_elements?: string;\n    whitespace_elements?: string;\n    special?: string;\n    disable_nodechange?: boolean;\n    forced_plugins?: string | string[];\n    plugin_base_urls?: Record<string, string>;\n    service_message?: string;\n    validate?: boolean;\n    [key: string]: any;\n}\ninterface RawEditorSettings extends BaseEditorSettings {\n    external_plugins?: Record<string, string>;\n    mobile?: RawEditorSettings;\n    plugins?: string | string[];\n}\ninterface EditorSettings extends BaseEditorSettings {\n    external_plugins: Record<string, string>;\n    plugins: string;\n}\ninterface ParamTypeMap {\n    'hash': Record<string, string>;\n    'string': string;\n    'number': number;\n    'boolean': boolean;\n    'string[]': string[];\n    'array': any[];\n}\ninterface BlobInfoImagePair {\n    image: HTMLImageElement;\n    blobInfo: BlobInfo;\n}\ndeclare class NodeChange {\n    private readonly editor;\n    private lastPath;\n    constructor(editor: Editor);\n    nodeChanged(args?: any): void;\n    private isSameElementPath;\n}\ninterface SelectionOverrides {\n    showCaret: (direction: number, node: Element, before: boolean, scrollIntoView?: boolean) => Range | null;\n    showBlockCaretContainer: (blockCaretContainer: Element) => void;\n    hideFakeCaret: () => void;\n    destroy: () => void;\n}\ninterface Quirks {\n    refreshContentEditable(): void;\n    isHidden(): boolean;\n}\ndeclare type DecoratorData = Record<string, any>;\ndeclare type Decorator = (uid: string, data: DecoratorData) => {\n    attributes?: {};\n    classes?: string[];\n};\ndeclare type AnnotationListener = (state: boolean, name: string, data?: {\n    uid: string;\n    nodes: any[];\n}) => void;\ndeclare type AnnotationListenerApi = AnnotationListener;\ninterface AnnotatorSettings {\n    decorate: Decorator;\n    persistent?: boolean;\n}\ninterface Annotator {\n    register: (name: string, settings: AnnotatorSettings) => void;\n    annotate: (name: string, data: DecoratorData) => void;\n    annotationChanged: (name: string, f: AnnotationListenerApi) => void;\n    remove: (name: string) => void;\n    getAll: (name: string) => Record<string, Element[]>;\n}\ninterface GeomRect {\n    readonly x: number;\n    readonly y: number;\n    readonly w: number;\n    readonly h: number;\n}\ninterface Rect {\n    inflate: (rect: GeomRect, w: number, h: number) => GeomRect;\n    relativePosition: (rect: GeomRect, targetRect: GeomRect, rel: string) => GeomRect;\n    findBestRelativePosition: (rect: GeomRect, targetRect: GeomRect, constrainRect: GeomRect, rels: string[]) => string | null;\n    intersect: (rect: GeomRect, cropRect: GeomRect) => GeomRect | null;\n    clamp: (rect: GeomRect, clampRect: GeomRect, fixedSize?: boolean) => GeomRect;\n    create: (x: number, y: number, w: number, h: number) => GeomRect;\n    fromClientRect: (clientRect: DOMRect) => GeomRect;\n}\ninterface StyleMap {\n    [s: string]: string | number;\n}\ninterface StylesSettings {\n    allow_script_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    url_converter?: URLConverter;\n    url_converter_scope?: any;\n}\ninterface Styles {\n    toHex: (color: string) => string;\n    parse: (css: string) => Record<string, string>;\n    serialize: (styles: StyleMap, elementName?: string) => string;\n}\ninterface DOMUtilsSettings {\n    schema: Schema;\n    url_converter: URLConverter;\n    url_converter_scope: any;\n    ownEvents: boolean;\n    keep_values: boolean;\n    hex_colors: boolean;\n    update_styles: boolean;\n    root_element: HTMLElement;\n    collect: Function;\n    onSetAttrib: Function;\n    contentCssCors: boolean;\n    referrerPolicy: ReferrerPolicy;\n}\ndeclare type Target = Node | Window;\ndeclare type RunArguments<T extends Node = Node> = string | T | Array<string | T>;\ndeclare type BoundEvent = [\n    Target,\n    string,\n    EventUtilsCallback<any>,\n    any\n];\ndeclare type Callback<K extends string> = EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>;\ninterface DOMUtils {\n    doc: Document;\n    settings: Partial<DOMUtilsSettings>;\n    win: Window;\n    files: Record<string, boolean>;\n    stdMode: boolean;\n    boxModel: boolean;\n    styleSheetLoader: StyleSheetLoader;\n    boundEvents: BoundEvent[];\n    styles: Styles;\n    schema: Schema;\n    events: EventUtils;\n    root: Node;\n    $: DomQueryConstructor;\n    $$: {\n        <T extends Node>(elm: T | T[] | DomQuery<T>): DomQuery<T>;\n        (elm: string): DomQuery<Node>;\n    };\n    isBlock: (node: string | Node) => boolean;\n    clone: (node: Node, deep: boolean) => Node;\n    getRoot: () => HTMLElement;\n    getViewPort: (argWin?: Window) => GeomRect;\n    getRect: (elm: string | HTMLElement) => GeomRect;\n    getSize: (elm: string | HTMLElement) => {\n        w: number;\n        h: number;\n    };\n    getParent: {\n        <K extends keyof HTMLElementTagNameMap>(node: string | Node, selector: K, root?: Node): HTMLElementTagNameMap[K] | null;\n        <T extends HTMLElement>(node: string | Node, selector: (node: HTMLElement) => node is T, root?: Node): T | null;\n        <T extends Element = Element>(node: string | Node, selector?: string | ((node: HTMLElement) => boolean | void), root?: Node): T | null;\n    };\n    getParents: {\n        <K extends keyof HTMLElementTagNameMap>(elm: string | Node, selector: K, root?: Node, collect?: boolean): Array<HTMLElementTagNameMap[K]>;\n        <T extends HTMLElement>(node: string | Node, selector: (node: HTMLElement) => node is T, root?: Node): T[];\n        <T extends Element = Element>(elm: string | Node, selector?: string | ((node: HTMLElement) => boolean | void), root?: Node, collect?: boolean): T[];\n    };\n    get: (elm: string | Node) => HTMLElement | null;\n    getNext: (node: Node, selector: string | ((node: Node) => boolean)) => Node | null;\n    getPrev: (node: Node, selector: string | ((node: Node) => boolean)) => Node | null;\n    select: {\n        <K extends keyof HTMLElementTagNameMap>(selector: K, scope?: string | Node): Array<HTMLElementTagNameMap[K]>;\n        <T extends HTMLElement = HTMLElement>(selector: string, scope?: string | Node): T[];\n    };\n    is: (elm: Node | Node[], selector: string) => boolean;\n    add: (parentElm: RunArguments, name: string | Node, attrs?: Record<string, string | boolean | number>, html?: string | Node, create?: boolean) => HTMLElement;\n    create: {\n        <K extends keyof HTMLElementTagNameMap>(name: K, attrs?: Record<string, string | boolean | number>, html?: string | Node): HTMLElementTagNameMap[K];\n        (name: string, attrs?: Record<string, string | boolean | number>, html?: string | Node): HTMLElement;\n    };\n    createHTML: (name: string, attrs?: Record<string, string>, html?: string) => string;\n    createFragment: (html?: string) => DocumentFragment;\n    remove: <T extends Node>(node: string | T | T[] | DomQuery<T>, keepChildren?: boolean) => T | T[];\n    setStyle: {\n        (elm: string | Node | Node[], name: string, value: string | number | null): void;\n        (elm: string | Node | Node[], styles: StyleMap): void;\n    };\n    getStyle: (elm: string | Node, name: string, computed?: boolean) => string;\n    setStyles: (elm: string | Node | Node[], stylesArg: StyleMap) => void;\n    removeAllAttribs: (e: RunArguments<Element>) => void;\n    setAttrib: (elm: string | Node | Node[], name: string, value: string | boolean | number | null) => void;\n    setAttribs: (elm: string | Node | Node[], attrs: Record<string, string | boolean | number | null>) => void;\n    getAttrib: (elm: string | Node, name: string, defaultVal?: string) => string;\n    getPos: (elm: string | Node, rootElm?: Node) => {\n        x: number;\n        y: number;\n    };\n    parseStyle: (cssText: string) => Record<string, string>;\n    serializeStyle: (stylesArg: StyleMap, name?: string) => string;\n    addStyle: (cssText: string) => void;\n    loadCSS: (url: string) => void;\n    addClass: (elm: string | Node | Node[], cls: string) => void;\n    removeClass: (elm: string | Node | Node[], cls: string) => void;\n    hasClass: (elm: string | Node, cls: string) => boolean;\n    toggleClass: (elm: string | Node | Node[], cls: string, state?: boolean) => void;\n    show: (elm: string | Node | Node[]) => void;\n    hide: (elm: string | Node | Node[]) => void;\n    isHidden: (elm: string | Node) => boolean;\n    uniqueId: (prefix?: string) => string;\n    setHTML: (elm: string | Node | Node[], html: string) => void;\n    getOuterHTML: (elm: string | Node) => string;\n    setOuterHTML: (elm: string | Node | Node[], html: string) => void;\n    decode: (text: string) => string;\n    encode: (text: string) => string;\n    insertAfter: {\n        <T extends Node>(node: T | T[], reference: string | Node): T;\n        <T extends Node>(node: RunArguments<T>, reference: string | Node): false | T;\n    };\n    replace: {\n        <T extends Node>(newElm: Node, oldElm: T | T[], keepChildren?: boolean): T;\n        <T extends Node>(newElm: Node, oldElm: RunArguments<T>, keepChildren?: boolean): false | T;\n    };\n    rename: {\n        <K extends keyof HTMLElementTagNameMap>(elm: Element, name: K): HTMLElementTagNameMap[K];\n        (elm: Element, name: string): Element;\n    };\n    findCommonAncestor: (a: Node, b: Node) => Node;\n    toHex: (rgbVal: string) => string;\n    run<R, T extends Node>(this: DOMUtils, elm: T | T[], func: (node: T) => R, scope?: any): R;\n    run<R, T extends Node>(this: DOMUtils, elm: RunArguments<T>, func: (node: T) => R, scope?: any): false | R;\n    getAttribs: (elm: string | Node) => NamedNodeMap | Attr[];\n    isEmpty: (node: Node, elements?: Record<string, any>) => boolean;\n    createRng: () => Range;\n    nodeIndex: (node: Node, normalized?: boolean) => number;\n    split: {\n        <T extends Node>(parentElm: Node, splitElm: Node, replacementElm: T): T;\n        <T extends Node>(parentElm: Node, splitElm: T): T;\n    };\n    bind: {\n        <K extends string>(target: Target, name: K, func: Callback<K>, scope?: any): Callback<K>;\n        <K extends string>(target: Target[], name: K, func: Callback<K>, scope?: any): Callback<K>[];\n    };\n    unbind: {\n        <K extends string>(target: Target, name?: K, func?: EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>): EventUtils;\n        <K extends string>(target: Target[], name?: K, func?: EventUtilsCallback<MappedEvent<HTMLElementEventMap, K>>): EventUtils[];\n    };\n    fire: (target: Node | Window, name: string, evt?: {}) => EventUtils;\n    getContentEditable: (node: Node) => string | null;\n    getContentEditableParent: (node: Node) => string | null;\n    destroy: () => void;\n    isChildOf: (node: Node, parent: Node) => boolean;\n    dumpRng: (r: Range) => string;\n}\ninterface ClientRect {\n    left: number;\n    top: number;\n    bottom: number;\n    right: number;\n    width: number;\n    height: number;\n}\ninterface GetSelectionContentArgs extends GetContentArgs {\n    selection?: boolean;\n    contextual?: boolean;\n}\ninterface SelectionSetContentArgs extends SetContentArgs {\n    selection?: boolean;\n}\ninterface BookmarkManager {\n    getBookmark: (type: number, normalized?: boolean) => Bookmark;\n    moveToBookmark: (bookmark: Bookmark) => void;\n}\ninterface ControlSelection {\n    isResizable: (elm: Element) => boolean;\n    showResizeRect: (elm: Element) => void;\n    hideResizeRect: () => void;\n    updateResizeRect: (evt: EditorEvent<any>) => void;\n    destroy: () => void;\n}\ninterface ParserArgs {\n    getInner?: boolean | number;\n    forced_root_block?: boolean | string;\n    context?: string;\n    isRootContent?: boolean;\n    format?: string;\n    invalid?: boolean;\n    no_events?: boolean;\n    [key: string]: any;\n}\ndeclare type ParserFilterCallback = (nodes: AstNode[], name: string, args: ParserArgs) => void;\ninterface ParserFilter {\n    name: string;\n    callbacks: ParserFilterCallback[];\n}\ninterface DomParserSettings {\n    allow_html_data_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    allow_conditional_comments?: boolean;\n    allow_html_in_named_anchor?: boolean;\n    allow_script_urls?: boolean;\n    allow_unsafe_link_target?: boolean;\n    convert_fonts_to_spans?: boolean;\n    fix_list_elements?: boolean;\n    font_size_legacy_values?: string;\n    forced_root_block?: boolean | string;\n    forced_root_block_attrs?: Record<string, string>;\n    padd_empty_with_br?: boolean;\n    preserve_cdata?: boolean;\n    remove_trailing_brs?: boolean;\n    root_name?: string;\n    validate?: boolean;\n    inline_styles?: boolean;\n    blob_cache?: BlobCache;\n    document?: Document;\n    images_dataimg_filter?: (img: HTMLImageElement) => boolean;\n}\ninterface DomParser {\n    schema: Schema;\n    addAttributeFilter: (name: string, callback: (nodes: AstNode[], name: string, args: ParserArgs) => void) => void;\n    getAttributeFilters: () => ParserFilter[];\n    addNodeFilter: (name: string, callback: (nodes: AstNode[], name: string, args: ParserArgs) => void) => void;\n    getNodeFilters: () => ParserFilter[];\n    filterNode: (node: AstNode) => AstNode;\n    parse: (html: string, args?: ParserArgs) => AstNode;\n}\ninterface WriterSettings {\n    element_format?: 'xhtml' | 'html';\n    entities?: string;\n    entity_encoding?: EntityEncoding;\n    indent?: boolean;\n    indent_after?: string;\n    indent_before?: string;\n}\ndeclare type Attributes = Array<{\n    name: string;\n    value: string;\n}>;\ninterface Writer {\n    cdata: (text: string) => void;\n    comment: (text: string) => void;\n    doctype: (text: string) => void;\n    end: (name: string) => void;\n    getContent: () => string;\n    pi: (name: string, text?: string) => void;\n    reset: () => void;\n    start: (name: string, attrs?: Attributes, empty?: boolean) => void;\n    text: (text: string, raw?: boolean) => void;\n}\ninterface HtmlSerializerSettings extends WriterSettings {\n    inner?: boolean;\n    validate?: boolean;\n}\ninterface HtmlSerializer {\n    serialize: (node: AstNode) => string;\n}\ninterface DomSerializerSettings extends DomParserSettings, WriterSettings, SchemaSettings, HtmlSerializerSettings {\n    url_converter?: URLConverter;\n    url_converter_scope?: {};\n}\ninterface DomSerializerImpl {\n    schema: Schema;\n    addNodeFilter: (name: string, callback: (nodes: AstNode[], name: string, args: ParserArgs) => void) => void;\n    addAttributeFilter: (name: string, callback: (nodes: AstNode[], name: string, args: ParserArgs) => void) => void;\n    getNodeFilters: () => ParserFilter[];\n    getAttributeFilters: () => ParserFilter[];\n    serialize: {\n        (node: Element, parserArgs: {\n            format: 'tree';\n        } & ParserArgs): AstNode;\n        (node: Element, parserArgs?: ParserArgs): string;\n    };\n    addRules: (rules: string) => void;\n    setRules: (rules: string) => void;\n    addTempAttr: (name: string) => void;\n    getTempAttrs: () => string[];\n}\ninterface DomSerializer extends DomSerializerImpl {\n}\ninterface EditorSelection {\n    bookmarkManager: BookmarkManager;\n    controlSelection: ControlSelection;\n    dom: DOMUtils;\n    win: Window;\n    serializer: DomSerializer;\n    editor: Editor;\n    collapse: (toStart?: boolean) => void;\n    setCursorLocation: {\n        (node: Node, offset: number): void;\n        (): void;\n    };\n    getContent: {\n        (args: {\n            format: 'tree';\n        } & GetSelectionContentArgs): AstNode;\n        (args?: GetSelectionContentArgs): string;\n    };\n    setContent: (content: string, args?: SelectionSetContentArgs) => void;\n    getBookmark: (type?: number, normalized?: boolean) => Bookmark;\n    moveToBookmark: (bookmark: Bookmark) => void;\n    select: (node: Node, content?: boolean) => Node;\n    isCollapsed: () => boolean;\n    isForward: () => boolean;\n    setNode: (elm: Element) => Element;\n    getNode: () => Element;\n    getSel: () => Selection | null;\n    setRng: (rng: Range, forward?: boolean) => void;\n    getRng: () => Range;\n    getStart: (real?: boolean) => Element;\n    getEnd: (real?: boolean) => Element;\n    getSelectedBlocks: (startElm?: Element, endElm?: Element) => Element[];\n    normalize: () => Range;\n    selectorChanged: (selector: string, callback: (active: boolean, args: {\n        node: Node;\n        selector: String;\n        parents: Element[];\n    }) => void) => EditorSelection;\n    selectorChangedWithUnbind: (selector: string, callback: (active: boolean, args: {\n        node: Node;\n        selector: String;\n        parents: Element[];\n    }) => void) => {\n        unbind: () => void;\n    };\n    getScrollContainer: () => HTMLElement;\n    scrollIntoView: (elm?: HTMLElement, alignToTop?: boolean) => void;\n    placeCaretAt: (clientX: number, clientY: number) => void;\n    getBoundingClientRect: () => ClientRect | DOMRect;\n    destroy: () => void;\n}\ndeclare type EditorCommandCallback = (ui: boolean, value: any, args: any) => void;\ndeclare type EditorCommandsCallback = (command: string, ui: boolean, value: any, args: any) => void;\ninterface Commands {\n    state: Record<string, (command: string) => boolean>;\n    exec: Record<string, EditorCommandsCallback>;\n    value: Record<string, (command: string) => string>;\n}\ninterface EditorCommandsConstructor {\n    readonly prototype: EditorCommands;\n    new (editor: Editor): EditorCommands;\n}\ndeclare class EditorCommands {\n    private readonly editor;\n    private selectionBookmark;\n    private commands;\n    constructor(editor: Editor);\n    execCommand(command: string, ui?: boolean, value?: any, args?: any): boolean;\n    queryCommandState(command: string): boolean;\n    queryCommandValue(command: string): string;\n    addCommands<K extends keyof Commands>(commandList: Commands[K], type: K): void;\n    addCommands(commandList: Record<string, EditorCommandsCallback>): void;\n    addCommand(command: string, callback: EditorCommandCallback, scope?: any): void;\n    queryCommandSupported(command: string): boolean;\n    addQueryStateHandler(command: string, callback: () => boolean, scope?: any): void;\n    addQueryValueHandler(command: string, callback: () => string, scope?: any): void;\n    hasCustomCommand(command: string): boolean;\n    private execNativeCommand;\n    private isFormatMatch;\n    private toggleFormat;\n    private storeSelection;\n    private restoreSelection;\n    private setupCommands;\n}\ninterface WindowParams {\n    readonly inline?: 'cursor' | 'toolbar';\n    readonly ariaAttrs?: boolean;\n}\ndeclare type InstanceApi<T> = UrlDialogInstanceApi | DialogInstanceApi<T>;\ninterface WindowManagerImpl {\n    open: <T>(config: DialogSpec<T>, params: WindowParams, closeWindow: (dialog: DialogInstanceApi<T>) => void) => DialogInstanceApi<T>;\n    openUrl: (config: UrlDialogSpec, closeWindow: (dialog: UrlDialogInstanceApi) => void) => UrlDialogInstanceApi;\n    alert: (message: string, callback: () => void) => void;\n    confirm: (message: string, callback: (state: boolean) => void) => void;\n    close: (dialog: InstanceApi<any>) => void;\n}\ninterface WindowManager {\n    open: <T>(config: DialogSpec<T>, params?: WindowParams) => DialogInstanceApi<T>;\n    openUrl: (config: UrlDialogSpec) => UrlDialogInstanceApi;\n    alert: (message: string, callback?: () => void, scope?: any) => void;\n    confirm: (message: string, callback?: (state: boolean) => void, scope?: any) => void;\n    close: () => void;\n}\ninterface ExecCommandEvent {\n    command: string;\n    ui?: boolean;\n    value?: any;\n}\ndeclare type GetContentEvent = GetContentArgs & {\n    source_view?: boolean;\n    selection?: boolean;\n    save?: boolean;\n};\ndeclare type SetContentEvent = SetContentArgs & {\n    source_view?: boolean;\n    paste?: boolean;\n    selection?: boolean;\n};\ninterface NewBlockEvent {\n    newBlock: Element;\n}\ninterface NodeChangeEvent {\n    element: Element;\n    parents: Node[];\n    selectionChange?: boolean;\n    initial?: boolean;\n}\ninterface FormatEvent {\n    format: string;\n    vars?: FormatVars;\n    node?: Node | RangeLikeObject;\n}\ninterface ObjectResizeEvent {\n    target: HTMLElement;\n    width: number;\n    height: number;\n    origin: string;\n}\ninterface ObjectSelectedEvent {\n    target: Node;\n    targetClone?: Node;\n}\ninterface ScrollIntoViewEvent {\n    elm: HTMLElement;\n    alignToTop: boolean;\n}\ninterface SetSelectionRangeEvent {\n    range: Range;\n    forward: boolean;\n}\ninterface ShowCaretEvent {\n    target: Node;\n    direction: number;\n    before: boolean;\n}\ninterface SwitchModeEvent {\n    mode: string;\n}\ninterface AddUndoEvent {\n    level: UndoLevel;\n    lastLevel: UndoLevel;\n    originalEvent: Event;\n}\ninterface UndoRedoEvent {\n    level: UndoLevel;\n}\ninterface WindowEvent<T extends DialogData> {\n    dialog: InstanceApi<T>;\n}\ninterface ProgressStateEvent {\n    state: boolean;\n    time?: number;\n}\ninterface AfterProgressStateEvent {\n    state: boolean;\n}\ninterface PlaceholderToggleEvent {\n    state: boolean;\n}\ninterface LoadErrorEvent {\n    message: string;\n}\ninterface PreProcessEvent extends ParserArgs {\n    node: Element;\n}\ninterface PostProcessEvent extends ParserArgs {\n    content: string;\n}\ninterface EditorEventMap extends Omit<NativeEventMap, 'blur' | 'focus'> {\n    'activate': {\n        relatedTarget: Editor;\n    };\n    'deactivate': {\n        relatedTarget: Editor;\n    };\n    'focus': {\n        blurredEditor: Editor;\n    };\n    'blur': {\n        focusedEditor: Editor;\n    };\n    'resize': UIEvent;\n    'scroll': UIEvent;\n    'detach': {};\n    'remove': {};\n    'init': {};\n    'ScrollIntoView': ScrollIntoViewEvent;\n    'AfterScrollIntoView': ScrollIntoViewEvent;\n    'ObjectResized': ObjectResizeEvent;\n    'ObjectResizeStart': ObjectResizeEvent;\n    'SwitchMode': SwitchModeEvent;\n    'ScrollWindow': UIEvent;\n    'ResizeWindow': UIEvent;\n    'SkinLoaded': {};\n    'SkinLoadError': LoadErrorEvent;\n    'PluginLoadError': LoadErrorEvent;\n    'IconsLoadError': LoadErrorEvent;\n    'LanguageLoadError': LoadErrorEvent;\n    'BeforeExecCommand': ExecCommandEvent;\n    'ExecCommand': ExecCommandEvent;\n    'NodeChange': NodeChangeEvent;\n    'FormatApply': FormatEvent;\n    'FormatRemove': FormatEvent;\n    'ShowCaret': ShowCaretEvent;\n    'SelectionChange': {};\n    'ObjectSelected': ObjectSelectedEvent;\n    'BeforeObjectSelected': ObjectSelectedEvent;\n    'GetSelectionRange': {\n        range: Range;\n    };\n    'SetSelectionRange': SetSelectionRangeEvent;\n    'AfterSetSelectionRange': SetSelectionRangeEvent;\n    'BeforeGetContent': GetContentEvent;\n    'GetContent': GetContentEvent;\n    'BeforeSetContent': SetContentEvent;\n    'SetContent': SetContentEvent;\n    'LoadContent': {};\n    'PreviewFormats': {};\n    'AfterPreviewFormats': {};\n    'ScriptsLoaded': {};\n    'PreInit': {};\n    'PostRender': {};\n    'NewBlock': NewBlockEvent;\n    'ClearUndos': {};\n    'TypingUndo': {};\n    'Redo': UndoRedoEvent;\n    'Undo': UndoRedoEvent;\n    'BeforeAddUndo': AddUndoEvent;\n    'AddUndo': AddUndoEvent;\n    'CloseWindow': WindowEvent<any>;\n    'OpenWindow': WindowEvent<any>;\n    'ProgressState': ProgressStateEvent;\n    'AfterProgressState': AfterProgressStateEvent;\n    'PlaceholderToggle': PlaceholderToggleEvent;\n    'tap': TouchEvent;\n    'longpress': TouchEvent;\n    'longpresscancel': {};\n    'PreProcess': PreProcessEvent;\n    'PostProcess': PostProcessEvent;\n}\ninterface EditorManagerEventMap {\n    'AddEditor': {\n        editor: Editor;\n    };\n    'RemoveEditor': {\n        editor: Editor;\n    };\n    'BeforeUnload': {\n        returnValue: any;\n    };\n}\ntype EventTypes_d_ExecCommandEvent = ExecCommandEvent;\ntype EventTypes_d_GetContentEvent = GetContentEvent;\ntype EventTypes_d_SetContentEvent = SetContentEvent;\ntype EventTypes_d_NewBlockEvent = NewBlockEvent;\ntype EventTypes_d_NodeChangeEvent = NodeChangeEvent;\ntype EventTypes_d_FormatEvent = FormatEvent;\ntype EventTypes_d_ObjectResizeEvent = ObjectResizeEvent;\ntype EventTypes_d_ObjectSelectedEvent = ObjectSelectedEvent;\ntype EventTypes_d_ScrollIntoViewEvent = ScrollIntoViewEvent;\ntype EventTypes_d_SetSelectionRangeEvent = SetSelectionRangeEvent;\ntype EventTypes_d_ShowCaretEvent = ShowCaretEvent;\ntype EventTypes_d_SwitchModeEvent = SwitchModeEvent;\ntype EventTypes_d_AddUndoEvent = AddUndoEvent;\ntype EventTypes_d_UndoRedoEvent = UndoRedoEvent;\ntype EventTypes_d_WindowEvent<_0> = WindowEvent<_0>;\ntype EventTypes_d_ProgressStateEvent = ProgressStateEvent;\ntype EventTypes_d_AfterProgressStateEvent = AfterProgressStateEvent;\ntype EventTypes_d_PlaceholderToggleEvent = PlaceholderToggleEvent;\ntype EventTypes_d_LoadErrorEvent = LoadErrorEvent;\ntype EventTypes_d_PreProcessEvent = PreProcessEvent;\ntype EventTypes_d_PostProcessEvent = PostProcessEvent;\ntype EventTypes_d_EditorEventMap = EditorEventMap;\ntype EventTypes_d_EditorManagerEventMap = EditorManagerEventMap;\ndeclare namespace EventTypes_d {\n    export { EventTypes_d_ExecCommandEvent as ExecCommandEvent, EventTypes_d_GetContentEvent as GetContentEvent, EventTypes_d_SetContentEvent as SetContentEvent, EventTypes_d_NewBlockEvent as NewBlockEvent, EventTypes_d_NodeChangeEvent as NodeChangeEvent, EventTypes_d_FormatEvent as FormatEvent, EventTypes_d_ObjectResizeEvent as ObjectResizeEvent, EventTypes_d_ObjectSelectedEvent as ObjectSelectedEvent, EventTypes_d_ScrollIntoViewEvent as ScrollIntoViewEvent, EventTypes_d_SetSelectionRangeEvent as SetSelectionRangeEvent, EventTypes_d_ShowCaretEvent as ShowCaretEvent, EventTypes_d_SwitchModeEvent as SwitchModeEvent, EventTypes_d_AddUndoEvent as AddUndoEvent, EventTypes_d_UndoRedoEvent as UndoRedoEvent, EventTypes_d_WindowEvent as WindowEvent, EventTypes_d_ProgressStateEvent as ProgressStateEvent, EventTypes_d_AfterProgressStateEvent as AfterProgressStateEvent, EventTypes_d_PlaceholderToggleEvent as PlaceholderToggleEvent, EventTypes_d_LoadErrorEvent as LoadErrorEvent, EventTypes_d_PreProcessEvent as PreProcessEvent, EventTypes_d_PostProcessEvent as PostProcessEvent, EventTypes_d_EditorEventMap as EditorEventMap, EventTypes_d_EditorManagerEventMap as EditorManagerEventMap, };\n}\ninterface RawString {\n    raw: string;\n}\ndeclare type Primitive = string | number | boolean | Record<string | number, any> | Function;\ndeclare type TokenisedString = [\n    string,\n    ...Primitive[]\n];\ndeclare type Untranslated = Primitive | TokenisedString | RawString;\ndeclare type TranslatedString = string;\ninterface I18n {\n    getData: () => Record<string, Record<string, string>>;\n    setCode: (newCode: string) => void;\n    getCode: () => string;\n    add: (code: string, items: Record<string, string>) => void;\n    translate: (text: Untranslated) => TranslatedString;\n    isRtl: () => boolean;\n    hasCode: (code: string) => boolean;\n}\ninterface Observable<T> {\n    fire<K extends string, U extends MappedEvent<T, K>>(name: K, args?: U, bubble?: boolean): EditorEvent<U>;\n    on<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void, prepend?: boolean): EventDispatcher<T>;\n    off<K extends string>(name?: K, callback?: (event: EditorEvent<MappedEvent<T, K>>) => void): EventDispatcher<T>;\n    once<K extends string>(name: K, callback: (event: EditorEvent<MappedEvent<T, K>>) => void): EventDispatcher<T>;\n    hasEventListeners(name: string): boolean;\n}\ninterface URISettings {\n    base_uri?: URI;\n}\ninterface URIConstructor {\n    readonly prototype: URI;\n    new (url: string, settings?: URISettings): URI;\n    getDocumentBaseUrl: (loc: {\n        protocol: string;\n        host?: string;\n        href?: string;\n        pathname?: string;\n    }) => string;\n    parseDataUri: (uri: string) => {\n        type: string;\n        data: string;\n    };\n}\ninterface SafeUriOptions {\n    readonly allow_html_data_urls?: boolean;\n    readonly allow_script_urls?: boolean;\n    readonly allow_svg_data_urls?: boolean;\n}\ndeclare class URI {\n    static parseDataUri(uri: string): {\n        type: string;\n        data: string;\n    };\n    static isDomSafe(uri: string, context?: string, options?: SafeUriOptions): boolean;\n    static getDocumentBaseUrl(loc: {\n        protocol: string;\n        host?: string;\n        href?: string;\n        pathname?: string;\n    }): string;\n    source: string;\n    protocol: string;\n    authority: string;\n    userInfo: string;\n    user: string;\n    password: string;\n    host: string;\n    port: string;\n    relative: string;\n    path: string;\n    directory: string;\n    file: string;\n    query: string;\n    anchor: string;\n    settings: URISettings;\n    constructor(url: string, settings?: URISettings);\n    setPath(path: string): void;\n    toRelative(uri: string): string;\n    toAbsolute(uri: string, noHost?: boolean): string;\n    isSameOrigin(uri: URI): boolean;\n    toRelPath(base: string, path: string): string;\n    toAbsPath(base: string, path: string): string;\n    getURI(noProtoHost?: boolean): string;\n}\ninterface EditorManager extends Observable<EditorManagerEventMap> {\n    $: DomQueryConstructor;\n    defaultSettings: RawEditorSettings;\n    majorVersion: string;\n    minorVersion: string;\n    releaseDate: string;\n    editors: Editor[];\n    activeEditor: Editor;\n    focusedEditor: Editor;\n    settings: RawEditorSettings;\n    baseURI: URI;\n    baseURL: string;\n    documentBaseURL: string;\n    i18n: I18n;\n    suffix: string;\n    add(this: EditorManager, editor: Editor): Editor;\n    addI18n: (code: string, item: Record<string, string>) => void;\n    createEditor(this: EditorManager, id: string, settings: RawEditorSettings): Editor;\n    execCommand(this: EditorManager, cmd: string, ui: boolean, value: any): boolean;\n    get(this: EditorManager): Editor[];\n    get(this: EditorManager, id: number | string): Editor;\n    init(this: EditorManager, settings: RawEditorSettings): Promise<Editor[]>;\n    overrideDefaults(this: EditorManager, defaultSettings: Partial<RawEditorSettings>): void;\n    remove(this: EditorManager): void;\n    remove(this: EditorManager, selector: string | Editor): Editor | void;\n    setActive(this: EditorManager, editor: Editor): void;\n    setup(this: EditorManager): void;\n    translate: (text: Untranslated) => TranslatedString;\n    triggerSave: () => void;\n    _setBaseUrl(this: EditorManager, baseUrl: string): void;\n}\ninterface EditorObservable extends Observable<EditorEventMap> {\n    bindPendingEventDelegates(this: Editor): void;\n    toggleNativeEvent(this: Editor, name: string, state: boolean): any;\n    unbindAllNativeEvents(this: Editor): void;\n}\ninterface UploadResult$1 {\n    element: HTMLImageElement;\n    status: boolean;\n    blobInfo: BlobInfo;\n    uploadUri: string;\n}\ndeclare type UploadCallback = (results: UploadResult$1[]) => void;\ninterface EditorUpload {\n    blobCache: BlobCache;\n    addFilter: (filter: (img: HTMLImageElement) => boolean) => void;\n    uploadImages: (callback?: UploadCallback) => Promise<UploadResult$1[]>;\n    uploadImagesAuto: (callback?: UploadCallback) => void | Promise<UploadResult$1[]>;\n    scanForImages: () => Promise<BlobInfoImagePair[]>;\n    destroy: () => void;\n}\ndeclare type FormatChangeCallback = (state: boolean, data: {\n    node: Node;\n    format: string;\n    parents: any;\n}) => void;\ninterface FormatRegistry {\n    get: {\n        (name: string): Format[] | undefined;\n        (): Record<string, Format[]>;\n    };\n    has: (name: string) => boolean;\n    register: (name: string | Formats, format?: Format[] | Format) => void;\n    unregister: (name: string) => Formats;\n}\ninterface Formatter extends FormatRegistry {\n    apply: (name: string, vars?: FormatVars, node?: Node | RangeLikeObject) => void;\n    remove: (name: string, vars?: FormatVars, node?: Node | Range, similar?: boolean) => void;\n    toggle: (name: string, vars?: FormatVars, node?: Node) => void;\n    match: (name: string, vars?: FormatVars, node?: Node, similar?: boolean) => boolean;\n    closest: (names: string[]) => string | null;\n    matchAll: (names: string[], vars?: FormatVars) => string[];\n    matchNode: (node: Node, name: string, vars?: FormatVars, similar?: boolean) => Format | undefined;\n    canApply: (name: string) => boolean;\n    formatChanged: (names: string, callback: FormatChangeCallback, similar?: boolean, vars?: FormatVars) => {\n        unbind: () => void;\n    };\n    getCssText: (format: string | Format) => string;\n}\ninterface EditorMode {\n    isReadOnly: () => boolean;\n    set: (mode: string) => void;\n    get: () => string;\n    register: (mode: string, api: EditorModeApi) => void;\n}\ninterface EditorModeApi {\n    activate: () => void;\n    deactivate: () => void;\n    editorReadOnly: boolean;\n}\ninterface Plugin {\n    getMetadata?: () => {\n        name: string;\n        url: string;\n    };\n    [key: string]: any;\n}\ndeclare type PluginManager = AddOnManager<Plugin>;\ninterface ShortcutsConstructor {\n    readonly prototype: Shortcuts;\n    new (editor: Editor): Shortcuts;\n}\ndeclare type CommandFunc = string | [\n    string,\n    boolean,\n    any\n] | (() => void);\ndeclare class Shortcuts {\n    private readonly editor;\n    private readonly shortcuts;\n    private pendingPatterns;\n    constructor(editor: Editor);\n    add(pattern: string, desc: string, cmdFunc: CommandFunc, scope?: any): boolean;\n    remove(pattern: string): boolean;\n    private normalizeCommandFunc;\n    private createShortcut;\n    private hasModifier;\n    private isFunctionKey;\n    private matchShortcut;\n    private executeShortcutAction;\n}\ninterface Theme {\n    ui?: any;\n    inline?: any;\n    execCommand?: (command: string, ui?: boolean, value?: any) => boolean;\n    destroy?: () => void;\n    init?: (editor: Editor, url: string, $: DomQueryConstructor) => void;\n    renderUI?: () => {\n        iframeContainer?: HTMLIFrameElement;\n        editorContainer: HTMLElement;\n        api?: Partial<EditorUiApi>;\n    };\n    getNotificationManagerImpl?: () => NotificationManagerImpl;\n    getWindowManagerImpl?: () => WindowManagerImpl;\n}\ndeclare type ThemeManager = AddOnManager<Theme>;\ninterface EditorConstructor {\n    readonly prototype: Editor;\n    new (id: string, settings: RawEditorSettings, editorManager: EditorManager): Editor;\n}\ndeclare class Editor implements EditorObservable {\n    documentBaseUrl: string;\n    baseUri: URI;\n    settings: EditorSettings;\n    id: string;\n    plugins: Record<string, Plugin>;\n    documentBaseURI: URI;\n    baseURI: URI;\n    contentCSS: string[];\n    contentStyles: string[];\n    ui: EditorUi;\n    mode: EditorMode;\n    setMode: (mode: string) => void;\n    $: DomQueryConstructor;\n    shortcuts: Shortcuts;\n    loadedCSS: Record<string, any>;\n    editorCommands: EditorCommands;\n    suffix: string;\n    editorManager: EditorManager;\n    inline: boolean;\n    isNotDirty: boolean;\n    callbackLookup: any;\n    _nodeChangeDispatcher: NodeChange;\n    editorUpload: EditorUpload;\n    annotator: Annotator;\n    bodyElement: HTMLElement;\n    bookmark: any;\n    composing: boolean;\n    container: HTMLElement;\n    contentAreaContainer: HTMLElement;\n    contentDocument: Document;\n    contentWindow: Window;\n    delegates: Record<string, (event: any) => void>;\n    destroyed: boolean;\n    dom: DOMUtils;\n    editorContainer: HTMLElement;\n    eventRoot?: Element;\n    formatter: Formatter;\n    formElement: HTMLElement;\n    formEventDelegate: (e: Event) => void;\n    hasHiddenInput: boolean;\n    hasVisual: boolean;\n    hidden: boolean;\n    iframeElement: HTMLIFrameElement | null;\n    iframeHTML: string;\n    initialized: boolean;\n    notificationManager: NotificationManager;\n    orgDisplay: string;\n    orgVisibility: string;\n    parser: DomParser;\n    quirks: Quirks;\n    readonly: boolean;\n    removed: boolean;\n    schema: Schema;\n    selection: EditorSelection;\n    serializer: DomSerializer;\n    startContent: string;\n    targetElm: HTMLElement;\n    theme: Theme;\n    undoManager: UndoManager;\n    validate: boolean;\n    windowManager: WindowManager;\n    _beforeUnload: () => void;\n    _eventDispatcher: EventDispatcher<NativeEventMap>;\n    _mceOldSubmit: any;\n    _pendingNativeEvents: string[];\n    _selectionOverrides: SelectionOverrides;\n    _skinLoaded: boolean;\n    bindPendingEventDelegates: EditorObservable['bindPendingEventDelegates'];\n    toggleNativeEvent: EditorObservable['toggleNativeEvent'];\n    unbindAllNativeEvents: EditorObservable['unbindAllNativeEvents'];\n    fire: EditorObservable['fire'];\n    on: EditorObservable['on'];\n    off: EditorObservable['off'];\n    once: EditorObservable['once'];\n    hasEventListeners: EditorObservable['hasEventListeners'];\n    constructor(id: string, settings: RawEditorSettings, editorManager: EditorManager);\n    render(): void;\n    focus(skipFocus?: boolean): void;\n    hasFocus(): boolean;\n    execCallback(name: string, ...x: any[]): any;\n    translate(text: Untranslated): TranslatedString;\n    getParam<K extends keyof ParamTypeMap>(name: string, defaultVal: ParamTypeMap[K], type: K): ParamTypeMap[K];\n    getParam<K extends keyof EditorSettings>(name: K, defaultVal?: EditorSettings[K], type?: string): EditorSettings[K];\n    getParam<T>(name: string, defaultVal: T, type?: string): T;\n    hasPlugin(name: string, loaded?: boolean): boolean;\n    nodeChanged(args?: any): void;\n    addCommand(name: string, callback: EditorCommandCallback, scope?: object): void;\n    addQueryStateHandler(name: string, callback: () => boolean, scope?: any): void;\n    addQueryValueHandler(name: string, callback: () => string, scope?: any): void;\n    addShortcut(pattern: string, desc: string, cmdFunc: string | [\n        string,\n        boolean,\n        any\n    ] | (() => void), scope?: any): void;\n    execCommand(cmd: string, ui?: boolean, value?: any, args?: any): boolean;\n    queryCommandState(cmd: string): boolean;\n    queryCommandValue(cmd: string): string;\n    queryCommandSupported(cmd: string): boolean;\n    show(): void;\n    hide(): void;\n    isHidden(): boolean;\n    setProgressState(state: boolean, time?: number): void;\n    load(args?: any): string;\n    save(args?: any): string;\n    setContent(content: string, args?: SetContentArgs): string;\n    setContent(content: AstNode, args?: SetContentArgs): AstNode;\n    setContent(content: Content, args?: SetContentArgs): Content;\n    getContent(args: {\n        format: 'tree';\n    } & GetContentArgs): AstNode;\n    getContent(args?: GetContentArgs): string;\n    insertContent(content: string, args?: any): void;\n    resetContent(initialContent?: string): void;\n    isDirty(): boolean;\n    setDirty(state: boolean): void;\n    getContainer(): HTMLElement;\n    getContentAreaContainer(): HTMLElement;\n    getElement(): HTMLElement;\n    getWin(): Window;\n    getDoc(): Document;\n    getBody(): HTMLElement;\n    convertURL(url: string, name: string, elm?: any): string;\n    addVisual(elm?: HTMLElement): void;\n    remove(): void;\n    destroy(automatic?: boolean): void;\n    uploadImages(callback?: UploadCallback): Promise<UploadResult$1[]>;\n    _scanForImages(): Promise<BlobInfoImagePair[]>;\n    addButton(): void;\n    addSidebar(): void;\n    addMenuItem(): void;\n    addContextToolbar(): void;\n}\ninterface UrlObject {\n    prefix: string;\n    resource: string;\n    suffix: string;\n}\ndeclare type WaitState = 'added' | 'loaded';\ndeclare type AddOnCallback<T> = (editor: Editor, url: string, $?: DomQueryConstructor) => void | T;\ndeclare type AddOnConstructor<T> = new (editor: Editor, url: string, $?: DomQueryConstructor) => T;\ninterface AddOnManager<T> {\n    items: AddOnConstructor<T>[];\n    urls: Record<string, string>;\n    lookup: Record<string, {\n        instance: AddOnConstructor<T>;\n        dependencies?: string[];\n    }>;\n    _listeners: {\n        name: string;\n        state: WaitState;\n        callback: () => void;\n    }[];\n    get: (name: string) => AddOnConstructor<T>;\n    dependencies: (name: string) => string[];\n    requireLangPack: (name: string, languages: string) => void;\n    add: (id: string, addOn: AddOnCallback<T>, dependencies?: string[]) => AddOnConstructor<T>;\n    remove: (name: string) => void;\n    createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject;\n    addComponents: (pluginName: string, scripts: string[]) => void;\n    load: (name: string, addOnUrl: string | UrlObject, success?: () => void, scope?: any, failure?: () => void) => void;\n    waitFor: (name: string, callback: () => void, state?: WaitState) => void;\n}\ninterface RangeUtils {\n    walk: (rng: Range, callback: (nodes: Node[]) => void) => void;\n    split: (rng: Range) => RangeLikeObject;\n    normalize: (rng: Range) => boolean;\n}\ninterface ScriptLoaderSettings {\n    referrerPolicy?: ReferrerPolicy;\n}\ninterface ScriptLoaderConstructor {\n    readonly prototype: ScriptLoader;\n    new (): ScriptLoader;\n    ScriptLoader: ScriptLoader;\n}\ndeclare class ScriptLoader {\n    static ScriptLoader: ScriptLoader;\n    private settings;\n    private states;\n    private queue;\n    private scriptLoadedCallbacks;\n    private queueLoadedCallbacks;\n    private loading;\n    constructor(settings?: ScriptLoaderSettings);\n    _setReferrerPolicy(referrerPolicy: ReferrerPolicy): void;\n    loadScript(url: string, success?: () => void, failure?: () => void): void;\n    isDone(url: string): boolean;\n    markDone(url: string): void;\n    add(url: string, success?: () => void, scope?: any, failure?: () => void): void;\n    load(url: string, success?: () => void, scope?: any, failure?: () => void): void;\n    remove(url: string): void;\n    loadQueue(success?: () => void, scope?: any, failure?: (urls: string[]) => void): void;\n    loadScripts(scripts: string[], success?: () => void, scope?: any, failure?: (urls: string[]) => void): void;\n}\ndeclare type TextProcessCallback = (node: Text, offset: number, text: string) => number;\ninterface Spot {\n    container: Text;\n    offset: number;\n}\ninterface TextSeeker {\n    backwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;\n    forwards: (node: Node, offset: number, process: TextProcessCallback, root?: Node) => Spot | null;\n}\ninterface DomTreeWalkerConstructor {\n    readonly prototype: DomTreeWalker;\n    new (startNode: Node, rootNode: Node): DomTreeWalker;\n}\ndeclare class DomTreeWalker {\n    private readonly rootNode;\n    private node;\n    constructor(startNode: Node, rootNode: Node);\n    current(): Node;\n    next(shallow?: boolean): Node;\n    prev(shallow?: boolean): Node;\n    prev2(shallow?: boolean): Node;\n    private findSibling;\n    private findPreviousNode;\n}\ninterface Version {\n    major: number;\n    minor: number;\n}\ninterface Env {\n    opera: boolean;\n    webkit: boolean;\n    ie: false | number;\n    gecko: boolean;\n    mac: boolean;\n    iOS: boolean;\n    android: boolean;\n    contentEditable: boolean;\n    transparentSrc: string;\n    caretAfter: boolean;\n    range: boolean;\n    documentMode: number;\n    fileApi: boolean;\n    ceFalse: boolean;\n    cacheSuffix: any;\n    container: any;\n    experimentalShadowDom: boolean;\n    canHaveCSP: boolean;\n    desktop: boolean;\n    windowsPhone: boolean;\n    browser: {\n        current: string | undefined;\n        version: Version;\n        isEdge: () => boolean;\n        isChrome: () => boolean;\n        isIE: () => boolean;\n        isOpera: () => boolean;\n        isFirefox: () => boolean;\n        isSafari: () => boolean;\n    };\n    os: {\n        current: string | undefined;\n        version: Version;\n        isWindows: () => boolean;\n        isiOS: () => boolean;\n        isAndroid: () => boolean;\n        isOSX: () => boolean;\n        isLinux: () => boolean;\n        isSolaris: () => boolean;\n        isFreeBSD: () => boolean;\n        isChromeOS: () => boolean;\n    };\n    deviceType: {\n        isiPad: () => boolean;\n        isiPhone: () => boolean;\n        isTablet: () => boolean;\n        isPhone: () => boolean;\n        isTouch: () => boolean;\n        isWebView: () => boolean;\n        isDesktop: () => boolean;\n    };\n}\ninterface FocusManager {\n    isEditorUIElement: (elm: Element) => boolean;\n}\ninterface EntitiesMap {\n    [name: string]: string;\n}\ninterface Entities {\n    encodeRaw: (text: string, attr?: boolean) => string;\n    encodeAllRaw: (text: string) => string;\n    encodeNumeric: (text: string, attr?: boolean) => string;\n    encodeNamed: (text: string, attr?: boolean, entities?: EntitiesMap) => string;\n    getEncodeFunc: (name: string, entities?: EntitiesMap | string) => (text: string, attr?: boolean) => string;\n    decode: (text: string) => string;\n}\ndeclare type AttrList = Array<{\n    name: string;\n    value: string;\n}> & {\n    map: Record<string, string>;\n};\ninterface SaxParserSettings {\n    allow_conditional_comments?: boolean;\n    allow_html_data_urls?: boolean;\n    allow_script_urls?: boolean;\n    allow_svg_data_urls?: boolean;\n    fix_self_closing?: boolean;\n    preserve_cdata?: boolean;\n    remove_internals?: boolean;\n    self_closing_elements?: Record<string, {}>;\n    validate?: boolean;\n    document?: Document;\n    cdata?: (text: string) => void;\n    comment?: (text: string) => void;\n    doctype?: (text: string) => void;\n    end?: (name: string) => void;\n    pi?: (name: string, text: string) => void;\n    start?: (name: string, attrs: AttrList, empty: boolean) => void;\n    text?: (text: string, raw?: boolean) => void;\n}\ndeclare type ParserFormat = 'html' | 'xhtml' | 'xml';\ninterface SaxParser {\n    parse: (html: string, format?: ParserFormat) => void;\n}\ninterface IconPack {\n    icons: Record<string, string>;\n}\ninterface IconManager {\n    add: (id: string, iconPack: IconPack) => void;\n    get: (id: string) => IconPack;\n    has: (id: string) => boolean;\n}\ninterface Resource {\n    load: <T = any>(id: string, url: string) => Promise<T>;\n    add: (id: string, data: any) => void;\n}\ndeclare type WithSubItems<T, K extends keyof T> = T[K] extends Array<any> ? (T & T[K][number]) : T;\ninterface Props<A extends any[] = any[]> {\n    Mixins?: Array<Record<string, any>>;\n    Methods?: string;\n    Properties?: string;\n    Statics?: Record<string, any>;\n    Defaults?: Record<string, any>;\n    init?: (...args: A) => void;\n}\ndeclare type ExtendedClass<T extends Props<A>, A extends any[]> = WithSubItems<T, 'Mixins'>;\ninterface ExtendedClassConstructor<T extends Props<A>, A extends any[] = any[]> extends Class {\n    readonly prototype: ExtendedClass<T, A>;\n    new (...args: A): ExtendedClass<T, A>;\n    [key: string]: T['Statics'];\n}\ninterface Class {\n    extend<T extends Props<A>, A extends any[] = any[]>(props: T): ExtendedClassConstructor<T, A>;\n}\ninterface RGB {\n    r: number;\n    g: number;\n    b: number;\n}\ninterface HSV {\n    h: number;\n    s: number;\n    v: number;\n}\ndeclare type ColorConstructor = new (value?: string | RGB | HSV) => Color;\ninterface Color {\n    toRgb: () => RGB;\n    toHsv: () => HSV;\n    toHex: () => string;\n    parse: (value: string | RGB | HSV) => Color;\n}\ninterface DebounceFunc<T extends (...args: any[]) => void> {\n    (...args: Parameters<T>): void;\n    stop: () => void;\n}\ninterface Delay {\n    requestAnimationFrame: (callback: () => void, element?: HTMLElement) => void;\n    setEditorInterval: (editor: Editor, callback: () => void, time?: number) => number;\n    setEditorTimeout: (editor: Editor, callback: () => void, time?: number) => number;\n    setInterval: (callback: () => void, time?: number) => number;\n    setTimeout: (callback: () => void, time?: number) => number;\n    clearInterval: (id?: number) => void;\n    clearTimeout: (id?: number) => void;\n    debounce: <T extends (...args: any[]) => any>(callback: T, time?: number) => DebounceFunc<T>;\n    throttle: <T extends (...args: any[]) => any>(callback: T, time?: number) => DebounceFunc<T>;\n}\ndeclare type UploadResult = UploadResult$2;\ninterface ImageUploader {\n    upload: (blobInfos: BlobInfo[], showNotification?: boolean) => Promise<UploadResult[]>;\n}\ninterface JSONUtils {\n    serialize: (obj: any) => string;\n    parse: (text: string) => any;\n}\ninterface JSONPSettings {\n    count?: number;\n    url: string;\n    callback: (json: string) => void;\n}\ninterface JSONP {\n    callbacks: {};\n    count: number;\n    send(this: JSONP, settings: JSONPSettings): void;\n}\ninterface JSONRequestSettings {\n    crossDomain?: boolean;\n    requestheaders?: Record<string, {\n        key: string;\n        value: string;\n    }>;\n    type?: string;\n    url?: string;\n    error_scope?: any;\n    success_scope?: any;\n    success?: (data: any) => void;\n    error?: (error: any, xhr: XMLHttpRequest) => void;\n}\ninterface JSONRequestArgs extends JSONRequestSettings {\n    id?: string;\n    method?: string;\n    params?: string;\n}\ninterface JSONRequestConstructor {\n    readonly prototype: JSONRequest;\n    new (settings?: JSONRequestSettings): JSONRequest;\n    sendRPC: (o: JSONRequestArgs) => void;\n}\ndeclare class JSONRequest {\n    static sendRPC(o: JSONRequestArgs): void;\n    settings: JSONRequestSettings;\n    count: number;\n    constructor(settings?: JSONRequestSettings);\n    send(args: JSONRequestArgs): void;\n}\ninterface KeyboardLikeEvent {\n    shiftKey: boolean;\n    ctrlKey: boolean;\n    altKey: boolean;\n    metaKey: boolean;\n}\ninterface VK {\n    BACKSPACE: number;\n    DELETE: number;\n    DOWN: number;\n    ENTER: number;\n    ESC: number;\n    LEFT: number;\n    RIGHT: number;\n    SPACEBAR: number;\n    TAB: number;\n    UP: number;\n    PAGE_UP: number;\n    PAGE_DOWN: number;\n    END: number;\n    HOME: number;\n    modifierPressed: (e: KeyboardLikeEvent) => boolean;\n    metaKeyPressed: (e: KeyboardLikeEvent) => boolean;\n}\ninterface XHRSettings {\n    async?: boolean;\n    content_type?: string;\n    crossDomain?: boolean;\n    data?: Document | BodyInit;\n    requestheaders?: Record<string, {\n        key: string;\n        value: string;\n    }>;\n    scope?: any;\n    type?: string;\n    url: string;\n    error_scope?: any;\n    success_scope?: any;\n    error?: (message: 'TIMED_OUT' | 'GENERAL', xhr: XMLHttpRequest, settings: XHRSettings) => void;\n    success?: (text: string, xhr: XMLHttpRequest, settings: XHRSettings) => void;\n}\ninterface XHREventMap {\n    beforeInitialize: {\n        settings: XHRSettings;\n    };\n    beforeSend: {\n        xhr: XMLHttpRequest;\n        settings: XHRSettings;\n    };\n}\ninterface XHR extends Observable<XHREventMap> {\n    send(this: XHR, settings: XHRSettings): void;\n}\ninterface DOMUtilsNamespace {\n    new (doc: Document, settings: Partial<DOMUtilsSettings>): DOMUtils;\n    DOM: DOMUtils;\n    nodeIndex: (node: Node, normalized?: boolean) => number;\n}\ninterface RangeUtilsNamespace {\n    new (dom: DOMUtils): RangeUtils;\n    compareRanges: (rng1: RangeLikeObject, rng2: RangeLikeObject) => boolean;\n    getCaretRangeFromPoint: (clientX: number, clientY: number, doc: Document) => Range;\n    getSelectedNode: (range: Range) => Node;\n    getNode: (container: Node, offset: number) => Node;\n}\ninterface AddOnManagerNamespace {\n    new <T>(): AddOnManager<T>;\n    language: string | undefined;\n    languageLoad: boolean;\n    baseURL: string;\n    PluginManager: PluginManager;\n    ThemeManager: ThemeManager;\n}\ninterface BookmarkManagerNamespace {\n    (selection: EditorSelection): BookmarkManager;\n    isBookmarkNode: (node: Node) => boolean;\n}\ninterface SaxParserNamespace {\n    new (settings?: SaxParserSettings, schema?: Schema): SaxParser;\n    findEndTag: (schema: Schema, html: string, startIndex: number) => number;\n}\ninterface TinyMCE extends EditorManager {\n    geom: {\n        Rect: Rect;\n    };\n    util: {\n        Promise: PromiseConstructor;\n        Delay: Delay;\n        Tools: Tools;\n        VK: VK;\n        URI: URIConstructor;\n        Class: Class;\n        EventDispatcher: EventDispatcherConstructor<any>;\n        Observable: Observable<any>;\n        I18n: I18n;\n        XHR: XHR;\n        JSON: JSONUtils;\n        JSONRequest: JSONRequestConstructor;\n        JSONP: JSONP;\n        LocalStorage: Storage;\n        Color: ColorConstructor;\n        ImageUploader: ImageUploader;\n    };\n    dom: {\n        EventUtils: EventUtilsConstructor;\n        Sizzle: any;\n        DomQuery: DomQueryConstructor;\n        TreeWalker: DomTreeWalkerConstructor;\n        TextSeeker: new (dom: DOMUtils, isBlockBoundary?: (node: Node) => boolean) => TextSeeker;\n        DOMUtils: DOMUtilsNamespace;\n        ScriptLoader: ScriptLoaderConstructor;\n        RangeUtils: RangeUtilsNamespace;\n        Serializer: new (settings: DomSerializerSettings, editor?: Editor) => DomSerializer;\n        ControlSelection: (selection: EditorSelection, editor: Editor) => ControlSelection;\n        BookmarkManager: BookmarkManagerNamespace;\n        Selection: new (dom: DOMUtils, win: Window, serializer: DomSerializer, editor: Editor) => EditorSelection;\n        StyleSheetLoader: new (documentOrShadowRoot: Document | ShadowRoot, settings: StyleSheetLoaderSettings) => StyleSheetLoader;\n        Event: EventUtils;\n    };\n    html: {\n        Styles: new (settings?: StylesSettings, schema?: Schema) => Styles;\n        Entities: Entities;\n        Node: AstNodeConstructor;\n        Schema: new (settings?: SchemaSettings) => Schema;\n        SaxParser: SaxParserNamespace;\n        DomParser: new (settings?: DomParserSettings, schema?: Schema) => DomParser;\n        Writer: new (settings?: WriterSettings) => Writer;\n        Serializer: new (settings?: HtmlSerializerSettings, schema?: Schema) => HtmlSerializer;\n    };\n    AddOnManager: AddOnManagerNamespace;\n    Annotator: new (editor: Editor) => Annotator;\n    Editor: EditorConstructor;\n    EditorCommands: EditorCommandsConstructor;\n    EditorManager: EditorManager;\n    EditorObservable: EditorObservable;\n    Env: Env;\n    FocusManager: FocusManager;\n    Formatter: new (editor: Editor) => Formatter;\n    NotificationManager: new (editor: Editor) => NotificationManager;\n    Shortcuts: ShortcutsConstructor;\n    UndoManager: new (editor: Editor) => UndoManager;\n    WindowManager: new (editor: Editor) => WindowManager;\n    DOM: DOMUtils;\n    ScriptLoader: ScriptLoader;\n    PluginManager: PluginManager;\n    ThemeManager: ThemeManager;\n    IconManager: IconManager;\n    Resource: Resource;\n    trim: Tools['trim'];\n    isArray: Tools['isArray'];\n    is: Tools['is'];\n    toArray: Tools['toArray'];\n    makeMap: Tools['makeMap'];\n    each: Tools['each'];\n    map: Tools['map'];\n    grep: Tools['grep'];\n    inArray: Tools['inArray'];\n    extend: Tools['extend'];\n    create: Tools['create'];\n    walk: Tools['walk'];\n    createNS: Tools['createNS'];\n    resolve: Tools['resolve'];\n    explode: Tools['explode'];\n    _addCacheSuffix: Tools['_addCacheSuffix'];\n    isOpera: boolean;\n    isWebKit: boolean;\n    isIE: false | number;\n    isGecko: boolean;\n    isMac: boolean;\n}\ndeclare const tinymce: TinyMCE;\nexport default tinymce;\nexport { AddOnManager, Annotator, AstNode, Bookmark, BookmarkManager, Class, Color, ControlSelection, DOMUtils, Delay, DomParser, DomParserSettings, DomQuery, DomSerializer, DomSerializerSettings, DomTreeWalker, Editor, EditorCommands, EditorEvent, EditorManager, EditorModeApi, EditorObservable, EditorSelection, EditorSettings, Entities, Env, EventDispatcher, EventUtils, EventTypes_d as Events, FocusManager, Format_d as Formats, Formatter, GeomRect, HtmlSerializer, HtmlSerializerSettings, I18n, IconManager, JSONUtils as JSON, JSONP, JSONRequest, JSONRequestArgs, JSONRequestSettings, NotificationApi, NotificationManager, NotificationSpec, Observable, Plugin, PluginManager, RangeUtils, RawEditorSettings, Rect, Resource, SaxParser, SaxParserSettings, Schema, SchemaSettings, ScriptLoader, Shortcuts, StyleSheetLoader, Styles, TextSeeker, Theme, ThemeManager, TinyMCE, Tools, URI, Ui_d as Ui, UndoManager, VK, WindowManager, Writer, WriterSettings, XHR, XHRSettings };\n"
  },
  {
    "path": "app/src/helpers/version-comparator.js",
    "content": "module.exports = function (v1, v2) {\n    let parts1 = v1.split('.').map(n => parseInt(n, 10));\n    let parts2 = v2.split('.').map(n => parseInt(n, 10));\n    let partsToCheck = Math.max(parts1.length, parts2.length);\n\n    for (let i = 0; i < partsToCheck; i++) {\n        let num1 = parts1[i] || 0;\n        let num2 = parts2[i] || 0;\n\n        if (num1 > num2) {\n            return 1;\n        }\n        \n        if (num1 < num2) {\n            return -1;\n        }\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "app/src/main.js",
    "content": "import moment from 'moment';\nimport Vue from 'vue';\nimport store from './store/index';\nimport router from './router';\nimport VueI18n from 'vue-i18n';\nimport App from './components/App';\nimport DOMPurify from 'dompurify';\nimport 'prismjs';\n\n// Basic elements\nimport Alert from './components/basic-elements/Alert';\nimport AuthorsDropDown from './components/basic-elements/AuthorsDropDown';\nimport Button from './components/basic-elements/Button';\nimport ButtonDropdown from './components/basic-elements/ButtonDropdown';\nimport CharCounter from './components/basic-elements/CharCounter';\nimport Checkbox from './components/basic-elements/Checkbox';\nimport CodeMirrorEditor from './components/basic-elements/CodeMirrorEditor';\nimport Collection from './components/basic-elements/Collection';\nimport CollectionCell from './components/basic-elements/CollectionCell';\nimport CollectionHeader from './components/basic-elements/CollectionHeader';\nimport CollectionRow from './components/basic-elements/CollectionRow';\nimport ColorPicker from './components/basic-elements/ColorPicker';\nimport Confirm from './components/basic-elements/Confirm';\nimport DirSelect from './components/basic-elements/DirSelect';\nimport Dropdown from './components/basic-elements/Dropdown';\nimport EmptyState from './components/basic-elements/EmptyState';\nimport Field from './components/basic-elements/Field';\nimport FieldsGroup from './components/basic-elements/FieldsGroup';\nimport FileSelect from './components/basic-elements/FileSelect';\nimport Footer from './components/basic-elements/Footer';\nimport Header from './components/basic-elements/Header';\nimport HeaderSearch from './components/basic-elements/HeaderSearch';\nimport Icon from './components/basic-elements/Icon';\nimport ImageUpload from './components/basic-elements/ImageUpload';\nimport LogoCreator from './components/basic-elements/LogoCreator';\nimport Overlay from './components/basic-elements/Overlay';\nimport PagesDropDown from './components/basic-elements/PagesDropDown';\nimport PostsDropDown from './components/basic-elements/PostsDropDown';\nimport ProgressBar from './components/basic-elements/ProgressBar';\nimport RadioButtons from './components/basic-elements/RadioButton';\nimport RangeSlider from './components/basic-elements/RangeSlider';\nimport Repeater from './components/basic-elements/Repeater';\nimport Separator from './components/basic-elements/Separator';\nimport SmallImageUpload from './components/basic-elements/SmallImageUpload';\nimport Switcher from './components/basic-elements/Switcher';\nimport Tabs from './components/basic-elements/Tabs';\nimport TagsDropDown from './components/basic-elements/TagsDropDown';\nimport TextArea from './components/basic-elements/TextArea';\nimport TextInput from './components/basic-elements/TextInput';\nimport vSelect from '../node_modules/vue-multiselect/dist/vue-multiselect.min.js';\nimport VuePrismEditor from 'vue-prism-editor/dist/VuePrismEditor.common';\nimport 'vue-prism-editor/dist/VuePrismEditor.css';\n\n// Prism JS languages\nimport 'prismjs/components/prism-markup-templating.min.js';\nimport 'prismjs/components/prism-apacheconf.min.js';\nimport 'prismjs/components/prism-aspnet.min.js';\nimport 'prismjs/components/prism-bash.min.js';\nimport 'prismjs/components/prism-basic.min.js';\nimport 'prismjs/components/prism-batch.min.js';\nimport 'prismjs/components/prism-c.min.js';\nimport 'prismjs/components/prism-cpp.min.js';\nimport 'prismjs/components/prism-csharp.min.js';\nimport 'prismjs/components/prism-css.min.js';\nimport 'prismjs/components/prism-dart.min.js';\nimport 'prismjs/components/prism-docker.min.js';\nimport 'prismjs/components/prism-elixir.min.js';\nimport 'prismjs/components/prism-elm.min.js';\nimport 'prismjs/components/prism-gdscript.min.js';\nimport 'prismjs/components/prism-git.min.js';\nimport 'prismjs/components/prism-glsl.min.js';\nimport 'prismjs/components/prism-go.min.js';\nimport 'prismjs/components/prism-graphql.min.js';\nimport 'prismjs/components/prism-haml.min.js';\nimport 'prismjs/components/prism-handlebars.min.js';\nimport 'prismjs/components/prism-haskell.min.js';\nimport 'prismjs/components/prism-http.min.js';\nimport 'prismjs/components/prism-ini.min.js';\nimport 'prismjs/components/prism-java.min.js';\nimport 'prismjs/components/prism-javascript.min.js';\nimport 'prismjs/components/prism-json.min.js';\nimport 'prismjs/components/prism-jsonp.min.js';\nimport 'prismjs/components/prism-jsx.min.js';\nimport 'prismjs/components/prism-kotlin.min.js';\nimport 'prismjs/components/prism-latex.min.js';\nimport 'prismjs/components/prism-less.min.js';\nimport 'prismjs/components/prism-lisp.min.js';\nimport 'prismjs/components/prism-lua.min.js';\nimport 'prismjs/components/prism-makefile.min.js';\nimport 'prismjs/components/prism-markdown.min.js';\nimport 'prismjs/components/prism-matlab.min.js';\nimport 'prismjs/components/prism-nasm.min.js';\nimport 'prismjs/components/prism-nginx.min.js';\nimport 'prismjs/components/prism-objectivec.min.js';\nimport 'prismjs/components/prism-pascal.min.js';\nimport 'prismjs/components/prism-perl.min.js';\nimport 'prismjs/components/prism-php.min.js';\nimport 'prismjs/components/prism-powershell.min.js';\nimport 'prismjs/components/prism-pug.min.js';\nimport 'prismjs/components/prism-python.min.js';\nimport 'prismjs/components/prism-r.min.js';\nimport 'prismjs/components/prism-regex.min.js';\nimport 'prismjs/components/prism-ruby.min.js';\nimport 'prismjs/components/prism-rust.min.js';\nimport 'prismjs/components/prism-sass.min.js';\nimport 'prismjs/components/prism-scss.min.js';\nimport 'prismjs/components/prism-scala.min.js';\nimport 'prismjs/components/prism-sql.min.js';\nimport 'prismjs/components/prism-swift.min.js';\nimport 'prismjs/components/prism-twig.min.js';\nimport 'prismjs/components/prism-typescript.min.js';\nimport 'prismjs/components/prism-vbnet.min.js';\nimport 'prismjs/components/prism-visual-basic.min.js';\nimport 'prismjs/components/prism-yaml.min.js';\nimport 'prismjs/components/prism-xml-doc.min.js';\n\nwindow.app = null;\n\n// i18n\nVue.use(VueI18n);\n\n// DOMPurify configuration\nDOMPurify.addHook('afterSanitizeAttributes', (node) => {\n    // set all elements owning target to target=_blank\n    if ('target' in node) {\n        node.setAttribute('target', '_blank');\n        node.setAttribute('rel', 'noopener noreferrer');\n    }\n  \n    // set non-HTML/MathML links to xlink:show=new\n    if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {\n        node.setAttribute('xlink:show', 'new');\n    }\n});\n  \n// Register v-pure-html directive\nVue.directive('pure-html', {\n    inserted: (el, binding) => {\n        if (binding.oldValue === binding.value) {\n            return;\n        }\n        el.innerHTML = DOMPurify.sanitize(binding.value);\n    },\n    update: (el, binding) => {\n        if (binding.oldValue === binding.value) {\n            return;\n        }\n        el.innerHTML = DOMPurify.sanitize(binding.value);\n    }\n});\n\n// Directive for using with initial HTML code for contenteditable elements\nVue.directive('initial-html', {\n    inserted: function (el, binding, vnode) {\n        el.innerHTML = binding.value;\n    }\n});\n\nmainProcessAPI.receive('app-data-loaded', function (initialData) {\n    // Set moment locale\n    if (initialData.currentLanguage.momentLocale) {\n        moment.locale(initialData.currentLanguage.momentLocale);\n    }\n\n    // Add global Vue properties for commonly used libraries\n    Vue.prototype.$moment = moment;\n\n    // Declare event bus\n    Vue.prototype.$bus = new Vue();\n\n    // Register global components\n    Vue.component('alert', Alert);\n    Vue.component('p-button', Button);\n    Vue.component('btn-dropdown', ButtonDropdown);\n    Vue.component('char-counter', CharCounter);\n    Vue.component('checkbox', Checkbox);\n    Vue.component('codemirror-editor', CodeMirrorEditor);\n    Vue.component('collection', Collection);\n    Vue.component('collection-cell', CollectionCell);\n    Vue.component('collection-header', CollectionHeader);\n    Vue.component('collection-row', CollectionRow);\n    Vue.component('color-picker', ColorPicker);\n    Vue.component('confirm', Confirm);\n    Vue.component('dir-select', DirSelect);\n    Vue.component('dropdown', Dropdown);\n    Vue.component('empty-state', EmptyState);\n    Vue.component('field', Field);\n    Vue.component('fields-group', FieldsGroup);\n    Vue.component('file-select', FileSelect);\n    Vue.component('p-footer', Footer);\n    Vue.component('p-header', Header);\n    Vue.component('header-search', HeaderSearch);\n    Vue.component('icon', Icon);\n    Vue.component('image-upload', ImageUpload);\n    Vue.component('logo-creator', LogoCreator);\n    Vue.component('overlay', Overlay);\n    Vue.component('progress-bar', ProgressBar);\n    Vue.component('radio-buttons', RadioButtons);\n    Vue.component('range-slider', RangeSlider);\n    Vue.component('separator', Separator);\n    Vue.component('small-image-upload', SmallImageUpload);\n    Vue.component('switcher', Switcher);\n    Vue.component('tabs', Tabs);\n    Vue.component('text-area', TextArea);\n    Vue.component('text-input', TextInput);\n    Vue.component('v-select', vSelect);\n    Vue.component('prism-editor', VuePrismEditor);\n    Vue.component('posts-dropdown', PostsDropDown);\n    Vue.component('pages-dropdown', PagesDropDown);\n    Vue.component('authors-dropdown', AuthorsDropDown);\n    Vue.component('tags-dropdown', TagsDropDown);\n    Vue.component('repeater', Repeater);\n\n    const i18n = new VueI18n({\n        locale: initialData.currentLanguage.name,\n        fallbackLocale: 'en',\n        messages: {\n            [initialData.currentLanguage.name]: initialData.currentLanguage.translations,\n            'en': initialData.defaultLanguage.translations\n        }\n    });\n\n    // Init Publii front-end\n    new Vue({\n        el: '#app',\n        store,\n        i18n,\n        router,\n        render: h => h(App, {\n            attrs: {\n                initialData\n            }\n        }),\n        components: {\n            'App': App\n        },\n        computed: {\n            overridedCssVariables () {\n                return [\n                    '--editor-font-size: ' + this.$store.state.app.config.editorFontSize + 'px', \n                    '--editor-font-family: ' + this.$store.state.app.config.editorFontFamily\n                ].join(';') + ';';\n            }\n        },\n        data () {\n            return {\n                skipThemeChangeEvents: false\n            };\n        },\n        async mounted () {\n            await this.setupAppTheme();\n            \n            window.app = {\n                languageLoadingError: !!initialData.currentLanguage.languageLoadingError,\n                getSiteName: () => this.$store.state.currentSite.config.name,\n                getSiteDir: () => this.$store.state.currentSite.siteDir,\n                getSiteTheme: () => this.$store.state.currentSite.config.theme,\n                getThemeCustomElementsMode: () => this.$store.state.currentSite.themeSettings.customElementsMode,\n                getThemeCustomElements: () => this.$store.state.currentSite.themeSettings ? this.$store.state.currentSite.themeSettings.customElements : false,\n                spellcheckerIsEnabled: () => this.$store.state.currentSite.config.spellchecking,\n                wysiwygAdditionalValidElements: () => this.$store.state.currentSite.config.advanced.editors.wysiwygAdditionalValidElements,\n                wysiwygCustomElements: () => this.$store.state.currentSite.config.advanced.editors.wysiwygCustomElements,\n                tinymceCustomConfig: () => this.$store.state.app.customConfig.tinymce,\n                getCurrentAppTheme: () => this.$root.getCurrentAppTheme(),\n                reportPossibleDataLoss: () => this.$bus.$emit('post-editor-possible-data-loss'),\n                writersPanelOpen: () => this.$bus.$emit('writers-panel-open'),\n                writersPanelRefresh: () => this.$bus.$emit('writers-panel-refresh'),\n                sourceCodeEditorShow: (content, editor) => this.$bus.$emit('source-code-editor-show', content, editor),\n                updateLinkEditor: (data) => this.$bus.$emit('update-link-editor', data),\n                updateGalleryPopup: (data) => this.$bus.$emit('update-gallery-popup', data),\n                hasPostEditorConfigOverride: () => this.$store.state.currentSite.themeSettings.extensions ? this.$store.state.currentSite.themeSettings.extensions.postEditorConfigOverride : false,\n                initLinkPopup: (data) => this.$bus.$emit('init-link-popup', data),\n                initLinkEditor: (iframe) => this.$bus.$emit('init-link-editor', iframe),\n                initInlineEditor: (customFormats) => this.$bus.$emit('init-inline-editor', customFormats),\n                updateInlineEditor: (data) => this.$bus.$emit('update-inline-editor', data),\n                galleryPopupUpdated: (callback) => this.$bus.$on('gallery-popup-updated', callback),\n                getWysiwygTranslation: () => this.$store.state.wysiwygTranslation,\n                translate: (phraseKey) => this.$t(phraseKey),\n                overridedCssVariables: () => this.overridedCssVariables,\n                showMessage: (messageConfig) => this.showMessage(messageConfig),\n                getCurrentSitePosts: () => window.structuredClone(this.$store.state.currentSite.posts),\n                getCurrentSitePages: () => window.structuredClone(this.$store.state.currentSite.pages),\n                getCurrentSiteTags: () => window.structuredClone(this.$store.state.currentSite.tags),\n                getCurrentSiteAuthors: () => window.structuredClone(this.$store.state.currentSite.authors),\n                setContentElementField: (itemType, itemID, fieldsToUpdate, callback) => this.setContentElementField(itemType, itemID, fieldsToUpdate, callback)\n            };\n\n            // Find issues with loading languages\n            if (window.app.languageLoadingError) {\n                window.app.languageLoadingError = false;\n                this.$bus.$emit('alert-display', {\n                    message: this.$t('langs.languageLoadingError'),\n                    buttonStyle: 'danger'\n                });\n            }\n\n            // Object for plugins\n            let pluginsAPI = {\n                saveConfigFile: (fileName, fileContent) => this.pluginsApiSaveConfigFile(fileName, fileContent),\n                saveLanguageFile: (fileName, fileContent) => this.pluginsApiSaveLanguageFile(fileName, fileContent),\n                readConfigFile: (fileName) => this.pluginsApiReadConfigFile(fileName),\n                readLanguageFile: (fileName) => this.pluginsApiReadLanguageFile(fileName),\n                readThemeFile: (themeName, fileName) => this.pluginsApiReadThemeFile(themeName, fileName),\n                deleteConfigFile: (fileName) => this.pluginsApiDeleteConfigFile(fileName),\n                deleteLanguageFile: (fileName) => this.pluginsApiDeleteLanguageFile(fileName),\n                getThemesList: () => this.pluginsApiGetThemesList(),\n                getCurrentTheme: () => this.pluginsApiGetCurrentTheme()\n            };\n\n            // Make window.pluginsAPI immutable\n            window.pluginsAPI = Object.freeze(pluginsAPI);\n        },\n        methods: {\n            async setupAppTheme () {\n                let currentTheme = this.$store.state.app.theme;\n\n                if (currentTheme === 'default') {\n                    mainProcessAPI.invoke('app-theme-mode:set-light');\n                } else if (currentTheme === 'dark') {\n                    mainProcessAPI.invoke('app-theme-mode:set-dark');\n                } else {\n                    currentTheme = await mainProcessAPI.invoke('app-theme-mode:get-theme');\n                }\n\n                document.querySelector('html').setAttribute('data-theme', currentTheme);\n                this.$bus.$on('app-theme-change', this.toggleTheme);\n\n                mainProcessAPI.receive('app-theme-mode:changed', () => {\n                    if (this.skipThemeChangeEvents) {\n                        return;\n                    }\n\n                    this.$bus.$emit('app-theme-change');\n                });\n            },\n            async getCurrentAppTheme () {\n                let currentTheme = this.$store.state.app.theme;\n\n                if (currentTheme === 'system') {\n                    return await mainProcessAPI.invoke('app-theme-mode:get-theme');\n                }\n\n                return currentTheme;\n            },\n            async toggleTheme () {\n                this.skipThemeChangeEvents = true;\n                let currentTheme = this.$store.state.app.theme;\n                let iframes = document.querySelectorAll('iframe[id$=\"_ifr\"], iframe#plugin-settings-root');\n                let theme;\n\n                if (currentTheme === 'dark') {\n                    theme = 'dark';\n                    mainProcessAPI.invoke('app-theme-mode:set-dark');\n                    currentTheme = 'dark';\n                } else if (currentTheme === 'default') {\n                    theme = 'default';\n                    mainProcessAPI.invoke('app-theme-mode:set-light');\n                    currentTheme = 'default';\n                } else {\n                    theme = 'system';\n                    mainProcessAPI.invoke('app-theme-mode:set-system');\n                    currentTheme = 'system';\n                    theme = await mainProcessAPI.invoke('app-theme-mode:get-theme')\n                }\n\n                this.$store.commit('setAppTheme', currentTheme);\n                localStorage.setItem('publii-theme', currentTheme);\n                mainProcessAPI.send('app-save-color-theme', currentTheme);\n\n                for (let i = 0; i < iframes.length; i++) {\n                    iframes[i].contentWindow.window.document.querySelector('html').setAttribute('data-theme', theme);\n                }\n\n                document.querySelector('html').setAttribute('data-theme', theme);\n\n                setTimeout(() => {\n                    this.skipThemeChangeEvents = false;\n                }, 500);\n            },\n            async pluginsApiSaveConfigFile (fileName, fileContent) {\n                let siteName = this.$store.state.currentSite.config.name;\n                let pluginName = this.$route.params.pluginname;\n                let result = await mainProcessAPI.invoke('app-plugins-api:save-config-file', {\n                    fileName, \n                    siteName,\n                    pluginName,\n                    fileContent\n                });\n                return result;\n            },\n            async pluginsApiSaveLanguageFile (fileName, fileContent) {\n                let siteName = this.$store.state.currentSite.config.name;\n                let pluginName = this.$route.params.pluginname;\n                let result = await mainProcessAPI.invoke('app-plugins-api:save-language-file', {\n                    fileName, \n                    siteName,\n                    pluginName,\n                    fileContent\n                });\n                return result;\n            },\n            async pluginsApiReadConfigFile (fileName) {\n                let siteName = this.$store.state.currentSite.config.name;\n                let pluginName = this.$route.params.pluginname;\n                let result = await mainProcessAPI.invoke('app-plugins-api:read-config-file', {\n                    fileName, \n                    siteName,\n                    pluginName\n                });\n                return result;\n            },\n            async pluginsApiReadLanguageFile (fileName) {\n                let siteName = this.$store.state.currentSite.config.name;\n                let result = await mainProcessAPI.invoke('app-plugins-api:read-language-file', {\n                    fileName, \n                    siteName\n                });\n                return result;\n            },\n            async pluginsApiReadThemeFile (themeName, fileName) {\n                let siteName = this.$store.state.currentSite.config.name;\n                let result = await mainProcessAPI.invoke('app-plugins-api:read-theme-file', {\n                    themeName,\n                    fileName, \n                    siteName\n                });\n                return result;\n            },\n            async pluginsApiDeleteConfigFile (fileName) {\n                let siteName = this.$store.state.currentSite.config.name;\n                let pluginName = this.$route.params.pluginname;\n                let result = await mainProcessAPI.invoke('app-plugins-api:delete-config-file', {\n                    fileName, \n                    siteName,\n                    pluginName\n                });\n                return result;\n            },\n            async pluginsApiDeleteLanguageFile (fileName) {\n                let siteName = this.$store.state.currentSite.config.name;\n                let pluginName = this.$route.params.pluginname;\n                let result = await mainProcessAPI.invoke('app-plugins-api:delete-language-file', {\n                    fileName, \n                    siteName,\n                    pluginName\n                });\n                return result;\n            },\n            pluginsApiGetThemesList () {\n                return JSON.parse(JSON.stringify(this.$store.state.currentSite.themes));\n            },\n            pluginsApiGetCurrentTheme () {\n                return this.$store.state.currentSite.config.theme;\n            },\n            showMessage (message) {\n                this.$bus.$emit('message-display', {\n                    message: message.text || '',\n                    type: message.type || 'warning',\n                    lifeTime: message.lifeTime || 3\n                });\n            },\n            async setContentElementField (itemType, itemID, fieldsToUpdate, callback) {\n                let tableName = 'posts';\n\n                if (itemType === 'tag') {\n                    tableName = 'tags';\n                } else if (itemType === 'author') {\n                    tableName = 'authors';\n                }\n\n                await mainProcessAPI.send('app-content-fields-update', {\n                    site: this.$store.state.currentSite.config.name,\n                    table: tableName,\n                    itemID,\n                    fieldsToUpdate\n                });\n\n                mainProcessAPI.receiveOnce('app-content-fields-updated', (result) => {\n                    if (result.status === 'success') {\n                        fieldsToUpdate.forEach(fieldObj => {\n                            this.$store.commit('updateCurrentSiteItem', {\n                                itemType: itemType,\n                                itemID: itemID,\n                                field: fieldObj.field,\n                                value: fieldObj.value,\n                                type: fieldObj.type,\n                                subfield: fieldObj.subfield\n                            });\n                        });\n                    }\n\n                    callback(result);\n                });\n            }\n        },\n        beforeDestroy () {\n            this.$bus.$off('app-theme-change', this.toggleTheme);\n        }\n    });\n});\n"
  },
  {
    "path": "app/src/router/index.js",
    "content": "import Vue from 'vue';\nimport Router from 'vue-router';\nimport Splashscreen from '../components/Splashscreen';\n\n// Lazy-loaded views\nconst About = () => import('../components/About.vue');\nconst AboutCredits = () => import('../components/AboutCredits');\nconst Site = () => import('../components/Site');\nconst AppSettings = () => import('../components/AppSettings');\nconst Posts = () => import('../components/Posts');\nconst Pages = () => import('../components/Pages');\nconst EditorBlockEditor = () => import('../components/PostEditorBlockEditor');\nconst EditorMarkdown = () => import('../components/PostEditorMarkdown');\nconst EditorTinyMCE = () => import('../components/PostEditorTinyMCE');\nconst Tags = () => import('../components/Tags');\nconst Menus = () => import('../components/Menus');\nconst Authors = () => import('../components/Authors');\nconst Tools = () => import('../components/Tools');\nconst ToolsPlugin = () => import('../components/ToolsPlugin');\nconst LogViewer = () => import('../components/LogViewer');\nconst RegenerateThumbnails = () => import('../components/RegenerateThumbnails');\nconst WPImport = () => import('../components/WPImport');\nconst Backups = () => import('../components/Backups');\nconst CustomCss = () => import('../components/CustomCss');\nconst CustomHtml = () => import('../components/CustomHtml');\nconst FileManager = () => import('../components/FileManager');\nconst ServerSettings = () => import('../components/ServerSettings');\nconst Settings = () => import('../components/Settings');\nconst AppThemes = () => import('../components/AppThemes');\nconst AppLanguages = () => import('../components/AppLanguages');\nconst AppPlugins = () => import('../components/AppPlugins');\nconst ThemeSettings = () => import('../components/ThemeSettings');\nconst NotificationsCenter = () => import('../components/NotificationsCenter');\n\n// Avoid NavigationDuplicated errors\nconst originalPush = Router.prototype.push;\n\nRouter.prototype.push = function push (location, onResolve, onReject) {\n    if (onResolve || onReject) {\n        return originalPush.call(this, location, onResolve, onReject);\n    }\n\n    return originalPush.call(this, location).catch(error => {\n        if (error.name !== 'NavigationDuplicated') {\n            throw error;\n        }\n    });\n}\n\nVue.use(Router);\n\nexport default new Router({\n    routes: [\n        {\n            path: '/',\n            name: 'Splashscreen',\n            component: Splashscreen\n        },\n        {\n            path: '/about',\n            name: 'About',\n            component: About\n        },\n        {\n            path: '/about/credits',\n            name: 'AboutCredits',\n            component: AboutCredits\n        },\n        {\n            path: '/site/:name',\n            redirect: to => {\n                const { params } = to;\n\n                if (params.name !== '!') {\n                    return { path: '/site/:name/posts/' };\n                }\n\n                return { path: '/site/!/posts' };\n            },\n            name: 'Site',\n            component: Site,\n            children: [\n                {\n                    path: 'posts',\n                    component: Posts\n                },\n                {\n                    path: 'posts/:filter',\n                    component: Posts\n                },\n                {\n                    path: 'pages',\n                    component: Pages\n                },\n                {\n                    path: 'pages/:filter',\n                    component: Pages\n                },\n                {\n                    path: 'menus',\n                    component: Menus\n                },\n                {\n                    path: 'settings',\n                    component: Settings\n                },\n                {\n                    path: 'settings/server',\n                    component: ServerSettings\n                },\n                {\n                    path: 'settings/themes',\n                    component: ThemeSettings\n                },\n                {\n                    path: 'tags',\n                    component: Tags\n                },\n                {\n                    path: 'authors',\n                    component: Authors\n                },\n                {\n                    path: 'tools',\n                    component: Tools\n                },\n                {\n                    path: 'tools/log-viewer',\n                    component: LogViewer\n                },\n                {\n                    path: 'tools/regenerate-thumbnails',\n                    component: RegenerateThumbnails\n                },\n                {\n                    path: 'tools/wp-importer',\n                    component: WPImport\n                },\n                {\n                    path: 'tools/backups',\n                    component: Backups\n                },\n                {\n                    path: 'tools/custom-css',\n                    component: CustomCss\n                },\n                {\n                    path: 'tools/custom-html',\n                    component: CustomHtml\n                },\n                {\n                    path: 'tools/file-manager',\n                    component: FileManager\n                },\n                {\n                    path: 'tools/plugins/:pluginname',\n                    component: ToolsPlugin\n                }\n            ]\n        },\n        {\n            path: '/app-settings',\n            name: 'AppSettings',\n            component: AppSettings\n        },\n        {\n            path: '/app-themes',\n            name: 'AppThemes',\n            component: AppThemes\n        },\n        {\n            path: '/app-plugins',\n            name: 'AppPlugins',\n            component: AppPlugins\n        },\n        {\n            path: '/app-languages',\n            name: 'AppLanguages',\n            component: AppLanguages\n        },\n        {\n            path: '/notifications-center',\n            name: 'NotificationsCenter',\n            component: NotificationsCenter\n        },\n        {\n            path: '/site/:name/posts/editor/blockeditor/:post_id?',\n            component: EditorBlockEditor\n        },\n        {\n            path: '/site/:name/posts/editor/markdown/:post_id?',\n            component: EditorMarkdown\n        },\n        {\n            path: '/site/:name/posts/editor/tinymce/:post_id?',\n            component: EditorTinyMCE\n        },\n        {\n            path: '/site/:name/pages/editor/blockeditor/:post_id?',\n            component: EditorBlockEditor\n        },\n        {\n            path: '/site/:name/pages/editor/markdown/:post_id?',\n            component: EditorMarkdown\n        },\n        {\n            path: '/site/:name/pages/editor/tinymce/:post_id?',\n            component: EditorTinyMCE\n        }\n    ]\n});\n"
  },
  {
    "path": "app/src/scss/codemirror.scss",
    "content": ".tools-log-viewer {\n    .CodeMirror {\n        height: calc(100vh - 40rem)!important;\n        top: 2rem;\n    }\n}\n\n.tools-custom-css {\n\n    .editor-wrapper > div {\n        display: flex;\n        flex-direction: column-reverse;\n    }\n    .CodeMirror {\n        height: calc(100vh - 35rem)!important;\n        top: 2rem;\n    }\n    .CodeMirror-advanced-dialog {\n        position: relative;\n    }\n}\n\n.tools-custom-html {\n\n    .tab > div {\n        display: flex;\n        flex-direction: column-reverse;\n    }\n    .CodeMirror {\n        background: var(--bg-primary);\n        border: 1px solid var(--input-border-color);\n        height: calc(100vh - 43rem)!important;\n    }\n    .CodeMirror-advanced-dialog {\n        position: relative;\n    }\n}\n\n\n// Special styles for win\nbody[data-os=\"win\"] {\n    .tools-log-viewer,\n    .tools-custom-html {\n        .CodeMirror {\n            height: calc(100vh - 45rem)!important;\n        }\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .tools-log-viewer,\n    .tools-custom-html {\n        .CodeMirror {\n            height: calc(100vh - 41.4rem)!important;\n        }\n    }\n}\n\n@media (max-width: 1400px) {\n    .tools-log-viewer,\n    .tools-custom-html {\n        .CodeMirror {\n            height: calc(100vh - 31rem)!important;\n        }\n    }\n\n    // Special styles for win\n    body[data-os=\"win\"] {\n        .tools-log-viewer,\n        .tools-custom-html {\n            .CodeMirror {\n                height: calc(100vh - 35rem)!important;\n            }\n        }\n    }\n\n    body[data-os=\"linux\"] {\n        .tools-log-viewer,\n        .tools-custom-html {\n            .CodeMirror {\n                height: calc(100vh - 31.4rem)!important;\n            }\n        }\n    }\n}\n\n/*\n * Responsive improvements\n */\n\n @media (max-width: 1540px) {\n    .tools-custom-html {\n\n        .CodeMirror-advanced-dialog {\n            position: fixed;\n            width: calc(100% - $app-sidebar - 1px);\n            left: $app-sidebar;\n        }\n    }\n}"
  },
  {
    "path": "app/src/scss/css-variables.scss",
    "content": "// common\nhtml {\n    --border-radius: 6px;\n    --font-base: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n    --font-serif: Georgia, Times New Roman, Times, Serif;\n    --font-monospace: Menlo, Monaco, Consolas, Courier New, monospace;       \n    --font-weight-normal: 400;\n    --font-weight-semibold: 500;\n    --font-weight-bold: 700;\n    --letter-spacing: -0.03rem;    \n    --topbar-height: 2.2rem;\n    --editor-font-size: 20px;\n    --editor-font-family: var(--font-base);\n    --editor-width: 39em;\n    --transition: all .2s ease-out;\n    --ui-zoom-level: 100%;\n}\n\nhtml[data-is-osx-11-or-higher=\"true\"] {\n    --topbar-height: 2.6rem;\n}\n\n// default\nhtml[data-theme=\"default\"] {\n\n    // color pallete\n    --color-5: #f4fafe;\n    --color-10: #d8ecfd;\n    --color-15: #c7e4fc;\n    --color-20: #b0d9fb;\n    --color-25: #9acffa;\n    --color-30: #84c4f9;\n    --color-35: #6db9f7;\n    --color-40: #57aef6;\n    --color-45: #40a4f5;\n    --color-50: #2a99f4;\n    --color-55: #0d8bf2;\n    --color-60: #0b7bd6;\n    --color-65: #0a6aba;\n    --color-70: #085a9d;\n    --color-75: #074a81;\n    --color-80: #053964;\n    --color-85: #042947;\n    --color-90: #02192b;\n    --color-95: #01080e;\n\n    --neutral-5: #f6f6f9;\n    --neutral-10: #e7e8ea;\n    --neutral-15: #d7d8dc;\n    --neutral-20: #c7c9ce;\n    --neutral-25: #b7b9c0;\n    --neutral-30: #a7a9b2;\n    --neutral-35: #969aa4;\n    --neutral-40: #868a96;\n    --neutral-45: #767b88;\n    --neutral-50: #686c78;\n    --neutral-55: #5d616b;\n    --neutral-60: #52555f;\n    --neutral-65: #474a52;\n    --neutral-70: #3c3f46;\n    --neutral-75: #313339;\n    --neutral-80: #26282c;\n    --neutral-85: #1b1c20;\n    --neutral-90: #101113;\n    --neutral-95: #050606;\n     \n    // basic\n    --color-primary: var(--color-55);\n    --color-primary-rgb: 13, 139, 242;\n\n    --gray-6: var(--neutral-10);\n    --gray-4: var(--neutral-50);\n    --gray-3: var(--neutral-55);\n    --gray-2: var(--neutral-15);\n    --gray-1: var(--neutral-5);\n    --white: #fff;\n    --white-rgb: 255,255,255;\n    --warning: #EA1A16;\n    --warning-rgb: 234,26,22;\n    --success: #40B771;\n    --success-rgb: 64,183,113;\n    --highlighted: #FFF8E1;\n    --yellow: 255, 196, 0;\n    --shadow: rgba(61, 70, 78, .25);\n    --box-shadow-small: 0 0 1px rgba(0, 0, 0, .06), 0 2px 6px rgba(0, 0, 0, .03);\n    --box-shadow-medium: 0 2px 6px rgba(0, 0, 0, 0.15);\n        \n    --overlay: rgba(0,0,0, .35);\n                \n    --icon-primary-color: var(--neutral-70);\n    --icon-secondary-color: var(--neutral-55);\n    --icon-tertiary-color:  var(--neutral-70);\n    --icon-quaternary-color: var(--color-30);\n        \n    --bg-primary: var(--white); \n    --bg-secondary: var(--white);\n    --bg-site: var(--neutral-5);    \n\n    --border-light-color: var(--neutral-10); \n        \n    --input-bg: var(--white);\n    --input-border-dark: var(--neutral-20);\n    --input-bg-light: var(--neutral-5);\n    --input-bg-lightest: var(--white);\n    --input-border-color: var(--neutral-15);\n    --input-border-focus: var(--color-55);\n    --input-data-time-popup: light;\n        \n    --button-bg: var(--color-55);\n    --button-bg-hover: var(--color-60);  \n    --button-secondary-bg: var(--color-10);\n    --button-secondary-bg-hover: var(--color-15); \n    --button-secondary-color: var(--color-65); \n    --button-secondary-color-hover: var(--color-70); \n    --button-tertiary-bg: var(--color-55);\n    --button-tertiary-bg-hover: var(--color-60);    \n    --button-red-bg: var(--warning);\n    --button-red-bg-hover: #cd1613; \n    --button-gray-bg: var(--neutral-50);\n    --button-gray-bg-hover: var(--neutral-55);\n        \n    --text-primary-color: var(--neutral-75);\n    --text-light-color: var(--neutral-50); \n    --text-lightest-color: var(--neutral-45); \n    --headings-color: var(--color-90);  \n    --label-color: var(--color-90);\n        \n    --link-primary-color: var(--color-65); \n    --link-primary-color-hover: var(--color-90); \n    --link-invert-color: var(--color-65); \n    --link-invert-color-hover: var(--color-90); \n\n    --collection-bg: var(--white);\n    --collection-bg-hover: var(--color-5);\n        \n    --sidebar-bg-top: var(--color-60);\n    --sidebar-bg-bottom: var(--color-55);     \n    --sidebar-link-color: var(--white);\n    --sidebar-link-color-hover: var(--white);   \n    --sidebar-link-color-active: var(--white);  \n    --sidebar-link-bg-hover: rgba(98, 185, 255, 0.35); \n    --sidebar-link-bg-active: rgba(98, 185, 255, 0.35); \n    --sidebar-link-opacity: .85; \n    --sidebar-link-icon: var(--white);\n    --sidebar-link-icon-hover: var(--white);\n    --sidebar-sync-btn-bg: rgba(98, 185, 255, 0.55); \n    --sidebar-sync-btn-color: var(--white);\n    --sidebar-preview-btn-color: var(--white);\n    --sidebar-preview-btn-color-hover: var(--white);\n    --sidebar-preview-btn-border-color: rgba(255,255,255, .4);\n    --sidebar-preview-btn-border-color-hover: var(--white);\n\n    --sidebar-icon: var(--white);\n        \n    --option-sidebar-bg: var(--white);\n        \n    --tab-color: var(--color-90);\n    --tab-color-hover: var(--color-65);\n    --tab-active-bg: var(--color-10); \n    --tab-active-color: var(--color-65); \n    --tab-parent-active-bg: var(--color-5);\n        \n    --scrollbar: var(--neutral-10);  \n    --scrollbar-hover: var(--neutral-15);\n\n    --top-app-bar: var(--neutral-15);\n        \n    --progress-bar: rgba(255, 255, 255, 0.3);\n        \n    --popup-bg: var(--white);\n    --popup-btn-cancel-color: var(--neutral-50);\n    --popup-btn-cancel-hover-color: var(--neutral-65);\n    --popup-btn-cancel-bg: var(--white);\n    --popup-btn-cancel-bg-hover: var(--neutral-5);\n\n    --pre-bg: var(--neutral-80);\n    --pre-bg-hover: var(--neutral-80);\n    --pre-color: var(--neutral-5);\n\n    --code-1: #708;\n    --code-2: #219;\n    --code-3: #164;\n    --code-4: #00f;\n    --code-5: #05a;\n    --code-6: #085;\n    --code-7: #a50;\n    --code-8: #a11;\n    --code-9: #f50;\n    --code-10: #555;\n    --code-11: #30a;\n    --code-12: #170;\n    --code-14: #00c;\n    --code-15: #999;\n    --code-16: #f00;\n    --code-17: #292;\n    --code-18: #090;\n    --code-19: #997;\n    --code-20: #d7d4f0;\n    --code-21: #000;\n\n    --text-selection-color: #b6d8fd;\n}\n  \n//dark\nhtml[data-theme=\"dark\"] { \n\n    // color pallete\n    --color-5: #f2f9ff;\n    --color-10: #d9ecff;\n    --color-15: #c0dfff;\n    --color-20: #a7d3ff;\n    --color-25: #8dc6ff;\n    --color-30: #74baff;\n    --color-35: #5badff;\n    --color-40: #42a0ff;\n    --color-45: #2994ff;\n    --color-50: #1089ff;\n    --color-55: #0079f2;\n    --color-60: #006bd5;\n    --color-65: #005cb9;\n    --color-70: #004e9c;\n    --color-75: #004080;\n    --color-80: #003264;\n    --color-85: #002447;\n    --color-90: #00152b;\n    --color-95: #00070e;\n\n    --neutral-5: #f2f2f5;\n    --neutral-10: #d9d9e0;\n    --neutral-15: #bfc0cc;\n    --neutral-20: #a5a7b7;\n    --neutral-25: #8c8ea3;\n    --neutral-30: #72758e;\n    --neutral-35: #5d6074;\n    --neutral-40: #494b5b;\n    --neutral-45: #393A47;\n    --neutral-46: #343541;\n    --neutral-47: #2F303A;\n    --neutral-48: #2A2B34;\n    --neutral-49: #25262E;\n    --neutral-50: #202128;\n    --neutral-55: #1c1d23;\n    --neutral-60: #191a1f;\n    --neutral-65: #16161b;\n    --neutral-70: #121317;\n    --neutral-75: #0f0f13;\n    --neutral-80: #0c0c0f;\n    --neutral-85: #08090a;\n    --neutral-90: #050506;\n    --neutral-95: #020202;\n\n    // basic\n    --color-primary: var(--color-50);\n    --color-primary-rgb: 16, 137, 255;\n    \n    --gray-6: var(--neutral-45);\n    --gray-4: var(--neutral-35);   \n    --gray-3: var(--neutral-20);\n    --gray-2: var(--neutral-45);\n    --gray-1: var(--neutral-48);  \n    --white: #fff;\n    --white-rgb: 255,255,255;\n    --warning: #ff4a4b;\n    --warning-rgb: 255,16,17;\n    --success: #56a900;\n    --success-rgb: 86,169,0;\n    --highlighted: #343540;\n    --yellow: 255, 196, 0;\n    --shadow: rgba(30, 33, 40, .25);\n    --box-shadow-small: 0 2px 6px rgb(23 25 31);\n    --box-shadow-medium: 0 2px 6px rgb(23 25 31);\n        \n    --overlay: rgba(28,29,35, .85);\n                \n    --icon-primary-color: var(--neutral-20);\n    --icon-secondary-color: var(--neutral-25);\n    --icon-tertiary-color: var(--neutral-10);\n    --icon-quaternary-color: var(--neutral-30);\n\n    --bg-primary: var(--neutral-60); \n    --bg-secondary: var(--neutral-50); \n    --bg-site: var(--neutral-60);   \n\n    --border-light-color: var(--neutral-48);\n        \n    --input-bg: var(--neutral-55);\n    --input-border-dark: var(--neutral-40);\n    --input-bg-light: var(--neutral-48);\n    --input-bg-lightest: var(--neutral-49);\n    --input-border-color: var(--neutral-45);\n    --input-border-focus: var(--color-50);\n    --input-data-time-popup: dark;\n  \n    --button-bg: var(--color-50);\n    --button-bg-hover: var(--color-55); \n    --button-secondary-bg: var(--color-75); \n    --button-secondary-bg-hover: var(--color-70); \n    --button-secondary-color: var(--color-25);    \n    --button-secondary-color-hover: var(--color-15); \n    --button-tertiary-bg: var(--color-50);\n    --button-tertiary-bg-hover: var(--color-55); \n    --button-red-bg: #ed0001;\n    --button-red-bg-hover: #a90001;\n    --button-gray-bg: var(--neutral-35);\n    --button-gray-bg-hover: var(--color-50);\n        \n    --text-primary-color: var(--neutral-20); \n    --text-light-color: var(--neutral-25); \n    --text-lightest-color: var(--neutral-35); \n    --headings-color: var(--neutral-5);\n    --label-color: var(--neutral-15);\n        \n    --link-primary-color: var(--color-40);\n    --link-primary-color-hover: var(--neutral-10);  \n    --link-invert-color: var(--neutral-10);\n    --link-invert-color-hover: var(--color-40);\n    \n    --collection-bg: var(--neutral-55);\n    --collection-bg-hover: var(--neutral-50);\n    \n    --sidebar-bg-top: var(--neutral-65);\n    --sidebar-bg-bottom: var(--neutral-60);\n    --sidebar-link-color: var(--neutral-15);  \n    --sidebar-link-color-hover: var(--neutral-10);\n    --sidebar-link-color-active: var(--neutral-10);\n    --sidebar-link-bg-hover: var(--neutral-55);  \n    --sidebar-link-bg-active: var(--neutral-49);  \n    --sidebar-link-opacity: 1; \n    --sidebar-link-icon: var(--neutral-20);\n    --sidebar-link-icon-hover: var(--white);\n    --sidebar-sync-btn-bg: var(--color-50); \n    --sidebar-sync-btn-color: var(--neutral-5);\n    --sidebar-preview-btn-color: var(--neutral-5);\n    --sidebar-preview-btn-color-hover: var(--white);\n    --sidebar-preview-btn-border-color: var(--neutral-45);\n    --sidebar-preview-btn-border-color-hover: var(--neutral-20);\n\n    --sidebar-icon: var(--neutral-5);\n        \n    --option-sidebar-bg: var(--neutral-50);\n        \n    --tab-color: var(--neutral-15);\n    --tab-color-hover: var(--neutral-10);\n    --tab-active-bg: var(--neutral-48); \n    --tab-active-color: var(--neutral-10); \n        \n    --scrollbar: var(--neutral-48);\n    --scrollbar-hover: var(--neutral-46);\n\n    --top-app-bar: var(--neutral-49);\n        \n    --progress-bar: rgba(25, 26, 31, 0.3);\n        \n    --popup-bg: var(--neutral-49); \n    --popup-btn-cancel-color: var(--neutral-20);\n    --popup-btn-cancel-hover-color: var(--neutral-5);\n    --popup-btn-cancel-bg: var(--neutral-49); \n    --popup-btn-cancel-bg-hover: #343540;\n\n    --pre-bg: var(--neutral-49);\n    --pre-bg-hover: var(--neutral-60);\n    --pre-color: var(--neutral-5);\n        \n    --code-1: #f2777a;\n    --code-2: #DC7EED;\n    --code-3: #DC7EED;\n    --code-4: #f99157;\n    --code-5: #8ED892;\n    --code-6: #95BDF8;\n    --code-7: #bd8064;\n    --code-8: #ffcc66;\n    --code-9: #f50;\n    --code-10: #8e929d;\n    --code-11: #2e98ff;\n    --code-12: #f2777a;\n    --code-14: #DC7EED;\n    --code-15: #999;\n    --code-16: #fff;\n    --code-17: #292;\n    --code-18: #090;\n    --code-19: #CCCCCC;\n    --code-20: #272831;\n    --code-21: #f2777a;\n\n    --text-selection-color: #94b6db;\n}\n"
  },
  {
    "path": "app/src/scss/editor/editor-markdown.scss",
    "content": "@import '../vendor/modularscale';\n@import '../variables';\n@import '../mixins';\n\n.post-editor-markdown {\n    /**\n     * simplemde v1.11.2\n     * Copyright Next Step Webs, Inc.\n     * @link https://github.com/sparksuite/simplemde-markdown-editor\n     * @license MIT\n     */\n    .CodeMirror {\n        font-size: var(--editor-font-size);\n        height: auto;\n        min-height: 300px;      \n        border-bottom-left-radius: 4px;\n        border-bottom-right-radius: 4px;\n        color: var(--text-primary-color);\n        font-family: var(--editor-font-family);\n        font-weight: var(--font-weight-normal);\n        line-height: $line-height;\n        padding: 0;\n        z-index: 1;\n    }\n\n    .CodeMirror-scroll {\n        min-height: 300px;\n        overflow: unset !important;\n    }\n\n    .CodeMirror-sizer {\n        border-right: none;\n    }\n\n    .CodeMirror-fullscreen {\n        background: #fff;\n        position: fixed !important;\n        top: 50px;\n        left: 0;\n        right: 0;\n        bottom: 0;\n        height: auto;\n        z-index: 9;\n    }\n\n    .CodeMirror-sided {\n        width: 50% !important;\n    }\n\n\n    .CodeMirror .CodeMirror-code .cm-tag {\n        color: var(--success);\n    }\n\n    .CodeMirror .CodeMirror-code .cm-attribute {\n        color: #795da3;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-string {\n        color: var(--color-primary);\n    }\n\n    .CodeMirror .CodeMirror-selected {\n        background: var(--gray-1);\n    }\n\n    .CodeMirror pre {\n        color: var(--text-primary-color);\n        font-size: inherit;\n\n    }\n\n    .CodeMirror .CodeMirror-code .cm-header {\n        font-family: var(--font-base);\n        color: var(--headings-color);\n        line-height: 1.3;\n\n    }\n\n    .CodeMirror .CodeMirror-code .cm-header-1 {\n        font-size: $h1;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-header-2 {\n        font-size: $h2;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-header-3 {\n        font-size: $h3;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-header-4 {\n        font-size: $h4;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-header-5 {\n        font-size: $h5;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-header-6 {\n        font-size: $h6;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-comment {\n        background: var(--highlighted);\n        border-radius: 2px;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-url {\n        border-bottom: 2px solid rgba(var(--yellow), 1);\n        color: var(--link-primary-color);\n        text-decoration: none;\n    }\n\n    .CodeMirror .CodeMirror-code .cm-strikethrough {\n        text-decoration: line-through;\n    }\n\n    .CodeMirror .CodeMirror-placeholder {\n        opacity: .5;\n    }\n}\n"
  },
  {
    "path": "app/src/scss/editor/editor-options.scss",
    "content": "/*\n * Basic files\n */\n@import '../vendor/modularscale';\n@import '../mixins';\n@import '../variables';\n@import '../css-variables';\n@import 'scrollbar';\n\n/*\n * Reset\n */\n*,\n*:before,\n*:after {\n    box-sizing: border-box;\n}\n\nhtml,\nbody {\n    font-family: var(--font-base);\n}\n\nbody,\ndiv,\ndl,\ndt,\ndd,\nul,\nol,\nli,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\npre,\nfigure,\nform,\nfieldset,\np,\nblockquote,\nth,\ntd {\n    margin: 0;\n    padding: 0;\n}\n\nimg {\n    height: auto;\n    max-width: 100%;\n    vertical-align: top;\n}\n\n/*\n * Main rules\n */\n\nhtml {\n    background: var(--bg-primary);\n    font-size: 14px;\n    height: 100%;\n    overflow-y: auto !important;\n}\n\nbody#tinymce {\n    color: var(--text-primary-color);\n    -webkit-font-smoothing: antialiased;\n    font-family: var(--font-base);\n    font-size: inherit;\n    line-height: 1.6;\n    padding: 1.8rem 1rem 0 1rem;\n\n    & > :nth-child(1) {\n        margin-top: 0;\n    }\n}\n\na:not(.btn) {\n    @include links (var(--link-invert-color), var(--link-invert-color), var(--link-invert-color), var(--link-invert-color));\n}\n\n\np,\nul,\nol,\ndl,\ntable,\nhr {\n    margin-top: baseline(4);\n}\n\nblockquote,\nfigure {\n    margin-top: baseline(4);\n    margin-bottom: baseline(4);\n}\n\npre,\ntable,\nscript {\n    margin-top: baseline(4);\n    margin-bottom: baseline(5);\n}\n\nfigcaption {\n    clear: both;\n    color: var(--gray-4);\n    font-size: 0.7rem;\n    margin: 0.8rem 0 0;\n    text-align: center;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    color: var(--headings-color);\n    font-weight: var(--font-weight-bold);\n    letter-spacing: var(--letter-spacing);\n    line-height: 1.3;\n    margin-top: baseline(5);\n}\n\nh1 {\n    font-size: $h1;\n}\n\nh2 {\n    font-size: $h2;\n}\n\nh3 {\n    font-size: $h3;\n}\n\nh4 {\n    font-size: $h4;\n}\n\nh5 {\n    font-size: $h5;\n}\n\nh6 {\n    font-size: $h6;\n}\n\nh2,\nh3,\nh4,\nh5,\nh6 {\n    & + p {\n        margin-top: baseline(3);\n    }\n}\n\nb,\nstrong {\n    font-weight: var(--font-weight-bold);\n}\n\nblockquote {\n    background: var(--gray-1);\n    border-left: 3px solid var(--color-primary);\n    font-style: italic;\n    padding: baseline(2) 0 baseline(2) baseline(8);\n\n    & > :nth-child(1) {\n        margin-top: 0;\n    }\n}\n\nul,\nol {\n    margin-left: 2rem;\n\n    & > li {\n        list-style: inherit;\n        padding-left: baseline(2);\n    }\n\n    ul,\n    ol {\n        margin-bottom: baseline(2);\n        margin-top: baseline(2);\n    }\n}\n\ndl {\n    dt {\n        font-weight: var(--font-weight-bold);\n    }\n}\n\npre {\n    background-color: var(--gray-1);\n    font-size: ms(-1);\n    padding: baseline(6);\n    white-space: pre-wrap;\n    word-wrap: break-word;\n\n    & > code {\n        background: none;\n        color: var(--headings-color);\n        display: inline-block;\n        font-size: ms(-1);\n        font-weight: normal;\n        padding: 0;\n    }\n}\n\ncode {\n    background-color: rgba(var(--warning-rgb), .15);\n    border-radius: 2px;\n    color: var(--warning);\n    font-size: ms(-1);\n    font-family: var(--font-monospace);\n    padding: baseline(1) baseline(2);\n}\n\ntable {\n    border: 1px solid var(--input-border-color);\n    border-collapse: collapse;\n    border-spacing: 0;\n    vertical-align: top;\n    text-align: left;\n    width: 100%;\n\n    th {\n        font-weight: var(--font-weight-semibold);\n        padding: baseline(2.5) baseline(4);\n    }\n\n    td {\n        border-top: 1px solid var(--input-border-color);\n        padding: baseline(2.5) baseline(4);\n    }\n}\n\nkbd {\n    background: var(--headings-color);\n    border-radius: 2px;\n    color: var(--bg-primary);\n    font-family: var(--font-monospace);\n    font-size: 0.8888em;\n    padding: baseline(0.5) baseline(1.5);\n}\n\nsub,\nsup {\n    font-size: 65%;\n}\n\nsmall {\n    font-size: ms(-2);\n}\n\nhr:not(#read-more) {\n    background: none;\n    border: none;\n    height: auto;\n    line-height: 1;\n    max-width: none;\n    text-align: center;\n\n    &::before {\n        content: \"* * *\";\n        color: var(--headings-color);\n        font-size: 30px;\n    }\n}\n\n/*\n * Alignment\n */\n\n.align-left {\n    text-align: left;\n}\n\n.align-right {\n    text-align: right;\n}\n\n.align-center {\n    text-align: center;\n}\n\n.align-justify {\n    text-align: justify;\n}\n"
  },
  {
    "path": "app/src/scss/editor/editor-overrides.scss",
    "content": "/*\n * Alignment styles for images, videos and iframes in editable regions\n */\n\n/* Center (default) */\n[data-editable] iframe,\n[data-editable] image,\n[data-editable] [data-ce-tag=img],\n[data-editable] img,\n[data-editable] video {\n    clear: both;\n    display: block;\n    margin-left: auto;\n    margin-right: auto;\n    max-width: 100%;\n}\n\n/* Alignment styles for text in editable regions */\n.text-center {\n    text-align: center;\n}\n\n.text-left {\n    text-align: left;\n}\n\n.text-right {\n    text-align: right;\n}\n\n.post-editor {\n    .tox-tinymce {\n        border: none;\n        opacity: 0;\n        transition: opacity .25s ease-out;\n        transition-delay: .5s;\n\n        &.is-loaded {\n            opacity: 1;\n        }\n\n        .tox-toolbar {           \n            justify-content: center;\n        }\n\n        .tox-statusbar__path {\n            display: none;\n        }\n\n        .tinymce-overlay {\n            border: 3px solid var(--color-primary);\n            background: rgba(var(--color-primary-rgb), .17);\n            display: none;\n            height: 100%;\n            left: 0;\n            opacity: 1;\n            position: absolute;\n            text-align: center;\n            top: 0;\n            width: 100%;\n            z-index: 10000;\n\n            & > div {\n                box-shadow: 0 0 3px rgba(black, .2);\n                background: var(--color-primary);\n                border-radius: var(--border-radius);\n                color: var(--white);\n                font-size: $app-font-base;\n                font-weight: var(--font-weight-semibold);\n                height: auto;\n                left: 50%;\n                line-height: 1.5;\n                padding: 1.4rem 4rem 1.4rem 3rem;\n                position: absolute;\n                top: 50%;\n                transform: translateX(-50%) translateY(-50%);\n                width: auto;\n            }\n\n            .upload-icon {               \n                fill: var(--white);\n                height: 2.4rem;\n                margin-right: 2rem;\n                width: 2.4rem;\n                vertical-align: middle;\n            }\n\n            .loader {\n                display: inline-block;\n                height: 2.4rem;\n                margin-right: 2rem;\n                width: 2.4rem;\n\n                & > span {\n                    animation: spin .9s infinite linear;\n                    border-top: 2px solid rgba(var(--white-rgb), 0.2);\n                    border-right: 2px solid rgba(var(--white-rgb), 0.2);\n                    border-bottom: 2px solid rgba(var(--white-rgb), 0.2);\n                    border-left: 2px solid var(--white);\n                    border-radius: 50%;\n                    display: inline-block;\n                    height: 2.1rem;                    \n                    vertical-align: middle;\n                    width: 2.1rem;\n\n                    &::after {\n                        border-radius: 50%;\n                        content: \"\";\n                        display: block;\n                    }\n\n                    @at-root {\n                        @keyframes spin {\n                            100% {\n                                transform: rotate(360deg);\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        &.is-hovered {\n            .tinymce-overlay {\n                display: block;\n            }\n        }\n\n        .tox-tbtn,\n        .tox-panel {\n            // background: var(--gray-1);\n        }\n        \n        .tox-tbtn {\n            svg {\n                color: var(--icon-tertiary-color);\n                fill: none;\n            }\n        }\n\n        .tox-tbtn:not(span),\n        .tox-split-button,\n        .tox-tbtn--select {\n            background: none;\n            border-radius: var(--border-radius);\n            border: 1px solid var(--border-light-color);\n            color: var(--text-primary-color);\n            height: 38px;\n            margin: 2px;\n            padding: 2px;\n            min-width: 38px;\n\n            &:active,\n            &:focus,\n            &:hover,\n            &.tox-tbtn--active[aria-expanded='true'],\n            &.tox-tbtn--enabled {\n                background: var(--gray-6);\n                border-color: var(--gray-6);\n\n                & > button {\n                    background: transparent;\n                }\n\n            }\n           \n        }\n\n        .tox-split-button {\n            box-shadow: none !important;\n            padding: 1px;\n\n            &:active,\n            &:focus,\n            &:hover {\n                background: none;\n                border-color: none;\n            }\n        }\n\n        .tox-tbtn--select {\n            padding: 2px 8px !important;\n        }\n\n        .tox-split-button__chevron {\n            margin-left: 1px !important;\n            min-width: 16px;\n        }\n\n        .tox-panel,\n        .tox-statusbar,\n        .tox-toolbar-grp {\n            background: none;\n            border: none;\n        }\n\n        .tox-statusbar {\n            align-self: flex-end;\n            bottom: 2rem;\n            padding: 0;\n            position: fixed;\n            right: 3.2rem;\n            width: auto;\n        }\n\n        .tox-toolbar-grp {\n            padding-bottom: 2.4rem;\n            position: relative;\n        }\n        \n        .tox-editor-header {\n            margin-bottom: 2.4rem;\n        }\n        \n        .tox-toolbar-overlord {\n            background: none;\n            border: none;\n            padding: 0;\n        }\n        \n        .tox-edit-area__iframe {\n            background: none;\n        }\n    }\n\n    .tox-flow-layout {\n        text-align: center;\n    }\n\n    .tox-path {\n        float: left;\n    }\n\n    /*\n     * Inline toolbar\n     */\n\n    #link-toolbar,\n    #inline-toolbar {\n        align-items: center;\n        background: var(--popup-bg);\n        border-radius: var(--border-radius);\n        box-shadow: 0 5px 10px -5px var(--shadow), 4px -11px 26px -12px var(--shadow), 0 24px 50px 2px var(--shadow);\n        display: none;\n        min-height: 44px;\n        padding: 6px;\n        position: absolute;\n        user-select: none;\n        z-index: 10;\n        \n        &::after {\n            border: 9px solid var(--input-bg);\n            border-left-color: transparent;\n            border-right-color: transparent;\n            border-bottom-color: transparent;\n            content: \"\";\n            height: 18px;\n            left: 50%;\n            position: absolute;\n            bottom: -18px;\n            transform: translateX(-50%);\n            width: 18px;\n            z-index: 1;\n        }    \n\n        button {       \n            align-items: center;\n            background: transparent;\n            border: none;\n            cursor: pointer;\n            display: inline-flex;\n            height: 100%;\n            justify-content: center;\n            min-height: 34px;   \n            margin: 0;\n            outline: none;\n            padding: 0;\n            position: relative;\n            width: 38px;\n            \n            // hover effect\n            &::before {\n               content: \"\";\n               background: var(--gray-6); \n               border-radius: var(--border-radius);\n               display: block;\n               left: 50%;\n               opacity: 0;\n               position: absolute;\n               height: 34px;\n               top: 50%;\n               transition: all .15s cubic-bezier(.4,0,.2,1);\n               transform: scale(.5) translate(-50%, -50%);\n               transform-origin: left top;\n               width: 34px;\n               z-index: -1;\n            }\n\n            &:hover {\n\n               &::before {\n                  opacity: 1;\n                  transform: scale(1) translate(-50%, -50%);\n               }\n            }\n           \n            svg {\n               color: var(--icon-tertiary-color);\n               fill: none;\n            }\n        }\n\n        select {\n            -webkit-appearance: none;\n            background: url('data:image/svg+xml;utf8,<svg fill=\"%238e929d\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 6\"><polygon points=\"10 0 5 0 0 0 5 6 10 0\"/></svg>') no-repeat calc(100% - 16px) 50%;\n            background-color: var(--input-bg);\n            background-size: 9px;\n            border: none;\n            border-radius: 0 !important;\n            border-left: 1px solid var(--input-border-color);\n            border-radius: var(--border-radius);\n            box-shadow: none;\n            box-sizing: content-box;\n            color: var(--icon-primary-color);\n            font: 400 #{$app-font-base}/1.3 var(--font-base);\n            margin-left: 15px;\n            max-width: 150px !important;\n            min-width: 125px;\n            min-height: 34px;\n            outline: none;\n            padding: 0 16px;\n            position: relative; \n            transition: var(--transition);\n\n            &:hover {               \n                color: var(--icon-tertiary-color);\n            }\n\n            &:focus {\n                box-shadow: none;\n            }\n\n            & + select {\n                margin-left: 0;\n            }\n        }\n\n        .tox-ico {\n            color: var(--icon-primary-color);\n\n            &:hover {\n                color: var(--icon-tertiary-color);\n            }\n        }\n    }\n\n\n}\n\n/*\n * Modal dialog and popups\n */\nbody {\n    .tox {\n        \n        &-tinymce {\n            border-color: var(--input-border-color);\n            border-radius: var(--border-radius);\n        }\n        \n        .tox-toolbar, \n        .tox-toolbar__overflow, \n        .tox-toolbar-overlord,\n        .tox-toolbar__primary {\n             background: transparent;\n        }\n        \n        .tox-toolbar-overlord {\n            border-bottom: 1px solid var(--input-border-color);\n            padding: 5px;\n        }\n        \n        &-tinymce-aux {\n            z-index: 999999;\n        }\n\n        .tox-dialog {\n            background-color: var(--popup-bg);\n            border: none;\n            border-radius: .6rem;            \n            font-size: $app-font-base;\n            font-weight: 400;\n            min-width: 62rem;\n            overflow: hidden;\n            text-align: center;\n            user-select: none;\n\n            &-wrap__backdrop {\n                background-color: var(--overlay);\n            }\n\n            &__header {\n                background: transparent;\n                padding: 4rem 4rem 0 4rem;\n            }\n\n            &__title {\n                color: var(--text-primary-color);\n                font-size: 1.8rem;\n                font-weight: var(--font-weight-semibold);\n                margin: 0 auto;\n                text-align: center;\n                text-transform: none;\n            }\n\n            &__footer {\n                background: transparent;\n                border: none;\n                display: block;\n                padding: 0;               \n\n                &-end {\n                    flex-direction: row-reverse;\n\n                    .tox-button {\n                        background: var(--button-bg);\n                        border: none;\n                        border-radius: initial;\n                        box-shadow: none;\n                        color: var(--white);\n                        cursor: pointer;\n                        display: inline-block;\n                        flex: 0 1 100%;\n\n                        font: {\n                            size: 1.4rem;\n                            family: var(--font-base);\n                            weight: 500;\n                        }\n\n                        height: 5.6rem;\n                        line-height: 5.5rem;\n                        margin: 0 !important;\n                        padding: 0 2rem;\n                        position: relative;\n                        transition: var(--transition);\n                        user-select: none;\n                        white-space: nowrap;\n\n                        &:focus {\n                            outline: none;\n                        }\n\n                        &:active,\n                        &:focus,\n                        &:hover,\n                        &.button-active {\n                            background: var(--button-bg-hover);\n                            color: var(--white);\n                        }\n                        \n                        &[disabled] {                           \n                            cursor: not-allowed;\n                        }\n\n                        &--secondary {\n                            background: var(--popup-btn-cancel-bg);\n                            border: none;\n                            border-top: 1px solid var(--input-border-color) !important;\n                            color: var(--popup-btn-cancel-color);\n\n                            &:active,\n                            &:focus,\n                            &:hover {\n                                background: var(--popup-btn-cancel-bg-hover);\n                                color: var(--popup-btn-cancel-hover-color);\n                            }\n                        }\n                    }\n                }\n            }\n            \n            &__body {\n                color: var(--text-primary-color);\n                \n                &-content {\n                    padding: 3rem 4rem 5rem 4rem;\n                }\n                \n                &-nav {\n                    padding: 4rem 2rem 4rem 4rem;\n\n                    &-item {\n                        color: var(--tab-color);\n                        font-size: 15px;\n                        padding: 0.3rem 0.1rem;   \n                        \n                        &:hover {\n                            color: var(--tab-active-color); \n                        }\n\n                        &:focus {\n                            background: none;\n                        }\n\n                        &--active {\n                            border-color: var(--button-tertiary-bg);\n                            color: var(--tab-active-color);  \n                        }\n                    }\n                }\n            }\n        }\n\n        .tox-label,\n        .tox-toolbar-label {\n            color: var(--label-color);\n            font-size: 1.4rem;\n            font-weight: var(--font-weight-semibold);\n            line-height: 2.6;\n        }\n\n        .tox-checkbox__label {\n            font-size: 1.4rem;\n        }\n\n        .tox-selectfield select,\n        .tox-listboxfield .tox-listbox--select,\n        .tox-textarea,\n        .tox-textfield,\n        .tox-toolbar-textfield {\n            background-color: var(--input-bg);\n            border: none;\n            border-radius: var(--border-radius);\n            box-shadow: inset 0 0 0 1px var(--input-border-color);\n            color: var(--text-primary-color);\n            font: 400 #{$app-font-base}/1.5 var(--font-base);\n            outline: none;\n            padding: 1.2rem 1.8rem;\n\n            &:focus {\n                background-color: var(--input-bg);\n                box-shadow: inset 0 0 2px 1px var(--input-border-focus);\n            }\n        }\n\n        .tox-listboxfield .tox-listbox--select {\n            &:focus {\n                background-color: var(--input-bg);\n                box-shadow: inset 0 0 0 1px var(--input-border-color);\n            }\n        }\n        \n        .tox-button--naked {\n            border-radius: 50%;\n            color: var(--icon-secondary-color);\n            transition: var(--transition);\n            \n            &:hover:not(:disabled) {\n                background-color: var(--input-border-color);\n                border-color: var(--input-border-color);\n                color: var(--icon-tertiary-color) !important;\n            }\n            \n            &[disabled] {\n                background-color: transparent;\n                border-color: transparent;\n                color: var(--icon-secondary-color);\n            }\n        }   \n        \n        .tox-listbox__select-chevron svg {\n            color: var(--icon-primary-color); \n            fill: none;\n        }\n        \n        .tox-tbtn {\n            color: var(--text-primary-color);\n            cursor: pointer;\n            margin: 2px 2px 3px 0;\n            transition: var(--transition);\n            \n            svg {\n                color: var(--icon-primary-color); \n                 fill: none;\n            }\n            \n            &:active,\n            &:focus,\n            &:hover,\n            &--enabled,\n            &--enabled:hover {\n                background-color: var(--gray-6);\n                color: var(--text-primary-color);\n                \n                svg {\n                    color: var(--icon-tertiary-color);\n                   fill: none;\n                }\n            }\n            \n            &:focus:not(.tox-tbtn--disabled) {\n                color: var(--text-primary-color);\n                \n                svg {\n                   color: var(--icon-tertiary-color);\n                   fill: none;\n                }\n            }  \n            \n            &__select-label {\n                cursor: pointer;\n            }\n        }\n        \n        .tox-menu {\n            background-color: var(--bg-secondary);\n            border: 1px solid var(--border-light-color);\n            border-radius: var(--border-radius);\n            margin-bottom: 3.2rem;\n        }\n        \n        .tox-collection {\n            \n            &--toolbar {\n                .tox-collection__item {\n                     color: var(--icon-primary-color); \n                     border-radius: var(--border-radius);\n                    \n                    &--enabled {\n                        background-color: var(--gray-1);\n                        color: var(--color-primary);\n                    }\n                    \n                    &--active {\n                        background-color: var(--gray-1);\n                        &:not(.tox-collection__item--state-disabled) {\n                            color: var(--icon-tertiary-color);\n                        }\n                    }\n                }\n            }\n            \n            &__item {\n                color: var(--text-light-color);\n                \n                &-caret svg {\n                    color: var(--icon-primary-color); \n                    fill: none;\n                }\n                \n                &-accessory {\n                    color: var(--text-light-color);\n                }\n\n                &-checkmark,\n                &-icon {\n                    svg {\n                        color: currentColor;\n                        fill: none;\n                    }\n                }\n            }\n            \n            &--list {\n                .tox-collection__item--enabled {\n                    background-color: var(--bg-secondary);\n                    color: var(--text-primary-color);\n                }\n                \n                .tox-collection__item--active {\n                    background-color: var(--gray-1);\n                    color: var(--text-primary-color);\n\n                    &:not(.tox-collection__item--state-disabled) {\n                        color: var(--icon-tertiary-color);\n                    }\n                }\n                \n                .tox-collection__item--enabled {\n                    background-color: var(--input-bg-light);\n                    color: var(--text-primary-color);\n                }\n\n                .tox-collection__group {\n                    border-color: var(--border-light-color);\n                }\n            }\n        }\n        \n       .tox-checkbox__icons {\n           .tox-checkbox-icon__indeterminate svg {\n              fill: var(--color-primary); \n           }\n           \n           .tox-checkbox-icon__unchecked svg {\n               fill: none;\n               stroke: currentColor;\n           }\n           \n           .tox-checkbox-icon__checked svg {\n               fill: none; \n               stroke: currentColor;\n           }\n        }\n        \n        input.tox-checkbox__input:focus+.tox-checkbox__icons {\n            box-shadow: none;\n        }\n        \n        .tox-selectfield__icon-js svg {\n            fill: var(--icon-secondary-color);\n        }\n        \n        // Tiny popups: like table toolbar or TOC refresh\n        .tox-pop {\n            \n            &__dialog {\n                background: var(--popup-bg);\n                border: 1px solid var(--popup-bg);\n                border-radius: 4px;\n                box-shadow: 0 5px 10px -5px var(--shadow), 4px -11px 26px -12px var(--shadow), 0 24px 50px 2px var(--shadow);\n            }\n            \n            &.tox-pop--bottom {\n                &::before {\n                    border-color: var(--popup-bg) transparent transparent transparent;\n                }\n                \n                &::after {\n                    border-color: var(--popup-bg) transparent transparent transparent;\n                }\n            }\n            \n            &.tox-pop--top {\n                &::before {\n                    border-color: transparent transparent var(--popup-bg) transparent;\n                }\n                \n                &::after {\n                    border-color: transparent transparent var(--popup-bg) transparent;\n                }\n            }         \n            \n            .tox-toolbar__group:not(:last-of-type) {\n                border-color: var(--input-border-color);\n            }\n        }\n    \n        .tox-statusbar {\n            background: transparent;\n            border-color: var(--border-light-color);\n            color: var(--text-light-color);\n            padding: 13px 16px;\n            text-transform: none;\n            \n            a, \n            &__path-item,\n            &__wordcount {\n               color: var(--text-light-color) !important;\n            }\n        }    \n\n        .tox-color-input span {\n            top: 11px;\n        }\n\n        .tox-swatches__picker-btn {\n\n            &:hover {\n               background: none;\n            }\n\n            svg {\n               fill: var(--icon-tertiary-color)\n            }\n        }  \n    }\n}\n\n/*\n * Mini WYSIWYG editor\n */\nbody .wysiwyg-mini-editor {\n    \n\n    .tox-toolbar-overlord {\n        padding: 3px 5px;\n    }\n\n    .tox-tbtn {\n        transform: scale(.9);\n    }\n\n    .tox-split-button__chevron {\n        width: 16px;\n    }\n\n    .tox-tbtn--select {\n        width: auto;\n    }\n\n    .tox-tbtn__select-label {\n        margin: 0;\n    }\n\n    .tox-statusbar__resize-handle {\n        margin-top: 4px;\n    }\n\n    .tox-statusbar__resize-handle svg {\n        fill: var(--icon-secondary-color);\n    }\n}\n\n\n/*\n * Special rules for Windows\n */\n\nbody[data-os=\"win\"] {\n    #inline-toolbar select {\n        height: 4.8rem;\n    }\n}\n\n.tox-label.tox-publii-text-stats {\n    float: right;\n    padding: 1rem 0;\n    user-select: none;\n\n    svg {\n        fill: var(--gray-4);\n        height: 1.6rem;\n        left: 0;\n        margin-right: .5rem;\n        position: relative;\n        top: -1px;\n        width: 1.6rem;\n    }\n\n    a {\n        border-bottom: 1px solid currentColor;\n        color: var(--link-primary-color);\n        cursor: pointer;\n        transition: var(--transition);\n\n        &:active,\n        &:focus {\n            color: var(--color-primary);\n        }\n\n        &:hover {\n            color: $color-4;\n        }\n\n    }\n}\n\n.tinymce-iframe {\n    font-size: $app-font-base;\n    margin: 2rem auto;\n    max-width: 1024px;\n}\n\n#tox-modal-block.tox-in {\n    opacity: .35 !important;\n}\n\n.tox-window {\n    border: none !important;\n    border-radius: .6rem !important;\n    padding: 4rem !important;\n\n    &[role=\"alertdialog\"] {\n        padding: 4rem 4rem 1.6rem 4rem !important;\n    }\n\n    .tox-reset .tox-window-head {\n        border: none;\n        margin: 0 0 4rem 0;\n        padding: 0 !important;\n        text-transform: none;\n\n        .tox-close {\n            right: -12px !important;\n            top: -8px !important;\n\n            &:active,\n            &:focus,\n            &:hover {\n                background: var(--white) !important;\n                color: var(--icon-primary-color) !important;\n            }\n\n            i {\n                color: var(--warning);\n                font-size: 1.4rem !important;\n            }\n        }\n\n        .tox-title {\n            color: $color-4;\n            font-family: var(--font-base) !important;\n            font-size: 1.8rem !important;\n            font-weight: 600 !important;\n            text-align: center;\n        }\n    }\n\n    .tox-textbox.tox-multiline {\n        box-sizing: border-box;\n        height: 100% !important;\n        left: 0 !important;\n        padding: 1.2rem 1.8rem !important;\n        top: 0 !important;\n        width: 100% !important;\n    }\n\n    .tox-foot {\n        border: none;\n        border-radius: 0 0 .6rem .6rem;\n        height: 4.8rem !important;\n        margin: 4rem -4rem 0 -4rem !important;\n        width: auto !important;\n\n        .tox-container-body {\n            border-radius: 0 0 .6rem .6rem;\n            height: 5.6rem !important;\n            width: 100% !important;\n\n            .tox-tbtn {\n                background: var(--white) !important;\n                border-radius: 0 0 0 .6rem !important;\n                box-shadow: none !important;\n                color: var(--white) !important;\n                font-size: $app-font-base;\n                height: 5.6rem;\n                left: 0 !important;\n                line-height: 5.6rem;\n                top: 0 !important;\n                width: 50% !important;\n\n                &:nth-child(2):last-child {\n                    width: 100% !important;\n                }\n\n                & > button {\n                    font-size: $app-font-base;\n                }\n\n                &.tox-disabled > button {\n                    color: var(--white) !important;\n                }\n\n                &:active,\n                &:focus,\n                &:hover {\n                    background: lighten($color-1, 10%) !important;\n                    color: var(--white) !important;\n                }\n\n                & + .tox-tbtn {\n                    background: var(--white) !important;\n                    border-top: 1px solid $color-8 !important;\n                    border-radius: 0 0 .6rem 0 !important;\n                    color: var(--gray-4) !important;\n                    left: 50% !important;\n\n                    &.tox-disabled > button {\n                        color: var(--gray-4) !important;\n                    }\n\n                    &:active > button,\n                    &:focus > button,\n                    &:hover > button {\n                        background: var(--gray-1) !important;\n                        color: var(--gray-4) !important;\n                    }\n                }\n            }\n        }\n    }\n\n    .tox-textbox {\n        background-color: var(--white);\n        border: none;\n        border-radius: var(--border-radius);\n        box-shadow: inset 0 0 0 1px $color-8;\n        box-sizing: border-box;\n        color: var(--text-primary-color);\n        font: 400 #{$app-font-base}/1.5 var(--font-base);\n        height: 4.8rem;\n        outline: none;\n        padding: 1.2rem 1.8rem;\n\n        &[size=\"3\"] {\n            text-align: center;\n            width: 70px !important;\n        }\n    }\n\n    .tox-combobox .tox-tbtn {\n        & > button {\n            border-radius: 0;\n            line-height: 4.6rem;\n            padding: 0 .8rem;\n\n            &:active,\n            &:focus,\n            &:hover {\n                background: $color-8;\n            }\n        }\n    }\n\n    .tox-listbox {\n        border-radius: var(--border-radius);\n        height: 48px !important;\n        width: 360px !important;\n\n        button {\n            background: var(--white) !important;\n            border-radius: var(--border-radius);\n            height: 48px !important;\n            line-height: 46px !important;\n            padding: 0 1.8rem !important;\n        }\n    }\n\n    label + input,\n    label + div > input {\n        width: 360px !important;\n    }\n}\n\n@media (max-height: 900px) {\n    .tox-window {\n        max-height: 590px !important;\n        padding: 2rem !important;\n\n        .tox-foot {\n            margin: 2.4rem -2rem 0 -2rem !important;\n        }\n\n        .tox-window-body {\n            max-height: 500px;\n\n            & > .tox-form.tox-abs-layout-item {\n                max-height: 500px;\n\n                & > .tox-container-body.tox-abs-layout {\n                    max-height: 500px;\n                }\n            }\n        }\n\n        .tox-reset .tox-window-head {\n            margin: 0 0 2rem 0 !important;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/scss/editor/editor.scss",
    "content": "@import '../vendor/modularscale';\n@import '../variables';\n@import '../css-variables';\n@import 'scrollbar';\n@import '../mixins';\n\n\n/*\n * Reset\n */\n*,\n*:before,\n*:after {\n    box-sizing: border-box;\n}\n\nbody,\ndiv,\ndl,\ndt,\ndd,\nul,\nol,\nli,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\npre,\nfigure,\nform,\nfieldset,\np,\nblockquote,\nth,\ntd {\n    margin: 0;\n    padding: 0;\n}\n\nimg {\n    height: auto;\n    max-width: 100%;\n    vertical-align: top;\n}\n\n/*\n * Main rules\n */\n\nhtml {\n    font-size: var(--editor-font-size);\n    height: 100%;\n    overflow-y: auto !important;\n}\n\nbody {\n    color: var(--text-primary-color);\n    font-family: var(--editor-font-family);\n    font-weight: var(--font-weight-normal);\n    line-height: $line-height;\n}\n\nbody#tinymce {\n    background: var(--bg-primary);\n    -webkit-font-smoothing: antialiased;\n    font-size: inherit;\n    margin: 1rem auto 0;\n    max-width: var(--editor-width);\n    overflow-y: initial !important;\n    padding: 0 0 1.4rem !important;\n    width: 100%;\n\n    & > :nth-child(1) {\n        margin-top: 0;\n    }\n}\n\na:not(.btn):not(.mce-item-anchor):not(.gallery__item a) {\n    @include links (var(--link-invert-color), var(--link-invert-color), var(--link-invert-color), var(--link-invert-color));\n}\n\np,\nul,\nol,\ndl,\ntable,\nblockquote,\nhr {\n    margin-top: baseline(7);\n}\n\nfigure,\n.gallery {\n    margin-top: baseline(9);\n    margin-bottom: baseline(9);\n}\n\npre,\ntable,\nscript {\n    margin-top: baseline(7);\n    margin-bottom: baseline(7);\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    color: var(--headings-color);\n    font-family: var(--font-base);\n    font-weight: var(--font-weight-bold);\n    letter-spacing: var(--letter-spacing);\n    line-height: 1.3;\n    margin-top: baseline(10);\n}\n\nh1 {\n    font-size: $h1;\n}\n\nh2 {\n    font-size: $h2;\n}\n\nh3 {\n    font-size: $h3;\n}\n\nh4 {\n    font-size: $h4;\n}\n\nh5 {\n    font-size: $h5;\n}\n\nh6 {\n    font-size: $h6;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    & + p {\n        margin-top: baseline(3);\n    }\n}\n\nb,\nstrong {\n    color: var(--headings-color);\n    font-weight: var(--font-weight-bold);\n}\n\nblockquote {\n    border-left: 2px solid var(--gray-3);\n    font-family: var(--font-serif);\n    font-style: italic;\n    padding: 0 0 0 baseline(6);\n    position: relative;\n\n    & > :nth-child(1) {\n        margin-top: 0;\n    }\n}\n\nul,\nol {\n    margin-left: 2rem;\n\n    & > li {\n        list-style: inherit;\n        padding-left: baseline(2);\n    }\n\n    ul,\n    ol {\n        margin-bottom: baseline(2);\n        margin-top: baseline(2);\n    }\n}\n\ndl {\n    dt {\n        font-weight: var(--font-weight-bold);\n    }\n}\n\ncode {\n    background-color: var(--gray-1);\n    border-radius: 2px;\n    color: var(--warning);\n    font-size: ms(-1);\n    font-family: var(--font-monospace);\n    padding: baseline(1) baseline(2);\n}\n\npre {\n    background-color: var(--gray-1);\n    font-size: ms(-2);\n    padding: baseline(6);\n    white-space: pre-wrap;\n    word-wrap: break-word;\n\n    & > code {\n        background: none;\n        color: var(--headings-color);\n        display: inline-block;\n        font-size: inherit;\n        font-weight: normal;\n        padding: 0;\n    }\n}\n\ntable {\n    border: 1px solid var(--input-border-color);\n    border-collapse: collapse;\n    border-spacing: 0;\n    vertical-align: top;\n    text-align: left;\n    width: 100%;\n\n    th {\n        font-weight: var(--font-weight-semibold);\n        padding: baseline(2.5) baseline(4);\n    }\n\n    td {\n        border-top: 1px solid var(--input-border-color);\n        padding: baseline(2.5) baseline(4);\n    }\n}\n\nkbd {\n    background: var(--headings-color);\n    border-radius: 2px;\n    color: var(--bg-primary);\n    font-family: var(--font-monospace);\n    font-size: 0.8888em;\n    padding: baseline(0.5) baseline(1.5);\n}\n\nsub,\nsup {\n    font-size: 65%;\n}\n\nsmall {\n    font-size: ms(-2);\n}\n\nhr:not(#read-more) {\n    background: none;\n    border: none;\n    height: auto;\n    line-height: 1;\n    max-width: none;\n    text-align: center;\n\n    &::before {\n        content: \"* * *\";\n        color: var(--headings-color);\n        font-size: 30px;\n    }\n}\n\n\nfigcaption {\n    clear: both;\n    color: var(--text-light-color);\n    font-size: ms(-3);\n    margin: baseline(3) 0 0;\n    text-align: center;\n}\n\nkbd {\n    background: var(--gray-1);\n    border-radius: 1px;\n    color: var(--headings-color);\n    font-family: Menlo, Monaco, Consolas, Courier New, monospace;\n    font-size: ms(-1);\n    padding: baseline(0.5) baseline(1.5);\n}\n\nsub,\nsup {\n    font-size: 65%;\n}\n\nsmall {\n    font-size: ms(-2);\n}\n\nhr#read-more {\n    border: none;\n    background-color: var(--text-primary-color);\n    cursor: default;\n    height: 1px;\n    margin: baseline(10) 0 baseline(12);\n    overflow: visible;\n    position: relative;\n\n    &:after {\n        background: var(--input-bg);\n        content: attr(data-translation);\n        display: inline-block;\n        font-family: var(--font-base);\n        font-size: ms(-3);\n        font-weight: var(--font-weight-semibold);\n        left: 50%;\n        padding: 6px 16px;\n        position: absolute;\n        top: 50%;\n        transform: translateX(-50%) translateY(-50%);\n    }\n}\n\n\n/*\n * Basic buttons\n */\n\n.btn {\n    background: var(--button-bg);\n    border: 1px solid var(--button-bg);\n    border-radius: var(--border-radius);\n    color: var(--white);\n    cursor: pointer;\n    display: inline-block;\n    font-family: inherit;\n    font-size: ms(-1);\n    font-weight: var(--font-weight-normal);\n    line-height: 1.6;\n    padding: baseline(2) baseline(4);\n    vertical-align: middle;\n    text-align: center;\n    transition: all 0.24s ease;\n    overflow: hidden;\n    will-change: transform;\n\n    &:hover,\n    &:active,\n    &:focus {\n        background: var(--button-bg-hover);\n        border-color: var(--button-bg-hover);\n        text-decoration: none !important;\n    }\n}\n\n[type=button],\n[type=submit],\nbutton {\n    @extend .btn;\n\n}\n\n/*\n * Basic forms\n */\n\nfieldset {\n    border: 1px solid var(--input-border-color);\n    margin: 0 0 baseline(6);\n    padding: baseline(6);\n\n    & > legend {\n        margin-left: -1rem;\n        padding: 0 1rem;\n    }\n}\n\nlegend {\n    font-weight: var(--font-weight-bold);\n}\n\nlabel {\n    font-weight: var(--font-weight-bold);\n    margin: 0 baseline(4) 0.8rem 0;\n}\n\noption {\n    font-weight: var(--font-weight-normal);\n}\n\n[type=text],\n[type=url],\n[type=tel],\n[type=number],\n[type=email],\n[type=search],\ntextarea,\nselect {\n    background-color: var(--input-bg);\n    border: 1px solid var(--input-border-color);\n    border-radius: 0;\n    font-family: inherit;\n    color: var(--text-primary-color);\n    font-size: ms(-1);\n    outline: none;\n    padding: baseline(2) baseline(3);\n    width: auto;\n    transition: all 0.24s ease;\n\n    &::-webkit-input-placeholder {\n        color: inherit;\n    }\n}\n\n\nselect {\n    max-width: 100%;\n    width: auto;\n    position: relative;\n}\n\nselect[multiple] {\n    padding: baseline(6);\n    width: 100%;\n}\n\n/*\n * Alignment\n */\n\n.align-left {\n    text-align: left;\n}\n\n.align-right {\n    text-align: right;\n}\n\n.align-center {\n    text-align: center;\n}\n\n/*\n * Post elements\n */\n.post {\n\n    &__image {\n        &--left {\n            float: left;\n            margin-bottom: baseline(6);\n            margin-right: baseline(6);\n            max-width: 50%;\n        }\n\n        &--right {\n            float: right;\n            margin-bottom: baseline(6);\n            margin-left: baseline(6);\n            max-width: 50%;\n        }\n\n        &--center {\n            display: block;\n            margin-left: auto;\n            margin-right: auto;\n            text-align: center;\n        }\n\n        &--wide {\n            margin-left: -10%;\n            margin-right: -10%;\n\n            & > img {\n                width: 100%;\n            }\n\n        }\n\n        &--full {\n            margin-left: calc(50% - 50vw + 1.6rem);\n            margin-right: calc(50% - 50vw + 1.6rem);\n\n            & > img {\n                width: 100%;\n            }\n        }\n    }\n\n    &__video {\n        background: var(--gray-1);\n        border: 2px dashed var(--input-border-color);\n        position: relative;\n\n        & > img.mce-object {\n            background: var(--gray-1);\n            border: 5px solid var(--bg-primary);\n            position: relative;\n            background-image: url(\"data:image/svg+xml;charset=UTF-8,%3c?xml version='1.0' encoding='utf-8'?%3e%3csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 68 48' xml:space='preserve'%3e%3cpath fill='%23cbcdd4' d='M64.82,6.36c-0.58-2.21-2.97-4.3-5.17-4.61C50.94,0.58,42.22,0,33.5,0S16.06,0.58,7.34,1.75 c-2.2,0.31-4.59,2.4-5.17,4.61c-2.9,11.43-2.9,22.85,0,34.28c0.58,2.21,2.97,4.3,5.17,4.61C16.06,46.42,24.78,47,33.5,47 s17.44-0.58,26.16-1.75c2.2-0.31,4.59-2.4,5.17-4.61C67.73,29.21,67.73,17.79,64.82,6.36z M43.21,24.78l-16.14,8.07 C26.12,33.32,25,32.63,25,31.57V23.5v-8.07c0-1.06,1.12-1.76,2.07-1.28l16.14,8.07C44.26,22.75,44.26,24.25,43.21,24.78z'/%3e%3c/svg%3e\");\n            background-repeat: no-repeat;\n            background-size: 68px 48px;\n            background-position: 50% 50%;\n            max-height: 17rem;\n        }\n    }\n\n    &__toc {\n\n        margin-top: baseline(6);\n\n        h3 {\n            font-size: ms(1);\n            margin: 0;\n        }\n\n        ul {\n            counter-reset: item;\n            list-style: decimal;\n            margin-top: baseline(2);\n\n            li {\n                counter-increment: item;\n                padding: 0;\n\n                a {\n                    background: none !important;\n                    border: none !important;\n                }\n            }\n\n            ul {\n                margin-top: 0;\n\n                li {\n                    display: block;\n\n                    &:before {\n                        content: counters(item, \".\") \". \";\n                        margin-left: -2rem;\n                    }\n                }\n            }\n        }\n    }\n}\n\n// Image alignment without parent container\nimg {\n\n    &.post__image--full {\n        margin-left: calc(50% - 50vw + 1.6rem);\n        margin-right: calc(50% - 50vw + 1.6rem);\n        width: 100vw;\n        max-width: calc(100vw - 3.2rem);\n    }\n\n    &.post__image--wide {    \n        margin-left: -10%;\n        margin-right: -10%;\n        max-width: 100vw;\n        width: calc(100% + 20%);\n    }\n}\n\n.msg {\n    border: 1px solid transparent;\n    border-radius: 3px;\n    padding: baseline(4);\n\n    &--highlight {\n        background-color: rgba(var(--yellow), .1);\n        border-color: rgba(var(--yellow), .5);\n    }\n\n    &--info {\n        background-color: rgba(var(--color-primary-rgb), .1);\n        border-color: rgba(var(--color-primary-rgb), .5);\n    }\n\n    &--success {\n        background-color: rgba(var(--success-rgb), .1);\n        border-color: rgba(var(--success-rgb), .51);\n    }\n\n    &--warning {\n        background-color: rgba(var(--warning-rgb), .1);\n        border-color: rgba(var(--warning-rgb), .5);\n    }\n}\n\n.dropcap {\n    &:first-letter {\n        color: var(--headings-color);\n        float: left;\n        font-size: ms(9);\n        font-weight: var(--font-weight-bold);\n        line-height: 0.7;\n        margin-right: baseline(2);\n        padding: baseline(1.5) baseline(1) baseline(2) 0;\n    }\n}\n\n/*\n * Gallery\n */\n.gallery {\n    cursor: pointer !important;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 1rem;\n    position: relative;\n    -webkit-user-select: none;\n    user-select: none;\n\n    &[data-is-empty=\"true\"] {\n        border: 2px dashed var(--input-border-color);\n        padding: 5rem;\n\n        &:after {\n            background: var(--button-secondary-bg);\n            border: 1px solid var(--button-secondary-bg);\n            border-radius: var(--border-radius);\n            color: var(--button-secondary-color);\n            content: attr(data-translation);\n            font-family: var(--font-base);\n            font-size: 15px;\n            font-weight: var(--font-weight-semibold);\n            left: 50%;\n            margin: 0;\n            padding: 0.3rem .9rem;\n            position: absolute;\n            pointer-events: none;\n            text-align: center;\n            top: 8rem;\n            transform: translateX(-50%) translateY(-50%);\n            white-space: nowrap;\n        }\n\n        &:before {\n            background-color: var(--icon-quaternary-color);\n            bottom: 0;\n            content: \"\";\n            left: 0;\n            mask: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 148'%3e%3cpath d='M157.887,44.459L52.274,10.298c-4.204-1.36-8.714.946-10.074,5.15l-25.236,78.02c-1.36,4.204.946,8.714,5.15,10.074l105.612,34.161c4.204,1.36,8.714-.946,10.074-5.15l25.236-78.02c1.36-4.204-.946-8.714-5.15-10.074ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3cg%3e%3cpath d='M26.525,101.386l23.854-17.207c1.309-.944,3.057-1.009,4.433-.165l13.435,8.247c1.432.879,3.261.769,4.577-.274l46.564-36.911c1.516-1.202,3.674-1.146,5.127.131l28.984,25.498-.033,34.303c-.004,4.415-3.585,7.992-8,7.992H34.5c-4.424,0-8.008-3.591-8-8.015l.025-13.599Z' /%3e%3cpath d='M145.5,25H34.5c-4.418,0-8,3.582-8,8v82c0,4.418,3.582,8,8,8h111c4.418,0,8-3.582,8-8V33c0-4.418-3.582-8-8-8ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3c/g%3e%3cg%3e%3cpath d='M13.5,27c-.829,0-1.5-.671-1.5-1.5v-3.105c0-.126.003-.252.01-.376.007-.129.016-.251.028-.373.013-.13.029-.251.047-.372.021-.129.043-.251.068-.372.026-.123.054-.243.085-.363.031-.117.064-.233.1-.346.035-.112.075-.227.115-.338.045-.121.091-.237.141-.351.045-.104.095-.215.148-.324.054-.11.111-.22.17-.327.055-.099.116-.204.179-.306.06-.097.126-.198.194-.297.071-.103.14-.2.211-.292.068-.089.142-.182.217-.272.078-.093.156-.181.234-.267.076-.084.16-.169.243-.252.087-.087.179-.173.271-.255.091-.081.18-.157.271-.23.086-.071.179-.142.272-.212.105-.077.202-.145.302-.21.104-.069.204-.132.306-.193.104-.062.209-.123.317-.179.108-.057.213-.11.319-.16.108-.051.219-.101.33-.147.113-.047.226-.091.339-.132.116-.041.229-.079.343-.114.112-.036.237-.069.361-.101.115-.029.234-.056.354-.081.124-.025.247-.047.37-.066.123-.018.241-.033.361-.046.135-.015.262-.023.391-.03.133-.006.259-.01.386-.01h4.012c.829,0,1.5.671,1.5,1.5s-.671,1.5-1.5,1.5h-4.012c-.079,0-.158.002-.236.006-.068.004-.143.009-.217.017l-.238.03c-.068.01-.142.023-.216.038s-.149.032-.222.05c-.068.018-.135.035-.2.055l-.226.076c-.066.024-.136.052-.204.08-.063.026-.13.057-.196.088l-.201.101c-.059.032-.121.067-.182.104l-.185.117c-.056.037-.115.078-.173.12l-.337.275c-.045.041-.094.085-.141.131-.05.05-.101.101-.149.154l-.41.514c-.036.052-.074.11-.11.169-.041.065-.075.126-.109.188-.036.065-.067.125-.097.187-.031.063-.061.127-.088.192-.029.068-.055.132-.079.196-.023.064-.048.133-.07.203l-.063.218c-.018.066-.034.136-.048.206-.015.071-.029.143-.04.215-.01.067-.02.143-.028.218-.007.07-.013.148-.017.225-.003.068-.006.145-.006.223v3.105c0,.829-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M24.5,133h-4.012c-.127,0-.253-.004-.379-.01-.127-.006-.253-.017-.378-.028-.13-.013-.261-.029-.389-.05-.114-.017-.243-.039-.37-.065-.104-.021-.221-.048-.337-.076-.127-.032-.241-.064-.351-.098-.132-.04-.252-.081-.369-.123-.106-.038-.229-.085-.348-.136-.104-.045-.21-.092-.314-.141-.106-.05-.211-.104-.316-.158-.114-.061-.224-.123-.331-.188-.09-.054-.19-.116-.29-.182-.098-.064-.193-.133-.288-.2-.101-.074-.197-.149-.291-.225-.096-.079-.185-.154-.272-.232-.083-.072-.168-.154-.251-.234-.094-.092-.178-.18-.259-.268-.085-.095-.163-.183-.238-.272-.071-.083-.145-.177-.218-.272-.07-.093-.137-.186-.202-.278-.077-.111-.143-.213-.206-.316-.062-.099-.123-.204-.183-.312-.055-.101-.111-.207-.164-.315-.053-.108-.104-.218-.151-.329-.049-.113-.095-.227-.137-.344s-.081-.228-.116-.339c-.036-.117-.069-.23-.1-.345-.032-.119-.06-.241-.085-.363-.025-.12-.048-.243-.067-.364-.019-.127-.036-.25-.048-.372-.013-.128-.022-.251-.029-.373-.006-.133-.01-.259-.01-.384v-5.105c0-.828.671-1.5,1.5-1.5s1.5.672,1.5,1.5v5.105c0,.076.002.153.006.23.004.068.01.146.018.224.007.067.017.144.028.218.01.064.024.137.039.207s.03.14.048.208l.136.43c.021.059.048.123.075.187.028.065.058.13.088.193.031.063.063.125.098.187.036.065.07.124.105.182.04.064.078.123.117.181l.124.17c.043.057.087.111.132.165l.151.173c.04.043.088.094.137.143l.325.29c.048.039.103.083.159.123l.364.241c.068.041.126.074.185.105l.411.194c.062.026.123.05.183.071.081.029.148.053.215.072l.435.109c.08.018.149.028.218.039.085.013.156.021.226.028.076.008.153.015.23.019.078.004.157.006.236.006h4.012c.829,0,1.5.672,1.5,1.5s-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M160.512,133h-4.012c-.828,0-1.5-.672-1.5-1.5s.672-1.5,1.5-1.5h4.012c.079,0,.158-.002.237-.006.066-.004.135-.008.201-.016.09-.01.165-.018.239-.029.085-.013.153-.024.221-.038.093-.019.166-.036.238-.054.062-.016.136-.036.207-.058l.206-.069c.077-.027.141-.054.203-.079.068-.028.137-.06.203-.091s.133-.065.197-.1c.059-.031.117-.064.174-.099l.196-.123c.059-.038.115-.079.172-.12l.334-.271c.058-.051.105-.097.152-.143l.282-.308c.057-.065.097-.117.136-.169l.134-.186c.04-.057.075-.111.109-.167l.11-.188c.033-.062.065-.124.097-.187l.092-.199c.023-.055.049-.116.07-.179l.136-.425c.021-.077.035-.143.049-.207.018-.081.029-.148.039-.215.014-.081.022-.15.028-.218.008-.074.015-.15.019-.225.004-.076.006-.152.006-.229v-3.105c0-.828.672-1.5,1.5-1.5s1.5.672,1.5,1.5v3.105c0,.126-.004.251-.01.375-.006.126-.017.251-.028.374-.014.13-.03.26-.051.387-.017.112-.04.24-.066.365-.022.112-.053.238-.086.363-.026.1-.06.213-.094.325-.042.132-.081.245-.122.355-.038.104-.085.223-.135.34-.05.114-.1.222-.151.328-.053.109-.108.217-.167.321-.057.104-.117.206-.179.307-.065.106-.134.212-.206.314-.064.093-.134.189-.205.282-.066.089-.146.188-.226.283-.061.073-.139.161-.217.247-.092.099-.171.181-.251.261-.096.095-.187.18-.279.262-.079.07-.167.146-.258.22-.101.081-.192.152-.287.222-.098.072-.195.141-.297.208-.1.065-.199.128-.301.188-.097.059-.206.121-.317.18-.105.056-.211.109-.318.16s-.219.1-.328.146c-.115.049-.233.095-.353.137-.098.036-.211.073-.323.109-.129.039-.247.071-.365.102-.128.032-.248.06-.37.084-.11.023-.237.045-.368.064-.106.017-.232.032-.358.045-.12.015-.255.024-.391.03-.127.006-.253.01-.381.01Z' /%3e%3cpath d='M166.5,29c-.828,0-1.5-.671-1.5-1.5v-5.105c0-.077-.002-.154-.006-.23-.004-.075-.011-.15-.019-.225-.007-.068-.015-.137-.026-.204-.012-.082-.024-.149-.038-.215-.017-.08-.031-.144-.049-.208l-.136-.429c-.025-.074-.049-.131-.073-.187l-.192-.392c-.033-.062-.069-.122-.106-.181-.034-.056-.069-.11-.106-.164-.045-.065-.083-.12-.125-.173-.052-.069-.095-.123-.14-.177l-.149-.17c-.039-.043-.083-.089-.129-.134l-.327-.293c-.055-.044-.109-.087-.166-.128l-.362-.241c-.061-.036-.122-.072-.185-.105-.064-.035-.131-.068-.197-.1l-.216-.096c-.064-.028-.13-.052-.195-.076-.06-.021-.128-.044-.194-.065-.075-.022-.144-.042-.214-.06l-.23-.053c-.074-.015-.146-.027-.22-.039-.071-.011-.15-.021-.228-.029-.07-.007-.15-.013-.229-.017-.071-.003-.15-.006-.229-.006h-4.012c-.828,0-1.5-.671-1.5-1.5s.672-1.5,1.5-1.5h4.012c.128,0,.254.003.38.009.132.007.257.017.379.029.129.013.253.029.374.047.131.02.256.042.378.067.116.023.233.05.351.08.119.029.24.063.357.1.112.033.233.074.35.116.122.045.24.09.354.138.105.045.212.092.315.141.107.051.213.105.318.16.107.057.214.118.317.179.102.061.201.124.301.189.102.068.194.134.287.201.103.075.198.149.292.226.092.074.179.149.267.227.088.077.172.158.256.239.091.087.178.178.263.271.083.091.16.179.235.268.073.087.147.181.221.278.065.084.137.185.207.286.064.092.133.197.198.303.062.103.124.207.183.313.057.104.111.208.163.314s.102.215.148.324c.052.12.101.241.145.365.035.095.074.206.11.319.038.125.072.241.102.356.03.112.061.237.086.365.024.111.047.238.066.365.018.113.035.242.048.372.012.124.022.248.028.373s.01.251.01.376v5.105c0,.829-.672,1.5-1.5,1.5Z' /%3e%3c/g%3e%3c/svg%3e\");\n            -webkit-mask: url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 148'%3e%3cpath d='M157.887,44.459L52.274,10.298c-4.204-1.36-8.714.946-10.074,5.15l-25.236,78.02c-1.36,4.204.946,8.714,5.15,10.074l105.612,34.161c4.204,1.36,8.714-.946,10.074-5.15l25.236-78.02c1.36-4.204-.946-8.714-5.15-10.074ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3cg%3e%3cpath d='M26.525,101.386l23.854-17.207c1.309-.944,3.057-1.009,4.433-.165l13.435,8.247c1.432.879,3.261.769,4.577-.274l46.564-36.911c1.516-1.202,3.674-1.146,5.127.131l28.984,25.498-.033,34.303c-.004,4.415-3.585,7.992-8,7.992H34.5c-4.424,0-8.008-3.591-8-8.015l.025-13.599Z' /%3e%3cpath d='M145.5,25H34.5c-4.418,0-8,3.582-8,8v82c0,4.418,3.582,8,8,8h111c4.418,0,8-3.582,8-8V33c0-4.418-3.582-8-8-8ZM58.702,68.763c-7.454,0-13.496-6.044-13.496-13.5s6.042-13.5,13.496-13.5,13.496,6.044,13.496,13.5-6.042,13.5-13.496,13.5Z' opacity='.39'/%3e%3c/g%3e%3cg%3e%3cpath d='M13.5,27c-.829,0-1.5-.671-1.5-1.5v-3.105c0-.126.003-.252.01-.376.007-.129.016-.251.028-.373.013-.13.029-.251.047-.372.021-.129.043-.251.068-.372.026-.123.054-.243.085-.363.031-.117.064-.233.1-.346.035-.112.075-.227.115-.338.045-.121.091-.237.141-.351.045-.104.095-.215.148-.324.054-.11.111-.22.17-.327.055-.099.116-.204.179-.306.06-.097.126-.198.194-.297.071-.103.14-.2.211-.292.068-.089.142-.182.217-.272.078-.093.156-.181.234-.267.076-.084.16-.169.243-.252.087-.087.179-.173.271-.255.091-.081.18-.157.271-.23.086-.071.179-.142.272-.212.105-.077.202-.145.302-.21.104-.069.204-.132.306-.193.104-.062.209-.123.317-.179.108-.057.213-.11.319-.16.108-.051.219-.101.33-.147.113-.047.226-.091.339-.132.116-.041.229-.079.343-.114.112-.036.237-.069.361-.101.115-.029.234-.056.354-.081.124-.025.247-.047.37-.066.123-.018.241-.033.361-.046.135-.015.262-.023.391-.03.133-.006.259-.01.386-.01h4.012c.829,0,1.5.671,1.5,1.5s-.671,1.5-1.5,1.5h-4.012c-.079,0-.158.002-.236.006-.068.004-.143.009-.217.017l-.238.03c-.068.01-.142.023-.216.038s-.149.032-.222.05c-.068.018-.135.035-.2.055l-.226.076c-.066.024-.136.052-.204.08-.063.026-.13.057-.196.088l-.201.101c-.059.032-.121.067-.182.104l-.185.117c-.056.037-.115.078-.173.12l-.337.275c-.045.041-.094.085-.141.131-.05.05-.101.101-.149.154l-.41.514c-.036.052-.074.11-.11.169-.041.065-.075.126-.109.188-.036.065-.067.125-.097.187-.031.063-.061.127-.088.192-.029.068-.055.132-.079.196-.023.064-.048.133-.07.203l-.063.218c-.018.066-.034.136-.048.206-.015.071-.029.143-.04.215-.01.067-.02.143-.028.218-.007.07-.013.148-.017.225-.003.068-.006.145-.006.223v3.105c0,.829-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M24.5,133h-4.012c-.127,0-.253-.004-.379-.01-.127-.006-.253-.017-.378-.028-.13-.013-.261-.029-.389-.05-.114-.017-.243-.039-.37-.065-.104-.021-.221-.048-.337-.076-.127-.032-.241-.064-.351-.098-.132-.04-.252-.081-.369-.123-.106-.038-.229-.085-.348-.136-.104-.045-.21-.092-.314-.141-.106-.05-.211-.104-.316-.158-.114-.061-.224-.123-.331-.188-.09-.054-.19-.116-.29-.182-.098-.064-.193-.133-.288-.2-.101-.074-.197-.149-.291-.225-.096-.079-.185-.154-.272-.232-.083-.072-.168-.154-.251-.234-.094-.092-.178-.18-.259-.268-.085-.095-.163-.183-.238-.272-.071-.083-.145-.177-.218-.272-.07-.093-.137-.186-.202-.278-.077-.111-.143-.213-.206-.316-.062-.099-.123-.204-.183-.312-.055-.101-.111-.207-.164-.315-.053-.108-.104-.218-.151-.329-.049-.113-.095-.227-.137-.344s-.081-.228-.116-.339c-.036-.117-.069-.23-.1-.345-.032-.119-.06-.241-.085-.363-.025-.12-.048-.243-.067-.364-.019-.127-.036-.25-.048-.372-.013-.128-.022-.251-.029-.373-.006-.133-.01-.259-.01-.384v-5.105c0-.828.671-1.5,1.5-1.5s1.5.672,1.5,1.5v5.105c0,.076.002.153.006.23.004.068.01.146.018.224.007.067.017.144.028.218.01.064.024.137.039.207s.03.14.048.208l.136.43c.021.059.048.123.075.187.028.065.058.13.088.193.031.063.063.125.098.187.036.065.07.124.105.182.04.064.078.123.117.181l.124.17c.043.057.087.111.132.165l.151.173c.04.043.088.094.137.143l.325.29c.048.039.103.083.159.123l.364.241c.068.041.126.074.185.105l.411.194c.062.026.123.05.183.071.081.029.148.053.215.072l.435.109c.08.018.149.028.218.039.085.013.156.021.226.028.076.008.153.015.23.019.078.004.157.006.236.006h4.012c.829,0,1.5.672,1.5,1.5s-.671,1.5-1.5,1.5Z' /%3e%3cpath d='M160.512,133h-4.012c-.828,0-1.5-.672-1.5-1.5s.672-1.5,1.5-1.5h4.012c.079,0,.158-.002.237-.006.066-.004.135-.008.201-.016.09-.01.165-.018.239-.029.085-.013.153-.024.221-.038.093-.019.166-.036.238-.054.062-.016.136-.036.207-.058l.206-.069c.077-.027.141-.054.203-.079.068-.028.137-.06.203-.091s.133-.065.197-.1c.059-.031.117-.064.174-.099l.196-.123c.059-.038.115-.079.172-.12l.334-.271c.058-.051.105-.097.152-.143l.282-.308c.057-.065.097-.117.136-.169l.134-.186c.04-.057.075-.111.109-.167l.11-.188c.033-.062.065-.124.097-.187l.092-.199c.023-.055.049-.116.07-.179l.136-.425c.021-.077.035-.143.049-.207.018-.081.029-.148.039-.215.014-.081.022-.15.028-.218.008-.074.015-.15.019-.225.004-.076.006-.152.006-.229v-3.105c0-.828.672-1.5,1.5-1.5s1.5.672,1.5,1.5v3.105c0,.126-.004.251-.01.375-.006.126-.017.251-.028.374-.014.13-.03.26-.051.387-.017.112-.04.24-.066.365-.022.112-.053.238-.086.363-.026.1-.06.213-.094.325-.042.132-.081.245-.122.355-.038.104-.085.223-.135.34-.05.114-.1.222-.151.328-.053.109-.108.217-.167.321-.057.104-.117.206-.179.307-.065.106-.134.212-.206.314-.064.093-.134.189-.205.282-.066.089-.146.188-.226.283-.061.073-.139.161-.217.247-.092.099-.171.181-.251.261-.096.095-.187.18-.279.262-.079.07-.167.146-.258.22-.101.081-.192.152-.287.222-.098.072-.195.141-.297.208-.1.065-.199.128-.301.188-.097.059-.206.121-.317.18-.105.056-.211.109-.318.16s-.219.1-.328.146c-.115.049-.233.095-.353.137-.098.036-.211.073-.323.109-.129.039-.247.071-.365.102-.128.032-.248.06-.37.084-.11.023-.237.045-.368.064-.106.017-.232.032-.358.045-.12.015-.255.024-.391.03-.127.006-.253.01-.381.01Z' /%3e%3cpath d='M166.5,29c-.828,0-1.5-.671-1.5-1.5v-5.105c0-.077-.002-.154-.006-.23-.004-.075-.011-.15-.019-.225-.007-.068-.015-.137-.026-.204-.012-.082-.024-.149-.038-.215-.017-.08-.031-.144-.049-.208l-.136-.429c-.025-.074-.049-.131-.073-.187l-.192-.392c-.033-.062-.069-.122-.106-.181-.034-.056-.069-.11-.106-.164-.045-.065-.083-.12-.125-.173-.052-.069-.095-.123-.14-.177l-.149-.17c-.039-.043-.083-.089-.129-.134l-.327-.293c-.055-.044-.109-.087-.166-.128l-.362-.241c-.061-.036-.122-.072-.185-.105-.064-.035-.131-.068-.197-.1l-.216-.096c-.064-.028-.13-.052-.195-.076-.06-.021-.128-.044-.194-.065-.075-.022-.144-.042-.214-.06l-.23-.053c-.074-.015-.146-.027-.22-.039-.071-.011-.15-.021-.228-.029-.07-.007-.15-.013-.229-.017-.071-.003-.15-.006-.229-.006h-4.012c-.828,0-1.5-.671-1.5-1.5s.672-1.5,1.5-1.5h4.012c.128,0,.254.003.38.009.132.007.257.017.379.029.129.013.253.029.374.047.131.02.256.042.378.067.116.023.233.05.351.08.119.029.24.063.357.1.112.033.233.074.35.116.122.045.24.09.354.138.105.045.212.092.315.141.107.051.213.105.318.16.107.057.214.118.317.179.102.061.201.124.301.189.102.068.194.134.287.201.103.075.198.149.292.226.092.074.179.149.267.227.088.077.172.158.256.239.091.087.178.178.263.271.083.091.16.179.235.268.073.087.147.181.221.278.065.084.137.185.207.286.064.092.133.197.198.303.062.103.124.207.183.313.057.104.111.208.163.314s.102.215.148.324c.052.12.101.241.145.365.035.095.074.206.11.319.038.125.072.241.102.356.03.112.061.237.086.365.024.111.047.238.066.365.018.113.035.242.048.372.012.124.022.248.028.373s.01.251.01.376v5.105c0,.829-.672,1.5-1.5,1.5Z' /%3e%3c/g%3e%3c/svg%3e\");\n            mask-repeat: no-repeat;\n            -webkit-mask-repeat: no-repeat;\n            mask-size: 125px 66px;\n            -webkit-mask-size: 125px 66px;\n            mask-position: 50% 2.3rem;\n            -webkit-mask-position: 50% 2.3rem;\n            position: absolute;\n            right: 0;\n            top: 0;\n        }\n\n        &:hover:after {\n            background: var(--button-secondary-bg-hover);\n            border-color: var(--button-secondary-bg-hover);\n            color: var(--button-secondary-color-hover)\n        }\n    }\n\n    &__item {\n        flex-grow: 1;\n        margin: 0;\n        pointer-events: none;\n\n        & a {\n            display: block;\n            height: 30vh;\n            position: relative;\n        }\n\n        & img {\n            height: inherit;            \n            max-width: 100%;\n            object-fit: cover;\n            position: absolute;  \n            width: 100%;\n        }\n    }\n\n    &[data-columns=\"1\"] .gallery__item {\n        width: 100%;\n    }\n\n    &[data-columns=\"2\"] .gallery__item {\n        width: calc(100% / 2 - 1rem);\n    }\n\n    &[data-columns=\"3\"] .gallery__item {\n        width: calc(100% / 3 - 1rem);\n    }\n\n    &[data-columns=\"4\"] .gallery__item {\n        width: calc(100% / 4 - 1rem);\n    }\n\n    &[data-columns=\"5\"] .gallery__item {\n        width: calc(100% / 5 - 1rem);\n    }\n\n    &[data-columns=\"6\"] .gallery__item {\n        width: calc(100% / 6 - 1rem);\n    }\n\n    &[data-columns=\"7\"] .gallery__item {\n        width: calc(100% / 7 - 1rem);\n    }\n\n    &[data-columns=\"8\"] .gallery__item {\n        width: calc(100% / 8 - 1rem);\n    }\n\n    &-wrapper--full {\n        margin-left: calc(50% - 50vw + 1.6rem);\n        margin-right: calc(50% - 50vw + 1.6rem);\n        width: 100vw;\n        max-width: calc(100vw - 3.2rem);\n\n        .gallery__item a {\n            height: 50vh;\n        }\n    }\n\n    &-wrapper--wide {\n        margin-left: -10%;\n        margin-right: -10%;\n        width: calc(100% + 20%);\n    }\n}\n\n/*\n * AMP conditional content tags\n */\npublii-amp,\npublii-non-amp {\n    border: 1px dashed var(--input-border-color);\n    display: block;\n    margin: 30px 0;\n    padding: 20px;\n    position: relative;\n}\n\npublii-amp:before,\npublii-non-amp:before {\n    background: var(--bg-primary);\n    color: var(--text-light-color);\n    font-size: 12px;\n    left: 50%;\n    padding: baseline(1) baseline(2);\n    position: absolute;\n    top: 0;\n    transform: translateX(-50%) translateY(-50%);\n}\n\npublii-amp:before {\n    content: \"This content will be visible only in the AMP version\";\n}\n\npublii-non-amp:before {\n    content: \"This content won't be visible in the AMP version\";\n}\n\n/*\n * Outline selected elements\n */\n.mce-match-marker-selected {\n    background: var(--color-primary);\n}\n\n.mce-content-body img[data-mce-selected],\n.mce-content-body table[data-mce-selected],\n.mce-content-body hr[data-mce-selected],\n.mce-content-body [contentEditable=false] [contentEditable=true]:focus,\n.mce-content-body [contentEditable=false] [contentEditable=true]:hover,\n.mce-content-body [contentEditable=false][data-mce-selected],\n.mce-content-body .mce-edit-focus {\n    outline: 3px solid rgba(var(--color-primary-rgb), .55) !important;\n}\n\n.mce-content-body [data-mce-selected=inline-boundary],\n.mce-content-body td[data-mce-selected],\n.mce-content-body th[data-mce-selected] {\n    background-color: rgba(var(--color-primary-rgb), .55) !important;\n}\n\n\n\n/*\n * Styling for obfuscated e-mails\n */\nscript {\n    background: var(--gray-1);\n    border: 2px dashed var(--input-border-color);\n    border-radius: 3px;\n    display: block;\n    font-size: ms(-2);\n    height: 2rem;\n    padding: 2rem;\n    overflow: hidden;\n    padding: 2rem;\n    position: relative;\n    width: 100%;\n\n    &:before {\n        background: var(--gray-1);\n        box-shadow: inset 0 0 0 5px var(--bg-primary);\n        color: var(--text-light-color);\n        content: \"<script>\";\n        left: 0;\n        line-height: 4rem;\n        position: absolute;\n        text-align: center;\n        top: 0;\n        bottom: 0;\n        left: 0;\n        right: 0;\n        width: 100%;\n    }\n}\n\na script {\n    display: inline-block;\n    height: 1.5rem;\n    margin: 0;\n    vertical-align: middle;\n    width: 5rem;\n\n    &:before {\n        line-height: 1.5rem;\n    }\n}\n\n\n/*\n * Styling for anchors\n */\n.mce-item-anchor {\n    background-color: var(--bg-primary);\n    border: 2px solid $color-helper-6;\n    border-radius: 50%;\n    height: 20px !important;\n    margin: 0 3px;\n    position: relative;\n    top: 4px;\n    width: 20px !important;\n}\n\n// Prism.js \n\ncode[class*=\"language-\"],\npre[class*=\"language-\"],\npre[class*=\"language-\"] > code {\n    color: var(--pre-color) !important;\n    background: none;\n    text-shadow: 0 1px rgba(0, 0, 0, 0.3);\n    font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\n    font-size: ms(-2);\n    text-align: left;\n    white-space: pre;\n    word-spacing: normal;\n    word-break: normal;\n    word-wrap: normal;\n    line-height: 1.5;\n\n    -moz-tab-size: 4;\n    -o-tab-size: 4;\n    tab-size: 4;\n\n    -webkit-hyphens: none;\n    -moz-hyphens: none;\n    -ms-hyphens: none;\n    hyphens: none;\n}\n\n/* Code blocks */\npre[class*=\"language-\"] {\n    padding: baseline(6);\n    margin: baseline(7) 0;\n    overflow: auto;\n    border-radius: 0.3em;\n}\n\n:not(pre) > code[class*=\"language-\"],\npre[class*=\"language-\"],\npre[class*=\"language-\"] > code {\n    background: #1e2128 !important;\n}\n\n/* Inline code */\n:not(pre) > code[class*=\"language-\"] {\n    padding: .1em;\n    border-radius: .3em;\n    white-space: normal;\n}\n\n.token.comment,\n.token.prolog,\n.token.doctype,\n.token.cdata {\n    color: rgba(var(--white-rgb), .45);\n}\n\n.token.punctuation {\n    color: #b3b9c5;\n}\n\n.namespace {\n    opacity: .7;\n}\n\n.token.property,\n.token.tag,\n.token.constant,\n.token.symbol,\n.token.deleted {\n    color: #6ab0f3;\n}\n\n.token.boolean,\n.token.number {\n    color: #e1a6f2;\n}\n\n.token.selector,\n.token.attr-name,\n.token.string,\n.token.char,\n.token.builtin,\n.token.inserted {\n    color: #ffd479;\n}\n\n.token.operator,\n.token.entity,\n.token.url,\n.language-css .token.string,\n.style .token.string,\n.token.variable {\n    color: #ac8d58;\n    background: none;\n}\n\n.token.atrule,\n.token.attr-value,\n.token.function,\n.token.class-name {\n    color: #92d192;\n}\n\n.token.keyword {\n    color: #ffeead;\n}\n\n.token.regex,\n.token.important {\n    color: #fd971f;\n}\n\n.token.important,\n.token.bold {\n    font-weight: bold;\n}\n.token.italic {\n    font-style: italic;\n}\n\n.token.entity {\n    cursor: help;\n}\n\n/* Prevent visual \"jump\" when classes are duplicated on img inside figure */\nfigure[class*=\"post__image\"] img[class*=\"post__image\"] {\n    width: 100% !important;\n    max-width: none !important;\n    float: none !important;\n    margin: 0 !important;\n    display: block !important;\n}\n/* Fix misleading cursor on figure with image */\n.mce-content-body figure[contentEditable=false][data-mce-selected],\n.mce-content-body figure[contentEditable=false][data-mce-selected] img {\n    cursor: pointer !important;\n}\n"
  },
  {
    "path": "app/src/scss/editor/post-editors-common.scss",
    "content": ".post-editor {\n   background: var(--bg-primary);\n\n    &-wrapper {\n        display: flex;\n        height: 100vh;\n        overflow: hidden;\n        padding-top: 11rem;\n        user-select: none;\n    }\n\n    .appbar {\n        height: var(--topbar-height)!important;\n        position: absolute!important;\n        width: 100%;\n    }\n\n    &-field-select-tags {\n        width: 480px;\n    }\n\n    &-form {\n        height: calc(100vh - 11rem);\n        overflow: hidden;\n        position: relative;\n        width: 100%;\n    }\n\n    #post-stats-button {\n        background: var(--bg-primary);\n        bottom: .4rem;\n        height: 4.4rem;        \n        left: 1.8rem;       \n        line-height: 4.3rem;\n        padding: 0 1.3rem 0 3.8rem;\n        position: absolute;\n        text-align: center;        \n        z-index: 101;\n    }\n}\n\n\n/*\n * Windows & linux adjustments\n */\nbody[data-os=\"win\"] {\n    .post-editor {\n        .topbar {\n            height: 2.2rem;\n        }\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .post-editor {\n        .appbar {\n            height: 0!important;\n        }\n\n        .topbar {\n            height: 0;\n        }\n    }\n}\n\n/*\n * Special styles for win & linux\n */\n\nbody[data-os=\"win\"] {\n    .post-editor-wrapper {\n        height: 100vh;\n        \n    }\n\n    .post-editor-form {       \n        &-content {\n            height: calc( 100vh - 27.6rem );\n        }\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .post-editor-wrapper {\n        height: 100vh;\n        padding-top: 5.6rem;\n    }\n\n    .post-editor-form {\n        height: calc(100vh - 5.6rem);\n\n        &-content {\n            height: calc( 100vh - 24.8rem );\n        }\n    }\n}\n\n\n/*\n * Responsive improvements\n */\n\n@media (min-width: 1200px) {\n    .post-editor-form {\n        &.writers-panel-open {\n            margin-left: $writers-panel-width;\n            width: calc(100% - $writers-panel-width);\n        }\n        &.writers-panel-open.sidebar-open {\n            width: 100%;\n        }\n    }\n}\n\n@media (min-width: 1380px) {\n    .post-editor-form {\n        &.sidebar-open {\n            width: calc(100% - $options-sidebar-width);\n        }\n    }\n}\n\n@media (min-width: 1500px) {\n    .post-editor-form {\n        &.writers-panel-open.sidebar-open {\n            margin-left: $writers-panel-width;\n            width: calc(100% - $writers-panel-width - $options-sidebar-width);\n        }\n    }\n}\n\n/*\n * Responsive improvements\n */\n @media (min-width: 1380px) and (max-width: 1500px) {\n    .post-editor-form {\n        &.sidebar-open {\n            .wrapper.contains-wide-image {\n                max-width: var(--editor-width)!important;\n            }\n            .contains-full-image {\n                width: calc(var(--editor-width) )!important;\n            }\n        }\n    }\n}\n\n@media (min-width: 1501px) and (max-width: 1600px) {\n    .post-editor-form {\n        &.sidebar-open {\n            .contains-full-image {\n                width: calc(var(--editor-width) + 168px) !important\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/scss/editor/scrollbar.scss",
    "content": "\n/*\n * Custom scrollbars for the app\n */\n\n::-webkit-scrollbar {\n    background: transparent;\n    overflow: visible;\n    width: 5px;\n}\n\n::-webkit-scrollbar:horizontal {\n    background: transparent;\n    height: 5px;\n}\n\n::-webkit-scrollbar-thumb {\n    background-color: var(--scrollbar);\n    border: none;\n}\n\n::-webkit-scrollbar-thumb:hover {\n    background: var(--scrollbar-hover);\n}\n\n::-webkit-scrollbar-thumb:horizontal {\n    border-width: 0;\n    width: 10px;\n}\n\n::-webkit-scrollbar-thumb:vertical {\n    border-width: 0;\n    height: 10px;\n}\n\n::-webkit-scrollbar-track-piece {\n    background-color: transparent;\n}\n\n::-webkit-scrollbar-corner {\n    background: transparent;\n}\n\n.use-wide-scrollbars {\n    ::-webkit-scrollbar {\n        width: 12px;\n    }\n\n    ::-webkit-scrollbar:horizontal {\n        height: 12px;\n    }\n}\n"
  },
  {
    "path": "app/src/scss/empty-states.scss",
    "content": ".empty-state {\n    clear: both;\n    color: var(--text-light-color);\n    font-size: $app-font-base;\n    left: 0;\n    padding: 0 2vw;\n    position: absolute;\n    text-align: center;\n    top: 40%;\n    transform: translateY(-40%);\n    user-select: none;\n    width: 100%;\n\n    &.page,\n    &.post {\n        display: grid;\n        grid-template-columns: repeat(3, fit-content(400px));\n        grid-gap: 2vw;\n        justify-content: center;\n    }\n\n    h3 {\n        color: var(--text-primary-color);\n        text-transform: none;\n    }\n\n    p {\n        margin: -0.5rem auto 3rem;\n        max-width: 66ch;\n    }\n}\n\n/*\n * Responsive improvements\n */\n@media (max-height: 900px) {\n    .empty-state {\n        img {\n            height: auto;\n            max-width: 26rem;\n            margin-bottom: 0.5rem;\n        }\n        \n        h3 {\n            font-size: 17px;\n        }\n\n        p {\n            font-size: 14px;\n        }\n    }\n}\n\n@media (max-width: 1400px) {\n    .empty-state {\n        img {\n            height: auto;\n            max-width: 26rem;\n            margin-bottom: 0.5rem;\n        }\n        \n        h3 {\n            font-size: 17px;\n        }\n\n        p {\n            font-size: 14px;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/scss/forms.scss",
    "content": "/*\n * Form elements\n */\n\n\noption {\n    font-weight: 400;\n}\n\n::-webkit-input-placeholder {\n    color: var(--text-primary-color);\n}\n\ninput[type=\"text\"],\ninput[type=\"url\"],\ninput[type=\"tel\"],\ninput[type=\"number\"],\ninput[type=\"email\"],\ninput[type=\"search\"],\ninput[type=\"password\"],\ninput[type=\"colorpicker\"],\n.small-image-upload,\nselect {\n    background-color: var(--input-bg);\n    border: none;\n    border-radius: var(--border-radius);\n    box-shadow: inset 0 0 0 1px var(--input-border-color);\n    color: var(--text-primary-color);\n    font: 400 #{$app-font-base}/1.5 var(--font-base);\n    outline: none;\n    padding: 1.2rem 1.8rem;\n\n    &:focus {\n        box-shadow: inset 0 0 2px 1px var(--input-border-focus);\n    }\n\n    &[disabled],\n    &[readonly] {\n        opacity: .5;\n        cursor: not-allowed;\n\n        &:focus {\n            box-shadow: inset 0 0 0 1px var(--input-border-color);\n        }\n    }\n}\n\ninput[type=\"colorpicker\"][readonly] {\n    opacity: 1;\n    cursor: auto;\n}\n\ninput[type=\"search\"] {\n    -webkit-appearance: none;\n}\n\ninput[type=\"file\"] {\n    margin-bottom: 1rem;\n    width: 100%;\n}\n\ninput {\n    &.invalid {\n        border: 1px solid var(--warning);\n\n        &:focus {\n            border: none;\n        }\n    }\n}\n\n/* disabled input */\ninput:disabled {\n    opacity: .6;\n}\n\ninput:disabled + label {\n    opacity: .6;\n    cursor:default;\n    -webkit-user-select: none;\n}\n\n/* Range */\ninput[type=\"range\"] {\n    -webkit-appearance: none;\n    background: transparent;\n    float: left;\n    margin: 1rem 0;\n    position: relative;\n    top: 1.6rem;\n    width: 80%!important;\n\n    & + span {\n        float: right;\n        position: relative;\n        text-align: center;\n        top: 1.6rem;\n        width: 20%!important;\n    }\n\n    &::-webkit-slider-thumb {\n        -webkit-appearance: none;\n        background: var(--color-primary);\n        border: none;\n        border-radius: 50%;\n        box-shadow: 1px 1px 1px rgba(0, 0, 0, .15);\n        cursor: pointer;\n        height: 2rem;\n        margin-top: -1rem;\n        outline: none;\n        width: 2rem;\n    }\n\n    &::-webkit-slider-runnable-track {\n        background: $color-8;\n        border: none;\n        cursor: pointer;\n        height: 2px;\n        outline: none;\n        width: 80%;\n    }\n\n    &:focus::-webkit-slider-runnable-track {\n        background: rgba($color-1, .5);\n        outline: none;\n    }\n}\n\n\n/*\n * Password field UI\n */\n.password-wrapper {\n    position: relative;\n\n    & > a {\n        font-size: 1.4rem;\n        position: absolute;\n        right: 2rem;\n        top: 1.4rem;\n        z-index: 1;\n    }\n}\n\n.custom-password-wrapper {\n    position: relative;\n\n    & > a {\n        font-size: 1.4rem;\n        position: absolute;\n        right: 2rem;\n        top: 73%;\n        transform: translateY(-50%);\n        z-index: 1;\n    }\n}\n\n/* Fix for the colorpicker */\n.colorpicker input {\n    box-shadow: none;\n    font: 400 1.2rem/1.5 var(--font-base);\n    padding: 2px;\n}\n\n/*\n * Small image uploader\n */\n.small-upload-image {\n    min-height: 48px;\n    position: relative;\n\n    .small-upload-remove {\n        background: var(--warning);\n        border-radius: 1.2rem;\n        color: transparent;\n        font-size: 1rem;\n        font-weight: var(--font-weight-semibold);\n        height: 2rem;\n        line-height: 1rem;\n        padding: 0;\n        position: absolute;\n        right: 1.5rem;\n        text-align: center;\n        text-indent: 1px;\n        top: 1.4rem;\n        width: 2rem;\n\n        &:before,\n        &:after {\n           background: var(--bg-primary);\n            content: \"\";\n            display: block;\n            height: .2rem;\n            left: 4px;\n            position: absolute;\n            top: 9px;\n            transform: rotate(45deg);\n            width: 1.2rem;\n        }\n\n        &:after {\n            transform: rotate(-45deg);\n        }\n\n        &:hover {\n            opacity: .75;\n        }\n    }\n\n    .small-upload-image-input {\n        display: block;\n        position: absolute;\n        right: .7rem;\n        top: .7rem;\n        width: 16rem!important; \n\n        &::-webkit-file-upload-button {\n            -webkit-appearance: none;\n            background: var(--button-secondary-bg);\n            border: 1px solid var(--button-secondary-bg);\n            border-radius: var(--border-radius);\n            color: var(--button-secondary-color);\n            cursor: pointer;\n            font-weight: var(--font-weight-semibold);\n            font-size: 1.4rem;\n            padding: .5rem;\n            text-align: center;\n            width: 16rem;\n            outline: none;\n\n            &:hover {\n                background: var(--button-secondary-bg-hover);\n                border-color: var(--button-secondary-bg-hover);\n                color: var(--button-secondary-color-hover);\n            }\n        }\n    }\n}\n\n/**\n * Multiselect\n */\n\n.multiselect__tags {\n    min-height: 47px;\n    padding: 0 4rem 0 1.8rem;\n}\n\n.multiselect__option--highlight {\n    background: var(--input-bg-light);\n    color: var(--text-primary-color);\n}\n\n.multiselect__input,\n.multiselect__single {\n    height: 47px;\n    line-height: 47px;\n}\n\n.multiselect__placeholder {\n    color: var(--text-primary-color);\n    font-size: $app-font-base;\n    height: inherit;\n    line-height: 47px;\n    margin: 0;\n    padding: 0;\n}\n"
  },
  {
    "path": "app/src/scss/global.scss",
    "content": "/*\n * Common HTML elements\n */\n\n@import url('../assets/vendor/css/normalize.css');\n\n*,\n*:before,\n*:after {\n    box-sizing: border-box;\n    outline: none;\n}\n\nhtml {\n    font-size: 62.5%;\n}\n\nbody {\n    background-color: var(--bg-site);\n    color: var(--text-primary-color);\n    font: 400 1rem var(--font-base);\n    -webkit-font-smoothing: subpixel-antialiased;\n    line-height: $line-height;\n}\n\nhtml,\nbody {\n    min-height: 100%;\n}\n\na {\n    color: var(--link-primary-color);\n    text-decoration: none;\n    transition: var(--transition);\n\n    &:active,\n    &:focus,\n    &:hover {\n        color: var(--link-primary-color-hover);\n    }\n\n    svg {\n        pointer-events: none;\n    }\n\n    &[disabled] {\n        pointer-events: none;\n    }\n}\n\nh1,\nh2,\nh3,\nh4 {\n    font-family: var(--font-base);\n}\n\nh1 {\n    color: var(--headings-color);\n    font-size: 2rem;\n    font-weight: 600;\n    letter-spacing: -.01em;\n}\n\nh2 {\n    color: var(--headings-color);\n    font-weight: var(--font-weight-semibold);\n    text-transform: uppercase;\n    margin: 0 0 2rem;\n}\n\nh3 {\n    color: var(--headings-color);\n    font-size: 1.8rem;\n    font-weight: 600;\n    margin: 0 0 2rem;\n}\n\nh4 {\n    color: var(--headings-color);\n    font-size: $app-font-base;\n    font-weight: 600;\n    margin: 0 0 1rem;\n}\n\nb {\n    font-weight: 400;\n}\n\ntable {\n    width: 100%;\n\n    td {\n        padding: 0 0 12 * $spacing;\n        vertical-align: top;\n    }\n}\n\nform p {\n    margin-bottom: 6 * $spacing;\n}\n\n\n\n/*\n * Screens\n */\n\n.screen {\n   background: var(--bg-primary);\n\n    &.screen-splashscreen {\n        -webkit-app-region: drag; // necessary for making window draggable\n        -webkit-user-select: none; // remove conflict with the text selection\n        height: 100vh;\n        width: 100vw;\n    }\n}\n\n/*\n * States\n */\n\n.is-hidden-dependency {\n    display: none;\n}\n\n.is-hidden {\n    display: none!important;\n}\n\n.is-validation-message,\n.is-required {\n    color: var(--warning);\n}\n\n.is-rotating {\n    animation:spin 2s linear infinite;\n}\n\n@keyframes spin {\n    100% {\n        transform:rotate(360deg);\n    }\n}\n\n/*\n * Custom scrollbars for the app\n */\n\n::-webkit-scrollbar {\n    background: transparent;\n    overflow: visible;\n    width: 5px;\n}\n\n::-webkit-scrollbar:horizontal {\n    background: transparent;\n    height: 5px;\n}\n\n::-webkit-scrollbar-thumb {\n    background-color: var(--scrollbar);\n    border: none;\n}\n\n::-webkit-scrollbar-thumb:hover {\n    background: var(--scrollbar-hover);\n}\n\n::-webkit-scrollbar-thumb:horizontal {\n    border-width: 0;\n    width: 10px;\n}\n\n::-webkit-scrollbar-thumb:vertical {\n    border-width: 0;\n    height: 10px;\n}\n\n::-webkit-scrollbar-track-piece {\n    background-color: transparent;\n}\n\n::-webkit-scrollbar-corner {\n    background: transparent;\n}\n\n.use-wide-scrollbars {\n    ::-webkit-scrollbar {\n        width: 12px;\n    }\n\n    ::-webkit-scrollbar:horizontal {\n        height: 12px;\n    }\n\n    .collection {\n        &:after {\n            right: 12px!important;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/scss/help-panel-common.scss",
    "content": ".help-panel {\n    background: var(--option-sidebar-bg);\n    border-left: 1px solid var(--input-border-color);\n    display: none;\n    height: calc(100vh - var(--topbar-height));\n    overflow: auto;\n    position: absolute;\n    right: 0;\n    top: var(--topbar-height);    \n    width: 44.2rem;\n    z-index: 9999;\n    \n     &::before {\n            background: linear-gradient(to bottom, var(--option-sidebar-bg) 0%,var(--option-sidebar-bg) 75%,transparent 100%);\n            content: \"\";\n            height: 10rem;\n            position: fixed;\n            top: var(--topbar-height);\n            right: 0;\n            width: 44.1rem;\n            z-index: 1;\n        }\n    \n     &::after {\n            background: linear-gradient(to top, var(--option-sidebar-bg) 0%,var(--option-sidebar-bg) 75%,transparent 100%);\n            content: \"\";\n            height: 6rem;\n            position: fixed;\n            bottom: 0;\n            right: 0;\n            width: 44.1rem;\n            z-index: 1;\n        }\n    \n    &.is-visible {\n        display: block;\n    }\n\n    & > div {\n        font-size: 14px;\n        padding:  8rem 3.6rem;\n         \n    }\n    \n    &-header {\n            border-bottom: 1px solid var(--input-border-color); \n            font-size: 1.8rem;\n            margin: -3.6rem 0 2rem;\n            padding: calc(1rem + 0.6vw) 0 2.25rem\n    }\n    \n    &-desc {\n        color: var(--text-light-color);\n        \n        span {\n            background: var(--input-border-color);\n            border-radius: 4px;\n            color: var(--text-primary-color);\n            padding: 2px 4px;\n        }\n    }\n    \n    &-table {\n        border-collapse: collapse;   \n        margin-top: .6rem;\n        \n        &.col-3 {\n            td {\n                width: auto !important;\n            } \n        }\n        \n        th {\n            border-bottom: 1px solid var(--input-border-color); \n            padding: 12px 14px;\n            text-align: left;\n        }\n        \n        td {            \n            border-bottom: 1px solid var(--input-border-color); \n            border-right: 1px solid var(--input-border-color); \n            padding: 12px 14px;\n            vertical-align: top;\n            white-space: nowrap;\n            width: 50%;\n            \n            &:last-child {\n                border-right: none;\n            }\n        }\n    }     \n}\n"
  },
  {
    "path": "app/src/scss/mixins.scss",
    "content": "@use \"sass:math\";\n\n/*\n * Clearfix placeholder\n */\n@mixin clearfix {\n    &:after {\n        content: \" \";\n        display: block;\n        clear: both;\n    }\n}\n\n/*\n * Typography mixins (for editor.scss)\n */\n\n// Links\n@mixin links ($link, $hover, $active, $focus) {\n    & {\n        border-bottom: 3px solid rgba(var(--yellow), 1);\n        color: $link;\n        cursor: pointer;\n        text-decoration: none !important;\n        text-decoration-skip-ink: auto;\n    }\n\n    &:hover {\n        background: rgba(var(--yellow), .2);\n        color: $hover;\n    }\n\n    &:active {\n        color: $active;\n    }\n\n    &:focus {\n        color: $focus;\n        outline: 2px dotted var(--color-primary);\n    }\n}\n\n// Horizontal and vertical centering helper\n@mixin centerXY($horizontal: true, $vertical: true) {\n    position: absolute;\n\n    @if ($horizontal and $vertical) {\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n    }\n\n    @else if ($horizontal) {\n        left: 50%;\n        transform: translate(-50%, 0);\n    }\n\n    @else if ($vertical) {\n        top: 50%;\n        transform: translate(0, -50%);\n    }\n}\n\n\n// Fluid typography\n@mixin fluid-type($properties, $min-vw, $max-vw, $min-value, $max-value) {\n    & {\n        @each $property in $properties {\n            #{$property}: $min-value;\n        }\n\n        @media screen and (min-width: $min-vw) {\n            @each $property in $properties {\n                #{$property}: calc(#{$min-value} + #{strip-unit($max-value - $min-value)} * ((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)}));\n            }\n        }\n\n        @media screen and (min-width: $max-vw) {\n            @each $property in $properties {\n                #{$property}: $max-value;\n            }\n        }\n    }\n}\n\n\n@function strip-unit($value) {\n    @return math.div($value, $value * 0 + 1);\n}\n\n@function baseline($value, $amount: rem) {\n    $product: ($baseline) * $value;\n    @return #{$product}#{$amount};\n  }\n\n\n"
  },
  {
    "path": "app/src/scss/notifications.scss",
    "content": ".msg {\n    background: var(--input-bg-light);\n    border-left: 4px solid transparent;\n    color: var(--text-light-color);\n    font-size: 1.4rem;\n    line-height: 1.6;\n    padding: 1.5rem 2rem;\n\n    a {\n        &:hover {\n            text-decoration: underline;\n        }\n    }\n\n    &-icon {\n        align-items: center;\n        display: grid;\n        grid-template-columns: 28px 1fr;\n        grid-gap: 2.2rem;\n\n        & > * {          \n            margin: 0;\n        }\n    }\n\n    &-small {\n        line-height: 1.4;\n        padding: 1.5rem 1.4rem 1.5rem 1.7rem;\n\n        &.msg-icon {\n            grid-gap: 1.2rem;\n        }\n    }\n\n    &-info {\n        background-color: rgba(var(--color-primary-rgb), .065);\n        border-color: var(--color-primary);\n\n        svg {\n            fill: var(--color-primary);\n        }\n    }\n\n    &-alert {\n        background-color: rgba(var(--warning-rgb), .065);\n        border-color: var(--warning);\n\n        svg {\n            fill: var(--warning);\n        }\n    }\n\n    &-onbg {\n        background: var(--bg-secondary);\n    }\n}\n"
  },
  {
    "path": "app/src/scss/options-sidebar.scss",
    "content": ".options-sidebar {\n    font-size: 1.4rem;\n    padding: 3.5rem;\n\n    h2 {\n        color: var(--gray-3);\n        font-size: 1.2rem;\n        font-weight: 600;\n        margin: -0.2rem 0 0;\n        padding: 0 0 1.5rem;\n        text-transform: uppercase;\n    }\n\n    label {\n        color: var(--label-color);\n        cursor: default;\n        display: block;\n        font-size: 1.3rem;\n        font-weight: var(--font-weight-semibold);\n        line-height: 2.6;\n        margin-bottom: 1.2rem;\n        user-select: none;\n\n        small {\n           font-size: 90%;\n        }\n\n        input[type=\"text\"],\n        input[type=\"number\"],\n        select,\n        textarea {\n            background-color: var(--input-bg);\n            width: 100%;\n        }\n\n        textarea {\n            height: 100px;\n        }\n\n        &.with-char-counter {\n            .note {\n                margin-top: -3rem;\n                width: 70%;\n            }\n        }\n\n        &.no-margin {\n            margin: 0;\n        }\n\n        &.is-invalid {\n            input {\n                box-shadow: inset 0 0 0 1px var(--warning) !important;\n            }\n        }\n    }\n\n    &-container {\n        background: var(--option-sidebar-bg);\n        box-shadow: var(--box-shadow-small);\n        height: 100%;\n        overflow: auto;\n        position: absolute;\n        right: 0;\n        top: 0;\n        user-select: none;\n        width: $options-sidebar-width;\n        z-index: 9999;\n\n        &.v-enter-active,\n        &.v-leave-active {\n            transition: all .25s ease;\n            transition-delay: .1s;\n        }\n\n        &.v-enter,\n        &.v-leave-to {\n            right: -55rem;\n        }\n\n        &.v-enter-to,\n        &.v-leave {\n            right: 0;\n        }\n\n        &[data-animate=\"false\"] {\n            &.v-enter-active,\n            &.v-leave-active {\n                transition: none;\n                transition-delay: .1s;\n            }\n\n            &.v-enter,\n            &.v-leave-to {\n                right: 0;\n            }\n\n            &.v-enter-to,\n            &.v-leave {\n                right: 0;\n            }\n        }     \n    }\n\n    &-item {\n        margin-top: 5px;\n\n        &:first-of-type {\n            .options-sidebar-header {\n                border-top: none;\n            }\n        }\n\n        &-slug {\n            position: relative;\n\n            input {\n                padding-right: 6rem;\n            }\n\n            .button {\n                border-radius: var(--border-radius);\n                bottom: 0;\n                height: calc(100% - 4px);\n                margin: 2px;\n                padding: 0;\n                position: absolute;\n                right: 0;\n                top: 0;\n                width: 43px;\n            }\n        }\n    }\n\n    &-header {\n        align-items: center;\n        background-color: var(--input-bg-light);\n        border-radius: var(--border-radius);\n        color: var(--link-primary-color-hover);\n        cursor: pointer;\n        display: flex;\n        font-size: 1.4rem;\n        height: 5rem;\n        margin-left: 0;\n        margin-top: -1px;\n        padding: 0 1.6rem;\n        position: relative;\n        transition: var(--transition);\n        user-select: none;\n        width: 100%;\n\n        &:hover {\n            color: var(--link-primary-color);\n        }\n\n        &.is-open {\n            background-color: transparent;\n            padding: 0;\n\n            .options-sidebar {\n                &-label {\n                    color: var(--link-primary-color);\n                    left: -3.6rem;\n                }\n\n                &-icon {\n                    left: -1.6rem;\n                    position: relative;\n                    opacity: 0;\n                }\n            }\n        }\n    }\n\n    &-label {\n        font-weight: 600;\n        left: 0;\n        position: relative;\n        transition: left .25s ease-out, color .0s ease-out;\n        width: calc(100% - 5.8rem);\n\n        &-warning {\n            color: var(--warning);\n            font-size: 1.4rem;\n            font-weight: var(--font-weight-normal);\n            margin-left: 1rem;\n        }\n    }\n\n    &-icon {\n        left: 0;\n        height: 2.4rem;\n        margin-right: 1.6rem;\n        opacity: 1;\n        position: relative;\n        transition: var(--transition);\n        width: 2.4rem;\n\n        &-button-suggestion {\n            color: var(--icon-secondary-color);\n            cursor: pointer;\n            float: right;\n\n            &:hover {\n                color: var(--link-primary-color);\n            }\n\n            svg {\n                margin-right: .5rem !important;\n            }\n        }\n    }\n\n    &-preview-button {\n        display: inline-flex;\n\n        & > span {\n            align-self: center;\n            display: flex;\n            margin-left: 1rem;\n        }\n    }\n\n    &-close {\n        border-radius: 50%;\n        color: var(--icon-secondary-color);\n        cursor: pointer;\n        font-size: 2.4rem;\n        font-weight: 300;\n        height: 3rem;\n        line-height: 1.1;\n        padding: 0;\n        position: absolute;\n        right: 3.5rem;\n        text-align: center;\n        transition: var(--transition);\n        top: 3rem;\n        width: 3rem;\n\n        &:active,\n        &:focus,\n        &:hover {\n            color: var(--icon-tertiary-color);\n        }\n\n        &:hover {\n            background: var(--input-border-color);\n        }\n    }\n\n    &-buttons {\n        display: flex;\n        flex-wrap: wrap;\n        gap: 0.625rem;\n        padding: 2rem 0 0 0;\n\n        & > .button{\n            flex-grow: 1;\n            justify-content: center;\n            margin-left: 0;\n            text-align: center;\n        }\n    }\n\n    .msg {\n        font-size: 1.3rem;\n    }\n\n    .note {\n        clear: both;\n        color: var(--text-light-color);\n        display: block;\n        font-style: italic;\n        font-weight: var(--font-weight-normal);\n        line-height: 1.4;\n        padding-top: .5rem;\n\n        &.is-warning {\n            color: var(--warning);\n            opacity: 1;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/scss/popup-common.scss",
    "content": ".overlay {\n    background: var(--overlay);\n    bottom: 0;\n    display: block;\n    height: auto;\n    left: 0;\n    overflow: hidden;\n    position: fixed;\n    right: 0;\n    text-align: center;\n    top: 0;\n    z-index: 100002;\n    \n    &.as-page {\n       background: var(--bg-primary);\n       height: 100vh;   \n       padding: 4.4rem 3.2rem;\n       pointer-events: auto;   \n       width: 100%;    \n        \n       h1 {\n          font-size: 1.9rem;\n          margin-bottom: 1.5rem;\n       }\n    }\n}\n\n.popup {\n    background-color: var(--popup-bg);\n    border: none;\n    border-radius: .6rem;\n    display: inline-block;\n    font-size: $app-font-base;\n    font-weight: var(--font-weight-normal);\n    left: 50%;  \n    overflow: hidden;\n    position: absolute;\n    text-align: center;\n    top: 50%;\n    transform: translateX(-50%) translateY(-50%);\n    user-select: none;\n\n    .buttons .button {\n        font-size: 1.4rem;\n    }\n}\n\n.message {\n    color: var(--text-primary-color);\n    font-weight: var(--font-weight-normal);\n    margin: 0;\n    padding: 4rem;\n    position: relative;\n    text-align: left;\n\n    &.text-centered {\n        text-align: center;\n    }\n}\n\nh1,\nh2 {\n    font-size: 1.6rem;\n    font-weight: var(--font-weight-semibold);\n    margin: 0 0 7rem 0;\n    text-align: center;\n    text-transform: none;\n}\n\nh1 {\n    margin: 1rem 0 3rem 0;\n}\n\nbody[data-os=\"win\"] {\n    .overlay {\n        height: calc(100% - 22px);\n        top: 22px;\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .overlay {\n        height: calc(100%);\n        top: 0;\n    }\n}\n"
  },
  {
    "path": "app/src/scss/scope-fix.scss",
    "content": "/**\n * Error log in the popup\n */\n.popup {\n    .error-log {\n        strong {\n            color: var(--warning);\n            font-size: $app-font-base;\n            font-weight: normal;\n        }\n\n        pre {\n            background: rgba(0, 0, 0, .025);\n            border: 1px solid var(--gray-1);\n            color: var(--gray-4);\n            font-size: 1.2rem;\n            padding: .5rem 1rem;\n            white-space: pre-wrap;\n            word-break: break-all;\n        }\n    }\n}\n\n/**\n * vue-multiselect\n */\n.multiselect__tags {\n   // padding: 0 4rem .5rem 0.5rem;\n\n    &-wrap {\n        display: inline;\n    }\n}\n\n.multiselect__tag {\n    background: var(--input-border-color);\n    border: none;\n    border-radius: var(--border-radius);\n    color: var(--text-primary-color);\n    display: inline-block;\n    font-size: 1.4rem;\n    font-style: italic;\n    font-weight: 400;\n    margin: .5rem .5rem 0 0;\n    padding: 7px 10px;\n    vertical-align: middle;\n\n    & > span {\n        display: inline-block;\n        height: 2.4rem;\n        line-height: 2.4rem;\n    }\n\n    &-icon {\n        cursor: pointer;\n        display: inline-block;\n        float: left;\n        font-size: $app-font-base;\n        font-weight: bold;\n        margin-left: 0;\n        margin-right: 5px;\n        position: static;\n        width: 10px;\n\n        &:after {\n            color: var(--icon-secondary-color)!important;\n        }\n\n        &:focus,\n        &:hover {\n            background: transparent;\n\n            &:after {\n                color: var(--icon-tertiary-color)!important;\n            }\n        }\n    }\n}\n\n.multiselect__input {\n    height: 47px;\n    line-height: 47px;\n    margin-top: .75rem;\n}\n\n.multiselect__select {\n    top: 1px;\n    padding: 4px 6px 4px 0;\n\n    &:before {\n        border-color: var(--icon-secondary-color) transparent transparent;\n        border-width: 6px 5px;\n    }\n}\n\n.multiselect__option--selected {\n    color: var(--text-primary-color);\n    font-weight: 600;\n}\n\n/**\n * Number fields in theme settings\n */\n.theme-settings {\n    fieldset:first-of-type {\n        .field {\n            .input-wrapper.is-number {\n                width: 100%!important;\n            }\n        }\n    }\n\n    .footer {\n        .buttons {\n            float: none;\n            width: 100%;\n\n            .button-outline {\n                float: left!important;\n                margin-left: 0!important;\n            }\n        }\n    }\n}\n\n/**\n * Field TinyMCE\n */\n\n.field {\n    .mce-tinymce {\n        border: 1px solid var(--input-border-color);\n        border-radius: var(--border-radius);\n    }\n\n    .mce-toolbar-grp {\n        padding: 1rem;\n    }\n\n    .mce-panel {\n        background: transparent;\n\n        .mce-btn {\n            background: transparent;\n            border: none;\n\n            button {\n                padding: .4rem .5rem;\n\n                &:hover {\n                    background: var(--gray-1);\n                }\n            }\n        }\n    }\n}\n\n/**\n * Source code editor\n */\n\n.source-code-editor {\n    .CodeMirror {\n        min-height: calc(100vh - 10rem); \n    }\n\n    .CodeMirror-advanced-dialog + .CodeMirror-wrap {\n        padding-bottom: 103px;\n    }\n}\n\nbody[data-os=\"linux\"] {\n    .source-code-editor {        \n        .CodeMirror {\n            min-height: calc(100vh - 8rem);\n        }\n    }\n}\n\n/**\n * Link add/edit popup\n */\n.popup-link-add {\n    .field {\n        label {\n            font-size: $app-font-base;\n            padding: 0;\n            text-align: left;\n            vertical-align: middle;\n            width: 16rem!important;\n        }\n    }\n\n    .multiselect__tags {\n        min-height: 5rem;\n        padding: 0 4rem 0 2rem;\n    }\n\n    .multiselect__single {\n        line-height: 4.8rem;\n    }\n}\n\n/**\n * Improved layout of v-select on site settings\n */\n.site-settings {\n    .multiselect__tags {\n        min-height: 48px;\n    }\n}\n\n/**\n * Improved layout of v-select for long items\n */\n\n.multiselect__tags {\n    .multiselect__single,\n    .multiselect__option {\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n    }\n}\n\n.multiselect__content {\n    width: 100%;\n\n    .multiselect__option {\n        white-space: normal;\n    }\n}\n"
  },
  {
    "path": "app/src/scss/variables.scss",
    "content": "@use \"sass:math\";\n\n$color-1: #2a99f4;\n$color-4: #2a2e30;\n$color-8: #dddddd;\n\n$color-helper-6: #F2B900;\n$color-helper-7: #1e2128;\n$color-helper-8: #e5e6e8;\n\n/*\n * Others\n */\n\n$wrapper: 960px; // app pages view\n$spacing: 0.25rem; // ($baseline / 6.4)\n$options-sidebar-width: 47.5rem; \n$writers-panel-width: 185px; \n$app-sidebar: 28rem;\n$app-sidebar-margin: 3rem;\n$app-font-base: 1.4rem;\n\n// Modular scale\n\n$modularscale: (\n  base: 1em, // 'em' only\n  ratio: 1.125 //$major-second \n); \n\n// Min and Max screen width\n$minScreen: 49rem;\n$maxScreen: 100rem; \n\n\n// Baseline\n$line-height: 1.5;\n$baseline: calc(1.6/7); \n\n// Font size map\n$h1: ms(5);\n$h2: ms(4);\n$h3: ms(3);\n$h4: ms(2);\n$h5: ms(1);\n$h6: ms(0);\n"
  },
  {
    "path": "app/src/scss/vendor/_modularscale.scss",
    "content": "// Defaults and variables\n@import 'modularscale/vars';\n\n// Core functions\n@import 'modularscale/settings';\n@import 'modularscale/pow';\n@import 'modularscale/strip-units';\n@import 'modularscale/sort';\n@import 'modularscale/target';\n@import 'modularscale/function';\n@import 'modularscale/round-px';\n\n// Mixins\n@import 'modularscale/respond';\n\n// Syntax sugar\n@import 'modularscale/sugar';"
  },
  {
    "path": "app/src/scss/vendor/codemirror.css",
    "content": "/* BASICS */\n\n.CodeMirror {\n  /* Set height, width, borders, and global font properties here */\n  font-family: Lucida Console, Monaco, Monospace;\n  height: 300px;\n  color: black;\n}\n\n/* PADDING */\n\n.CodeMirror-lines {\n  padding: 4px 0; /* Vertical padding around content */\n}\n.CodeMirror pre {\n  padding: 0 4px; /* Horizontal padding of content */\n}\n\n.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {\n  background-color: white; /* The little square between H and V scrollbars */\n}\n\n/* GUTTER */\n\n.CodeMirror-gutters {\n  border-right: 1px solid #ddd;\n  background-color: #f7f7f7;\n  white-space: nowrap;\n}\n.CodeMirror-linenumbers {}\n.CodeMirror-linenumber {\n  font-size: 1.4rem;\n  padding: 0 3px 0 5px;\n  min-width: 20px;\n  text-align: right;\n  color: #999;\n  white-space: nowrap;\n}\n\n.CodeMirror-guttermarker { color: black; }\n.CodeMirror-guttermarker-subtle { color: #999; }\n\n/* CURSOR */\n\n.CodeMirror-cursor {\n  border-left: 1px solid black;\n  border-right: none;\n  width: 0;\n}\n/* Shown when moving in bi-directional text */\n.CodeMirror div.CodeMirror-secondarycursor {\n  border-left: 1px solid silver;\n}\n.cm-fat-cursor .CodeMirror-cursor {\n  width: auto;\n  border: 0 !important;\n  background: #7e7;\n}\n.cm-fat-cursor div.CodeMirror-cursors {\n  z-index: 1;\n}\n\n.cm-animate-fat-cursor {\n  width: auto;\n  border: 0;\n  -webkit-animation: blink 1.06s steps(1) infinite;\n  animation: blink 1.06s steps(1) infinite;\n  background-color: #7e7;\n}\n@-webkit-keyframes blink {\n  0% {}\n  50% { background-color: transparent; }\n  100% {}\n}\n@keyframes blink {\n  0% {}\n  50% { background-color: transparent; }\n  100% {}\n}\n\n/* Can style cursor different in overwrite (non-insert) mode */\n.CodeMirror-overwrite .CodeMirror-cursor {}\n\n.cm-tab { display: inline-block; text-decoration: inherit; }\n\n.CodeMirror-rulers {\n  position: absolute;\n  left: 0; right: 0; top: -50px; bottom: -20px;\n  overflow: hidden;\n}\n.CodeMirror-ruler {\n  border-left: 1px solid #ccc;\n  top: 0; bottom: 0;\n  position: absolute;\n}\n\n/* DEFAULT THEME */\n\n.cm-s-default { font-weight: 400; }\n.cm-s-default .cm-header {color: blue;}\n.cm-s-default .cm-quote {color: #090;}\n.cm-negative {color: #d44;}\n.cm-positive {color: #292;}\n.cm-header, .cm-strong {font-weight: bold;}\n.cm-em {font-style: italic;}\n.cm-link {text-decoration: underline;}\n.cm-strikethrough {text-decoration: line-through;}\n\n.cm-s-default .cm-keyword {color: #708;}\n.cm-s-default .cm-atom {color: #219;}\n.cm-s-default .cm-number {color: #164;}\n.cm-s-default .cm-def {color: #00f;}\n.cm-s-default .cm-variable,\n.cm-s-default .cm-punctuation,\n.cm-s-default .cm-property,\n.cm-s-default .cm-operator {}\n.cm-s-default .cm-variable-2 {color: #05a;}\n.cm-s-default .cm-variable-3 {color: #085;}\n.cm-s-default .cm-comment {color: #a50;}\n.cm-s-default .cm-string {color: #a11;}\n.cm-s-default .cm-string-2 {color: #f50;}\n.cm-s-default .cm-meta {color: #555;}\n.cm-s-default .cm-qualifier {color: #555;}\n.cm-s-default .cm-builtin {color: #30a;}\n.cm-s-default .cm-bracket {color: #997;}\n.cm-s-default .cm-tag {color: #170;}\n.cm-s-default .cm-attribute {color: #00c;}\n.cm-s-default .cm-hr {color: #999;}\n.cm-s-default .cm-link {color: #00c;}\n\n.cm-s-default .cm-error {color: #f00;}\n.cm-invalidchar {color: #f00;}\n\n.CodeMirror-composing { border-bottom: 2px solid; }\n\n/* Default styles for common addons */\n\ndiv.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}\ndiv.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}\n.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }\n.CodeMirror-activeline-background {background: #e8f2ff;}\n\n/* STOP */\n\n/* The rest of this file contains styles related to the mechanics of\n   the editor. You probably shouldn't touch them. */\n\n.CodeMirror {\n  position: relative;\n  overflow: hidden;\n  background: white;\n}\n\n.CodeMirror-scroll {\n  overflow: scroll !important; /* Things will break if this is overridden */\n  /* 30px is the magic margin used to hide the element's real scrollbars */\n  /* See overflow: hidden in .CodeMirror */\n  margin-bottom: -30px; margin-right: -30px;\n  padding-bottom: 30px;\n  height: 100%;\n  outline: none; /* Prevent dragging from highlighting the element */\n  position: relative;\n}\n.CodeMirror-sizer {\n  position: relative;\n  border-right: 30px solid transparent;\n}\n\n/* The fake, visible scrollbars. Used to force redraw during scrolling\n   before actual scrolling happens, thus preventing shaking and\n   flickering artifacts. */\n.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {\n  position: absolute;\n  z-index: 6;\n  display: none;\n}\n.CodeMirror-vscrollbar {\n  right: 0; top: 0;\n  overflow-x: hidden;\n  overflow-y: scroll;\n}\n.CodeMirror-hscrollbar {\n  bottom: 0; left: 0;\n  overflow-y: hidden;\n  overflow-x: scroll;\n}\n.CodeMirror-scrollbar-filler {\n  right: 0; bottom: 0;\n}\n.CodeMirror-gutter-filler {\n  left: 0; bottom: 0;\n}\n\n.CodeMirror-gutters {\n  position: absolute; left: 0; top: 0;\n  min-height: 100%;\n  z-index: 3;\n}\n.CodeMirror-gutter {\n  font-size: $app-font-base;\n  white-space: normal;\n  height: 100%;\n  display: inline-block;\n  vertical-align: top;\n  margin-bottom: -30px;\n}\n.CodeMirror-gutter-wrapper {\n  position: absolute;\n  z-index: 4;\n  background: none !important;\n  border: none !important;\n}\n.CodeMirror-gutter-background {\n  position: absolute;\n  top: 0; bottom: 0;\n  z-index: 4;\n}\n.CodeMirror-gutter-elt {\n  position: absolute;\n  cursor: default;\n  z-index: 4;\n}\n.CodeMirror-gutter-wrapper {\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n      user-select: none;\n}\n\n.CodeMirror-lines {\n  cursor: text;\n  min-height: 1px; /* prevents collapsing before first draw */\n}\n.CodeMirror pre {\n  /* Reset some styles that the rest of the page might have set */ border-radius: 0;\n  border-width: 0;\n  background: transparent;\n  font-family: inherit;\n  font-size: 1.4rem;\n  margin: 0;\n  white-space: pre;\n  word-wrap: normal;\n  line-height: inherit;\n  color: inherit;\n  z-index: 2;\n  position: relative;\n  overflow: visible;\n  -webkit-tap-highlight-color: transparent;\n  -webkit-font-variant-ligatures: none;\n  font-variant-ligatures: none;\n}\n.CodeMirror-wrap pre {\n  word-wrap: break-word;\n  white-space: pre-wrap;\n  word-break: normal;\n}\n\n.CodeMirror-linebackground {\n  position: absolute;\n  left: 0; right: 0; top: 0; bottom: 0;\n  z-index: 0;\n}\n\n.CodeMirror-linewidget {\n  position: relative;\n  z-index: 2;\n  overflow: auto;\n}\n\n.CodeMirror-widget {}\n\n.CodeMirror-code {\n  outline: none;\n}\n\n/* Force content-box sizing for the elements where we expect it */\n.CodeMirror-scroll,\n.CodeMirror-sizer,\n.CodeMirror-gutter,\n.CodeMirror-gutters,\n.CodeMirror-linenumber {\n  -webkit-box-sizing: content-box;\n          box-sizing: content-box;\n}\n\n.CodeMirror-measure {\n  position: absolute;\n  width: 100%;\n  height: 0;\n  overflow: hidden;\n  visibility: hidden;\n}\n\n.CodeMirror-cursor {\n  position: absolute;\n  pointer-events: none;\n}\n.CodeMirror-measure pre { position: static; }\n\ndiv.CodeMirror-cursors {\n  visibility: hidden;\n  position: relative;\n  z-index: 3;\n}\ndiv.CodeMirror-dragcursors {\n  visibility: visible;\n}\n\n.CodeMirror-focused div.CodeMirror-cursors {\n  visibility: visible;\n}\n\n.CodeMirror-selected { background: #d9d9d9; }\n.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }\n.CodeMirror-crosshair { cursor: crosshair; }\n.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }\n.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }\n.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }\n\n.cm-searching {\n  background: #ffa;\n  background: rgba(255, 255, 0, .4);\n}\n\n/* Used to force a border model for a node */\n.cm-force-border { padding-right: .1px; }\n\n@media print {\n  /* Hide the cursor when printing */\n  .CodeMirror div.CodeMirror-cursors {\n    visibility: hidden;\n  }\n}\n\n/* See issue #2901 */\n.cm-tab-wrap-hack:after { content: ''; }\n\n/* Help users use markselection to safely style text background */\nspan.CodeMirror-selectedtext { background: none; }\n"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_function.scss",
    "content": "@use \"sass:math\";\n\n@function ms-function($v: 0, $base: false, $ratio: false, $thread: false, $settings: $modularscale) {\n\n  // Parse settings\n  $ms-settings: ms-settings($base,$ratio,$thread,$settings);\n  $base: nth($ms-settings, 1);\n  $ratio: nth($ms-settings, 2);\n\n  // Render target values from settings.\n  @if unit($ratio) != '' {\n    $ratio: ms-target($ratio,$base)\n  }\n\n  // Fast calc if not multi stranded\n  @if(length($base) == 1) {\n    @return ms-pow($ratio, $v) * $base;\n  }\n\n  // Create new base array\n  $ms-bases: nth($base,1);\n\n  // Normalize base values\n  @for $i from 2 through length($base) {\n    // initial base value\n    $ms-base: nth($base,$i);\n    // If the base is bigger than the main base\n    @if($ms-base > nth($base,1)) {\n      // divide the value until it aligns with main base.\n      @while($ms-base > nth($base,1)) {\n        $ms-base: math.div($ms-base, $ratio);\n      }\n      $ms-base: $ms-base * $ratio;\n    }\n    // If the base is smaller than the main base.\n    @else if ($ms-base < nth($base,1)) {\n      // pump up the value until it aligns with main base.\n      @while $ms-base < nth($base,1) {\n        $ms-base: $ms-base * $ratio;\n      }\n    }\n    // Push into new array\n    $ms-bases: append($ms-bases,$ms-base);\n  }\n\n  // Sort array from smallest to largest.\n  $ms-bases: ms-sort($ms-bases);\n\n  // Find step to use in calculation\n  $vtep: floor(math.div($v, length($ms-bases)));\n  // Find base to use in calculation\n  $ms-base: round((math.div($v, length($ms-bases) - $vtep)) * length($ms-bases)) + 1;\n\n  @return ms-pow($ratio, $vtep) * nth($ms-bases,$ms-base);\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_pow.scss",
    "content": "@use \"sass:math\";\n\n// Sass does not have native pow() support so this needs to be added.\n// Compass and other libs implement this more extensively.\n// In order to keep this simple, use those when they are avalible.\n// Issue for pow() support in Sass: https://github.com/sass/sass/issues/684\n\n@function ms-pow($b,$e) {\n\n  // Return 1 if exponent is 0\n  @if $e == 0 {\n    @return 1;\n  }\n\n  // If pow() exists (compass or mathsass) use that.\n  @if function-exists('pow') {\n    @return pow($b,$e);\n  }\n\n  // This does not support non-integer exponents,\n  // Check and return an error if a non-integer exponent is passed.\n  @if (floor($e) != $e) {\n    @error 'Non-integer values are not supported in modularscale by default. Try using mathsass in your project to add non-integer scale support. https://github.com/terkel/mathsass'\n  }\n\n  // Seed the return.\n  $ms-return: $b;\n\n  // Multiply or divide by the specified number of times.\n  @if $e > 0 {\n    @for $i from 1 to $e {\n      $ms-return: $ms-return * $b;\n    }\n  }\n  @if $e < 0 {\n    @for $i from $e through 0 {\n      $ms-return: math.div($ms-return, $b);\n    }\n  }\n  @return $ms-return;\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_respond.scss",
    "content": "// Generate calc() function\n// based on Mike Riethmuller's Precise control over responsive typography\n// http://madebymike.com.au/writing/precise-control-responsive-typography/\n@function ms-fluid($val1: 1em, $val2: 1em, $break1: 0, $break2: 0) {\n  $diff: ms-unitless($val2) - ms-unitless($val1);\n\n  // v1 + (v2 - v1) * ( (100vw - b1) / b2 - b1 )\n  @return calc( #{$val1} + #{ms-unitless($val2) - ms-unitless($val1)} * ( ( 100vw - #{$break1}) / #{ms-unitless($break2) - ms-unitless($break1)} ) );\n}\n\n// Main responsive mixin\n@mixin ms-respond($prop, $val, $map: $modularscale, $ms-important: false) {\n  $base: $ms-base;\n  $ratio: $ms-ratio;\n\n  $first-write: true;\n  $last-break: null;\n\n  $important: '';\n\n  @if $ms-important == true {\n    $important: ' !important';\n  }\n\n  // loop through all settings with a breakpoint type value\n  @each $v, $s in $map {\n    @if type-of($v) == number {\n      @if unit($v) != '' {\n\n        // Write out the first value without a media query.\n        @if $first-write {\n          #{$prop}: unquote(\"#{ms-function($val, $thread: $v, $settings: $map)}#{$important}\");\n\n          // Not the first write anymore, reset to false to move on.\n          $first-write: false;\n          $last-break: $v;\n        }\n\n        // Write intermediate breakpoints.\n        @else {\n          @media (min-width: $last-break) and (max-width: $v) {\n            $val1: ms-function($val, $thread: $last-break, $settings: $map);\n            $val2: ms-function($val, $thread: $v, $settings: $map);\n            #{$prop}: unquote(\"#{ms-fluid($val1,$val2,$last-break,$v)}#{$important}\");\n          }\n          $last-break: $v;\n        }\n      }\n    }\n  }\n\n  // Write the last breakpoint.\n  @if $last-break {\n    @media (min-width: $last-break) {\n      #{$prop}: unquote(\"#{ms-function($val, $thread: $last-break, $settings: $map)}#{$important}\");\n    }\n  }\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_round-px.scss",
    "content": "@function ms-round-px($r) {\n    @if unit($r) == 'px' {\n        @return round($r);\n    }\n    @warn \"ms-round-px is no longer used by modular scale and will be removed in the 3.1.0 release.\";\n    @return $r;\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_settings.scss",
    "content": "// Parse settings starting with defaults.\n// Settings should cascade down like you would expect in CSS.\n// More specific overrides previous settings.\n\n@function ms-settings($b: false, $r: false, $t: false, $m: $modularscale) {\n  $base: $ms-base;\n  $ratio: $ms-ratio;\n  $thread: map-get($m, $t);\n\n  // Override with user settings\n  @if map-get($m, base) {\n    $base: map-get($m, base);\n  }\n  @if map-get($m, ratio) {\n    $ratio: map-get($m, ratio);\n  }\n\n  // Override with thread settings\n  @if $thread {\n    @if map-get($thread, base) {\n      $base: map-get($thread, base);\n    }\n    @if map-get($thread, ratio) {\n      $ratio: map-get($thread, ratio);\n    }\n  }\n\n  // Override with inline settings\n  @if $b {\n    $base: $b;\n  }\n  @if $r {\n    $ratio: $r;\n  }\n\n  @return $base $ratio;\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_sort.scss",
    "content": "// Basic list sorting\n// Would like to replace with http://sassmeister.com/gist/30e4863bd03ce0e1617c\n// Unfortunately libsass has a bug with passing arguments into the min() funciton.\n\n@function ms-sort($l) {\n\n  // loop until the list is confirmed to be sorted\n  $sorted: false;\n  @while $sorted == false {\n\n    // Start with the assumption that the lists are sorted.\n    $sorted: true;\n\n    // Loop through the list, checking each value with the one next to it.\n    // Swap the values if they need to be swapped.\n    // Not super fast but simple and modular scale doesn't lean hard on sorting.\n    @for $i from 2 through length($l) {\n      $n1: nth($l,$i - 1);\n      $n2: nth($l,$i);\n\n      // If the first value is greater than the 2nd, swap them.\n      @if $n1 > $n2 {\n        $l: set-nth($l, $i, $n1);\n        $l: set-nth($l, $i - 1, $n2);\n\n        // The list isn't sorted and needs to be looped through again.\n        $sorted: false;\n      }\n    }\n  }\n\n  // Return the sorted list.\n  @return $l;\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_strip-units.scss",
    "content": "// Stripping units is not a best practice\n// This function should not be used elsewhere\n// It is used here because calc() doesn't do unit logic\n// AND target ratios use units as a hack to get a number.\n@function ms-unitless($val) {\n  @return ($val / ($val - $val + 1));\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_sugar.scss",
    "content": "// To attempt to avoid conflicts with other libraries\n// all funcitons are namespaced with `ms-`.\n// However, to increase usability, a shorthand function is included here.\n\n@function ms($v: 0, $base: false, $ratio: false, $thread: false, $settings: $modularscale) {\n  @return ms-function($v, $base, $ratio, $thread, $settings);\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_target.scss",
    "content": "// Convert number string to number\n@function ms-to-num($n) {\n  $l: str-length($n);\n  $r: 0;\n  $m: str-index($n,'.');\n  @if $m == null {\n    $m: $l + 1;\n  }\n  // Loop through digits and convert to numbers\n  @for $i from 1 through $l {\n    $v: str-slice($n,$i,$i);\n    @if $v == '1' { $v: 1; }\n    @else if $v == '2' { $v: 2; }\n    @else if $v == '3' { $v: 3; }\n    @else if $v == '4' { $v: 4; }\n    @else if $v == '5' { $v: 5; }\n    @else if $v == '6' { $v: 6; }\n    @else if $v == '7' { $v: 7; }\n    @else if $v == '8' { $v: 8; }\n    @else if $v == '9' { $v: 9; }\n    @else if $v == '0' { $v: 0; }\n    @else { $v: null; }\n    @if $v != null {\n      $m: $m - 1;\n      $r: $r + ms-pow(10,$m - 1) * $v;\n    } @else {\n      $l: $l - 1;\n    }\n  }\n  @return $r;\n}\n\n// Find a ratio based on a target value\n@function ms-target($t,$b) {\n  // Convert to string\n  $t: $t + '';\n  // Remove base units to calulate ratio\n  $b: ms-unitless(nth($b,1));\n  // Find where 'at' is in the string\n  $at: str-index($t,'at');\n\n  // Slice the value and target out\n  // and convert strings to numbers\n  $v: ms-to-num(str-slice($t,0,$at - 1));\n  $t: ms-to-num(str-slice($t,$at + 2));\n\n  // Solve the modular scale function for the ratio.\n  @return ms-pow(($v/$b),(1/$t));\n}"
  },
  {
    "path": "app/src/scss/vendor/modularscale/_vars.scss",
    "content": "// Ratios\n$double-octave    : 4                 ;\n$pi               : 3.14159265359     ;\n$major-twelfth    : 3                 ;\n$major-eleventh   : 2.666666667       ;\n$major-tenth      : 2.5               ;\n$octave           : 2                 ;\n$major-seventh    : 1.875             ;\n$minor-seventh    : 1.777777778       ;\n$major-sixth      : 1.666666667       ;\n$phi              : 1.618034          ;\n$golden           : $phi              ;\n$minor-sixth      : 1.6               ;\n$fifth            : 1.5               ;\n$augmented-fourth : 1.41421           ;\n$fourth           : 1.333333333       ;\n$major-third      : 1.25              ;\n$minor-third      : 1.2               ;\n$major-second     : 1.125             ;\n$minor-second     : 1.066666667       ;\n\n// Base config\n$ms-base          : 1em       !default;\n$ms-ratio         : $fifth    !default;\n$modularscale     : ()        !default;"
  },
  {
    "path": "app/src/scss/vendor/normalize.css",
    "content": "/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */\n\n/**\n * 1. Change the default font family in all browsers (opinionated).\n * 2. Prevent adjustments of font size after orientation changes in IE and iOS.\n */\n\nhtml {\n    font-family: sans-serif; /* 1 */\n    -ms-text-size-adjust: 100%; /* 2 */\n    -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/**\n * Remove the margin in all browsers (opinionated).\n */\n\nbody {\n    margin: 0;\n}\n\n/* HTML5 display definitions\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 9-.\n * 1. Add the correct display in Edge, IE, and Firefox.\n * 2. Add the correct display in IE.\n */\n\narticle,\naside,\ndetails, /* 1 */\nfigcaption,\nfigure,\nfooter,\nheader,\nmain, /* 2 */\nmenu,\nnav,\nsection,\nsummary { /* 1 */\n    display: block;\n}\n\n/**\n * Add the correct display in IE 9-.\n */\n\naudio,\ncanvas,\nprogress,\nvideo {\n    display: inline-block;\n}\n\n/**\n * Add the correct display in iOS 4-7.\n */\n\naudio:not([controls]) {\n    display: none;\n    height: 0;\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n    vertical-align: baseline;\n}\n\n/**\n * Add the correct display in IE 10-.\n * 1. Add the correct display in IE.\n */\n\ntemplate, /* 1 */\n[hidden] {\n    display: none;\n}\n\n/* Links\n   ========================================================================== */\n\n/**\n * 1. Remove the gray background on active links in IE 10.\n * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.\n */\n\na {\n    background-color: transparent; /* 1 */\n    -webkit-text-decoration-skip: objects; /* 2 */\n}\n\n/**\n * Remove the outline on focused links when they are also active or hovered\n * in all browsers (opinionated).\n */\n\na:active,\na:hover {\n    outline-width: 0;\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * 1. Remove the bottom border in Firefox 39-.\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n    border-bottom: none; /* 1 */\n    text-decoration: underline; /* 2 */\n    text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Prevent the duplicate application of `bolder` by the next rule in Safari 6.\n */\n\nb,\nstrong {\n    font-weight: inherit;\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n    font-weight: bolder;\n}\n\n/**\n * Add the correct font style in Android 4.3-.\n */\n\ndfn {\n    font-style: italic;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n    font-size: 2em;\n    margin: 0.67em 0;\n}\n\n/**\n * Add the correct background and color in IE 9-.\n */\n\nmark {\n    background-color: #ff0;\n    color: #000;\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n    font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n}\n\nsub {\n    bottom: -0.25em;\n}\n\nsup {\n    top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10-.\n */\n\nimg {\n    border-style: none;\n}\n\n/**\n * Hide the overflow in IE.\n */\n\nsvg:not(:root) {\n    overflow: hidden;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\npre,\nsamp {\n    font-family: monospace, monospace; /* 1 */\n    font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct margin in IE 8.\n */\n\nfigure {\n    margin: 1em 40px;\n}\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n    box-sizing: content-box; /* 1 */\n    height: 0; /* 1 */\n    overflow: visible; /* 2 */\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change font properties to `inherit` in all browsers (opinionated).\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\nselect,\ntextarea {\n    font: inherit; /* 1 */\n    margin: 0; /* 2 */\n}\n\n/**\n * Restore the font weight unset by the previous rule.\n */\n\noptgroup {\n    font-weight: bold;\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n    overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n    text-transform: none;\n}\n\n/**\n * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n *    controls in Android 4.\n * 2. Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\nhtml [type=\"button\"], /* 1 */\n[type=\"reset\"],\n[type=\"submit\"] {\n    -webkit-appearance: button; /* 2 */\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n    border-style: none;\n    padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n    outline: 1px dotted ButtonText;\n}\n\n/**\n * Change the border, margin, and padding in all browsers (opinionated).\n */\n\nfieldset {\n    border: 1px solid #c0c0c0;\n    margin: 0 2px;\n    padding: 0.35em 0.625em 0.75em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n    box-sizing: border-box; /* 1 */\n    color: inherit; /* 2 */\n    display: table; /* 1 */\n    max-width: 100%; /* 1 */\n    padding: 0; /* 3 */\n    white-space: normal; /* 1 */\n}\n\n/**\n * Remove the default vertical scrollbar in IE.\n */\n\ntextarea {\n    overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10-.\n * 2. Remove the padding in IE 10-.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n    box-sizing: border-box; /* 1 */\n    padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n    height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n    -webkit-appearance: textfield; /* 1 */\n    outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding and cancel buttons in Chrome and Safari on OS X.\n */\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n    -webkit-appearance: none;\n}\n\n/**\n * Correct the text style of placeholders in Chrome, Edge, and Safari.\n */\n\n::-webkit-input-placeholder {\n    color: inherit;\n    opacity: 0.54;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n    -webkit-appearance: button; /* 1 */\n    font: inherit; /* 2 */\n}\n"
  },
  {
    "path": "app/src/scss/vendor/vue-multiselect.scss",
    "content": "@import '../../node_modules/vue-multiselect/dist/vue-multiselect.min.css';\n@import '../variables.scss';\n\n.multiselect {\n    &__tags {\n        background: var(--input-bg);\n        border-color: var(--input-border-color);\n        border-radius: var(--border-radius);\n        color: var(--text-primary-color);\n    }\n\n    &__single {\n        background: var(--input-bg);\n        font-size: $app-font-base;\n        line-height: 1.4;\n        margin-bottom: 0;\n        padding: 0;\n    }\n\n    &__select {\n        height: 45px;\n    }\n\n    &__option {\n        font-size: 15px;\n        font-weight: 400;\n        padding: 12px 2rem;\n\n        &--highlight,\n        &--highlight:after {\n            background: var(--input-bg-light);\n            color: var(--text-primary-color);\n        }\n\n        &.multiselect__option--selected {\n            background: var(--input-bg-light);\n            color: var(--text-primary-color);\n            font-weight: 600;\n        }\n         \n        &--selected,\n        &--group-selected {\n            &.multiselect__option--highlight,\n            &.multiselect__option--highlight:after {\n                background: var(--input-bg-light);\n                color: var(--text-primary-color);\n            }                \n        }\n    }\n\n    &__input {\n        background: var(--input-bg);\n        border: none !important;\n        box-shadow: none !important;\n        margin: 0 !important;\n        padding: 0 !important;\n        \n        &::placeholder {\n            color: var(--text-primary-color);\n        }\n    }\n\n    &__content-wrapper {\n        border-color: var(--input-border-color);\n        background: var(--input-bg);\n        color: var(--text-primary-color);\n    }\n\n    &--active {\n        z-index: 2;\n    }\n}\n.options-sidebar {\n    label {\n        &.is-invalid {\n            .multiselect__tags {\n                border-color: var(--warning);\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/store/default.state.js",
    "content": "export default {\n    // Application front-end status\n    app: {\n        config: {},\n        customConfig: {},\n        notifications: {},\n        notificationsCount: 0,\n        notificationsReadStatus: '',\n        versionInfo: {\n            number: 0,\n            build: 0,\n            os: ''\n        },\n        editorOpened: false,\n        editorType: 'blockeditor',\n        windowIsMaximized: false,\n        theme: ['system', 'dark', 'default'].indexOf(localStorage.getItem('publii-theme')) > -1 ? localStorage.getItem('publii-theme') : 'default' \n    },\n    // Persistent components\n    components: {\n        sidebar: {\n            status: false,\n            syncInProgress: false\n        }\n    },\n    // Data about installed themes and their location\n    themes: [],\n    themesPath: '',\n    languages: [],\n    languagesPath: '',\n    languagesDefaultPath: '',\n    plugins: [],\n    pluginsPath: '',\n    wysiwygTranslation: {},\n    dirs: {},\n    vendorPath: '',\n    // Data about available sites\n    sites: [],\n    // Data about currently displayed site\n    currentSite: {\n        config: {},\n        posts: [],\n        pages: [],\n        tags: [],\n        postsTags: [],\n        postsAuthors: [],\n        postTemplates: [],\n        pagesAuthors: [],\n        pageTemplates: [],\n        themes: [],\n        images: [],\n        menuStructure: [],\n        themeSettings: [],\n        siteDir: ''\n    },\n    // ordering temporary data\n    ordering: {\n        posts: {\n            orderBy: 'id',\n            order: 'DESC'\n        },\n        pages: {\n            orderBy: 'id',\n            order: 'DESC'\n        },\n        tags: {\n            orderBy: 'id',\n            order: 'DESC'\n        },\n        authors: {\n            orderBy: 'id',\n            order: 'DESC'\n        }\n    }\n};\n"
  },
  {
    "path": "app/src/store/getters/app-version.js",
    "content": "/**\n * Returns object with version info\n *\n * @param state\n * @param getters\n *\n * @returns {object}\n */\n\nexport default (state, getters) => {\n    return state.app.versionInfo;\n};\n"
  },
  {
    "path": "app/src/store/getters/author-templates.js",
    "content": "/**\n * Returns prepared array with author templates\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => {\n    let authorTemplates = {\n        \"\": \"Default\"\n    };\n\n    if(!state.currentSite.authorTemplates || !state.currentSite.authorTemplates[0]) {\n        return authorTemplates;\n    }\n\n    for(let i = 0; i < state.currentSite.authorTemplates.length; i++) {\n        authorTemplates[state.currentSite.authorTemplates[i][0]] = state.currentSite.authorTemplates[i][1];\n    }\n\n    return authorTemplates;\n};\n"
  },
  {
    "path": "app/src/store/getters/languages.js",
    "content": "/**\n * Returns prepared array with languages\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\n export default (state, getters) => {\n    return state.languages.map(language => {\n        let basePath = state.languagesPath;\n\n        if (language.type === 'default') {\n            basePath = state.languagesDefaultPath;\n        }\n        \n        return {\n            type: language.type,\n            name: language.name,\n            version: language.version,\n            directory: language.directory,\n            publiiSupport: language.publiiSupport,\n            thumbnail: basePath + '/' + language.directory + '/' + 'thumbnail.svg',\n            momentLocale: language.momentLocale,\n            wysiwygTranslation: language.wysiwygTranslation\n        };\n    });\n};\n"
  },
  {
    "path": "app/src/store/getters/notifications-count.js",
    "content": "/**\n * Returns number of available updates\n *\n * @param state\n * @param getters\n *\n * @returns {boolean, string}\n */\n\nexport default (state) => {\n    return state.app.notificationsCount;\n};\n"
  },
  {
    "path": "app/src/store/getters/notifications-status.js",
    "content": "/**\n * Returns state of the notifcations\n *\n * @param state\n * @param getters\n *\n * @returns {boolean, string}\n */\n\nexport default (state, getters) => {\n    return state.app.config.notificationsStatus;\n};\n"
  },
  {
    "path": "app/src/store/getters/notifications.js",
    "content": "/**\n * Returns content of noticiations\n *\n * @param state\n *\n * @returns {boolean, string}\n */\n\nexport default (state) => {\n    return state.app.notifications;\n};\n"
  },
  {
    "path": "app/src/store/getters/plugins.js",
    "content": "/**\n * Returns prepared array with plugins\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\n export default (state, getters) => {\n    return state.plugins.map(plugin => {\n        let basePath = state.pluginsPath;\n        \n        return {\n            scope: plugin.scope,\n            name: plugin.name,\n            version: plugin.version,\n            directory: plugin.directory,\n            minimumPubliiVersion: plugin.minimumPubliiVersion,\n            thumbnail: basePath + '/' + plugin.directory + '/' + 'thumbnail.svg',\n            assets: plugin.assets,\n            path: plugin.path\n        };\n    });\n};\n"
  },
  {
    "path": "app/src/store/getters/site-authors.js",
    "content": "/**\n * Returns array of current site authors\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nimport Utils from '../../helpers/utils.js';\n\nexport default (state, getters) => (filterValue, orderBy = 'id', order = 'DESC') => {\n    let deletedPostsIDs = state.currentSite.posts.filter(post => post.status.indexOf('trashed') > -1).map(post => post.id);\n    \n    let authors = state.currentSite.authors.map(author => {\n        let indexingOptionsEnabled = true;\n        let authorTemplates = [];\n        let avatarUrlPrefix = 'file:///';\n        let postsCounter = 0;\n        let config = {\n            description: '',\n            metaTitle: '',\n            metaDescription: '',\n            template: '',\n            email: '',\n            website: '',\n            avatar: '',\n            useGravatar: false\n        };\n\n        if(typeof author.config === 'string' && author.config !== '') {\n            config = Object.assign(config, JSON.parse(author.config));\n        }\n\n        if(\n            state.currentSite.config.advanced &&\n            state.currentSite.config.advanced.metaRobotsAuthors.indexOf('noindex') !== -1\n        ) {\n            indexingOptionsEnabled = false;\n        }\n\n        for(let i = 0; i < state.currentSite.authorTemplates.length; i++) {\n            authorTemplates.push({\n                value: state.currentSite.authorTemplates[i][0],\n                name: state.currentSite.authorTemplates[i][1]\n            });\n        }\n\n        postsCounter = state.currentSite.postsAuthors.filter(postAuthor => {\n            return postAuthor.authorID === author.id && deletedPostsIDs.indexOf(postAuthor.postID) === -1;;\n        }).length;\n\n        return {\n            id: author.id,\n            name: author.name,\n            username: author.username,\n            email: config.email,\n            website: config.website,\n            avatar: config.avatar,\n            useGravatar: config.useGravatar,\n            description: config.description,\n            postsCounter: postsCounter,\n            metaTitle: config.metaTitle,\n            metaDescription: config.metaDescription,\n            template: config.template,\n            authorTemplates: authorTemplates,\n            additionalData: author.additionalData,\n            visibleIndexingOptions: indexingOptionsEnabled\n        };\n    });\n\n    authors.sort((authorA, authorB) => {\n        if (orderBy === 'name') {\n            if (order === 'DESC') {\n                return -(authorA.name.localeCompare(authorB.name))\n            }\n\n            return authorA.name.localeCompare(authorB.name);\n        }\n        \n        if (order === 'DESC') {\n            return authorB[orderBy] - authorA[orderBy];\n        }\n\n        return authorA[orderBy] - authorB[orderBy];\n    });\n\n    authors = authors.filter(author => {\n        if(!filterValue) {\n            return true;\n        }\n\n        if(author.name.toLowerCase().indexOf(filterValue) > -1) {\n            return true;\n        }\n\n        return false;\n    });\n\n    return authors;\n};\n"
  },
  {
    "path": "app/src/store/getters/site-display-names.js",
    "content": "/**\n * Returns array with website names\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => {\n    let displayNames = {};\n    let siteNames = Object.keys(state.sites);\n\n    for(let name of siteNames) {\n        let displayName = state.sites[name].displayName;\n\n        if(!displayName) {\n            displayName = state.sites[name].name;\n        }\n\n        displayNames[name] = displayName;\n    }\n\n    return displayNames;\n};\n"
  },
  {
    "path": "app/src/store/getters/site-names.js",
    "content": "/**\n * Returns array with website names\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => {\n    let siteNames = Object.keys(state.sites);\n    siteNames.sort((a, b) => a.localeCompare(b));\n    return siteNames;\n};\n"
  },
  {
    "path": "app/src/store/getters/site-pages.js",
    "content": "import pageFilter from '../helpers/page-filter.js';\nimport pageGetAuthor from '../helpers/page-get-author.js';\n\n/**\n * Returns array of current site pages\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => (filterValue, orderBy = '', order = 'DESC') => {\n    if (!state.currentSite.pages) {\n        return [];\n    }\n\n    let pages = state.currentSite.pages.filter(page => {\n        if (!pageFilter(state, page, filterValue)) {\n            return false;\n        }\n\n        return true;\n    }).map(page => {\n        let additionalData = JSON.parse(page.additional_data);\n        let pageEditor = 'tinymce';\n\n        if (additionalData && additionalData.editor) {\n            pageEditor = additionalData.editor;\n        }\n\n        return {\n            id: page.id,\n            editor: pageEditor,\n            title: page.title,\n            slug: page.slug,\n            status: page.status,\n            created: page.created_at,\n            modified: page.modified_at,\n            isDraft: page.status.indexOf('draft') > -1,\n            isTrashed: page.status.indexOf('trashed') > -1,\n            author_id: page.authors,\n            author: pageGetAuthor(state, page.id)\n        }\n    });\n\n    pages.sort((pageA, pageB) => {\n        if (orderBy === 'title') {\n            if (order === 'DESC') {\n                return -(pageA.title.localeCompare(pageB.title))\n            }\n\n            return pageA.title.localeCompare(pageB.title);\n        }\n\n        if (orderBy === 'author') {\n            if (order === 'DESC') {\n                return -(pageA.author.localeCompare(pageB.author))\n            }\n\n            return pageA.author.localeCompare(pageB.author);\n        }\n\n        if (orderBy !== '' && order === 'DESC') {\n            return pageB[orderBy] - pageA[orderBy];\n        }\n\n        return pageA[orderBy] - pageB[orderBy];\n    });\n\n    return pages;\n};\n"
  },
  {
    "path": "app/src/store/getters/site-plugins.js",
    "content": "/**\n * Returns prepared array with site plugins\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\n export default (state, getters) => {\n    return state.plugins.filter(plugin => plugin.scope === 'site').map(plugin => {\n        let basePath = state.pluginsPath;\n        \n        return {\n            scope: plugin.scope,\n            name: plugin.name,\n            version: plugin.version,\n            directory: plugin.directory,\n            minimumPubliiVersion: plugin.minimumPubliiVersion,\n            thumbnail: basePath + '/' + plugin.directory + '/' + 'thumbnail.svg',\n            assets: plugin.assets,\n            path: plugin.path\n        };\n    });\n};\n"
  },
  {
    "path": "app/src/store/getters/site-posts.js",
    "content": "import postFilter from '../helpers/post-filter.js';\nimport postGetAuthor from '../helpers/post-get-author.js';\nimport postGetTags from '../helpers/post-get-tags.js';\n\n/**\n * Returns array of current site tags\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => (filterValue, orderBy = 'id', order = 'DESC') => {\n    if(!state.currentSite.posts) {\n        return [];\n    }\n\n    let posts = state.currentSite.posts.filter(post => {\n        if(!postFilter(state, post, filterValue)) {\n            return false;\n        }\n\n        return true;\n    }).map(post => {\n        let additionalData = JSON.parse(post.additional_data);\n        let mainTag = '';\n        let postEditor = 'tinymce';\n\n        if (additionalData && additionalData.mainTag) {\n            mainTag = parseInt(additionalData.mainTag, 10);\n        }\n\n        if (additionalData && additionalData.editor) {\n            postEditor = additionalData.editor;\n        }\n\n        return {\n            id: post.id,\n            editor: postEditor,\n            title: post.title,\n            slug: post.slug,\n            tags: postGetTags(state, post.id),\n            status: post.status,\n            created: post.created_at,\n            modified: post.modified_at,\n            isHidden: post.status.indexOf('hidden') > -1,\n            isExcludedOnHomepage: post.status.indexOf('excluded_homepage') > -1,\n            isDraft: post.status.indexOf('draft') > -1,\n            isFeatured: post.status.indexOf('featured') > -1,\n            isTrashed: post.status.indexOf('trashed') > -1,\n            author_id: post.authors,\n            author: postGetAuthor(state, post.id),\n            mainTag: mainTag\n        }\n    });\n\n    posts.sort((postA, postB) => {\n        if (orderBy === 'title') {\n            if (order === 'DESC') {\n                return -(postA.title.localeCompare(postB.title))\n            }\n\n            return postA.title.localeCompare(postB.title);\n        }\n\n        if (orderBy === 'author') {\n            if (order === 'DESC') {\n                return -(postA.author.localeCompare(postB.author))\n            }\n\n            return postA.author.localeCompare(postB.author);\n        }\n        \n        if (order === 'DESC') {\n            return postB[orderBy] - postA[orderBy];\n        }\n\n        return postA[orderBy] - postB[orderBy];\n    });\n\n    return posts;\n};\n"
  },
  {
    "path": "app/src/store/getters/site-tags.js",
    "content": "/**\n * Returns array of current site tags\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => (filterValue, orderBy = 'id', order = 'DESC') => {\n    let tags = JSON.parse(JSON.stringify(state.currentSite.tags));\n    \n    tags = tags.filter(tag => {\n        if(!filterValue) {\n            return true;\n        }\n\n        if(tag.name.toLowerCase().indexOf(filterValue) > -1) {\n            return true;\n        }\n\n        return false;\n    });\n\n    let deletedPostsIDs = state.currentSite.posts.filter(post => post.status && post.status.indexOf('trashed') > -1).map(post => post.id);\n\n    tags = tags.map(tag => {\n        let postsCounter = 0;\n\n        postsCounter = state.currentSite.postsTags.filter(postTag => {\n            return postTag.tagID === tag.id && deletedPostsIDs.indexOf(postTag.postID) === -1;\n        }).length;\n\n        tag.postsCounter = postsCounter;\n\n        return tag;\n    });\n\n    tags.sort((tagA, tagB) => {\n        if (orderBy === 'name') {\n            if (order === 'DESC') {\n                return -(tagA.name.localeCompare(tagB.name))\n            }\n\n            return tagA.name.localeCompare(tagB.name);\n        }\n        \n        if (order === 'DESC') {\n            return tagB[orderBy] - tagA[orderBy];\n        }\n\n        return tagA[orderBy] - tagB[orderBy];\n    });\n\n    return tags;\n};\n"
  },
  {
    "path": "app/src/store/getters/tag-templates.js",
    "content": "/**\n * Returns prepared array with tag templates\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => {\n    let tagTemplates = {\n        \"\": \"Default\"\n    };\n\n    if(!state.currentSite.tagTemplates || !state.currentSite.tagTemplates[0]) {\n        return tagTemplates;\n    }\n\n    for(let i = 0; i < state.currentSite.tagTemplates.length; i++) {\n        tagTemplates[state.currentSite.tagTemplates[i][0]] = state.currentSite.tagTemplates[i][1];\n    }\n\n    return tagTemplates;\n};\n"
  },
  {
    "path": "app/src/store/getters/theme-select.js",
    "content": "/**\n * Returns prepared array with themes\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => {\n    let themes = {\n        \"use\": {\n            \"name\": \"Use\",\n            \"items\": {}\n        },\n        \"install-use\": {\n            \"name\": \"Install and use\",\n            \"items\": {}\n        },\n        \"uninstall\": {\n            \"name\": \"Uninstall\",\n            \"items\": {}\n        }\n    };\n\n    if (!state.currentSite.themes) {\n        return themes;\n    }\n\n    state.currentSite.themes.map(theme => {\n        if(theme.location === 'app') {\n            themes['install-use'].items['install-use-' + theme.directory] = theme.name + '(v.' + theme.version + ')';\n        } else {\n            themes['use'].items['use-' + theme.directory] = theme.name + '(v.' + theme.version + ')';\n            themes['uninstall'].items['uninstall-' + theme.directory] = theme.name + '(v.' + theme.version + ')';\n        }\n    });\n\n    return themes;\n};\n"
  },
  {
    "path": "app/src/store/getters/themes.js",
    "content": "/**\n * Returns prepared array with themes\n *\n * @param state\n * @param getters\n *\n * @returns {array}\n */\n\nexport default (state, getters) => {\n    return state.themes.map(theme => {\n        return {\n            name: theme.name,\n            version: theme.version,\n            directory: theme.directory,\n            thumbnail: state.themesPath + '/' + theme.directory + '/' + 'thumbnail.png'\n        };\n    });\n};\n"
  },
  {
    "path": "app/src/store/helpers/mutations.js",
    "content": "import Vue from 'vue';\nimport defaultAstAppConfig from './../../../config/AST.app.config';\nimport Utils from './../../helpers/utils.js';\n\nexport default {\n    init (state, initialData) {\n        state.app.config = Object.assign(JSON.parse(JSON.stringify(defaultAstAppConfig)), initialData.config);\n        state.app.customConfig = initialData.customConfig;\n        state.app.versionInfo = initialData.version;\n        state.currentSite = {};\n        state.languages = initialData.languages;\n        state.languagesPath = initialData.languagesPath;\n        state.languagesDefaultPath = initialData.languagesDefaultPath;\n        state.plugins = initialData.plugins;\n        state.pluginsPath = initialData.pluginsPath;\n        state.sites = initialData.sites;\n        state.themes = initialData.themes;\n        state.themesPath = initialData.themesPath;\n        state.dirs = initialData.dirs;\n        state.vendorPath = initialData.vendorPath;\n        state.wysiwygTranslation = initialData.currentLanguage.wysiwygTranslation;\n\n        // Set default ordering based on the app config\n        let pagesOrdering = state.app.config.pagesOrdering ? state.app.config.pagesOrdering.split(' ') : ['', 'DESC'];\n        let postsOrdering = state.app.config.postsOrdering ? state.app.config.postsOrdering.split(' ') : ['id', 'DESC'];\n        let tagsOrdering = state.app.config.tagsOrdering ? state.app.config.tagsOrdering.split(' ') : ['id', 'DESC'];\n        let authorsOrdering = state.app.config.authorsOrdering ? state.app.config.authorsOrdering.split(' ') : ['id', 'DESC'];\n\n        Vue.set(state, 'ordering', {\n            pages: {\n                orderBy: pagesOrdering[0],\n                order: pagesOrdering[1]\n            },\n            posts: {\n                orderBy: postsOrdering[0],\n                order: postsOrdering[1]\n            },\n            tags: {\n                orderBy: tagsOrdering[0],\n                order: tagsOrdering[1]\n            },\n            authors: {\n                orderBy: authorsOrdering[0],\n                order: authorsOrdering[1]\n            }\n        });\n    },\n    setAppConfig (state, newAppConfig) {\n        state.app.config = Utils.deepMerge(state.app.config, newAppConfig);\n    },\n    setAppTheme (state, newTheme) {\n        state.app.theme = newTheme;\n    },\n    setSiteDir (state, newSitesLocation) {\n        if (!state.currentSite.siteDir) {\n            return;\n        }\n\n        state.currentSite.siteDir = state.currentSite.siteDir.replace(\n            state.app.config.sitesLocation,\n            newSitesLocation\n        );\n    },\n    clearCurrentSite (state) {\n        state.currentSite = {\n            config: {},\n            posts: [],\n            pages: [],\n            tags: [],\n            postsTags: [],\n            postsAuthors: [],\n            postTemplates: [],\n            pagesAuthors: [],\n            pageTemplates: [],\n            themes: [],\n            images: [],\n            menuStructure: [],\n            themeSettings: [],\n            siteDir: ''\n        };\n    },\n    copySiteConfig (state, siteName) {\n        state.currentSite.config = JSON.parse(JSON.stringify(state.sites[siteName]));\n    },\n    setSiteConfig (state, siteData) {\n        let siteName = siteData.name;\n\n        if (!siteName) {\n            siteName = siteData.config.name;\n        }\n\n        Vue.set(state.sites, siteName, JSON.parse(JSON.stringify(siteData.config)));\n        state.currentSite.config = JSON.parse(JSON.stringify(siteData.config));\n    },\n    replaceSiteConfig (state, data) {\n        let currentSiteConfigCopy = JSON.parse(JSON.stringify(state.currentSite.config));\n        state.currentSite.config = Utils.deepMerge(currentSiteConfigCopy, data.newSettings);\n        let oldSiteCopy = JSON.parse(JSON.stringify(state.sites[data.oldName]));\n        state.sites[state.currentSite.config.name] = Utils.deepMerge(oldSiteCopy, data.newSettings);\n    },\n    replaceSite (state, sitesData) {\n        state.sites[sitesData.newSiteName] = state.sites[sitesData.oldSiteName];\n        state.sites[sitesData.newSiteName].displayName = sitesData.displayName;\n        Vue.delete(state.sites, sitesData.oldSiteName);\n    },\n    removeWebsite (state, name) {\n        Vue.delete(state.sites, name);\n    },\n    cloneWebsite (state, data) {\n        let copiedData = JSON.parse(JSON.stringify(data.newSiteConfig));\n        Vue.set(state.sites, data.newSiteCatalog, copiedData);\n    },\n    addNewSite (state, siteData) {\n        state.currentSite = {\n            config: JSON.parse(JSON.stringify(siteData.siteConfig)),\n            posts: [],\n            pages: [],\n            tags: [],\n            authors: JSON.parse(JSON.stringify(siteData.authors)),\n            postsTags: [],\n            postsAuthors: [],\n            postTemplates: [],\n            pagesAuthors: [],\n            pageTemplates: [],\n            themes: [],\n            images: [],\n            menuStructure: [],\n            themeSettings: [],\n            siteDir: siteData.siteDir\n        };\n\n        Vue.set(state.sites, siteData.siteConfig.name, Object.assign({}, siteData.siteConfig));\n    },\n    switchSite (state, data) {\n        state.currentSite = {\n            config: JSON.parse(JSON.stringify(state.currentSite.config))\n        };\n\n        Utils.deepMerge(state.currentSite, {\n            posts: data ? data.posts : [],\n            pages: data ? data.pages : [],\n            tags: data ? data.tags : [],\n            postsTags: data ? data.postsTags: [],\n            authors: data ? data.authors : [],\n            postsAuthors: data ? data.postsAuthors : [],\n            postTemplates: data ? data.postTemplates : [],\n            pagesAuthors: data ? data.pagesAuthors : [],\n            pageTemplates: data ? data.pageTemplates : [],\n            tagTemplates: data ? data.tagTemplates : [],\n            authorTemplates: data ? data.authorTemplates : [],\n            themes: data ? data.themes : AST.themes,\n            themeHasOverrides: data ? data.themeHasOverrides : false,\n            themeSettings: data ? data.themeSettings : {},\n            menuStructure: data ? data.menuStructure : [],\n            siteDir: data ? data.siteDir : '',\n            sidebar: {\n                status: state.currentSite.config.synced\n            }\n        });\n\n        state.currentSite = Object.assign({}, state.currentSite);\n        state.components.sidebar.status = state.currentSite.config.synced;\n        \n        // Reset ordering after website switch\n        let pagesOrdering = state.app.config.pagesOrdering ? state.app.config.pagesOrdering.split(' ') : ['', 'DESC'];\n        let postsOrdering = state.app.config.postsOrdering ? state.app.config.postsOrdering.split(' ') : ['id', 'DESC'];\n        let tagsOrdering = state.app.config.tagsOrdering ? state.app.config.tagsOrdering.split(' ') : ['id', 'DESC'];\n        let authorsOrdering = state.app.config.authorsOrdering ? state.app.config.authorsOrdering.split(' ') : ['id', 'DESC'];\n\n        Vue.set(state, 'ordering', {\n            pages: {\n                orderBy: pagesOrdering[0],\n                order: pagesOrdering[1]\n            },\n            posts: {\n                orderBy: postsOrdering[0],\n                order: postsOrdering[1]\n            },\n            tags: {\n                orderBy: tagsOrdering[0],\n                order: tagsOrdering[1]\n            },\n            authors: {\n                orderBy: authorsOrdering[0],\n                order: authorsOrdering[1]\n            }\n        });\n\n        /*\n        if (window.spellCheckHandler) {\n            window.spellCheckHandler.switchLanguage(state.currentSite.config.language);\n        }\n        */\n    },\n    setSites (state, sites) {\n        state.sites = Object.assign({}, sites);\n    },\n    setNewThemeConfig (state, data) {\n        state.currentSite.config.theme = data.themeName;\n        state.currentSite.themeSettings = data.newThemeConfig;\n    },\n    setNotifications (state, notificationsData) {\n        state.app.notifications = Object.assign({}, notificationsData);\n    },\n    setNotificationsCount (state, notificationsCount) {\n        state.app.notificationsCount = notificationsCount;\n    },\n    replaceAppThemes (state, newThemes) {\n        state.themes = newThemes.slice();\n    },\n    updateSiteThemes (state) {\n        state.currentSite.themes = state.currentSite.themes.filter(theme => theme.location !== 'app');\n        state.currentSite.themes = [...state.currentSite.themes, ...state.themes].slice();\n    },\n    replaceAppLanguages (state, newLanguages) {\n        state.languages = newLanguages.slice();\n    },\n    replaceAppPlugins (state, newPlugins) {\n        state.plugins = newPlugins.slice();\n    },\n    setTags (state, tags) {\n        Vue.set(state.currentSite, 'tags', tags.slice());\n    },\n    setPostsTags (state, postsTags) {\n        Vue.set(state.currentSite, 'postsTags', postsTags.slice());\n    },\n    removeTags (state, tagIDs) {\n        state.currentSite.tags = state.currentSite.tags.filter(tag => tagIDs.indexOf(tag.id) === -1);\n        state.currentSite.postsTags = state.currentSite.postsTags.filter(postTag => tagIDs.indexOf(postTag.tagID) === -1);\n    },\n    setAuthors (state, authors) {\n        Vue.set(state.currentSite, 'authors', authors.slice());\n    },\n    setPostAuthors (state, postsAuthors) {\n        Vue.set(state.currentSite, 'postsAuthors', postsAuthors.slice());\n    },\n    setPageAuthors (state, pagesAuthors) {\n        Vue.set(state.currentSite, 'pagesAuthors', pagesAuthors.slice());\n    },\n    removeAuthors (state, authorIDs) {\n        state.currentSite.authors = state.currentSite.authors.filter(author => authorIDs.indexOf(author.id) === -1);\n        state.currentSite.postsAuthors = state.currentSite.postsAuthors.filter(postAuthor => authorIDs.indexOf(postAuthor.authorID) === -1);\n        state.currentSite.pagesAuthors = state.currentSite.pagesAuthors.filter(pageAuthor => authorIDs.indexOf(pageAuthor.authorID) === -1);\n    },\n    removePosts (state, postIDs) {\n        state.currentSite.posts = state.currentSite.posts.filter(post => postIDs.indexOf(post.id) === -1);\n    },\n    removePages (state, pageIDs) {\n        state.currentSite.pages = state.currentSite.pages.filter(page => pageIDs.indexOf(page.id) === -1);\n    },\n    changePostsToPages (state, config) {\n        // Move posts with the given IDs to pages\n        state.currentSite.pages = state.currentSite.pages.concat(state.currentSite.posts.filter(post => config.postIDs.indexOf(post.id) !== -1));\n        // remove posts with the given IDs \n        state.currentSite.posts = state.currentSite.posts.filter(post => config.postIDs.indexOf(post.id) === -1);\n        // remove tag references\n        state.currentSite.postsTags = state.currentSite.postsTags.filter(postTags => config.postIDs.indexOf(postTags.postID) === -1);\n        // move authors references\n        state.currentSite.postsAuthors.forEach(postAuthor => {\n            if (config.postIDs.indexOf(postAuthor.postID) > -1) {\n                state.currentSite.pagesAuthors.push({\n                    authorID: postAuthor.authorID,\n                    authorName: postAuthor.authorName,\n                    pageID: postAuthor.postID\n                });\n            }\n        });\n        state.currentSite.postsAuthors = state.currentSite.postsAuthors.filter(postAuthor => config.postIDs.indexOf(postAuthor.postID) === -1);\n        // add status is-page to the pages with given IDs\n        state.currentSite.pages = state.currentSite.pages.map(function(page) {\n            if(config.postIDs.indexOf(page.id) !== -1) {\n                let currentStatus = page.status.split(',');\n\n                if(currentStatus.indexOf('is-page') === -1) {\n                    currentStatus.push('is-page');\n                }\n\n                page.status = currentStatus.filter(status => ['excluded_homepage', 'featured', 'hidden'].indexOf(status) === -1).join(',');\n                page.template = '*';\n            }\n\n            return page;\n        });\n    },\n    changePagesToPosts (state, config) {\n        // Move pages with the given IDs to pages\n        state.currentSite.posts = state.currentSite.posts.concat(state.currentSite.pages.filter(page => config.pageIDs.indexOf(page.id) !== -1));\n        // remove pages with the given IDs \n        state.currentSite.pages = state.currentSite.pages.filter(page => config.pageIDs.indexOf(page.id) === -1);\n        // remove status is-page from the pages with given IDs\n        state.currentSite.posts = state.currentSite.posts.map(function(post) {\n            if(config.pageIDs.indexOf(post.id) !== -1) {\n                let currentStatus = post.status.split(',');\n                post.status = currentStatus.filter(status => status !== 'is-page').join(',');\n                post.template = '*';\n            }\n\n            return post;\n        });\n    },\n    changePostsStatus (state, config) {\n        state.currentSite.posts = state.currentSite.posts.map(function(post) {\n            if(config.postIDs.indexOf(post.id) !== -1) {\n                let currentStatus = post.status.split(',');\n\n                if(!config.inverse) {\n                    if(currentStatus.indexOf(config.status) === -1) {\n                        currentStatus.push(config.status);\n                    }\n                } else {\n                    if(currentStatus.indexOf(config.status) > -1) {\n                        currentStatus = currentStatus.filter(postStatus => postStatus !== config.status);\n                    }\n                }\n\n                post.status = currentStatus.join(',');\n            }\n\n            return post;\n        });\n    },\n    changePagesStatus (state, config) {\n        state.currentSite.pages = state.currentSite.pages.map(function(page) {\n            if(config.pageIDs.indexOf(page.id) !== -1) {\n                let currentStatus = page.status.split(',');\n\n                if(!config.inverse) {\n                    if(currentStatus.indexOf(config.status) === -1) {\n                        currentStatus.push(config.status);\n                    }\n                } else {\n                    if(currentStatus.indexOf(config.status) > -1) {\n                        currentStatus = currentStatus.filter(pageStatus => pageStatus !== config.status);\n                    }\n                }\n\n                page.status = currentStatus.join(',');\n            }\n\n            return page;\n        });\n    },\n    changeTagsVisibility (state, config) {\n        state.currentSite.tags = state.currentSite.tags.map(function(tag) {\n            if (config.tagsIDs.indexOf(tag.id) !== -1) {\n                tag.additionalData = JSON.parse(tag.additionalData);\n\n                if (!config.inverse) {\n                    tag.additionalData.isHidden = true;\n                } else {\n                    tag.additionalData.isHidden = false;\n                }\n\n                tag.additionalData = JSON.stringify(tag.additionalData);\n            }\n\n            return tag;\n        });\n    },\n    refreshSiteConfig (state, newData) {\n        let currentSiteConfigCopy = Object.assign({}, state.currentSite.config);\n        state.currentSite.config = Utils.deepMerge(currentSiteConfigCopy, newData.newSettings);\n        let oldSiteConfigCopy = Object.assign({}, state.sites[newData.siteName]);\n        state.sites[state.currentSite.config.name] = Object.assign({}, Utils.deepMerge(oldSiteConfigCopy, newData.newSettings));\n    },\n    refreshSiteThemeConfig (state, newData) {\n        state.currentSite.config.theme = newData.themeName;\n        state.currentSite.themeSettings = Object.assign({}, newData.newThemeConfig);\n    },\n    setSidebarStatus (state, newStatus) {\n        state.components.sidebar.status = newStatus;\n    },\n    setSyncStatus (state, newStatus) {\n        state.components.sidebar.syncInProgress = newStatus;\n    },\n    setMenuPosition (state, newPositionData) {\n        state.currentSite.menuStructure[newPositionData.index].position = newPositionData.position;\n        state.currentSite.menuStructure[newPositionData.index].maxLevels = newPositionData.maxLevels ? newPositionData.maxLevels : ''; \n    },\n    addNewMenu (state, newMenuName) {\n        state.currentSite.menuStructure.push({\n            name: newMenuName,\n            position: \"\",\n            items: []\n        });\n    },\n    deleteMenuByIDs (state, menuIDs) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.filter((item, index) => menuIDs.indexOf(index) === -1);\n    },\n    editMenuName (state, newMenuData) {\n        state.currentSite.menuStructure[newMenuData.index].name = newMenuData.newName;\n    },\n    addNewMenuItem (state, data) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n            if (index === data.menuID) {\n                menu.items.push(data.menuItem);\n            }\n\n            return menu;\n        });\n    },\n    addNewSubmenuItem (state, data) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n            if (index === data.menuID) {\n                menu.items = insertSubmenuItem(menu.items, data.menuItem, data.parentID);\n            }\n\n            return menu;\n        });\n    },\n    editMenuItem (state, data) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n            if (index === data.menuID) {\n                menu.items = menu.items.map(item => editMenuItemByID(item, data.menuItem));\n            }\n\n            return menu;\n        });\n    },\n    showMenuItem (state, data) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n            if (index === data.menuID) {\n                menu.items = menu.items.map(item => showMenuItemByID(item, data.itemID));\n            }\n\n            return menu;\n        });\n    },\n    hideMenuItem (state, data) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n            if (index === data.menuID) {\n                menu.items = menu.items.map(item => hideMenuItemByID(item, data.itemID));\n            }\n\n            return menu;\n        });\n    },\n    deleteMenuItem(state, data) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n            if(index === data.menuID) {\n                menu.items = menu.items.filter(item => deleteMenuItemByID(item, data.menuItemID));\n            }\n\n            return menu;\n        });\n    },\n    duplicateMenuItem(state, data) {\n        state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n            if (index === data.menuID) {\n                let dataToDuplicate = findMenuItemByIDwithParent(menu.items, data.menuItemID);\n                let itemToDuplicate = dataToDuplicate.item;\n\n                if (itemToDuplicate) {\n                    let clone = JSON.parse(JSON.stringify(itemToDuplicate));\n                    let newID = +new Date();\n                    let cloneUpdated = updateMenuItemIDs(clone, newID);\n                    menu.items = insertMenuItem(menu.items, cloneUpdated, dataToDuplicate.parentItemID);\n                }\n            }\n\n            return menu;\n        });\n    },\n    moveMenuItem (state, data) {\n        if (data.position === 'child') {\n            let destination = findMenuItemByID(state.currentSite.menuStructure[data.menuID].items, data.destinationID);\n            let target = JSON.parse(JSON.stringify(findMenuItemByID(state.currentSite.menuStructure[data.menuID].items, data.targetID)));\n            \n            state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n                if (index === data.menuID) {\n                    menu.items = menu.items.filter(item => deleteMenuItemByID(item, data.targetID));\n                }\n    \n                return menu;\n            });\n\n            destination.items.push(target);\n        } else if (data.position === 'after' || data.position === 'before') {\n            let destination = findMenuItemByIDwithParent(state.currentSite.menuStructure[data.menuID].items, data.destinationID);\n            let target = JSON.parse(JSON.stringify(findMenuItemByID(state.currentSite.menuStructure[data.menuID].items, data.targetID)));\n            let destinationParent;\n            \n            if (destination.parentItemID) {\n                destinationParent = findMenuItemByID(state.currentSite.menuStructure[data.menuID].items, destination.parentItemID);\n            } else {\n                destinationParent = state.currentSite.menuStructure[data.menuID];\n            }\n            \n            state.currentSite.menuStructure = state.currentSite.menuStructure.map((menu, index) => {\n                if (index === data.menuID) {\n                    menu.items = menu.items.filter(item => deleteMenuItemByID(item, data.targetID));\n                }\n    \n                return menu;\n            });\n\n            let destinationIndex = destinationParent.items.findIndex(item => item.id === destination.item.id);\n\n            if (destinationIndex === -1) {\n                return;\n            }\n\n            if (data.position === 'after') {\n                if (destinationIndex < destinationParent.items.length - 1) {\n                    destinationParent.items.splice(destinationIndex + 1, 0, target);\n                } else {\n                    destinationParent.items.push(target);\n                }\n            } else if (data.position === 'before') {\n                if (destinationIndex > 0) {\n                    destinationParent.items.splice(destinationIndex, 0, target);\n                } else {\n                    destinationParent.items.unshift(target);\n                }\n            }\n        }\n    },\n    reorderMenuItems (state, data) {\n        let itemToModify = findMenuItemByID(state.currentSite.menuStructure[data.menuID].items, data.itemID);\n        Vue.set(itemToModify, 'items', data.items.slice());\n    },\n    refreshAfterPostUpdate (state, data) {\n        state.currentSite.posts = data.posts;\n        state.currentSite.tags = data.tags;\n        state.currentSite.postsTags = data.postsTags;\n        state.currentSite.postsAuthors = data.postsAuthors;\n        state.currentSite.authors = data.authors;\n    },\n    refreshAfterPageUpdate (state, data) {\n        state.currentSite.pages = data.pages;\n        state.currentSite.pagesAuthors = data.pagesAuthors;\n        state.currentSite.authors = data.authors;\n    },\n    setThemeConfig (state, data) {\n        state.currentSite.themeSettings.config = data.newConfig.config.slice();\n        state.currentSite.themeSettings.customConfig = data.newConfig.customConfig.slice();\n        state.currentSite.themeSettings.postConfig = data.newConfig.postConfig.slice();\n        state.currentSite.themeSettings.pageConfig = data.newConfig.pageConfig.slice();\n        state.currentSite.themeSettings.tagConfig = data.newConfig.tagConfig.slice();\n        state.currentSite.themeSettings.authorConfig = data.newConfig.authorConfig.slice();\n        state.currentSite.themeSettings.defaultTemplates = JSON.parse(JSON.stringify(data.newConfig.defaultTemplates));\n    },\n    setEditorOpenState (state, isOpened) {\n        state.editorOpened = isOpened;\n    },\n    setOrdering (state, data) {\n        state.ordering[data.type].orderBy = data.orderBy;\n        state.ordering[data.type].order = data.order;\n    },\n    setSyncDate (state, date) {\n        Vue.set(state.currentSite.config, 'syncDate', date);\n        Vue.set(state.currentSite.config, 'synced', 'synced');\n        Vue.set(state.components.sidebar, 'status', state.currentSite.config.synced);\n    },\n    setWindowState (state, newState) {\n        state.app.windowIsMaximized = newState;\n    },\n    setAppLanguage (state, language) {\n        state.app.config.language = language;\n    },\n    setAppLanguageType (state, type) {\n        state.app.config.languageType = type;\n    },\n    setWysiwygTranslation (state, translations) {\n        state.wysiwygTranslation = translations;\n    },\n    setAppUIZoomLevel (state, zoomLevel) {\n        state.app.config.uiZoomLevel = parseFloat(zoomLevel);\n    },\n    setAppNotificationsStatus (state, enabled) {\n        state.app.config.notificationsStatus = enabled;\n    },\n    setNotificationsReadStatus (state, status) {\n        state.app.notificationsReadStatus = status;\n    },\n    updateCurrentSiteItem(state, { itemType, itemID, field, value, type, subfield }) {\n        let itemArray;\n\n        switch (itemType) {\n            case 'post': itemArray = state.currentSite.posts; break;\n            case 'page': itemArray = state.currentSite.pages; break;\n            case 'tag': itemArray = state.currentSite.tags; break;\n            case 'author': itemArray = state.currentSite.authors; break;\n            default: console.error(`[Vuex Mutation] Unknown itemType: ${itemType}`); return;\n        }\n\n        let  item = itemArray.find(i => i.id === itemID);\n        \n        if (!item) {\n            console.error(`[Vuex Mutation] Element not found: ${itemType} using ID: ${itemID}`);\n            return;\n        }\n\n        if (type === 'field') {\n            item[field] = value;\n        } else if (type === 'json') {\n            let jsonFieldName = field;\n            \n            if ((itemType === 'post' || itemType === 'page') && field === '_core') {\n                jsonFieldName = 'additional_data';\n            }\n\n            if (typeof item[jsonFieldName] === 'undefined') {\n                console.warn(`[Vuex Mutation] Non-existing JSON field: ${jsonFieldName}`);\n                return;\n            }\n\n            let jsonData = {};\n\n            try {\n                jsonData = JSON.parse(item[jsonFieldName] || '{}');\n            } catch (e) {\n                console.error(`[Vuex Mutation] JSON parsing error in ${itemType} #${itemID}, field ${jsonFieldName}:`, e);\n            }\n            \n            jsonData[subfield] = value;\n            item[jsonFieldName] = JSON.stringify(jsonData);\n        }\n    }\n};\n\nfunction findMenuItemByID (items, menuItemID) {\n    if (items) {\n        for (var i = 0; i < items.length; i++) {\n            if (items[i].id == menuItemID) {\n                return items[i];\n            }\n\n            var found = findMenuItemByID(items[i].items, menuItemID);\n\n            if (found) {\n                return found;\n            }\n        }\n    }\n}\n\nfunction findMenuItemByIDwithParent (items, menuItemID, parentItemID = 0) {\n    if (items) {\n        for (let i = 0; i < items.length; i++) {\n            if (items[i].id == menuItemID) {\n                return {\n                    item: items[i],\n                    parentItemID: parentItemID\n                };\n            }\n\n            let found = findMenuItemByIDwithParent(items[i].items, menuItemID, items[i].id);\n\n            if (found) {\n                return found;\n            }\n        }\n    }\n}\n\nfunction insertSubmenuItem (menuItems, menuItem, parentItemID) {\n    return menuItems.map(item => {\n        if (item.id === parentItemID) {\n            item.items.push(menuItem);\n            return item;\n        }\n\n        if (item.items) {\n            item.items = insertSubmenuItem(item.items, menuItem, parentItemID);\n        }\n\n        return item;\n    });\n}\n\nfunction insertMenuItem (menuItems, menuItem, parentItemID) {\n    if (parentItemID === 0) {\n        menuItems.push(menuItem);\n        return menuItems;\n    }\n    \n    return menuItems.map(item => {\n        if (item.id === parentItemID) {\n            item.items.push(menuItem);\n            return item;\n        }\n\n        if (item.items) {\n            item.items = insertMenuItem(item.items, menuItem, parentItemID);\n        }\n\n        return item;\n    });\n}\n\nfunction editMenuItemByID (item, editedMenuItem) {\n    item.items = item.items.map(item => editMenuItemByID(item, editedMenuItem));\n\n    if (item.id === editedMenuItem.id) {\n        item.label = editedMenuItem.label;\n        item.title = editedMenuItem.title;\n        item.type = editedMenuItem.type;\n        item.link = editedMenuItem.link;\n        item.target = editedMenuItem.target;\n        item.rel = editedMenuItem.rel;\n        item.cssClass = editedMenuItem.cssClass;\n        item.isHidden = editedMenuItem.isHidden || false;\n    }\n\n    return item;\n}\n\nfunction deleteMenuItemByID (item, id) {\n    if(item.id === id) {\n        return false;\n    }\n\n    item.items = item.items.filter(item => deleteMenuItemByID(item, id));\n\n    return item.id !== id;\n}\n\nfunction updateMenuItemIDs (item, newID) {\n    item = JSON.stringify(item);\n    let offsetIndex = 0;\n\n    item = item.replace(/\"id\":[0-9]{1,}/g, () => {\n        offsetIndex++;\n        return ('\"id\":' + newID + offsetIndex);\n    });\n\n    return JSON.parse(item);\n}\n\nfunction hideMenuItemByID (item, itemID) {\n    item.items = item.items.map(subitem => hideMenuItemByID(subitem, itemID));\n\n    if (item.id === itemID) {\n        item.isHidden = true;\n    }\n\n    return item;\n}\n\nfunction showMenuItemByID (item, itemID) {\n    item.items = item.items.map(subitem => showMenuItemByID(subitem, itemID));\n\n    if (item.id === itemID) {\n        item.isHidden = false;\n    }\n\n    return item;\n}\n"
  },
  {
    "path": "app/src/store/helpers/page-filter.js",
    "content": "export default (state, page, filterValue) => {\n    filterValue = filterValue.toLowerCase();\n    page = JSON.parse(JSON.stringify(page));\n    page.title = page.title.toLowerCase();\n    page.slug = page.slug.toLowerCase();\n    let deletedPagesIDs = state.currentSite.pages.filter(page => page.status.indexOf('trashed') > -1).map(page => page.id);\n\n    // Check for author\n    if (filterValue.indexOf('author:') === 0) {\n        let authorToFind = filterValue.replace('author:', '');\n\n        let results = state.currentSite.pagesAuthors.filter(pageAuthor => {\n            return  pageAuthor.pageID === page.id && \n                    deletedPagesIDs.indexOf(pageAuthor.pageID) === -1 && \n                    pageAuthor.authorName.toLowerCase() === authorToFind;\n        });\n\n        if(results.length) {\n            return true;\n        }\n\n        return false;\n    }\n\n    let searchPhrase = filterValue.replace('is:published', '')\n                                  .replace('is:trashed', '')\n                                  .replace('is:draft', '')\n                                  .trim();\n    searchPhrase = searchPhrase.toLowerCase();\n\n    searchPhrase = searchPhrase.toLowerCase();\n\n    // Check for published pages\n    if(\n        filterValue.indexOf('is:published') === 0 &&\n        page.status.indexOf('draft') === -1 &&\n        page.status.indexOf('trashed') === -1\n    ) {\n        if(searchPhrase !== '') {\n            return page.title.indexOf(searchPhrase) > -1 || page.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check for trashed pages\n    if(\n        filterValue.indexOf('is:trashed') === 0 &&\n        page.status.indexOf('trashed') > -1\n    ) {\n        if(searchPhrase !== '') {\n            return page.title.indexOf(searchPhrase) > -1 || page.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check for draft pages\n    if(\n        filterValue.indexOf('is:draft') === 0 &&\n        page.status.indexOf('draft') > -1 &&\n        page.status.indexOf('trashed') === -1\n    ) {\n        if(searchPhrase !== '') {\n            return page.title.indexOf(searchPhrase) > -1 || page.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check the easy cases first\n    if(\n        (\n            filterValue.trim() === '' || \n            page.title.indexOf(filterValue) > -1 ||\n            page.slug.indexOf(filterValue) > -1\n        ) &&\n        page.status.indexOf('trashed') === -1\n    ) {\n        return true;\n    }\n\n    // Unfortunately - there is no criteria which this page meets\n    return false;\n};\n"
  },
  {
    "path": "app/src/store/helpers/page-get-author.js",
    "content": "export default (state, pageID) => {\n    let authorName = state.currentSite.pagesAuthors.filter(pageAuthor => {\n        return pageAuthor.pageID === pageID;\n    });\n\n    if (authorName.length) {\n        return authorName[0].authorName;\n    }\n\n    return '';\n};\n"
  },
  {
    "path": "app/src/store/helpers/post-filter.js",
    "content": "export default (state, post, filterValue) => {\n    filterValue = filterValue.toLowerCase();\n    post = JSON.parse(JSON.stringify(post));\n    post.title = post.title.toLowerCase();\n    post.slug = post.slug.toLowerCase();\n    let deletedPostsIDs = state.currentSite.posts.filter(post => post.status.indexOf('trashed') > -1).map(post => post.id);\n\n    // Check for author\n    if(filterValue.indexOf('author:') === 0) {\n        let authorToFind = filterValue.replace('author:', '');\n\n        let results = state.currentSite.postsAuthors.filter(postAuthor => {\n            return  postAuthor.postID === post.id && \n                    deletedPostsIDs.indexOf(postAuthor.postID) === -1 && \n                    postAuthor.authorName.toLowerCase() === authorToFind;\n        });\n\n        if(results.length) {\n            return true;\n        }\n\n        return false;\n    }\n\n    // Check for tag\n    if(filterValue.indexOf('tag:') === 0) {\n        let tagToFind = filterValue.replace('tag:', '');\n        let tagID = state.currentSite.tags.filter(tag => {\n            return tag.name.toLowerCase() === tagToFind;\n        });\n\n        if(tagID.length) {\n            tagID = tagID[0].id;\n        }\n\n        let results = state.currentSite.postsTags.filter(postTags => {\n            return  postTags.postID === post.id && \n                    deletedPostsIDs.indexOf(postTags.postID) === -1 && \n                    postTags.tagID === tagID;\n        });\n\n        if(results.length) {\n            return true;\n        }\n\n        return false;\n    }\n\n    let searchPhrase = filterValue.replace('is:published', '')\n                                  .replace('is:featured', '')\n                                  .replace('is:trashed', '')\n                                  .replace('is:draft', '')\n                                  .replace('is:hidden', '')\n                                  .replace('is:excluded', '')\n                                  .trim();\n    searchPhrase = searchPhrase.toLowerCase();\n\n    // Check for published posts\n    if(\n        filterValue.indexOf('is:published') === 0 &&\n        post.status.indexOf('draft') === -1 &&\n        post.status.indexOf('trashed') === -1\n    ) {\n        if(searchPhrase !== '') {\n            return post.title.indexOf(searchPhrase) > -1 || post.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check for featured posts\n    if(\n        filterValue.indexOf('is:featured') === 0 &&\n        post.status.indexOf('featured') > -1 &&\n        post.status.indexOf('trashed') === -1\n    ) {\n        if(searchPhrase !== '') {\n            return post.title.indexOf(searchPhrase) > -1 || post.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check for trashed posts\n    if(\n        filterValue.indexOf('is:trashed') === 0 &&\n        post.status.indexOf('trashed') > -1\n    ) {\n        if(searchPhrase !== '') {\n            return post.title.indexOf(searchPhrase) > -1 || post.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check for draft posts\n    if(\n        filterValue.indexOf('is:draft') === 0 &&\n        post.status.indexOf('draft') > -1 &&\n        post.status.indexOf('trashed') === -1\n    ) {\n        if(searchPhrase !== '') {\n            return post.title.indexOf(searchPhrase) > -1 || post.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check for hidden posts\n    if(\n        filterValue.indexOf('is:hidden') === 0 &&\n        post.status.indexOf('hidden') > -1 &&\n        post.status.indexOf('trashed') === -1\n    ) {\n        if(searchPhrase !== '') {\n            return post.title.indexOf(searchPhrase) > -1 || post.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check for excluded posts\n    if(\n        filterValue.indexOf('is:excluded') === 0 &&\n        post.status.indexOf('excluded') > -1 &&\n        post.status.indexOf('trashed') === -1\n    ) {\n        if(searchPhrase !== '') {\n            return post.title.indexOf(searchPhrase) > -1 || post.slug.indexOf('searchPhrase') > -1;\n        }\n\n        return true;\n    }\n\n    // Check the easy cases first\n    if(\n        (\n            filterValue.trim() === '' || \n            post.title.indexOf(filterValue) > -1 ||\n            post.slug.indexOf(filterValue) > -1\n        ) &&\n        post.status.indexOf('trashed') === -1\n    ) {\n        return true;\n    }\n\n    // Unfortunately - there is no criteria which this post meets\n    return false;\n};\n"
  },
  {
    "path": "app/src/store/helpers/post-get-author.js",
    "content": "export default (state, postID) => {\n    let authorName = state.currentSite.postsAuthors.filter(postAuthor => {\n        return postAuthor.postID === postID;\n    });\n\n    if(authorName.length) {\n        return authorName[0].authorName;\n    }\n\n    return '';\n};\n"
  },
  {
    "path": "app/src/store/helpers/post-get-tags.js",
    "content": "export default (state, postID) => {\n    let postsTags = state.currentSite.postsTags.filter(postTag => {\n        return postTag.postID === postID;\n    }).map(postTag => {\n        return postTag.tagID;\n    });\n\n    if(postsTags.length) {\n        postsTags = postsTags.map(postTag => {\n            let tagName = state.currentSite.tags.filter(tag => {\n                return postTag === tag.id;\n            });\n\n            if(tagName.length) {\n                tagName = tagName[0].name;\n            } else {\n                tagName = '';\n            }\n\n            return {\n                name: tagName,\n                id: postTag\n            };\n        });\n\n        postsTags = postsTags.filter(postTag => {\n            return postTag.name !== ''\n        });\n\n        return postsTags;\n    }\n\n    return false;\n}\n"
  },
  {
    "path": "app/src/store/index.js",
    "content": "// Modules\nimport Vuex from 'vuex';\nimport Vue from 'vue';\n// Helpers\nimport defaultState from './default.state';\nimport mutations from './helpers/mutations';\n// Getters\nimport siteNames from './getters/site-names';\nimport siteDisplayNames from './getters/site-display-names';\nimport siteTags from './getters/site-tags';\nimport siteAuthors from './getters/site-authors';\nimport sitePages from './getters/site-pages';\nimport sitePosts from './getters/site-posts';\nimport sitePlugins from './getters/site-plugins';\nimport notifications from './getters/notifications';\nimport notificationsCount from './getters/notifications-count';\nimport notificationsStatus from './getters/notifications-status';\nimport appVersion from './getters/app-version';\nimport languages from './getters/languages';\nimport plugins from './getters/plugins';\nimport themes from './getters/themes';\nimport themeSelect from './getters/theme-select';\nimport tagTemplates from './getters/tag-templates';\nimport authorTemplates from './getters/author-templates';\n// Actions\n\n\nVue.use(Vuex);\n\nexport default new Vuex.Store({\n    state: defaultState,\n    getters: {\n        languages,\n        plugins,\n        siteNames,\n        siteDisplayNames,\n        siteTags,\n        siteAuthors,\n        sitePages,\n        sitePosts,\n        sitePlugins,\n        appVersion,\n        themes,\n        themeSelect,\n        tagTemplates,\n        authorTemplates,\n        notifications,\n        notificationsCount,\n        notificationsStatus\n    },\n    mutations\n});\n"
  },
  {
    "path": "build/config.gypi",
    "content": "# Do not edit. File was generated by node-gyp's \"configure\" step\n{\n  \"target_defaults\": {\n    \"cflags\": [],\n    \"default_configuration\": \"Release\",\n    \"defines\": [],\n    \"include_dirs\": [],\n    \"libraries\": []\n  },\n  \"variables\": {\n    \"asan\": 0,\n    \"build_v8_with_gn\": \"false\",\n    \"coverage\": \"false\",\n    \"debug_nghttp2\": \"false\",\n    \"enable_lto\": \"false\",\n    \"enable_pgo_generate\": \"false\",\n    \"enable_pgo_use\": \"false\",\n    \"force_dynamic_crt\": 0,\n    \"host_arch\": \"x64\",\n    \"icu_data_in\": \"../../deps/icu-small/source/data/in/icudt62l.dat\",\n    \"icu_endianness\": \"l\",\n    \"icu_gyp_path\": \"tools/icu/icu-generic.gyp\",\n    \"icu_locales\": \"en,root\",\n    \"icu_path\": \"deps/icu-small\",\n    \"icu_small\": \"true\",\n    \"icu_ver_major\": \"62\",\n    \"llvm_version\": \"0\",\n    \"node_byteorder\": \"little\",\n    \"node_debug_lib\": \"false\",\n    \"node_enable_d8\": \"false\",\n    \"node_enable_v8_vtunejit\": \"false\",\n    \"node_install_npm\": \"true\",\n    \"node_module_version\": 64,\n    \"node_no_browser_globals\": \"false\",\n    \"node_prefix\": \"/usr/local\",\n    \"node_release_urlbase\": \"https://nodejs.org/download/release/\",\n    \"node_shared\": \"false\",\n    \"node_shared_cares\": \"false\",\n    \"node_shared_http_parser\": \"false\",\n    \"node_shared_libuv\": \"false\",\n    \"node_shared_nghttp2\": \"false\",\n    \"node_shared_openssl\": \"false\",\n    \"node_shared_zlib\": \"false\",\n    \"node_tag\": \"\",\n    \"node_target_type\": \"executable\",\n    \"node_use_bundled_v8\": \"true\",\n    \"node_use_dtrace\": \"true\",\n    \"node_use_etw\": \"false\",\n    \"node_use_large_pages\": \"false\",\n    \"node_use_openssl\": \"true\",\n    \"node_use_pch\": \"false\",\n    \"node_use_perfctr\": \"false\",\n    \"node_use_v8_platform\": \"true\",\n    \"node_with_ltcg\": \"false\",\n    \"node_without_node_options\": \"false\",\n    \"openssl_fips\": \"\",\n    \"openssl_no_asm\": 0,\n    \"shlib_suffix\": \"64.dylib\",\n    \"target_arch\": \"x64\",\n    \"v8_enable_gdbjit\": 0,\n    \"v8_enable_i18n_support\": 1,\n    \"v8_enable_inspector\": 1,\n    \"v8_no_strict_aliasing\": 1,\n    \"v8_optimized_debug\": 0,\n    \"v8_promise_internal_field_count\": 1,\n    \"v8_random_seed\": 0,\n    \"v8_trace_maps\": 0,\n    \"v8_typed_array_max_size_in_heap\": 0,\n    \"v8_use_snapshot\": \"true\",\n    \"want_separate_host_toolset\": 0,\n    \"xcode_version\": \"7.0\",\n    \"nodedir\": \"/Users/dziudek/.node-gyp/5.0.5\",\n    \"standalone_static_library\": 1,\n    \"target\": \"5.0.5\"\n  }\n}\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.cs.debugger</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n    <key>com.apple.security.cs.disable-executable-page-protection</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "build/installer.nsh",
    "content": "!macro customInit\n  ; Workaround for installer handing when the app directory is removed manually\n  ${ifNot} ${FileExists} \"$INSTDIR\"\n    DeleteRegKey HKCU \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{${UNINSTALL_APP_KEY}}\"\n  ${EndIf}\n\n  ; Workaround for the old-format uninstall registry key (some people report it causes hangups, too)\n  ReadRegStr $0 HKCU \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${UNINSTALL_APP_KEY}\" \"QuietUninstallString\"\n  StrCmp $0 \"\" proceed 0\n  DeleteRegKey HKCU \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${UNINSTALL_APP_KEY}\"\n  proceed:\n!macroend\n"
  },
  {
    "path": "build/license_en.txt",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "build/scripts/afterPack.js",
    "content": "'use strict';\nconst path = require('path');\nconst fs = require('fs');\n\nexports.default = context => {\n  const APP_NAME = context.packager.appInfo.productFilename;\n  const APP_OUT_DIR = context.appOutDir;\n  const PLATFORM = context.packager.platform.name;\n  const cwd = path.join(`${APP_OUT_DIR}`, 'chrome-sandbox');\n  switch (PLATFORM) {\n    case 'linux':\n      console.log('OUT:', APP_OUT_DIR);\n      fs.chmodSync(cwd, '4755');\n      break;\n    default:\n      break;\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "build/scripts/update-build-number.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst buildDataPath = path.join(__dirname, '..', '..', 'app', 'back-end', 'builddata.json');\nlet buildData = JSON.parse(fs.readFileSync(buildDataPath, 'utf8'));\nbuildData.build += 1;\nfs.writeFileSync(buildDataPath, JSON.stringify(buildData, null, 2), 'utf8');\n"
  },
  {
    "path": "gulpfile.js",
    "content": "\"use strict\";\n\n/*\n * Necessary plugins\n */\nconst gulp = require('gulp');\nconst sass = require('gulp-dart-sass');\nconst exec = require('child_process').exec;\nconst fs = require('fs');\n\n/*\n * Paths configuration\n */\nconst paths = {\n    \"frontend-js\":      'app/js/front-end/',\n    \"backend-js\":       'app/back-end/',\n    \"sass\":             'app/src/scss/editor/',\n    \"css\":              'app/dist/css/',\n    \"base\":             'app/'\n};\n\n/*\n * Parse SASS into CSS\n */\ngulp.task('prepare-editor-css', gulp.series(\n    () => gulp.src(paths.sass + 'editor.scss')\n        .pipe(sass())\n        .pipe(gulp.dest(paths['css'])), \n    () => gulp.src(paths.sass + 'editor-options.scss')\n        .pipe(sass())\n        .pipe(gulp.dest(paths['css']))\n));\n\n/*\n * Build\n */\nfunction updateBuildNumber (cb) {\n    let buildData = JSON.parse(fs.readFileSync('app/back-end/builddata.json'));\n    buildData.build += 1;\n    buildData = JSON.stringify(buildData);\n    fs.writeFileSync('app/back-end/builddata.json', buildData);\n    cb();\n}\n\ngulp.task('build', gulp.series(updateBuildNumber));\n"
  },
  {
    "path": "internal-tools/loc.js",
    "content": "// Components lines counter\nconst glob = require('glob');\nconst fs = require('fs');\n\nglob(__dirname + '/../app/src/?(components|router|store)/**/*.?(vue|js)', {}, async (err, files) => {\n    let basePath = __dirname.replace('/internal-tools', '/');\n    let results = [];\n    let maximumFilenameLength = 0\n    console.log('Total number of analyzed files: ', files.length);\n    console.log(''.padEnd(70, '='));\n\n    for (let i = 0; i < files.length; i++) {\n        let fileName =  files[i].replace(basePath + 'app/src/components/', '')\n                                .replace(basePath + 'app/src/router/', '')\n                                .replace(basePath + 'app/src/store/', '');\n\n        if (fileName.length > maximumFilenameLength) {\n            maximumFilenameLength = fileName.length;\n        }\n\n        try {\n            let lines = await countLines(files[i]);\n            let rating = '';\n\n            if (lines > 250) {\n                rating = '!';\n            }\n\n            if (lines > 500) {\n                rating = '!!';\n            }\n\n            if (lines > 750) {\n                rating = '!!!';\n            }\n\n            if (lines > 1000) {\n                rating = '!!!!';\n            }\n\n            if (lines > 1500) {\n                rating = '!!!!!';\n            }\n\n            results.push({\n                file: fileName,\n                lines: lines,\n                rating: rating\n            });\n        } catch (err) {\n            console.log(err);\n        }\n    }\n\n    results.sort((a, b) => b.lines - a.lines);\n\n    for (let i = 0; i < results.length; i++) {\n        console.log(results[i].file.padEnd(maximumFilenameLength + 5, ' '), (results[i].lines + ' LOC').padEnd(10, ' '), results[i].rating);\n    }\n\n    console.log(''.padEnd(70, '='));\n    console.log('Total lines of code:', results.reduce((prev, next) => prev + next.lines, 0));\n});\n\nfunction countLines (filePath) {\n    return new Promise ((resolve, reject) => {\n        let lines = 0;\n\n        fs.createReadStream(filePath).on('data', (buffer) => {\n            let id = -1;\n            lines--;\n            \n            do {\n                id = buffer.indexOf(10, id + 1);\n                lines++;\n            } while (id !== -1);\n        }).on('end', () => {\n            resolve(lines);\n        }).on('error', reject);\n    });\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"productName\": \"Publii\",\n  \"name\": \"Publii\",\n  \"version\": \"0.47.5\",\n  \"description\": \"Static Site CMS\",\n  \"homepage\": \"https://getpublii.com\",\n  \"author\": {\n    \"name\": \"TidyCustoms\",\n    \"email\": \"bob@tidycustoms.net\"\n  },\n  \"license\": \"GPL-3.0\",\n  \"main\": \"app/main.js\",\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development webpack --mode=development --progress --watch\",\n    \"prod\": \"cross-env NODE_ENV=production webpack --mode=production --progress\",\n    \"build\": \"node ./build/scripts/update-build-number.js && cross-env NODE_ENV=development ./node_modules/.bin/electron app/\",\n    \"build2\": \"cross-env NODE_ENV=development ./node_modules/.bin/electron app/\",\n    \"notarize\": \"xcrun notarytool submit ./dist/$npm_config_file --keychain-profile 'Publii' --wait\",\n    \"start\": \"electron app/main.js --enable-logging\",\n    \"test\": \"mocha app/back-end/**/*.spec.js --reporter dot\",\n    \"prepare-editor\": \"npm run prepare-editor-css && npm run remove-vendor-dist-dir && npm run prepare-vendor-dist-dir && npm run copy-vendor-files\",\n    \"remove-vendor-dist-dir\": \"rm -rf app/dist/vendor\",\n    \"prepare-editor-css\": \"./node_modules/.bin/sass app/src/scss/editor/editor.scss app/dist/css/editor.css && ./node_modules/.bin/sass app/src/scss/editor/editor-options.scss app/dist/css/editor-options.css\",\n    \"prepare-editor-css:win\": \"\\\"node_modules/.bin/sass\\\" app/src/scss/editor/editor.scss app/dist/css/editor.css && \\\"node_modules/.bin/sass\\\" app/src/scss/editor/editor-options.scss app/dist/css/editor-options.css\",\n    \"prepare-vendor-dist-dir\": \"if [ ! -d \\\"app/dist/vendor\\\" ]; then mkdir app/dist/vendor; fi && npm run prepare-editor-css\",\n    \"copy-vendor-files\": \"cp -r app/src/helpers/vendor/tinymce app/dist/vendor/tinymce && cp -r app/src/helpers/vendor/jquery app/dist/vendor/jquery\",\n    \"prepare-editor:win\": \"npm run prepare-editor-css:win && npm run remove-vendor-dist-dir-win && npm run prepare-vendor-dist-dir-win && npm run copy-vendor-files-win\",\n    \"remove-vendor-dist-dir-win\": \"if exist app\\\\dist\\\\vendor rmdir /s /q app\\\\dist\\\\vendor\",\n    \"prepare-vendor-dist-dir-win\": \"md app\\\\dist\\\\vendor 2>nul || echo Directory already exists && npm run prepare-editor-css\",\n    \"copy-vendor-files-win\": \"xcopy /s /i /q app\\\\src\\\\helpers\\\\vendor\\\\tinymce app\\\\dist\\\\vendor\\\\tinymce && xcopy /s /i /q app\\\\src\\\\helpers\\\\vendor\\\\jquery app\\\\dist\\\\vendor\\\\jquery\",\n    \"packager:win\": \"electron-builder build --win --x64\",\n    \"packager:mac-intel\": \"electron-builder build --mac\",\n    \"packager:mac-m\": \"electron-builder build --mac --arm64\",\n    \"packager:linux\": \"electron-builder build --linux\"\n  },\n  \"repository\": {\n    \"url\": \"https://github.com/GetPublii/Publii/\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/GetPublii/Publii/issues\"\n  },\n  \"keywords\": [],\n  \"devDependencies\": {\n    \"cross-env\": \"7.0.3\",\n    \"css-loader\": \"6.8.1\",\n    \"electron\": \"37.10.3\",\n    \"electron-builder\": \"26.0.12\",\n    \"file-loader\": \"6.2.0\",\n    \"glob\": \"10.3.10\",\n    \"mocha\": \"10.2.0\",\n    \"node-abi\": \"4.12.0\",\n    \"node-loader\": \"2.0.0\",\n    \"sass\": \"1.44\",\n    \"sass-loader\": \"13.3.2\",\n    \"vue-loader\": \"15.11.1\",\n    \"vue-style-loader\": \"4.1.3\",\n    \"vue-template-compiler\": \"2.6.14\",\n    \"webpack\": \"5.89.0\",\n    \"webpack-cli\": \"5.1.4\"\n  },\n  \"build\": {\n    \"appId\": \"com.tidycustoms.publii\",\n    \"electronVersion\": \"37.10.3\",\n    \"productName\": \"Publii\",\n    \"copyright\": \"Copyright (C) 2016-2026 TidyCustoms. All rights reserved.\",\n    \"asar\": true,\n    \"asarUnpack\": [\n      \"*.+(dylib|node|dll|aff|dic)\",\n      \"default-files/default-themes/**/*\",\n      \"default-files/default-languages/**/*\",\n      \"default-files/gdpr-assets/**/*\",\n      \"default-files/vendor/**/*\",\n      \"**/node_modules/sharp/**/*\",\n      \"**/node_modules/@img/**/*\",\n      \"node_modules/keytar/**/*\",\n      \"node_modules/better-sqlite3/**/*\"\n    ],\n    \"afterPack\": \"./build/scripts/afterPack.js\",\n    \"files\": [\n      \"**/*\",\n      \"dist/**/*\",\n      \"!app/node_modules/devtron/**/*\",\n      \"!app/node_modules/accessibility-developer-tools/**/*\",\n      \"!app/node_modules/babel*\",\n      \"!app/node_modules/postcss*\",\n      \"!app/node_modules/asn1/**/*\",\n      \"!app/node_modules/electron-*\",\n      \"app/node_modules/publii-block-editor\"\n    ],\n    \"linux\": {\n      \"icon\": \"./build/installation/icon.icns\",\n      \"target\": [\n        \"rpm\",\n        \"AppImage\",\n        \"deb\"\n      ],\n      \"category\": \"Development\",\n      \"description\": \"Static Site CMS\",\n      \"executableName\": \"Publii\"\n    },\n    \"win\": {\n      \"icon\": \"build/installation/icon.ico\",\n      \"target\": \"nsis\"\n    },\n    \"mac\": {\n      \"category\": \"public.app-category.utilities\",\n      \"icon\": \"build/installation/icon.icns\",\n      \"hardenedRuntime\": true,\n      \"gatekeeperAssess\": false,\n      \"target\": [\n        \"dmg\",\n        \"zip\"\n      ],\n      \"entitlements\": \"build/entitlements.mac.plist\",\n      \"entitlementsInherit\": \"build/entitlements.mac.plist\",\n      \"minimumSystemVersion\": \"10.10\"\n    },\n    \"nsis\": {\n      \"oneClick\": true,\n      \"createDesktopShortcut\": \"always\"\n    },\n    \"dmg\": {\n      \"sign\": false,\n      \"icon\": \"build/installation/volume.icns\",\n      \"title\": \"Install Publii\"\n    }\n  },\n  \"deb\": {\n    \"depends\": [\n      \"libsecret-1-dev\",\n      \"gnome-keyring\"\n    ]\n  },\n  \"rpm\": {\n    \"depends\": [\n      \"libsecret-devel\",\n      \"gnome-keyring\"\n    ]\n  },\n  \"appImage\": {\n    \"license\": \"license.txt\"\n  },\n  \"dependencies\": {\n    \"node-gyp\": \"10.0.1\"\n  }\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const path = require('path');\nconst webpack = require('webpack');\nconst { VueLoaderPlugin } = require('vue-loader');\n\nmodule.exports = {\n  entry: './app/src/main.js',\n  target: 'electron-renderer',\n  output: {\n    path: path.resolve(__dirname, './app/dist/'),\n    publicPath: 'auto',\n    filename: 'build.js',\n    chunkFilename: '[name].bundle.js',\n  },\n  plugins: [\n    new VueLoaderPlugin()\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.css$/,\n        use: [\n          'vue-style-loader',\n          'css-loader'\n        ],\n      },\n      {\n        test: /\\.scss$/,\n        use: [\n          'vue-style-loader',\n          'css-loader',\n          'sass-loader'\n        ],\n      },\n      {\n        test: /\\.sass$/,\n        use: [\n          'vue-style-loader',\n          'css-loader',\n          'sass-loader?indentedSyntax'\n        ],\n      },\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: {\n          loaders: {\n            'scss': [\n              'vue-style-loader',\n              'css-loader',\n              'sass-loader'\n            ],\n            'sass': [\n              'vue-style-loader',\n              'css-loader',\n              'sass-loader?indentedSyntax'\n            ]\n          }\n        }\n      },\n      {\n        test: /\\.(png|jpg|gif|svg)$/,\n        type: 'asset/resource',\n        generator: {\n          filename: 'assets/images/[name].[hash][ext][query]'\n        }\n      },\n      {\n        test: /\\.node$/,\n        loader: 'node-loader',\n      }\n    ]\n  },\n  resolve: {\n    alias: {\n      'vue$': 'vue/dist/vue.esm.js'\n    },\n    extensions: ['*', '.js', '.vue', '.json']\n  },\n  devServer: {\n    historyApiFallback: true,\n    noInfo: true,\n    overlay: true\n  },\n  performance: {\n    hints: false\n  },\n  devtool: process.env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map',\n  externals: {\n    'sharp': 'commonjs sharp'\n  }\n};\n\nif (process.env.NODE_ENV === 'production') {\n  module.exports.plugins = (module.exports.plugins || []).concat([\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"production\"'\n      }\n    })\n  ]);\n}\n"
  }
]