Browse Source

add anime+boucleproj

kevin tessier 5 years ago
parent
commit
3d878b3cb6
100 changed files with 8667 additions and 5456 deletions
  1. 256 0
      CHANGELOG.md
  2. 0 0
      README.md
  3. BIN
      bin/composer.phar
  4. 15 16
      bin/gpm
  5. 31 10
      bin/grav
  6. 42 28
      bin/plugin
  7. 53 26
      composer.json
  8. 561 133
      composer.lock
  9. 17 15
      index.php
  10. 16 0
      system/assets/debugger.css
  11. BIN
      system/assets/grav.png
  12. 125 0
      system/blueprints/config/backups.yaml
  13. 76 0
      system/blueprints/config/scheduler.yaml
  14. 8 0
      system/blueprints/config/security.yaml
  15. 0 7
      system/blueprints/config/site.yaml
  16. 1402 1236
      system/blueprints/config/system.yaml
  17. 121 119
      system/blueprints/user/account.yaml
  18. 29 0
      system/blueprints/user/accounts.yaml
  19. 15 0
      system/config/backups.yaml
  20. 7 0
      system/config/security.yaml
  21. 5 5
      system/config/site.yaml
  22. 1 1
      system/config/streams.yaml
  23. 19 2
      system/config/system.yaml
  24. 4 4
      system/defines.php
  25. 17 0
      system/install.php
  26. 62 74
      system/languages/ar.yaml
  27. 62 0
      system/languages/bg.yaml
  28. 61 74
      system/languages/ca.yaml
  29. 143 80
      system/languages/cs.yaml
  30. 61 74
      system/languages/da.yaml
  31. 143 88
      system/languages/de.yaml
  32. 62 21
      system/languages/el.yaml
  33. 120 99
      system/languages/en.yaml
  34. 94 89
      system/languages/es.yaml
  35. 62 0
      system/languages/et.yaml
  36. 62 0
      system/languages/eu.yaml
  37. 62 0
      system/languages/fa.yaml
  38. 133 59
      system/languages/fi.yaml
  39. 120 137
      system/languages/fr.yaml
  40. 63 0
      system/languages/he.yaml
  41. 75 74
      system/languages/hr.yaml
  42. 96 137
      system/languages/hu.yaml
  43. 95 0
      system/languages/id.yaml
  44. 69 0
      system/languages/is.yaml
  45. 100 61
      system/languages/it.yaml
  46. 27 18
      system/languages/ja.yaml
  47. 63 0
      system/languages/ko.yaml
  48. 77 68
      system/languages/lt.yaml
  49. 4 2
      system/languages/nb.yaml
  50. 143 63
      system/languages/nl.yaml
  51. 76 92
      system/languages/no.yaml
  52. 61 74
      system/languages/pl.yaml
  53. 36 78
      system/languages/pt.yaml
  54. 95 100
      system/languages/ro.yaml
  55. 100 80
      system/languages/ru.yaml
  56. 59 41
      system/languages/sk.yaml
  57. 62 0
      system/languages/sl.yaml
  58. 60 61
      system/languages/sv.yaml
  59. 55 74
      system/languages/th.yaml
  60. 62 58
      system/languages/tr.yaml
  61. 62 74
      system/languages/uk.yaml
  62. 62 74
      system/languages/vi.yaml
  63. 42 0
      system/languages/zh-tw.yaml
  64. 63 0
      system/languages/zh.yaml
  65. 6 5
      system/router.php
  66. 188 1257
      system/src/Grav/Common/Assets.php
  67. 198 0
      system/src/Grav/Common/Assets/BaseAsset.php
  68. 40 0
      system/src/Grav/Common/Assets/Css.php
  69. 32 0
      system/src/Grav/Common/Assets/InlineCss.php
  70. 32 0
      system/src/Grav/Common/Assets/InlineJs.php
  71. 36 0
      system/src/Grav/Common/Assets/Js.php
  72. 273 0
      system/src/Grav/Common/Assets/Pipeline.php
  73. 182 0
      system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
  74. 118 0
      system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
  75. 343 0
      system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
  76. 261 0
      system/src/Grav/Common/Backup/Backups.php
  77. 0 144
      system/src/Grav/Common/Backup/ZipBackup.php
  78. 15 3
      system/src/Grav/Common/Browser.php
  79. 204 60
      system/src/Grav/Common/Cache.php
  80. 7 6
      system/src/Grav/Common/Composer.php
  81. 9 10
      system/src/Grav/Common/Config/CompiledBase.php
  82. 13 10
      system/src/Grav/Common/Config/CompiledBlueprints.php
  83. 11 13
      system/src/Grav/Common/Config/CompiledConfig.php
  84. 8 10
      system/src/Grav/Common/Config/CompiledLanguages.php
  85. 19 9
      system/src/Grav/Common/Config/Config.php
  86. 3 2
      system/src/Grav/Common/Config/ConfigFileFinder.php
  87. 30 2
      system/src/Grav/Common/Config/Languages.php
  88. 57 19
      system/src/Grav/Common/Config/Setup.php
  89. 202 26
      system/src/Grav/Common/Data/Blueprint.php
  90. 172 24
      system/src/Grav/Common/Data/BlueprintSchema.php
  91. 8 4
      system/src/Grav/Common/Data/Blueprints.php
  92. 33 21
      system/src/Grav/Common/Data/Data.php
  93. 3 2
      system/src/Grav/Common/Data/DataInterface.php
  94. 132 104
      system/src/Grav/Common/Data/Validation.php
  95. 4 3
      system/src/Grav/Common/Data/ValidationException.php
  96. 229 66
      system/src/Grav/Common/Debugger.php
  97. 3 2
      system/src/Grav/Common/Errors/BareHandler.php
  98. 8 16
      system/src/Grav/Common/Errors/Errors.php
  99. 11 10
      system/src/Grav/Common/Errors/SimplePageHandler.php
  100. 3 2
      system/src/Grav/Common/Errors/SystemFacade.php

+ 256 - 0
CHANGELOG.md

@@ -1,3 +1,259 @@
+# v1.6.8
+## 04/23/2019
+
+1. [](#new)
+    * Added `FlexCollection::filterBy()` method
+1. [](#bugfix)
+    * Revert `Use Null Coalesce Operator` [#2466](https://github.com/getgrav/grav/pull/2466)
+    * Fixed `FormTrait::render()` not providing config variable
+    * Updated `bin/grav clean` to clear `cache/compiled` and `user/config/security.yaml`
+
+# v1.6.7
+## 04/22/2019
+
+1. [](#new)
+    * Added a new `bin/grav yamllinter` CLI command to find YAML Linting issues [#2468](https://github.com/getgrav/grav/issues/2468#issuecomment-485151681)
+1. [](#improved)
+    * Improve `FormTrait` backwards compatibility with existing forms
+    * Added a new `Utils::getSubnet()` function for IPv4/IPv6 parsing [#2465](https://github.com/getgrav/grav/pull/2465)
+1. [](#bugfix)
+    * Remove disabled fields from the form schema
+    * Fix issue when excluding `inlineJs` and `inlineCss` from Assets pipeline [#2468](https://github.com/getgrav/grav/issues/2468)
+    * Fix for manually set position on external URLs [#2470](https://github.com/getgrav/grav/issues/2470)
+
+# v1.6.6
+## 04/17/2019
+
+1. [](#new)
+    * `FormInterface` now implements `RenderInterface` 
+    * Added new `FormInterface::getTask()` method which reads the task from `form.task` in the blueprint 
+1. [](#improved)
+    * Updated vendor libraries to latest
+1. [](#bugfix)
+    * Rollback `redirect_default_route` logic as it has issues with multi-lang [#2459](https://github.com/getgrav/grav/issues/2459)
+    * Fix potential issue with `|contains` Twig filter on PHP 7.3 
+    * Fixed bug in text field filtering: return empty string if value isn't a string or number [#2460](https://github.com/getgrav/grav/issues/2460)
+    * Force Asset `priority` to be an integer and not throw error if invalid string passed [#2461](https://github.com/getgrav/grav/issues/2461)
+    * Fixed bug in text field filtering: return empty string if value isn't a string or number
+    * Fixed `FlexForm` missing getter methods for defining form variables
+    
+# v1.6.5
+## 04/15/2019
+
+1. [](#bugfix)
+    * Backwards compatiblity with old `Uri::__toString()` output
+
+# v1.6.4
+## 04/15/2019
+
+1. [](#bugfix)
+    * Improved `redirect_default_route` logic as well as `Uri::toArray()` to take into account `root_path` and `extension`
+    * Rework logic to pull out excluded files from pipeline more reliably [#2445](https://github.com/getgrav/grav/issues/2445)
+    * Better logic in `Utils::normalizePath` to handle externals properly [#2216](https://github.com/getgrav/grav/issues/2216)
+    * Fixed to force all `Page::taxonomy` to be treated as strings [#2446](https://github.com/getgrav/grav/issues/2446)
+    * Fixed issue with `Grav['user']` not being available [form#332](https://github.com/getgrav/grav-plugin-form/issues/332)
+    * Updated rounding logic for `Utils::parseSize()` [#2394](https://github.com/getgrav/grav/issues/2394)
+    * Fixed Flex simple storage not being properly initialized if used with caching
+
+# v1.6.3
+## 04/12/2019
+
+1. [](#new)
+    * Added `Blueprint::addDynamicHandler()` method to allow custom dynamic handlers, for example `custom-options@: getCustomOptions`
+1. [](#bugfix)
+    * Missed a `CacheCommand` reference in `bin/grav` [#2442](https://github.com/getgrav/grav/issues/2442)
+    * Fixed issue with `Utils::normalizePath` messing with external URLs [#2216](https://github.com/getgrav/grav/issues/2216)
+    * Fix for `vUndefined` versions when upgrading
+
+# v1.6.2
+## 04/11/2019
+
+1. [](#bugfix)
+    * Revert renaming of `ClearCacheCommand` to ensure CLI GPM upgrades go smoothly
+
+# v1.6.1
+## 04/11/2019
+
+1. [](#improved)
+    * Improved CSS for the bottom filter bar of DebugBar
+1. [](#bugfix)
+    * Fixed issue with `@import` not being added to top of pipelined css [#2440](https://github.com/getgrav/grav/issues/2440)
+
+# v1.6.0
+## 04/11/2019
+
+1. [](#new)
+    * Set minimum requirements to [PHP 7.1.3](https://getgrav.org/blog/raising-php-requirements-2018)
+    * New `Scheduler` functionality for periodic jobs
+    * New `Backup` functionality with multiple backup profiles and scheduler integration
+    * Refactored `Assets Manager` to be more powerful and flexible
+    * Updated Doctrine Collections to 1.6
+    * Updated Doctrine Cache to 1.8
+    * Updated Symfony Components to 4.2
+    * Added new Cache purge functionality old cache manually via CLI/Admin as well as scheduler integration
+    * Added new `{% throw 404 'Not Found' %}` twig tag (with custom code/message)
+    * Added `Grav\Framework\File` classes for handling YAML, Markdown, JSON, INI and PHP serialized files
+    * Added `Grav\Framework\Collection\AbstractIndexCollection` class
+    * Added `Grav\Framework\Object\ObjectIndex` class
+    * Added `Grav\Framework\Flex` classes
+    * Added support for hiding form fields in blueprints by using dynamic property like `security@: admin.foobar`, `scope@: object` or `scope-ignore@: object` to any field
+    * New experimental **FlexObjects** powered `Users` for increased performance and capability (**disabled** by default)    
+    * Added PSR-7 and PSR-15 classes
+    * Added `Grav\Framework\DI\Container` class
+    * Added `Grav\Framework\RequestHandler\RequestHandler` class
+    * Added `Page::httpResponseCode()` and `Page::httpHeaders()` methods
+    * Added `Grav\Framework\Form\Interfaces\FormInterface`
+    * Added `Grav\Framework\Form\Interfaces\FormFactoryInterface`
+    * Added `Grav\Framework\Form\FormTrait`
+    * Added `Page::forms()` method to get normalized list of all form headers defined in the page
+    * Added `onPageAction`, `onPageTask`, `onPageAction.{$action}` and `onPageTask.{$task}` events
+    * Added `Blueprint::processForm()` method to filter form inputs
+    * Move `processMarkdown()` method from `TwigExtension` to more general `Utils` class
+    * Added support to include extra files into `Media` (such as uploaded files)
+    * Added form preview support for `FlexObject`, including a way to render newly uploaded files before saving them
+    * Added `FlexObject::getChanges()` to determine what fields change during an update
+    * Added `arrayDiffMultidimensional`, `arrayIsAssociative`, `arrayCombine` Util functions
+    * New `$grav['users']` service to allow custom user classes implementing `UserInterface`
+    * Added `LogViewer` helper class and CLI command: `bin/grav logviewer`
+    * Added `select()` and `unselect()` methods to `CollectionInterface` and its base classes
+    * Added `orderBy()` and `limit()` methods to `ObjectCollectionInterface` and its base classes
+    * Added `user-data://` which is a writable stream (`user://data` is not and should be avoided)
+    * Added support for `/action:{$action}` (like task but used without nonce when only receiving data)
+    * Added `onAction.{$action}` event
+    * Added `Grav\Framework\Form\FormFlash` class to contain AJAX uploaded files in more reliable way
+    * Added `Grav\Framework\Form\FormFlashFile` class which implements `UploadedFileInterface` from PSR-7
+    * Added `Grav\Framework\Filesystem\Filesystem` class with methods to manipulate stream URLs
+    * Added new `$grav['filesystem']` service using an instance of the new `Filesystem` object
+    * Added `{% render object layout: 'default' with { variable: true } %}` for Flex objects and collections
+    * Added `$grav->setup()` to simplify CLI and custom access points
+    * Added `CsvFormatter` and `CsvFile` classes
+    * Added new system config option to `pages.hide_empty_folders` if a folder has no valid `.md` file available. Default behavior is `false` for compatibility.
+    * Added new system config option for `languages.pages_fallback_only` forcing only 'fallback' to find page content through supported languages, default behavior is to display any language found if active language is missing
+    * Added `Utils::arrayFlattenDotNotation()` and `Utils::arrayUnflattenDotNotation()` helper methods
+1. [](#improved)
+    * Add the page to onMarkdownInitialized event [#2412](https://github.com/getgrav/grav/issues/2412)
+    * Doctrine filecache is now namespaced with prefix to support purging
+    * Register all page types into `blueprint://pages` stream
+    * Removed `apc` and `xcache` support, made `apc` alias of `apcu`
+    * Support admin and regular translations via the `|t` twig filter and `t()` twig function
+    * Improved Grav Core installer/updater to run installer script
+    * Updated vendor libraries including Symfony `4.2.3`
+    * Renamed old `User` class to `Grav\Common\User\DataUser\User` with multiple improvements and small fixes
+    * `User` class now acts as a compatibility layer to older versions of Grav
+    * Deprecated `new User()`, `User::load()`, `User::find()` and `User::delete()` in favor of `$grav['users']` service
+    * `Media` constructor has now support to not to initialize the media objects
+    * Cleanly handle session corruption due to changing Flex object types
+    * Added `FlexObjectInterface::getDefaultValue()` and `FormInterface::getDefaultValue()`
+    * Added new `onPageContent()` event for every call to `Page::content()`
+    * Added phpstan: PHP Static Analysis Tool [#2393](https://github.com/getgrav/grav/pull/2393)
+    * Added `composer test-plugins` to test plugin issues with the current version of Grav
+    * Added `Flex::getObjects()` and `Flex::getMixedCollection()` methods for co-mingled collections
+    * Added support to use single Flex key parameter in `Flex::getObject()` method
+    * Added `FlexObjectInterface::search()` and `FlexCollectionInterface::search()` methods
+    * Override `system.media.upload_limit` with PHP's `post_max_size` or `upload_max_filesize`
+    * Class `Grav\Common\Page\Medium\AbstractMedia` now use array traits instead of extending `Grav\Common\Getters`
+    * Implemented `Grav\Framework\Psr7` classes as `Nyholm/psr7` decorators
+    * Added a new `cache-clear` scheduled job to go along with `cache-purge`
+    * Renamed `Grav\Framework\File\Formatter\FormatterInterface` to `Grav\Framework\File\Interfaces\FileFormatterInterface`
+    * Improved `File::save()` to use a temporary file if file isn't locked
+    * Improved `|t` filter to better support admin `|tu` style filter if in admin
+    * Update all classes to rely on `PageInterface` instead of `Page` class
+    * Better error checking in `bin/plugin` for existence and enabled
+    * Removed `media.upload_limit` references
+    * Twig `nicenumber`: do not use 0 + string casting hack
+    * Converted Twig tags to use namespaced Twig classes
+    * Site shows error on page rather than hard-crash when page has invalid frontmatter [#2343](https://github.com/getgrav/grav/issues/2343)
+    * Added `languages.default_lang` option to override the default lang (usually first supported language)
+    * Added `Content-Type: application/json` body support for PSR-7 `ServerRequest`
+    * Remove PHP time limit in `ZipArchive`
+    * DebugBar: Resolve twig templates in deprecated backtraces in order to help locating Twig issues
+    * Added `$grav['cache']->getSimpleCache()` method for getting PSR-16 compatible cache
+    * MediaTrait: Use PSR-16 cache
+    * Improved `Utils::normalizePath()` to support non-protocol URLs
+    * Added ability to reset `Page::metadata` to allow rebuilding from automatically generated values
+    * Added back missing `page.types` field in system content configuration [admin#1612](https://github.com/getgrav/grav-plugin-admin/issues/1612)
+    * Console commands: add method for invalidating cache
+    * Updated languages    
+    * Improved `$page->forms()` call, added `$page->addForms()`
+    * Updated languages from crowdin
+    * Fixed `ImageMedium` constructor warning when file does not exist
+    * Improved `Grav\Common\User` class; added `$user->update()` method
+    * Added trim support for text input fields `validate: trim: true`
+    * Improved `Grav\Framework\File\Formatter` classes to have abstract parent class and some useful methods
+    * Support negotiated content types set via the Request `Accept:` header
+    * Support negotiated language types set via the Request `Accept-Language:` header
+    * Cleaned up and sorted the Service `idMap`
+    * Updated `Grav` container object to implement PSR-11 `ContainerInterface`
+    * Updated Grav `Processor` classes to implement PSR-15 `MiddlewareInterface`
+    * Make `Data` class to extend `JsonSerializable`
+    * Modified debugger icon to use retina space-dude version
+    * Added missing `Video::preload()` method
+    * Set session name based on `security.salt` rather than `GRAV_ROOT` [#2242](https://github.com/getgrav/grav/issues/2242)
+    * Added option to configure list of `xss_invalid_protocols` in `Security` config [#2250](https://github.com/getgrav/grav/issues/2250)
+    * Smarter `security.salt` checking now we use `security.yaml` for other options
+    * Added apcu autoloader optimization
+    * Additional helper methods in `Language`, `Languages`, and `LanguageCodes` classes
+    * Call `onFatalException` event also on internal PHP errors
+    * Built-in PHP Webserver: log requests before handling them 
+    * Added support for syslog and syslog facility logging (default: 'file')
+    * Improved usability of `System` configuration blueprint with side-tabs
+ 1. [](#bugfix)
+    * Fixed issue with `Truncator::truncateWords` and `Truncator::truncateLetters` when string not wrapped in tags [#2432](https://github.com/getgrav/grav/issues/2432)
+    * Fixed `Undefined method closure::fields()` when getting avatar for user, thanks @Romarain [#2422](https://github.com/getgrav/grav/issues/2422)
+    * Fixed cached images not being updated when source image is modified
+    * Fixed deleting last list item in the form
+    * Fixed issue with `Utils::url()` method would append extra `base_url` if URL already included it
+    * Fixed `mkdir(...)` race condition
+    * Fixed `Obtaining write lock failed on file...`
+    * Fixed potential undefined property in `onPageNotFound` event handling
+    * Fixed some potential issues/bugs found by phpstan
+    * Fixed regression in GPM packages casted to Array (ref, getgrav/grav-plugin-admin@e3fc4ce)
+    * Fixed session_start(): Setting option 'session.name' failed [#2408](https://github.com/getgrav/grav/issues/2408)
+    * Fixed validation for select field type with selectize
+    * Fixed validation for boolean toggles
+    * Fixed non-namespaced exceptions in scheduler
+    * Fixed trailing slash redirect in multlang environment [#2350](https://github.com/getgrav/grav/issues/2350)
+    * Fixed some issues related to Medium objects losing query string attributes
+    * Broke out Medium timestamp so it's not cleared on `reset()`s
+    * Fixed issue with `redirect_trailing_slash` losing query string [#2269](https://github.com/getgrav/grav/issues/2269)
+    * Fixed failed login if user attempts to log in with upper case non-english letters
+    * Removed extra authenticated/authorized fields when saving existing user from a form
+    * Fixed `Grav\Framework\Route::__toString()` returning relative URL, not relative route
+    * Fixed handling of `append_url_extension` inside of `Page::templateFormat()` [#2264](https://github.com/getgrav/grav/issues/2264) 
+    * Fixed a broken language string [#2261](https://github.com/getgrav/grav/issues/2261)
+    * Fixed clearing cache having no effect on Doctrine cache
+    * Fixed `Medium::relativePath()` for streams
+    * Fixed `Object` serialization breaking if overriding `jsonSerialize()` method
+    * Fixed `YamlFormatter::decode()` when calling `init_set()` with integer
+    * Fixed session throwing error in CLI if initialized
+    * Fixed `Uri::hasStandardPort()` to support reverse proxy configurations [#1786](https://github.com/getgrav/grav/issues/1786)
+    * Use `append_url_extension` from page header to set template format if set [#2604](https://github.com/getgrav/grav/pull/2064)
+    * Fixed some bugs in Grav environment selection logic
+    * Use login provider User avatar if set
+    * Fixed `Folder::doDelete($folder, false)` removing symlink when it should not
+    * Fixed asset manager to not add empty assets when they don't exist in the filesystem
+    * Update `script` and `style` Twig tags to use the new `Assets` classes
+    * Fixed asset pipeline to rewrite remote URLs as well as local [#2216](https://github.com/getgrav/grav/issues/2216)
+
+# v1.5.10
+## 03/21/2019
+
+1. [](#new)
+    * Added new `deferred` Twig extension
+
+# v1.5.9
+## 03/20/2019
+
+1. [](#new)
+    * Added new `onPageContent()` event for every call to `Page::content()`
+1. [](#improved)
+    * Fixed phpdoc generation
+    * Updated vendor libraries
+    * Force Toolbox v1.4.2
+1. [](#bugfix)
+    * EXIF fix for streams
+    * Fix for User avatar not working due to uppercase or spaces in email [#2403](https://github.com/getgrav/grav/pull/2403)
+
 # v1.5.8
 ## 02/07/2019
 

File diff suppressed because it is too large
+ 0 - 0
README.md


BIN
bin/composer.phar


+ 15 - 16
bin/gpm

@@ -1,26 +1,24 @@
 #!/usr/bin/env php
 <?php
-define('GRAV_CLI', true);
-
-if (!file_exists(__DIR__ . '/../vendor')){
-    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
-}
 
 use Grav\Common\Composer;
-use Grav\Common\Config\Setup;
+use Symfony\Component\Console\Application;
+use Grav\Common\Grav;
+
+\define('GRAV_CLI', true);
+\define('GRAV_REQUEST_TIME', microtime(true));
 
-if (!file_exists(__DIR__ . '/../vendor')){
+if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
     // Before we can even start, we need to run composer first
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+
     $composer = Composer::getComposerExecutor();
     echo "Preparing to install vendor dependencies...\n\n";
     echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
     echo "\n\n";
 }
 
-use Symfony\Component\Console\Application;
-use Grav\Common\Grav;
-
-$autoload = require_once(__DIR__ . '/../vendor/autoload.php');
+$autoload = require __DIR__ . '/../vendor/autoload.php';
 
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
@@ -30,7 +28,7 @@ if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
 }
 
-if (!file_exists(ROOT_DIR . 'index.php')) {
+if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
 
@@ -48,15 +46,16 @@ $climate->arguments->add([
     ]
 ]);
 $climate->arguments->parse();
-$environment = $climate->arguments->get('environment');
 
 // Set up environment based on params.
-Setup::$environment = $environment;
+$environment = $climate->arguments->get('environment');
 
 $grav = Grav::instance(array('loader' => $autoload));
-$grav['uri']->init();
+$grav->setup($environment);
+
 $grav['config']->init();
-$grav['streams'];
+$grav['uri']->init();
+$grav['users'];
 
 $app = new Application('Grav Package Manager', GRAV_VERSION);
 $app->addCommands(array(

+ 31 - 10
bin/grav

@@ -1,34 +1,52 @@
 #!/usr/bin/env php
 <?php
-define('GRAV_CLI', true);
-
-if (!file_exists(__DIR__ . '/../vendor')){
-    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
-}
 
 use Grav\Common\Composer;
+use Grav\Common\Grav;
+use League\CLImate\CLImate;
+use Symfony\Component\Console\Application;
+
+\define('GRAV_CLI', true);
+\define('GRAV_REQUEST_TIME', microtime(true));
 
-if (!file_exists(__DIR__ . '/../vendor')){
+if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
     // Before we can even start, we need to run composer first
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+
     $composer = Composer::getComposerExecutor();
     echo "Preparing to install vendor dependencies...\n\n";
     echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
     echo "\n\n";
 }
 
-use Symfony\Component\Console\Application;
-
-require_once __DIR__ . '/../vendor/autoload.php';
+$autoload = require __DIR__ . '/../vendor/autoload.php';
 
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
 }
 
+$climate = new League\CLImate\CLImate;
+$climate->arguments->add([
+    'environment' => [
+        'prefix'        => 'e',
+        'longPrefix'    => 'env',
+        'description'   => 'Configuration Environment',
+        'defaultValue'  => 'localhost'
+    ]
+]);
+$climate->arguments->parse();
+
+// Set up environment based on params.
+$environment = $climate->arguments->get('environment');
+
+$grav = Grav::instance(array('loader' => $autoload));
+$grav->setup($environment);
+
 if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
 }
 
-if (!file_exists(ROOT_DIR . 'index.php')) {
+if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
 
@@ -41,6 +59,9 @@ $app->addCommands(array(
     new \Grav\Console\Cli\ClearCacheCommand(),
     new \Grav\Console\Cli\BackupCommand(),
     new \Grav\Console\Cli\NewProjectCommand(),
+    new \Grav\Console\Cli\SchedulerCommand(),
     new \Grav\Console\Cli\SecurityCommand(),
+    new \Grav\Console\Cli\LogViewerCommand(),
+    new \Grav\Console\Cli\YamlLinterCommand(),
 ));
 $app->run();

+ 42 - 28
bin/plugin

@@ -1,30 +1,28 @@
 #!/usr/bin/env php
 <?php
-define('GRAV_CLI', true);
-
-if (!file_exists(__DIR__ . '/../vendor')) {
-    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
-}
 
 use Grav\Common\Composer;
-
-if (!file_exists(__DIR__ . '/../vendor')) {
-    // Before we can even start, we need to run composer first
-    $composer = Composer::getComposerExecutor();
-    echo "Preparing to install vendor dependencies...\n\n";
-    echo system($composer . ' --working-dir="' . __DIR__ . '/../" --no-interaction --no-dev --prefer-dist -o install');
-    echo "\n\n";
-}
-
 use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Input\ArgvInput;
 use Symfony\Component\Console\Output\ConsoleOutput;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Grav\Common\Grav;
-use Grav\Common\Config\Setup;
 use Grav\Common\Filesystem\Folder;
 
-$autoload = require_once(__DIR__ . '/../vendor/autoload.php');
+\define('GRAV_CLI', true);
+\define('GRAV_REQUEST_TIME', microtime(true));
+
+if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
+    // Before we can even start, we need to run composer first
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+
+    $composer = Composer::getComposerExecutor();
+    echo "Preparing to install vendor dependencies...\n\n";
+    echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
+    echo "\n\n";
+}
+
+$autoload = require __DIR__ . '/../vendor/autoload.php';
 
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
@@ -34,7 +32,7 @@ if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
 }
 
-if (!file_exists(ROOT_DIR . 'index.php')) {
+if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
 
@@ -48,19 +46,18 @@ $climate->arguments->add([
     ]
 ]);
 $climate->arguments->parse();
-$environment = $climate->arguments->get('environment');
 
-// Set up environment based on params.
-Setup::$environment = $environment;
+$environment = $climate->arguments->get('environment');
 
 $grav = Grav::instance(array('loader' => $autoload));
-$grav['uri']->init();
+$grav->setup($environment);
+
 $grav['config']->init();
-$grav['streams'];
+$grav['uri']->init();
+$grav['users'];
 $grav['plugins']->init();
 $grav['themes']->init();
 
-
 $app     = new Application('Grav Plugins Commands', GRAV_VERSION);
 $pattern = '([A-Z]\w+Command\.php)';
 
@@ -75,21 +72,36 @@ $argv = array_merge([$bin], $argv);
 
 $input = new ArgvInput($argv);
 
+/** @var \Grav\Common\Data\Data $plugin */
 $plugin = $grav['plugins']->get($name);
 
 $output = new ConsoleOutput();
 $output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold')));
 $output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold')));
 
+if (is_null($plugin)) {
+    $output->writeln('');
+    $output->writeln("<red>$name plugin not found</red>");
+    die;
+}
+
+if (!$plugin->enabled) {
+    $output->writeln('');
+    $output->writeln("<red>$name not enabled</red>");
+    die;
+}
+
+
 if (!$name) {
     $output->writeln('');
-    $output->writeln("<red>Usage:</red>");
+    $output->writeln('<red>Usage:</red>');
     $output->writeln("  {$bin} [slug] [command] [arguments]");
     $output->writeln('');
-    $output->writeln("<red>Example:</red>");
+    $output->writeln('<red>Example:</red>');
     $output->writeln("  {$bin} error log -l 1 --trace");
     $list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 2]);
 
+    $total = 0;
     if (count($list)) {
         $available = [];
         $output->writeln('');
@@ -98,13 +110,15 @@ if (!$name) {
             $split = explode('/', $entry);
             $entry = array_shift($split);
             $index = str_pad($index++ + 1, 2, '0', STR_PAD_LEFT);
-
-            if (in_array($entry, $available)) {
+            $total = str_pad($total++ + 1, 2, '0', STR_PAD_LEFT);
+            if (\in_array($entry, $available, true)) {
+                $total--;
                 continue;
             }
 
             $available[] = $entry;
-            $output->writeln('  ' . $index . ". <red>" . str_pad($entry, 15) . "</red> <white>${bin} ${entry} list</white>");
+            $commands_count = $index - $total + 1;
+            $output->writeln('  ' . $total . '. <red>' . str_pad($entry, 15) . "</red> <white>{$bin} {$entry} list</white>");
         }
     }
 

+ 53 - 26
composer.json

@@ -6,47 +6,71 @@
     "homepage": "https://getgrav.org",
     "license": "MIT",
     "require": {
-        "php": ">=5.6.4",
-        "twig/twig": "~1.24",
-        "erusev/parsedown": "1.6.4",
-        "erusev/parsedown-extra": "~0.7",
-        "symfony/yaml": "~3.4",
-        "symfony/console": "~3.4",
-        "symfony/event-dispatcher": "~3.4",
-        "symfony/var-dumper": "~3.4",
-        "symfony/polyfill-iconv": "~1.0",
-        "doctrine/cache": "^1.6",
-        "doctrine/collections": "^1.4",
+        "php": ">=7.1.3",
+        "ext-json": "*",
+        "ext-mbstring": "*",
+        "ext-openssl": "*",
+        "ext-curl": "*",
+        "ext-zip": "*",
+        "ext-dom": "*",
+        "symfony/polyfill-iconv": "^1.9",
+        "symfony/polyfill-php72": "^1.9",
+        "symfony/polyfill-php73": "^1.9",
+
         "psr/simple-cache": "^1.0",
         "psr/http-message": "^1.0",
+        "psr/http-server-middleware": "^1.0",
+
+        "kodus/psr7-server": "*",
+        "nyholm/psr7": "^1.0",
+
+        "twig/twig": "~1.35",
+        "erusev/parsedown": "1.6.4",
+        "erusev/parsedown-extra": "~0.7",
+        "symfony/yaml": "~4.2",
+        "symfony/console": "~4.2",
+        "symfony/event-dispatcher": "~4.2",
+        "symfony/var-dumper": "~4.2",
+        "symfony/process": "~4.2",
+        "doctrine/cache": "^1.8",
+        "doctrine/collections": "^1.5",
         "guzzlehttp/psr7": "^1.4",
-        "filp/whoops": "~2.0",
+        "filp/whoops": "~2.2",
+
         "matthiasmullie/minify": "^1.3",
         "monolog/monolog": "~1.0",
         "gregwar/image": "2.*",
-        "donatj/phpuseragentparser": "~0.3",
+        "donatj/phpuseragentparser": "~0.10",
         "pimple/pimple": "~3.2",
         "rockettheme/toolbox": "~1.4",
-        "maximebf/debugbar": "~1.10",
-        "ext-mbstring": "*",
-        "ext-openssl": "*",
-        "ext-curl": "*",
-        "ext-zip": "*",
-        "ext-json": "*",
-        "league/climate": "^3.2",
+        "maximebf/debugbar": "~1.15",
+        "league/climate": "^3.4",
         "antoligy/dom-string-iterators": "^1.0",
-        "miljar/php-exif": "^0.6.3",
-        "composer/ca-bundle": "^1.0"
+        "miljar/php-exif": "^0.6.4",
+        "composer/ca-bundle": "^1.0",
+        "dragonmantank/cron-expression": "^1.2",
+        "phive/twig-extensions-deferred": "^1.0",
+        "willdurand/negotiation": "^2.3"
     },
     "require-dev": {
-        "codeception/codeception": "^2.1",
-        "phpunit/php-code-coverage": "~2.0",
-        "fzaninotto/faker": "^1.5",
+        "codeception/codeception": "^2.4",
+        "phpstan/phpstan": "^0.11",
+        "phpstan/phpstan-deprecation-rules": "^0.11.0",
+        "phpunit/php-code-coverage": "~6.0",
+        "fzaninotto/faker": "^1.8",
         "victorjonsson/markdowndocs": "dev-master"
     },
+    "suggest": {
+        "ext-zend-opcache": "Recommended for better performance",
+        "ext-intl": "Recommended for multi-language sites",
+        "ext-memcache": "Needed to support Memcache servers",
+        "ext-memcached": "Needed to support Memcached servers",
+        "ext-redis": "Needed to support Redis servers"
+    },
     "config": {
+        "apcu-autoloader": true,
         "platform": {
-            "php": "5.6.4"
+            "php": "7.1.3"
         }
     },
     "repositories": [
@@ -66,6 +90,9 @@
     },
     "scripts": {
         "post-create-project-cmd": "bin/grav install",
+        "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M",
+        "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M",
+        "test-plugins": "vendor/bin/phpstan analyse -l 0 -c ./tests/phpstan/plugins.neon user/plugins --memory-limit=256M",
         "test": "vendor/bin/codecept run unit",
         "test-windows": "vendor\\bin\\codecept run unit"
     },

File diff suppressed because it is too large
+ 561 - 133
composer.lock


+ 17 - 15
index.php

@@ -9,35 +9,34 @@
 
 namespace Grav;
 
-define('GRAV_PHP_MIN', '5.6.4');
+\define('GRAV_REQUEST_TIME', microtime(true));
+\define('GRAV_PHP_MIN', '7.1.3');
 
-// Ensure vendor libraries exist
-$autoload = __DIR__ . '/vendor/autoload.php';
-if (!is_file($autoload)) {
-    die("Please run: <i>bin/grav install</i>");
+if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
+    die(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req));
 }
 
-if (PHP_SAPI === 'cli-server') {
-    if (!isset($_SERVER['PHP_CLI_ROUTER'])) {
-       die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
-    }
+if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) {
+    die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
 }
 
-use Grav\Common\Grav;
-use RocketTheme\Toolbox\Event\Event;
-
-if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
-    die(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req));
+// Ensure vendor libraries exist
+$autoload = __DIR__ . '/vendor/autoload.php';
+if (!is_file($autoload)) {
+    die('Please run: <i>bin/grav install</i>');
 }
 
 // Register the auto-loader.
 $loader = require $autoload;
 
+use Grav\Common\Grav;
+use RocketTheme\Toolbox\Event\Event;
+
 // Set timezone to default, falls back to system if php.ini not set
 date_default_timezone_set(@date_default_timezone_get());
 
 // Set internal encoding if mbstring loaded
-if (!extension_loaded('mbstring')) {
+if (!\extension_loaded('mbstring')) {
     die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
 }
 mb_internal_encoding('UTF-8');
@@ -52,6 +51,9 @@ $grav = Grav::instance(
 // Process the page
 try {
     $grav->process();
+} catch (\Error $e) {
+    $grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
+    throw $e;
 } catch (\Exception $e) {
     $grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
     throw $e;

+ 16 - 0
system/assets/debugger.css

@@ -30,9 +30,25 @@ div.phpdebugbar {
 }
 
 .phpdebugbar .phpdebugbar-widgets-toolbar {
+    border-top: 1px solid #ddd;
     padding-left: 5px;
+    padding-right: 2px;
+    padding-top: 2px;
+    background-color: #fafafa !important;
+    width: auto !important;
+    left: 0;
+    right: 0;
 }
 
+.phpdebugbar .phpdebugbar-widgets-toolbar input {
+    background: transparent !important;
+}
+
+.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter {
+
+}
+
+
 .phpdebugbar input[type=text] {
     padding: 0;
     display: inline;

BIN
system/assets/grav.png


+ 125 - 0
system/blueprints/config/backups.yaml

@@ -0,0 +1,125 @@
+title: PLUGIN_ADMIN.BACKUPS
+
+form:
+    validation: loose
+
+    fields:
+        history_title:
+            type: section
+            title: PLUGIN_ADMIN.BACKUPS_HISTORY
+            underline: true
+
+        history:
+            type: backupshistory
+
+        config_title:
+            type: section
+            title: PLUGIN_ADMIN.BACKUPS_PURGE_CONFIG
+            underline: true
+
+        purge.trigger:
+            type: select
+            label: PLUGIN_ADMIN.BACKUPS_STORAGE_PURGE_TRIGGER
+            size: medium
+            default: space
+            options:
+                space: Maximum Backup Space
+                number: Maximum Number of Backups
+                time: maximum Retention Time
+            validate:
+                required: true
+
+        purge.max_backups_count:
+            type: number
+            label: PLUGIN_ADMIN.BACKUPS_MAX_COUNT
+            default: 25
+            size: x-small
+            help: PLUGIN_ADMIN.BACKUPS_MAX_COUNT
+            validate:
+                min: 0
+                type: number
+                required: true
+                message: Must be a number 0 or greater
+
+        purge.max_backups_space:
+            type: number
+            label: PLUGIN_ADMIN.BACKUPS_MAX_SPACE
+            append: in GB
+            size: x-small
+            default: 5
+            validate:
+                min: 1
+                type: number
+                required: true
+                message: Space must be 1GB or greater
+
+        purge.max_backups_time:
+            type: number
+            label: PLUGIN_ADMIN.BACKUPS_MAX_RETENTION_TIME
+            append: PLUGIN_ADMIN.BACKUPS_MAX_RETENTION_TIME_APPEND
+            size: x-small
+            default: 365
+            validate:
+                min: 7
+                type: number
+                required: true
+                message: Rentenion days must be 7 or greater
+
+        profiles_title:
+          type: section
+          title: PLUGIN_ADMIN.BACKUPS_PROFILES
+          underline: true
+
+        profiles:
+          type: list
+          style: vertical
+          label:
+          classes: backups-list compact
+          sort: false
+
+          fields:
+            .name:
+              type: text
+              label: PLUGIN_ADMIN.NAME
+              placeholder: PLUGIN_ADMIN.BACKUPS_PROFILE_NAME
+              validate:
+                  max: 20
+                  message: 'Name must be less than 20 characters'
+                  required: true
+            .root:
+              type: text
+              label: PLUGIN_ADMIN.BACKUPS_PROFILE_ROOT_FOLDER
+              help: PLUGIN_ADMIN.BACKUPS_PROFILE_ROOT_FOLDER_HELP
+              placeholder: '/'
+              default: '/'
+              validate:
+                  required: true
+            .exclude_paths:
+              type: textarea
+              label: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_PATHS
+              rows: 5
+              placeholder: "/backup\r/cache\r/images\r/logs\r/tmp"
+              help: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_PATHS_HELP
+            .exclude_files:
+                type: textarea
+                label: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_FILES
+                rows: 5
+                placeholder: ".DS_Store\r.git\r.svn\r.hg\r.idea\r.vscode\rnode_modules"
+                help: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_FILES_HELP
+            .schedule:
+                type: toggle
+                label: PLUGIN_ADMIN.BACKUPS_PROFILE_SCHEDULE
+                highlight: 1
+                default: 1
+                options:
+                    1: PLUGIN_ADMIN.YES
+                    0: PLUGIN_ADMIN.NO
+                validate:
+                    type: bool
+            .schedule_at:
+                type: cron
+                label: PLUGIN_ADMIN.BACKUPS_PROFILE_SCHEDULE_AT
+                default: '* 3 * * *'
+                validate:
+                    required: true
+

+ 76 - 0
system/blueprints/config/scheduler.yaml

@@ -0,0 +1,76 @@
+title: PLUGIN_ADMIN.SCHEDULER
+
+form:
+    validation: loose
+
+    fields:
+
+        status_title:
+            type: section
+            title: PLUGIN_ADMIN.SCHEDULER_STATUS
+            underline: true
+
+        status:
+            type: cronstatus
+            validate:
+                type: commalist
+
+        jobs_title:
+            type: section
+            title: PLUGIN_ADMIN.SCHEDULER_JOBS
+            underline: true
+
+        custom_jobs:
+          type: list
+          style: vertical
+          label:
+          classes: cron-job-list compact
+          key: id
+          fields:
+            .id:
+              type: key
+              label: ID
+              placeholder: 'process-name'
+              validate:
+                  required: true
+                  pattern: '[a-zа-я0-9_\-]+'
+                  max: 20
+                  message: 'ID must be lowercase with dashes/underscores only and less than 20 characters'
+            .command:
+              type: text
+              label: PLUGIN_ADMIN.COMMAND
+              placeholder: 'cd ~;ls -lah;'
+              validate:
+                  required: true
+            .args:
+              type: text
+              label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
+            .at:
+              type: cron
+              label: PLUGIN_ADMIN.SCHEDULER_RUNAT
+              help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
+              placeholder: '* * * * *'
+              validate:
+                  required: true
+            .output:
+              type: text
+              label: PLUGIN_ADMIN.SCHEDULER_OUTPUT
+              help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_HELP
+              placeholder: 'logs/ls-cron.out'
+            .output_mode:
+              type: select
+              label: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE
+              help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE_HELP
+              default: append
+              options:
+                  append: Append
+                  overwrite: Overwrite
+            .email:
+                type: text
+                label: PLUGIN_ADMIN.SCHEDULER_EMAIL
+                help: PLUGIN_ADMIN.SCHEDULER_EMAIL_HELP
+                placeholder: 'notifications@yoursite.com'
+
+
+
+

+ 8 - 0
system/blueprints/config/security.yaml

@@ -41,6 +41,14 @@ form:
             validate:
                 type: bool
 
+        xss_invalid_protocols:
+            type: selectize
+            size: large
+            label: PLUGIN_ADMIN.XSS_INVALID_PROTOCOLS_LIST
+            classes: fancy
+            validate:
+                type: commalist
+
         xss_enabled.moz_binding:
             type: toggle
             label: PLUGIN_ADMIN.XSS_MOZ_BINDINGS

+ 0 - 7
system/blueprints/config/site.yaml

@@ -16,13 +16,6 @@ form:
                     placeholder: PLUGIN_ADMIN.SITE_TITLE_PLACEHOLDER
                     help: PLUGIN_ADMIN.SITE_TITLE_HELP
 
-                header.status:
-                    type: text
-                    label: Status
-                    size: large
-                    placeholder: PLUGIN_ADMIN.SITE_TITLE_PLACEHOLDER
-                    help: PLUGIN_ADMIN.SITE_TITLE_HELP
-
                 default_lang:
                     type: text
                     label: PLUGIN_ADMIN.SITE_DEFAULT_LANG

+ 1402 - 1236
system/blueprints/config/system.yaml

@@ -4,1241 +4,1407 @@ form:
     validation: loose
     fields:
 
-        content:
-            type: section
-            title: PLUGIN_ADMIN.CONTENT
-            underline: true
+        system_tabs:
+            type: tabs
+            classes: side-tabs
 
             fields:
-                home.alias:
-                    type: pages
-                    size: large
-                    classes: fancy
-                    label: PLUGIN_ADMIN.HOME_PAGE
-                    show_all: false
-                    show_modular: false
-                    show_root: false
-                    show_slug: true
-                    help: PLUGIN_ADMIN.HOME_PAGE_HELP
-
-                home.hide_in_urls:
-                    type: toggle
-                    label: PLUGIN_ADMIN.HIDE_HOME_IN_URLS
-                    help: PLUGIN_ADMIN.HIDE_HOME_IN_URLS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-
-                pages.theme:
-                    type: themeselect
-                    classes: fancy
-                    selectize: true
-                    size: medium
-                    label: PLUGIN_ADMIN.DEFAULT_THEME
-                    help: PLUGIN_ADMIN.DEFAULT_THEME_HELP
-
-                pages.process:
-                    type: checkboxes
-                    label: PLUGIN_ADMIN.PROCESS
-                    help: PLUGIN_ADMIN.PROCESS_HELP
-                    default: [markdown: true, twig: true]
-                    options:
-                        markdown: Markdown
-                        twig: Twig
-                    use: keys
-
-                timezone:
-                    type: select
-                    label: PLUGIN_ADMIN.TIMEZONE
-                    size: medium
-                    classes: fancy
-                    help: PLUGIN_ADMIN.TIMEZONE_HELP
-                    data-options@: '\Grav\Common\Utils::timezones'
-                    default: ''
-                    options:
-                        '': 'Default (Server Timezone)'
-
-                pages.dateformat.default:
-                    type: select
-                    size: medium
-                    selectize:
-                        create: true
-                    label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT
-                    help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP
-                    placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER
-                    data-options@: '\Grav\Common\Utils::dateFormats'
-                    validate:
-                        type: string
-
-                pages.dateformat.short:
-                    type: dateformat
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.SHORT_DATE_FORMAT
-                    help: PLUGIN_ADMIN.SHORT_DATE_FORMAT_HELP
-                    default: "jS M Y"
-                    options:
-                        "F jS \\a\\t g:ia": Date1
-                        "l jS \\of F g:i A": Date2
-                        "D, d M Y G:i:s": Date3
-                        "d-m-y G:i": Date4
-                        "jS M Y": Date5
-
-                pages.dateformat.long:
-                    type: dateformat
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.LONG_DATE_FORMAT
-                    help: PLUGIN_ADMIN.LONG_DATE_FORMAT_HELP
-                    options:
-                        "F jS \\a\\t g:ia": Date1
-                        "l jS \\of F g:i A": Date2
-                        "D, d M Y G:i:s": Date3
-                        "d-m-y G:i": Date4
-                        "jS M Y": Date5
-
-                pages.order.by:
-                    type: select
-                    size: large
-                    classes: fancy
-                    label: PLUGIN_ADMIN.DEFAULT_ORDERING
-                    help: PLUGIN_ADMIN.DEFAULT_ORDERING_HELP
-                    options:
-                        default: PLUGIN_ADMIN.DEFAULT_ORDERING_DEFAULT
-                        folder: PLUGIN_ADMIN.DEFAULT_ORDERING_FOLDER
-                        title: PLUGIN_ADMIN.DEFAULT_ORDERING_TITLE
-                        date: PLUGIN_ADMIN.DEFAULT_ORDERING_DATE
-
-                pages.order.dir:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION
-                    highlight: asc
-                    default: desc
-                    help: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION_HELP
-                    options:
-                        asc: PLUGIN_ADMIN.ASCENDING
-                        desc: PLUGIN_ADMIN.DESCENDING
-
-                pages.list.count:
-                    type: text
-                    size: x-small
-                    append: PLUGIN_ADMIN.PAGES
-                    label: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT
-                    help: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT_HELP
-                    validate:
-                        type: number
-                        min: 1
-
-                pages.publish_dates:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DATE_BASED_PUBLISHING
-                    help: PLUGIN_ADMIN.DATE_BASED_PUBLISHING_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.events:
-                     type: checkboxes
-                     label: PLUGIN_ADMIN.EVENTS
-                     help: PLUGIN_ADMIN.EVENTS_HELP
-                     default: [page: true, twig: true]
-                     options:
-                         page: Page Events
-                         twig: Twig Events
-                     use: keys
-
-                pages.append_url_extension:
-                    type: text
-                    size: x-small
-                    placeholder: "e.g. .html"
-                    label: PLUGIN_ADMIN.APPEND_URL_EXT
-                    help: PLUGIN_ADMIN.APPEND_URL_EXT_HELP
-
-                pages.redirect_default_route:
-                    type: toggle
-                    label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
-                    help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.redirect_default_code:
-                    type: select
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE
-                    help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP
-                    options:
-                        301: 301 - Permanent
-                        302: 302 - Found
-                        303: 303 - Other
-                        304: 304 - Not Modified
-
-                pages.redirect_trailing_slash:
-                    type: toggle
-                    label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH
-                    help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.ignore_hidden:
-                    type: toggle
-                    label: PLUGIN_ADMIN.IGNORE_HIDDEN
-                    help: PLUGIN_ADMIN.IGNORE_HIDDEN_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.ignore_files:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.IGNORE_FILES
-                    help: PLUGIN_ADMIN.IGNORE_FILES_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                pages.ignore_folders:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.IGNORE_FOLDERS
-                    help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                pages.url_taxonomy_filters:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS
-                    help: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.twig_first:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_FIRST
-                    help: PLUGIN_ADMIN.TWIG_FIRST_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.never_cache_twig:
-                    type: toggle
-                    label: PLUGIN_ADMIN.NEVER_CACHE_TWIG
-                    help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.frontmatter.process_twig:
-                    type: toggle
-                    label: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG
-                    help: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.frontmatter.ignore_fields:
-                    type: selectize
-                    size: large
-                    placeholder: "e.g. forms"
-                    label: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS
-                    help: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-        languages:
-            type: section
-            title: PLUGIN_ADMIN.LANGUAGES
-            underline: true
-
-            fields:
-
-                languages.supported:
-                    type: selectize
-                    size: large
-                    placeholder: "e.g. en, fr"
-                    label: PLUGIN_ADMIN.SUPPORTED
-                    help: PLUGIN_ADMIN.SUPPORTED_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                languages.include_default_lang:
-                    type: toggle
-                    label: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG
-                    help: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-
-                languages.translations:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TRANSLATIONS_ENABLED
-                    help: PLUGIN_ADMIN.TRANSLATIONS_ENABLED_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.translations_fallback:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK
-                    help: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.session_store_active:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION
-                    help: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.http_accept_language:
-                    type: toggle
-                    label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE
-                    help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.override_locale:
-                    type: toggle
-                    label: PLUGIN_ADMIN.OVERRIDE_LOCALE
-                    help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        http_headers:
-            type: section
-            title: PLUGIN_ADMIN.HTTP_HEADERS
-            underline: true
-
-            fields:
-                pages.expires:
-                    type: text
-                    size: small
-                    append: NICETIME.SECOND_PLURAL
-                    label: PLUGIN_ADMIN.EXPIRES
-                    help: PLUGIN_ADMIN.EXPIRES_HELP
-                    validate:
-                        type: number
-                        min: 1
-                pages.cache_control:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.CACHE_CONTROL
-                    help: PLUGIN_ADMIN.CACHE_CONTROL_HELP
-                    placeholder: 'e.g. public, max-age=31536000'
-                pages.last_modified:
-                    type: toggle
-                    label: PLUGIN_ADMIN.LAST_MODIFIED
-                    help: PLUGIN_ADMIN.LAST_MODIFIED_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.etag:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ETAG
-                    help: PLUGIN_ADMIN.ETAG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.vary_accept_encoding:
-                    type: toggle
-                    label: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING
-                    help: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        markdown:
-            type: section
-            title: Markdown
-            underline: true
-
-            fields:
-                pages.markdown.extra:
-                    type: toggle
-                    label: Markdown extra
-                    help: PLUGIN_ADMIN.MARKDOWN_EXTRA_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.markdown.auto_line_breaks:
-                    type: toggle
-                    label: PLUGIN_ADMIN.AUTO_LINE_BREAKS
-                    help: PLUGIN_ADMIN.AUTO_LINE_BREAKS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.markdown.auto_url_links:
-                    type: toggle
-                    label: PLUGIN_ADMIN.AUTO_URL_LINKS
-                    help: PLUGIN_ADMIN.AUTO_URL_LINKS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.markdown.escape_markup:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ESCAPE_MARKUP
-                    help: PLUGIN_ADMIN.ESCAPE_MARKUP_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        caching:
-            type: section
-            title: PLUGIN_ADMIN.CACHING
-            underline: true
-
-            fields:
-                cache.enabled:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CACHING
-                    help: PLUGIN_ADMIN.CACHING_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.check.method:
-                    type: select
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.CACHE_CHECK_METHOD
-                    help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP
-                    options:
-                        file: Markdown + Yaml file timestamps
-                        folder: Folder timestamps
-                        hash: All files timestamps
-                        none: No timestamp checking
-
-                cache.driver:
-                    type: select
-                    size: small
-                    classes: fancy
-                    label: PLUGIN_ADMIN.CACHE_DRIVER
-                    help: PLUGIN_ADMIN.CACHE_DRIVER_HELP
-                    options:
-                        auto: Auto detect
-                        file: File
-                        apc: APC
-                        apcu: APCu
-                        xcache: Xcache
-                        memcache: Memcache
-                        memcached: Memcached
-                        wincache: WinCache
-                        redis: Redis
-
-                cache.prefix:
-                    type: text
-                    size: x-small
-                    label: PLUGIN_ADMIN.CACHE_PREFIX
-                    help: PLUGIN_ADMIN.CACHE_PREFIX_HELP
-                    placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER
-
-                cache.clear_images_by_default:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT
-                    help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.cli_compatibility:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CLI_COMPATIBILITY
-                    help: PLUGIN_ADMIN.CLI_COMPATIBILITY_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.lifetime:
-                    type: text
-                    size: small
-                    append: NICETIME.SECOND_PLURAL
-                    label: PLUGIN_ADMIN.LIFETIME
-                    help: PLUGIN_ADMIN.LIFETIME_HELP
-                    validate:
-                        type: number
-
-                cache.gzip:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GZIP_COMPRESSION
-                    help: PLUGIN_ADMIN.GZIP_COMPRESSION_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.allow_webserver_gzip:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP
-                    help: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.memcache.server:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.MEMCACHE_SERVER
-                    help: PLUGIN_ADMIN.MEMCACHE_SERVER_HELP
-                    placeholder: "localhost"
-
-                cache.memcache.port:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.MEMCACHE_PORT
-                    help: PLUGIN_ADMIN.MEMCACHE_PORT_HELP
-                    placeholder: "11211"
-
-                cache.memcached.server:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.MEMCACHED_SERVER
-                    help: PLUGIN_ADMIN.MEMCACHED_SERVER_HELP
-                    placeholder: "localhost"
-
-                cache.memcached.port:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.MEMCACHED_PORT
-                    help: PLUGIN_ADMIN.MEMCACHED_PORT_HELP
-                    placeholder: "11211"
-
-                cache.redis.socket:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.REDIS_SOCKET
-                    help: PLUGIN_ADMIN.REDIS_SOCKET_HELP
-                    placeholder: "/var/run/redis/redis.sock"
-
-                cache.redis.server:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.REDIS_SERVER
-                    help: PLUGIN_ADMIN.REDIS_SERVER_HELP
-                    placeholder: "localhost"
-
-                cache.redis.port:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.REDIS_PORT
-                    help: PLUGIN_ADMIN.REDIS_PORT_HELP
-                    placeholder: "6379"
-
-                cache.redis.password:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.REDIS_PASSWORD
-
-
-
-        twig:
-            type: section
-            title: PLUGIN_ADMIN.TWIG_TEMPLATING
-            underline: true
-
-            fields:
-                twig.cache:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_CACHING
-                    help: PLUGIN_ADMIN.TWIG_CACHING_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.debug:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_DEBUG
-                    help: PLUGIN_ADMIN.TWIG_DEBUG_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.auto_reload:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DETECT_CHANGES
-                    help: PLUGIN_ADMIN.DETECT_CHANGES_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.autoescape:
-                    type: toggle
-                    label: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES
-                    help: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.umask_fix:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_UMASK_FIX
-                    help: PLUGIN_ADMIN.TWIG_UMASK_FIX_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        assets:
-            type: section
-            title: PLUGIN_ADMIN.ASSETS
-            underline: true
-
-            fields:
-                assets.css_pipeline:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_PIPELINE
-                    help: PLUGIN_ADMIN.CSS_PIPELINE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_pipeline_include_externals:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS
-                    help: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_pipeline_before_excludes:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES
-                    help: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_minify:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_MINIFY
-                    help: PLUGIN_ADMIN.CSS_MINIFY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_minify_windows:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE
-                    help: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.css_rewrite:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_REWRITE
-                    help: PLUGIN_ADMIN.CSS_REWRITE_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.js_pipeline:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE
-                    help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.js_pipeline_include_externals:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS
-                    help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.js_pipeline_before_excludes:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES
-                    help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.js_minify:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY
-                    help: PLUGIN_ADMIN.JAVASCRIPT_MINIFY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.enable_asset_timestamp:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS
-                    help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.collections:
-                    type: multilevel
-                    label: PLUGIN_ADMIN.COLLECTIONS
-                    placeholder_key: collection_name
-                    placeholder_value: collection_path
-                    validate:
-                        type: array
-
-        errors:
-            type: section
-            title: PLUGIN_ADMIN.ERROR_HANDLER
-            underline: true
-
-            fields:
-                errors.display:
-                    type: select
-                    label: PLUGIN_ADMIN.DISPLAY_ERRORS
-                    help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP
-                    size: medium
-                    highlight: 1
-                    options:
-                        -1: PLUGIN_ADMIN.ERROR_SYSTEM
-                        0: PLUGIN_ADMIN.ERROR_SIMPLE
-                        1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE
-                    validate:
-                        type: int
-
-
-                errors.log:
-                    type: toggle
-                    label: PLUGIN_ADMIN.LOG_ERRORS
-                    help: PLUGIN_ADMIN.LOG_ERRORS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        debugger:
-            type: section
-            title: PLUGIN_ADMIN.DEBUGGER
-            underline: true
-
-            fields:
-                debugger.enabled:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DEBUGGER
-                    help: PLUGIN_ADMIN.DEBUGGER_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                debugger.shutdown.close_connection:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION
-                    help: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        media:
-            type: section
-            title: PLUGIN_ADMIN.MEDIA
-            underline: true
-
-            fields:
-                images.default_image_quality:
-                    type: range
-                    append: '%'
-                    label: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY
-                    help: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY_HELP
-                    validate:
-                        min: 1
-                        max: 100
-
-                images.cache_all:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CACHE_ALL
-                    help: PLUGIN_ADMIN.CACHE_ALL_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                images.cache_perms:
-                    type: select
-                    size: small
-                    label: PLUGIN_ADMIN.CACHE_PERMS
-                    help: PLUGIN_ADMIN.CACHE_PERMS_HELP
-                    highlight: '0755'
-                    options:
-                        '0755': '0755'
-                        '0775': '0775'
-
-                images.debug:
-                    type: toggle
-                    label: PLUGIN_ADMIN.IMAGES_DEBUG
-                    help: PLUGIN_ADMIN.IMAGES_DEBUG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                images.auto_fix_orientation:
-                    type: toggle
-                    label: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION
-                    help: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                media.enable_media_timestamp:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP
-                    help: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                media.auto_metadata_exif:
-                      type: toggle
-                      label: PLUGIN_ADMIN.ENABLE_AUTO_METADATA
-                      help: PLUGIN_ADMIN.ENABLE_AUTO_METADATA_HELP
-                      highlight: 0
-                      options:
-                          1: PLUGIN_ADMIN.YES
-                          0: PLUGIN_ADMIN.NO
-                      validate:
-                          type: bool
-
-
-
-                media.allowed_fallback_types:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.FALLBACK_TYPES
-                    help: PLUGIN_ADMIN.FALLBACK_TYPES_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                media.unsupported_inline_types:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.INLINE_TYPES
-                    help: PLUGIN_ADMIN.INLINE_TYPES_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-        session:
-            type: section
-            title: PLUGIN_ADMIN.SESSION
-            underline: true
-
-            fields:
-                session.enabled:
-                    type: hidden
-                    label: PLUGIN_ADMIN.ENABLED
-                    help: PLUGIN_ADMIN.SESSION_ENABLED_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                session.initialize:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_INITIALIZE
-                    help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                session.timeout:
-                    type: text
-                    size: small
-                    append: NICETIME.SECOND_PLURAL
-                    label: PLUGIN_ADMIN.TIMEOUT
-                    help: PLUGIN_ADMIN.TIMEOUT_HELP
-                    validate:
-                        type: number
-                        min: 0
-
-                session.name:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.NAME
-                    help: PLUGIN_ADMIN.SESSION_NAME_HELP
-
-                session.secure:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_SECURE
-                    help: PLUGIN_ADMIN.SESSION_SECURE_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: false
-                    validate:
-                        type: bool
-
-                session.httponly:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_HTTPONLY
-                    help: PLUGIN_ADMIN.SESSION_HTTPONLY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                session.path:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.SESSION_PATH
-                    help: PLUGIN_ADMIN.SESSION_PATH_HELP
-
-                session.split:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_SPLIT
-                    help: PLUGIN_ADMIN.SESSION_SPLIT_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-        advanced:
-            type: section
-            title: PLUGIN_ADMIN.ADVANCED
-            underline: true
-
-            fields:
-                gpm.releases:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_RELEASES
-                    highlight: stable
-                    help: PLUGIN_ADMIN.GPM_RELEASES_HELP
-                    options:
-                        stable: PLUGIN_ADMIN.STABLE
-                        testing: PLUGIN_ADMIN.TESTING
-
-                gpm.proxy_url:
-                    type: text
-                    size: medium
-                    placeholder: "e.g. 127.0.0.1:3128"
-                    label: PLUGIN_ADMIN.PROXY_URL
-                    help: PLUGIN_ADMIN.PROXY_URL_HELP
-
-                gpm.method:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_METHOD
-                    highlight: auto
-                    help: PLUGIN_ADMIN.GPM_METHOD_HELP
-                    options:
-                        auto: PLUGIN_ADMIN.AUTO
-                        fopen: PLUGIN_ADMIN.FOPEN
-                        curl: PLUGIN_ADMIN.CURL
-
-                gpm.official_gpm_only:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY
-                    highlight: auto
-                    help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                gpm.verify_peer:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_VERIFY_PEER
-                    highlight: 1
-                    help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                reverse_proxy_setup:
-                    type: toggle
-                    label: PLUGIN_ADMIN.REVERSE_PROXY
-                    highlight: 0
-                    help: PLUGIN_ADMIN.REVERSE_PROXY_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                username_regex:
-                    type: text
-                    size: large
-                    label: PLUGIN_ADMIN.USERNAME_REGEX
-                    help: PLUGIN_ADMIN.USERNAME_REGEX_HELP
-
-                pwd_regex:
-                    type: text
-                    size: large
-                    label: PLUGIN_ADMIN.PWD_REGEX
-                    help: PLUGIN_ADMIN.PWD_REGEX_HELP
-
-                intl_enabled:
-                    type: toggle
-                    label: PLUGIN_ADMIN.INTL_ENABLED
-                    highlight: 1
-                    help: PLUGIN_ADMIN.INTL_ENABLED_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                wrapped_site:
-                    type: toggle
-                    label: PLUGIN_ADMIN.WRAPPED_SITE
-                    highlight: 0
-                    help: PLUGIN_ADMIN.WRAPPED_SITE_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                absolute_urls:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ABSOLUTE_URLS
-                    highlight: 0
-                    help: PLUGIN_ADMIN.ABSOLUTE_URLS_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                param_sep:
-                    type: select
-                    size: medium
-                    label: PLUGIN_ADMIN.PARAMETER_SEPARATOR
-                    classes: fancy
-                    help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP
-                    default: ''
-                    options:
-                        ':': ': (default)'
-                        ';': '; (for Apache running on Windows)'
-
-                force_ssl:
-                    type: toggle
-                    label: PLUGIN_ADMIN.FORCE_SSL
-                    highlight: 0
-                    help: PLUGIN_ADMIN.FORCE_SSL_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                force_lowercase_urls:
-                    type: toggle
-                    label: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS
-                    highlight: 1
-                    default: 1
-                    help: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                custom_base_url:
-                    type: text
-                    size: medium
-                    placeholder: "e.g. http://yoursite.com/yourpath"
-                    label: PLUGIN_ADMIN.CUSTOM_BASE_URL
-                    help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
-
-                strict_mode.yaml_compat:
-                    type: toggle
-                    label: PLUGIN_ADMIN.STRICT_YAML_COMPAT
-                    highlight: 1
-                    default: 1
-                    help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                strict_mode.twig_compat:
-                    type: toggle
-                    label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
-                    highlight: 1
-                    default: 1
-                    help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
+                content:
+                    type: tab
+                    title: PLUGIN_ADMIN.CONTENT
+
+                    fields:
+                        content_section:
+                            type: section
+                            title: PLUGIN_ADMIN.CONTENT
+                            underline: true
+
+                        home.alias:
+                            type: pages
+                            size: large
+                            classes: fancy
+                            label: PLUGIN_ADMIN.HOME_PAGE
+                            show_all: false
+                            show_modular: false
+                            show_root: false
+                            show_slug: true
+                            help: PLUGIN_ADMIN.HOME_PAGE_HELP
+
+                        home.hide_in_urls:
+                            type: toggle
+                            label: PLUGIN_ADMIN.HIDE_HOME_IN_URLS
+                            help: PLUGIN_ADMIN.HIDE_HOME_IN_URLS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.theme:
+                            type: themeselect
+                            classes: fancy
+                            selectize: true
+                            size: medium
+                            label: PLUGIN_ADMIN.DEFAULT_THEME
+                            help: PLUGIN_ADMIN.DEFAULT_THEME_HELP
+
+                        pages.process:
+                            type: checkboxes
+                            label: PLUGIN_ADMIN.PROCESS
+                            help: PLUGIN_ADMIN.PROCESS_HELP
+                            default: [markdown: true, twig: true]
+                            options:
+                                markdown: Markdown
+                                twig: Twig
+                            use: keys
+
+                        pages.types:
+                            type: array
+                            label: PLUGIN_ADMIN.PAGE_TYPES
+                            help: PLUGIN_ADMIN.PAGE_TYPES_HELP
+                            size: small
+                            default: ['html','htm','json','xml','txt','rss','atom']
+                            value_only: true
+
+                        timezone:
+                            type: select
+                            label: PLUGIN_ADMIN.TIMEZONE
+                            size: medium
+                            classes: fancy
+                            help: PLUGIN_ADMIN.TIMEZONE_HELP
+                            data-options@: '\Grav\Common\Utils::timezones'
+                            default: ''
+                            options:
+                                '': 'Default (Server Timezone)'
+
+                        pages.dateformat.default:
+                            type: select
+                            size: medium
+                            selectize:
+                                create: true
+                            label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT
+                            help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP
+                            placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER
+                            data-options@: '\Grav\Common\Utils::dateFormats'
+                            validate:
+                                type: string
+
+                        pages.dateformat.short:
+                            type: dateformat
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.SHORT_DATE_FORMAT
+                            help: PLUGIN_ADMIN.SHORT_DATE_FORMAT_HELP
+                            default: "jS M Y"
+                            options:
+                                "F jS \\a\\t g:ia": Date1
+                                "l jS \\of F g:i A": Date2
+                                "D, d M Y G:i:s": Date3
+                                "d-m-y G:i": Date4
+                                "jS M Y": Date5
+
+                        pages.dateformat.long:
+                            type: dateformat
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.LONG_DATE_FORMAT
+                            help: PLUGIN_ADMIN.LONG_DATE_FORMAT_HELP
+                            options:
+                                "F jS \\a\\t g:ia": Date1
+                                "l jS \\of F g:i A": Date2
+                                "D, d M Y G:i:s": Date3
+                                "d-m-y G:i": Date4
+                                "jS M Y": Date5
+
+                        pages.order.by:
+                            type: select
+                            size: large
+                            classes: fancy
+                            label: PLUGIN_ADMIN.DEFAULT_ORDERING
+                            help: PLUGIN_ADMIN.DEFAULT_ORDERING_HELP
+                            options:
+                                default: PLUGIN_ADMIN.DEFAULT_ORDERING_DEFAULT
+                                folder: PLUGIN_ADMIN.DEFAULT_ORDERING_FOLDER
+                                title: PLUGIN_ADMIN.DEFAULT_ORDERING_TITLE
+                                date: PLUGIN_ADMIN.DEFAULT_ORDERING_DATE
+
+                        pages.order.dir:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION
+                            highlight: asc
+                            default: desc
+                            help: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION_HELP
+                            options:
+                                asc: PLUGIN_ADMIN.ASCENDING
+                                desc: PLUGIN_ADMIN.DESCENDING
+
+                        pages.list.count:
+                            type: text
+                            size: x-small
+                            append: PLUGIN_ADMIN.PAGES
+                            label: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT
+                            help: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT_HELP
+                            validate:
+                                type: number
+                                min: 1
+
+                        pages.publish_dates:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DATE_BASED_PUBLISHING
+                            help: PLUGIN_ADMIN.DATE_BASED_PUBLISHING_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.events:
+                             type: checkboxes
+                             label: PLUGIN_ADMIN.EVENTS
+                             help: PLUGIN_ADMIN.EVENTS_HELP
+                             default: [page: true, twig: true]
+                             options:
+                                 page: Page Events
+                                 twig: Twig Events
+                             use: keys
+
+                        pages.append_url_extension:
+                            type: text
+                            size: x-small
+                            placeholder: "e.g. .html"
+                            label: PLUGIN_ADMIN.APPEND_URL_EXT
+                            help: PLUGIN_ADMIN.APPEND_URL_EXT_HELP
+
+                        pages.redirect_default_route:
+                            type: toggle
+                            label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
+                            help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.redirect_default_code:
+                            type: select
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE
+                            help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP
+                            options:
+                                301: 301 - Permanent
+                                302: 302 - Found
+                                303: 303 - Other
+                                304: 304 - Not Modified
+
+                        pages.redirect_trailing_slash:
+                            type: toggle
+                            label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH
+                            help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.ignore_hidden:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IGNORE_HIDDEN
+                            help: PLUGIN_ADMIN.IGNORE_HIDDEN_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.ignore_files:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.IGNORE_FILES
+                            help: PLUGIN_ADMIN.IGNORE_FILES_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        pages.ignore_folders:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.IGNORE_FOLDERS
+                            help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        pages.hide_empty_folders:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS
+                            help: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        pages.url_taxonomy_filters:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS
+                            help: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.twig_first:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_FIRST
+                            help: PLUGIN_ADMIN.TWIG_FIRST_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.never_cache_twig:
+                            type: toggle
+                            label: PLUGIN_ADMIN.NEVER_CACHE_TWIG
+                            help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.frontmatter.process_twig:
+                            type: toggle
+                            label: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG
+                            help: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.frontmatter.ignore_fields:
+                            type: selectize
+                            size: large
+                            placeholder: "e.g. forms"
+                            label: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS
+                            help: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                languages:
+                    type: tab
+                    title: PLUGIN_ADMIN.LANGUAGES
+
+                    fields:
+                        languages-section:
+                            type: section
+                            title: PLUGIN_ADMIN.LANGUAGES
+                            underline: true
+
+                        languages.supported:
+                            type: selectize
+                            size: large
+                            placeholder: "e.g. en, fr"
+                            label: PLUGIN_ADMIN.SUPPORTED
+                            help: PLUGIN_ADMIN.SUPPORTED_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        languages.default_lang:
+                            type: text
+                            size: x-small
+                            label: PLUGIN_ADMIN.DEFAULT_LANG
+                            help: PLUGIN_ADMIN.DEFAULT_LANG_HELP
+
+                        languages.include_default_lang:
+                            type: toggle
+                            label: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG
+                            help: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.pages_fallback_only:
+                            type: toggle
+                            label: PLUGIN_ADMIN.PAGES_FALLBACK_ONLY
+                            help: PLUGIN_ADMIN.PAGES_FALLBACK_ONLY_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.translations:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TRANSLATIONS_ENABLED
+                            help: PLUGIN_ADMIN.TRANSLATIONS_ENABLED_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.translations_fallback:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK
+                            help: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.session_store_active:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION
+                            help: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.http_accept_language:
+                            type: toggle
+                            label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE
+                            help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.override_locale:
+                            type: toggle
+                            label: PLUGIN_ADMIN.OVERRIDE_LOCALE
+                            help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                http_headers:
+                    type: tab
+                    title: PLUGIN_ADMIN.HTTP_HEADERS
+
+                    fields:
+                        http_headers_section:
+                            type: section
+                            title: PLUGIN_ADMIN.HTTP_HEADERS
+                            underline: true
+
+                        pages.expires:
+                            type: text
+                            size: small
+                            append: GRAV.NICETIME.SECOND_PLURAL
+                            label: PLUGIN_ADMIN.EXPIRES
+                            help: PLUGIN_ADMIN.EXPIRES_HELP
+                            validate:
+                                type: number
+                                min: 1
+                        pages.cache_control:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.CACHE_CONTROL
+                            help: PLUGIN_ADMIN.CACHE_CONTROL_HELP
+                            placeholder: 'e.g. public, max-age=31536000'
+                        pages.last_modified:
+                            type: toggle
+                            label: PLUGIN_ADMIN.LAST_MODIFIED
+                            help: PLUGIN_ADMIN.LAST_MODIFIED_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.etag:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ETAG
+                            help: PLUGIN_ADMIN.ETAG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.vary_accept_encoding:
+                            type: toggle
+                            label: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING
+                            help: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                markdown:
+                    type: tab
+                    title: PLUGIN_ADMIN.MARKDOWN
+
+                    fields:
+                        markdow_section:
+                            type: section
+                            title: PLUGIN_ADMIN.MARKDOWN
+                            underline: true
+
+                        pages.markdown.extra:
+                            type: toggle
+                            label: Markdown extra
+                            help: PLUGIN_ADMIN.MARKDOWN_EXTRA_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.markdown.auto_line_breaks:
+                            type: toggle
+                            label: PLUGIN_ADMIN.AUTO_LINE_BREAKS
+                            help: PLUGIN_ADMIN.AUTO_LINE_BREAKS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.markdown.auto_url_links:
+                            type: toggle
+                            label: PLUGIN_ADMIN.AUTO_URL_LINKS
+                            help: PLUGIN_ADMIN.AUTO_URL_LINKS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.markdown.escape_markup:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ESCAPE_MARKUP
+                            help: PLUGIN_ADMIN.ESCAPE_MARKUP_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                caching:
+                    type: tab
+                    title: PLUGIN_ADMIN.CACHING
+
+                    fields:
+                        caching_section:
+                            type: section
+                            title: PLUGIN_ADMIN.CACHING
+                            underline: true
+
+                        cache.enabled:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CACHING
+                            help: PLUGIN_ADMIN.CACHING_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.check.method:
+                            type: select
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.CACHE_CHECK_METHOD
+                            help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP
+                            options:
+                                file: Markdown + Yaml file timestamps
+                                folder: Folder timestamps
+                                hash: All files timestamps
+                                none: No timestamp checking
+
+                        cache.driver:
+                            type: select
+                            size: small
+                            classes: fancy
+                            label: PLUGIN_ADMIN.CACHE_DRIVER
+                            help: PLUGIN_ADMIN.CACHE_DRIVER_HELP
+                            options:
+                                auto: Auto detect
+                                file: File
+                                apc: APC
+                                apcu: APCu
+                                xcache: Xcache
+                                memcache: Memcache
+                                memcached: Memcached
+                                wincache: WinCache
+                                redis: Redis
+
+                        cache.prefix:
+                            type: text
+                            size: x-small
+                            label: PLUGIN_ADMIN.CACHE_PREFIX
+                            help: PLUGIN_ADMIN.CACHE_PREFIX_HELP
+                            placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER
+
+                        cache.purge_at:
+                            type: cron
+                            label: PLUGIN_ADMIN.CACHE_PURGE_JOB
+                            help: PLUGIN_ADMIN.CACHE_PURGE_JOB_HELP
+                            default: '* 4 * * *'
+
+                        cache.clear_at:
+                            type: cron
+                            label: PLUGIN_ADMIN.CACHE_CLEAR_JOB
+                            help: PLUGIN_ADMIN.CACHE_CLEAR_JOB_HELP
+                            default: '* 3 * * *'
+
+                        cache.clear_job_type:
+                            type: select
+                            size: medium
+                            label: PLUGIN_ADMIN.CACHE_JOB_TYPE
+                            help: PLUGIN_ADMIN.CACHE_JOB_TYPE_HELP
+                            options:
+                                standard: Standard Cache Folders
+                                all: All Cache Folders
+
+                        cache.clear_images_by_default:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT
+                            help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.cli_compatibility:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CLI_COMPATIBILITY
+                            help: PLUGIN_ADMIN.CLI_COMPATIBILITY_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.lifetime:
+                            type: text
+                            size: small
+                            append: GRAV.NICETIME.SECOND_PLURAL
+                            label: PLUGIN_ADMIN.LIFETIME
+                            help: PLUGIN_ADMIN.LIFETIME_HELP
+                            validate:
+                                type: number
+
+                        cache.gzip:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GZIP_COMPRESSION
+                            help: PLUGIN_ADMIN.GZIP_COMPRESSION_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.allow_webserver_gzip:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP
+                            help: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.memcache.server:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.MEMCACHE_SERVER
+                            help: PLUGIN_ADMIN.MEMCACHE_SERVER_HELP
+                            placeholder: "localhost"
+
+                        cache.memcache.port:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.MEMCACHE_PORT
+                            help: PLUGIN_ADMIN.MEMCACHE_PORT_HELP
+                            placeholder: "11211"
+
+                        cache.memcached.server:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.MEMCACHED_SERVER
+                            help: PLUGIN_ADMIN.MEMCACHED_SERVER_HELP
+                            placeholder: "localhost"
+
+                        cache.memcached.port:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.MEMCACHED_PORT
+                            help: PLUGIN_ADMIN.MEMCACHED_PORT_HELP
+                            placeholder: "11211"
+
+                        cache.redis.socket:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.REDIS_SOCKET
+                            help: PLUGIN_ADMIN.REDIS_SOCKET_HELP
+                            placeholder: "/var/run/redis/redis.sock"
+
+                        cache.redis.server:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.REDIS_SERVER
+                            help: PLUGIN_ADMIN.REDIS_SERVER_HELP
+                            placeholder: "localhost"
+
+                        cache.redis.port:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.REDIS_PORT
+                            help: PLUGIN_ADMIN.REDIS_PORT_HELP
+                            placeholder: "6379"
+
+                        cache.redis.password:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.REDIS_PASSWORD
+
+                twig:
+                    type: tab
+                    title: PLUGIN_ADMIN.TWIG_TEMPLATING
+
+                    fields:
+                        twig_section:
+                            type: section
+                            title: PLUGIN_ADMIN.TWIG_TEMPLATING
+                            underline: true
+
+                        twig.cache:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_CACHING
+                            help: PLUGIN_ADMIN.TWIG_CACHING_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.debug:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_DEBUG
+                            help: PLUGIN_ADMIN.TWIG_DEBUG_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.auto_reload:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DETECT_CHANGES
+                            help: PLUGIN_ADMIN.DETECT_CHANGES_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.autoescape:
+                            type: toggle
+                            label: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES
+                            help: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.umask_fix:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_UMASK_FIX
+                            help: PLUGIN_ADMIN.TWIG_UMASK_FIX_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                assets:
+                    type: tab
+                    title: PLUGIN_ADMIN.ASSETS
+
+                    fields:
+                        assets_section:
+                            type: section
+                            title: PLUGIN_ADMIN.ASSETS
+                            underline: true
+
+                        assets.css_pipeline:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_PIPELINE
+                            help: PLUGIN_ADMIN.CSS_PIPELINE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_pipeline_include_externals:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS
+                            help: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_pipeline_before_excludes:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES
+                            help: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_minify:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_MINIFY
+                            help: PLUGIN_ADMIN.CSS_MINIFY_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_minify_windows:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE
+                            help: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.css_rewrite:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_REWRITE
+                            help: PLUGIN_ADMIN.CSS_REWRITE_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.js_pipeline:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE
+                            help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.js_pipeline_include_externals:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS
+                            help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.js_pipeline_before_excludes:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES
+                            help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.js_minify:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY
+                            help: PLUGIN_ADMIN.JAVASCRIPT_MINIFY_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.enable_asset_timestamp:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS
+                            help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.collections:
+                            type: multilevel
+                            label: PLUGIN_ADMIN.COLLECTIONS
+                            placeholder_key: collection_name
+                            placeholder_value: collection_path
+                            validate:
+                                type: array
+
+                errors:
+                    type: tab
+                    title: PLUGIN_ADMIN.ERROR_HANDLER
+
+                    fields:
+                        errors_section:
+                            type: section
+                            title: PLUGIN_ADMIN.ERROR_HANDLER
+                            underline: true
+
+                        errors.display:
+                            type: select
+                            label: PLUGIN_ADMIN.DISPLAY_ERRORS
+                            help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP
+                            size: medium
+                            highlight: 1
+                            options:
+                                -1: PLUGIN_ADMIN.ERROR_SYSTEM
+                                0: PLUGIN_ADMIN.ERROR_SIMPLE
+                                1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE
+                            validate:
+                                type: int
+
+
+                        errors.log:
+                            type: toggle
+                            label: PLUGIN_ADMIN.LOG_ERRORS
+                            help: PLUGIN_ADMIN.LOG_ERRORS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        log.handler:
+                            type: select
+                            size: small
+                            label: PLUGIN_ADMIN.LOG_HANDLER
+                            help: PLUGIN_ADMIN.LOG_HANDLER_HELP
+                            default: 'file'
+                            options:
+                                'file': 'File'
+                                'syslog': 'Syslog'
+
+                        log.syslog.facility:
+                            type: select
+                            size: small
+                            label: PLUGIN_ADMIN.SYSLOG_FACILITY
+                            help: PLUGIN_ADMIN.SYSLOG_FACILITY_HELP
+                            default: local6
+                            options:
+                              auth: auth
+                              authpriv: authpriv
+                              cron: cron
+                              daemon: daemon
+                              kern: kern
+                              lpr: lpr
+                              mail: mail
+                              news: news
+                              syslog: syslog
+                              user: user
+                              uucp: uucp
+                              local0: local0
+                              local1: local1
+                              local2: local2
+                              local3: local3
+                              local4: local4
+                              local5: local5
+                              local6: local6
+                              local7: local7
+
+                debugger:
+                    type: tab
+                    title: PLUGIN_ADMIN.DEBUGGER
+
+                    fields:
+                        debugger_section:
+                            type: section
+                            title: PLUGIN_ADMIN.DEBUGGER
+                            underline: true
+
+                        debugger.enabled:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DEBUGGER
+                            help: PLUGIN_ADMIN.DEBUGGER_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        debugger.shutdown.close_connection:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION
+                            help: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                media:
+                    type: tab
+                    title: PLUGIN_ADMIN.MEDIA
+
+                    fields:
+                        media_section:
+                            type: section
+                            title: PLUGIN_ADMIN.MEDIA
+                            underline: true
+
+                        images.default_image_quality:
+                            type: range
+                            append: '%'
+                            label: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY
+                            help: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY_HELP
+                            validate:
+                                min: 1
+                                max: 100
+
+                        images.cache_all:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CACHE_ALL
+                            help: PLUGIN_ADMIN.CACHE_ALL_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        images.cache_perms:
+                            type: select
+                            size: small
+                            label: PLUGIN_ADMIN.CACHE_PERMS
+                            help: PLUGIN_ADMIN.CACHE_PERMS_HELP
+                            highlight: '0755'
+                            options:
+                                '0755': '0755'
+                                '0775': '0775'
+
+                        images.debug:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IMAGES_DEBUG
+                            help: PLUGIN_ADMIN.IMAGES_DEBUG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        images.auto_fix_orientation:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION
+                            help: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        images.seofriendly:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IMAGES_SEOFRIENDLY
+                            help: PLUGIN_ADMIN.IMAGES_SEOFRIENDLY_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        media.enable_media_timestamp:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP
+                            help: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        media.auto_metadata_exif:
+                              type: toggle
+                              label: PLUGIN_ADMIN.ENABLE_AUTO_METADATA
+                              help: PLUGIN_ADMIN.ENABLE_AUTO_METADATA_HELP
+                              highlight: 0
+                              options:
+                                  1: PLUGIN_ADMIN.YES
+                                  0: PLUGIN_ADMIN.NO
+                              validate:
+                                  type: bool
+
+
+                        media.allowed_fallback_types:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.FALLBACK_TYPES
+                            help: PLUGIN_ADMIN.FALLBACK_TYPES_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        media.unsupported_inline_types:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.INLINE_TYPES
+                            help: PLUGIN_ADMIN.INLINE_TYPES_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                session:
+                    type: tab
+                    title: PLUGIN_ADMIN.SESSION
+
+                    fields:
+                        session_section:
+                            type: section
+                            title: PLUGIN_ADMIN.SESSION
+                            underline: true
+
+                        session.enabled:
+                            type: hidden
+                            label: PLUGIN_ADMIN.ENABLED
+                            help: PLUGIN_ADMIN.SESSION_ENABLED_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        session.initialize:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_INITIALIZE
+                            help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        session.timeout:
+                            type: text
+                            size: small
+                            append: GRAV.NICETIME.SECOND_PLURAL
+                            label: PLUGIN_ADMIN.TIMEOUT
+                            help: PLUGIN_ADMIN.TIMEOUT_HELP
+                            validate:
+                                type: number
+                                min: 0
+
+                        session.name:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.NAME
+                            help: PLUGIN_ADMIN.SESSION_NAME_HELP
+
+                        session.uniqueness:
+                            type: select
+                            size: medium
+                            label: PLUGIN_ADMIN.SESSION_UNIQUENESS
+                            help: PLUGIN_ADMIN.SESSION_UNIQUENESS_HELP
+                            highlight: path
+                            default: path
+                            options:
+                                path: Grav's root file path
+                                salt: Grav's random security salt
+
+                        session.secure:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_SECURE
+                            help: PLUGIN_ADMIN.SESSION_SECURE_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: false
+                            validate:
+                                type: bool
+
+                        session.httponly:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_HTTPONLY
+                            help: PLUGIN_ADMIN.SESSION_HTTPONLY_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        session.path:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.SESSION_PATH
+                            help: PLUGIN_ADMIN.SESSION_PATH_HELP
+
+                        session.split:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_SPLIT
+                            help: PLUGIN_ADMIN.SESSION_SPLIT_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                advanced:
+                    type: tab
+                    title: PLUGIN_ADMIN.ADVANCED
+
+                    fields:
+                        advanced_section:
+                            type: section
+                            title: PLUGIN_ADMIN.ADVANCED
+                            underline: true
+
+                        gpm.releases:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_RELEASES
+                            highlight: stable
+                            help: PLUGIN_ADMIN.GPM_RELEASES_HELP
+                            options:
+                                stable: PLUGIN_ADMIN.STABLE
+                                testing: PLUGIN_ADMIN.TESTING
+
+                        gpm.proxy_url:
+                            type: text
+                            size: medium
+                            placeholder: "e.g. 127.0.0.1:3128"
+                            label: PLUGIN_ADMIN.PROXY_URL
+                            help: PLUGIN_ADMIN.PROXY_URL_HELP
+
+                        gpm.method:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_METHOD
+                            highlight: auto
+                            help: PLUGIN_ADMIN.GPM_METHOD_HELP
+                            options:
+                                auto: PLUGIN_ADMIN.AUTO
+                                fopen: PLUGIN_ADMIN.FOPEN
+                                curl: PLUGIN_ADMIN.CURL
+
+                        gpm.official_gpm_only:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY
+                            highlight: 1
+                            help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        gpm.verify_peer:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_VERIFY_PEER
+                            highlight: 1
+                            help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        reverse_proxy_setup:
+                            type: toggle
+                            label: PLUGIN_ADMIN.REVERSE_PROXY
+                            highlight: 0
+                            help: PLUGIN_ADMIN.REVERSE_PROXY_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        username_regex:
+                            type: text
+                            size: large
+                            label: PLUGIN_ADMIN.USERNAME_REGEX
+                            help: PLUGIN_ADMIN.USERNAME_REGEX_HELP
+
+                        pwd_regex:
+                            type: text
+                            size: large
+                            label: PLUGIN_ADMIN.PWD_REGEX
+                            help: PLUGIN_ADMIN.PWD_REGEX_HELP
+
+                        intl_enabled:
+                            type: toggle
+                            label: PLUGIN_ADMIN.INTL_ENABLED
+                            highlight: 1
+                            help: PLUGIN_ADMIN.INTL_ENABLED_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        wrapped_site:
+                            type: toggle
+                            label: PLUGIN_ADMIN.WRAPPED_SITE
+                            highlight: 0
+                            help: PLUGIN_ADMIN.WRAPPED_SITE_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        absolute_urls:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ABSOLUTE_URLS
+                            highlight: 0
+                            help: PLUGIN_ADMIN.ABSOLUTE_URLS_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        param_sep:
+                            type: select
+                            size: medium
+                            label: PLUGIN_ADMIN.PARAMETER_SEPARATOR
+                            classes: fancy
+                            help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP
+                            default: ''
+                            options:
+                                ':': ': (default)'
+                                ';': '; (for Apache running on Windows)'
+
+                        force_ssl:
+                            type: toggle
+                            label: PLUGIN_ADMIN.FORCE_SSL
+                            highlight: 0
+                            help: PLUGIN_ADMIN.FORCE_SSL_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        force_lowercase_urls:
+                            type: toggle
+                            label: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS
+                            highlight: 1
+                            default: 1
+                            help: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        custom_base_url:
+                            type: text
+                            size: medium
+                            placeholder: "e.g. http://yoursite.com/yourpath"
+                            label: PLUGIN_ADMIN.CUSTOM_BASE_URL
+                            help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
+
+                        accounts.type:
+                            type: hidden
+
+                        accounts.storage:
+                            type: hidden
+
+                        strict_mode.yaml_compat:
+                            type: toggle
+                            label: PLUGIN_ADMIN.STRICT_YAML_COMPAT
+                            highlight: 1
+                            default: 1
+                            help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        strict_mode.twig_compat:
+                            type: toggle
+                            label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
+                            highlight: 1
+                            default: 1
+                            help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool

+ 121 - 119
system/blueprints/user/account.yaml

@@ -4,122 +4,124 @@ form:
 
     fields:
 
-            info:
-                type: userinfo
-                size: large
-
-            avatar:
-                type: file
-                size: large
-                destination: 'user://accounts/avatars'
-                multiple: false
-                random_name: true
-
-            content:
-                type: section
-                title: PLUGIN_ADMIN.ACCOUNT
-                underline: true
-
-            username:
-                type: text
-                size: large
-                label: PLUGIN_ADMIN.USERNAME
-                disabled: true
-                readonly: true
-
-            email:
-                type: email
-                size: large
-                label: PLUGIN_ADMIN.EMAIL
-                validate:
-                  type: email
-                  message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE
-                  required: true
-
-            password:
-                type: password
-                size: large
-                label: PLUGIN_ADMIN.PASSWORD
-                autocomplete: new-password
-                validate:
-                  required: false
-                  message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
-                  config-pattern@: system.pwd_regex
-
-            fullname:
-                type: text
-                size: large
-                label: PLUGIN_ADMIN.FULL_NAME
-                validate:
-                  required: true
-
-            title:
-                type: text
-                size: large
-                label: PLUGIN_ADMIN.TITLE
-
-            language:
-                type: select
-                label: PLUGIN_ADMIN.LANGUAGE
-                size: medium
-                classes: fancy
-                data-options@: '\Grav\Plugin\Admin\Admin::adminLanguages'
-                default: 'en'
-                help: PLUGIN_ADMIN.LANGUAGE_HELP
-
-            twofa_check:
-                type: conditional
-                condition: config.plugins.admin.twofa_enabled
-
-                fields:
-
-                    twofa:
-                        title: PLUGIN_ADMIN.2FA_TITLE
-                        type: section
-                        underline: true
-
-                    twofa_enabled:
-                        type: toggle
-                        label: PLUGIN_ADMIN.2FA_ENABLED
-                        classes: twofa-toggle
-                        highlight: 1
-                        default: 0
-                        options:
-                          1: PLUGIN_ADMIN.YES
-                          0: PLUGIN_ADMIN.NO
-                        validate:
-                          type: bool
-
-
-                    twofa_secret:
-                        type: 2fa_secret
-                        outerclasses: 'twofa-secret'
-                        markdown: true
-                        label: PLUGIN_ADMIN.2FA_SECRET
-                        sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP
-
-
-            security:
-                title: PLUGIN_ADMIN.ACCESS_LEVELS
-                type: section
-                security: admin.super
-                underline: true
-
-                fields:
-                    groups:
-                        type: select
-                        multiple: true
-                        size: large
-                        label: PLUGIN_ADMIN.GROUPS
-                        data-options@: '\Grav\Common\User\Group::groupNames'
-                        classes: fancy
-                        help: PLUGIN_ADMIN.GROUPS_HELP
-                        validate:
-                            type: commalist
-
-                    access:
-                        type: permissions
-                        label: PLUGIN_ADMIN.PERMISSIONS
-                        ignore_empty: true
-                        validate:
-                            type: array
+        info:
+            type: userinfo
+            size: large
+
+        avatar:
+            type: file
+            size: large
+            destination: 'user://accounts/avatars'
+            multiple: false
+            random_name: true
+
+        content:
+            type: section
+            title: PLUGIN_ADMIN.ACCOUNT
+            underline: true
+
+        username:
+            type: text
+            size: large
+            label: PLUGIN_ADMIN.USERNAME
+            disabled: true
+            readonly: true
+
+        email:
+            type: email
+            size: large
+            label: PLUGIN_ADMIN.EMAIL
+            validate:
+              type: email
+              message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE
+              required: true
+
+        password:
+            type: password
+            size: large
+            label: PLUGIN_ADMIN.PASSWORD
+            autocomplete: new-password
+            validate:
+              required: false
+              message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
+              config-pattern@: system.pwd_regex
+
+        fullname:
+            type: text
+            size: large
+            label: PLUGIN_ADMIN.FULL_NAME
+            validate:
+              required: true
+
+        title:
+            type: text
+            size: large
+            label: PLUGIN_ADMIN.TITLE
+
+        language:
+            type: select
+            label: PLUGIN_ADMIN.LANGUAGE
+            size: medium
+            classes: fancy
+            data-options@: '\Grav\Plugin\Admin\Admin::adminLanguages'
+            default: 'en'
+            help: PLUGIN_ADMIN.LANGUAGE_HELP
+
+        twofa_check:
+            type: conditional
+            condition: config.plugins.admin.twofa_enabled
+
+            fields:
+
+                twofa:
+                    title: PLUGIN_ADMIN.2FA_TITLE
+                    type: section
+                    underline: true
+
+                twofa_enabled:
+                    type: toggle
+                    label: PLUGIN_ADMIN.2FA_ENABLED
+                    classes: twofa-toggle
+                    highlight: 1
+                    default: 0
+                    options:
+                      1: PLUGIN_ADMIN.YES
+                      0: PLUGIN_ADMIN.NO
+                    validate:
+                      type: bool
+
+
+                twofa_secret:
+                    type: 2fa_secret
+                    outerclasses: 'twofa-secret'
+                    markdown: true
+                    label: PLUGIN_ADMIN.2FA_SECRET
+                    sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP
+
+
+        security:
+            security@: admin.super
+            title: PLUGIN_ADMIN.ACCESS_LEVELS
+            type: section
+            underline: true
+
+            fields:
+                groups:
+                    security@: admin.super
+                    type: select
+                    multiple: true
+                    size: large
+                    label: PLUGIN_ADMIN.GROUPS
+                    data-options@: '\Grav\Common\User\Group::groupNames'
+                    classes: fancy
+                    help: PLUGIN_ADMIN.GROUPS_HELP
+                    validate:
+                        type: commalist
+
+                access:
+                    security@: admin.super
+                    type: permissions
+                    label: PLUGIN_ADMIN.PERMISSIONS
+                    ignore_empty: true
+                    validate:
+                        type: array

+ 29 - 0
system/blueprints/user/accounts.yaml

@@ -0,0 +1,29 @@
+title: User Accounts
+description: User Accounts
+type: flex-objects
+
+extends@: 'user/account'
+
+config:
+  admin:
+    list:
+      fields:
+        username:
+          link: edit
+          search: true
+        email:
+          search: true
+        fullname:
+          search: true
+      options:
+        per_page: 20
+        order:
+          by: username
+          dir: asc
+
+    menu:
+      list:
+        route: '/accounts'
+        title: Accounts
+        icon: fa-users
+        authorize: ['admin.users', 'admin.accounts', 'admin.super']

+ 15 - 0
system/config/backups.yaml

@@ -0,0 +1,15 @@
+purge:
+    trigger: space
+    max_backups_count: 25
+    max_backups_space: 5
+    max_backups_time: 365
+
+profiles:
+  -
+    name: 'Default Site Backup'
+    root: '/'
+    schedule: false
+    schedule_at: '0 3 * * *'
+    exclude_paths: "/backup\r\n/cache\r\n/images\r\n/logs\r\n/tmp"
+    exclude_files: ".DS_Store\r\n.git\r\n.svn\r\n.hg\r\n.idea\r\n.vscode\r\nnode_modules"
+

+ 7 - 0
system/config/security.yaml

@@ -5,6 +5,13 @@ xss_enabled:
     moz_binding: true
     html_inline_styles: true
     dangerous_tags: true
+xss_invalid_protocols:
+    - javascript
+    - livescript
+    - vbscript
+    - mocha
+    - feed
+    - data
 xss_dangerous_tags:
     - applet
     - meta

+ 5 - 5
system/config/site.yaml

@@ -17,17 +17,17 @@ summary:
   delimiter: ===                            # The summary delimiter
 
 redirects:
-#  '/redirect-test': '/'                         # Redirect test goes to home page
-#  '/old/(.*)': '/new/$1'                        # Would redirect /old/my-page to /new/my-page
+#  '/redirect-test': '/'                    # Redirect test goes to home page
+#  '/old/(.*)': '/new/$1'                   # Would redirect /old/my-page to /new/my-page
 
 routes:
-#  '/something/else': '/blog/sample-3'         # Alias for /blog/sample-3
-#  '/new/(.*)': '/blog/$1'                     # Regex any /new/my-page URL to /blog/my-page Route
+#  '/something/else': '/blog/sample-3'      # Alias for /blog/sample-3
+#  '/new/(.*)': '/blog/$1'                  # Regex any /new/my-page URL to /blog/my-page Route
 
 blog:
   route: '/blog'                            # Custom value added (accessible via system.blog.route)
 
-#menu:                                      # Sample Menu Example
+#menu:                                      # Menu Example
 #    - text: Source
 #      icon: github
 #      url: https://github.com/getgrav/grav

+ 1 - 1
system/config/streams.yaml

@@ -1,6 +1,6 @@
 schemes:
   image:
-    type: ReadOnlyStream
+    type: Stream
     paths:
       - user://images
       - system://images

+ 19 - 2
system/config/system.yaml

@@ -13,7 +13,9 @@ intl_enabled: true                               # Special logic for PHP Interna
 
 languages:
   supported: []                                  # List of languages supported. eg: [en, fr, de]
+  default_lang:                                  # Default is the first supported language. Must be one of the supported languages
   include_default_lang: true                     # Include the default lang prefix in all URLs
+  pages_fallback_only: false                     # Only fallback to find page content through supported languages
   translations: true                             # Enable translations by default
   translations_fallback: true                    # Fallback through supported translations if active lang doesn't exist
   session_store_active: false                    # Store active language in session
@@ -52,7 +54,7 @@ pages:
     special_chars:                               # List of special characters to automatically convert to entities
       '>': 'gt'
       '<': 'lt'
-  types: [txt,xml,html,htm,json,rss,atom]        # list of valid page types
+  types: [html,htm,xml,txt,json,rss,atom]        # list of valid page types
   append_url_extension: ''                       # Append page's extension in Page urls (e.g. '.html' results in /path/page.html)
   expires: 604800                                # Page expires time in seconds (604800 seconds = 7 days)
   cache_control:                                 # Can be blank for no setting, or a valid `cache-control` text value
@@ -65,6 +67,7 @@ pages:
   ignore_files: [.DS_Store]                      # Files to ignore in Pages
   ignore_folders: [.git, .idea]                  # Folders to ignore in Pages
   ignore_hidden: true                            # Ignore all Hidden files and folders
+  hide_empty_folders: false                      # If folder has no .md file, should it be hidden
   url_taxonomy_filters: true                     # Enable auto-magic URL-based taxonomy filters for page collections
   frontmatter:
     process_twig: false                          # Should the frontmatter be processed to replace Twig variables?
@@ -74,8 +77,11 @@ cache:
   enabled: true                                  # Set to true to enable caching
   check:
     method: file                                 # Method to check for updates in pages: file|folder|hash|none
-  driver: auto                                   # One of: auto|file|apc|xcache|memcache|wincache
+  driver: auto                                   # One of: auto|file|apcu|memcache|wincache
   prefix: 'g'                                    # Cache prefix string (prevents cache conflicts)
+  purge_at: '0 4 * * *'                          # How often to purge old file cache (using new scheduler)
+  clear_at: '0 3 * * *'                           # How often to clear cache (using new scheduler)
+  clear_job_type: 'standard'                     # Type to clear when processing the scheduled clear job `standard`|`all`
   clear_images_by_default: true                  # By default grav will include processed images in cache clear, this can be disabled
   cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
@@ -112,6 +118,11 @@ errors:
   display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error
   log: true                                      # Log errors to /logs folder
 
+log:
+  handler: file                                 # Log handler. Currently supported: file | syslog
+  syslog:
+    facility: local6                            # Syslog facilities output
+
 debugger:
   enabled: false                                 # Enable Grav debugger and following settings
   shutdown:
@@ -123,6 +134,7 @@ images:
   cache_perms: '0755'                            # MUST BE IN QUOTES!! Default cache folder perms. Usually '0755' or '0775'
   debug: false                                   # Show an overlay over images indicating the pixel depth of the image when working with retina for example
   auto_fix_orientation: false                    # Automatically fix the image orientation based on the Exif data
+  seofriendly: false                             # SEO-friendly processed image names
 
 media:
   enable_media_timestamp: false                  # Enable media timestamps
@@ -135,6 +147,7 @@ session:
   initialize: true                               # Initialize session from Grav (if false, plugin needs to start the session)
   timeout: 1800                                  # Timeout in seconds
   name: grav-site                                # Name prefix of the session cookie. Use alphanumeric, dashes or underscores only. Do not use dots in the session name
+  uniqueness: path                               # Should sessions be `path` based or `security.salt` based
   secure: false                                  # Set session secure. If true, indicates that communication for this cookie must be over an encrypted transmission. Enable this only on sites that run exclusively on HTTPS
   httponly: true                                 # Set session HTTP only. If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed.
   split: true                                    # Sessions should be independent between site and plugins (such as admin)
@@ -147,6 +160,10 @@ gpm:
   verify_peer: true                              # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
   official_gpm_only: true                        # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
 
+accounts:
+  type: data                                     # Account type: data or flex
+  storage: file                                  # Flex storage type: file or folder
+
 strict_mode:
   yaml_compat: true                              # Grav 1.5+: Enables YAML backwards compatibility
   twig_compat: true                              # Grav 1.5+: Enables deprecated Twig autoescape setting (autoescape: false)

+ 4 - 4
system/defines.php

@@ -1,19 +1,19 @@
 <?php
 /**
- * @package    Grav.Core
+ * @package    Grav\Core
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 // Some standard defines
 define('GRAV', true);
-define('GRAV_VERSION', '1.5.8');
+define('GRAV_VERSION', '1.6.8');
 define('GRAV_TESTING', false);
 define('DS', '/');
 
 if (!defined('GRAV_PHP_MIN')) {
-    define('GRAV_PHP_MIN', '5.6.4');
+    define('GRAV_PHP_MIN', '7.1.3');
 }
 
 // Directories and Paths

+ 17 - 0
system/install.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * @package    Grav\Core
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+if (!defined('GRAV_ROOT')) {
+    die();
+}
+
+use Grav\Installer\Install;
+
+require_once __DIR__ . '/src/Grav/Installer/Install.php';
+
+return Install::instance();

+ 62 - 74
system/languages/ar.yaml

@@ -1,75 +1,63 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  العنوان: %1$s
-  ---
-  # خطأ: مادة أمامية غير صحيحة
-  
-  مسار: '%2$s'
-  
-  **%3$s**
-  
-  , , ,
-  
-  %4$s
-  , , ,
-NICETIME:
-  NO_DATE_PROVIDED: لم يتم تقديم التاريخ
-  BAD_DATE: تاريخ خاطئ
-  AGO: من قبل
-  FROM_NOW: من الآن
-  SECOND: ثانية
-  MINUTE: دقيقة
-  HOUR: ساعة
-  DAY: يوم
-  WEEK: أسبوع
-  MONTH: شهر
-  YEAR: سنة
-  DECADE: عقد
-  SEC: ثانية
-  MIN: دقيقة
-  HR: ساعة
-  WK: أسبوع
-  MO: شهر
-  YR: سنة
-  DEC: عقد
-  SECOND_PLURAL: ثواني
-  MINUTE_PLURAL: ‮دقائق
-  HOUR_PLURAL: ساعات
-  DAY_PLURAL: أيام
-  WEEK_PLURAL: أسابيع
-  MONTH_PLURAL: أشهر
-  YEAR_PLURAL: سنوات
-  DECADE_PLURAL: عقود
-  SEC_PLURAL: ثواني
-  MIN_PLURAL: دقائق
-  HR_PLURAL: ساعات
-  WK_PLURAL: أسابيع
-  MO_PLURAL: أشهر
-  YR_PLURAL: سنوات
-  DEC_PLURAL: عقود
-FORM:
-  VALIDATION_FAIL: '<b>فشل التحقق من صحة:</b>'
-  INVALID_INPUT: إدخال غير صحيح في
-  MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:'
-MONTHS_OF_THE_YEAR:
-  - كانون الثاني
-  - شباط
-  - آذار/ مارس
-  - نيسان
-  - أيار
-  - حزيران
-  - تموز
-  - آب
-  - أيلول
-  - تشرين الأول
-  - تشرين الثاني
-  - كانون الأول
-DAYS_OF_THE_WEEK:
-  - الاثنين
-  - الثلاثاء
-  - الأربعاء
-  - الخميس
-  - الجمعة
-  - السبت
-  - الأحد
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nالعنوان: %1$s\n---\n# خطأ: مادة أمامية غير صحيحة\n\nمسار: '%2$s'\n\n**%3$s**\n\n, , ,\n\n%4$s\n, , ,"
+  NICETIME:
+    NO_DATE_PROVIDED: لم يتم تقديم التاريخ
+    BAD_DATE: تاريخ خاطئ
+    AGO: من قبل
+    FROM_NOW: من الآن
+    SECOND: ثانية
+    MINUTE: دقيقة
+    HOUR: ساعة
+    DAY: يوم
+    WEEK: أسبوع
+    MONTH: شهر
+    YEAR: سنة
+    DECADE: عقد
+    SEC: ثانية
+    MIN: دقيقة
+    HR: ساعة
+    WK: أسبوع
+    MO: شهر
+    YR: سنة
+    DEC: عقد
+    SECOND_PLURAL: ثواني
+    MINUTE_PLURAL: '‮دقائق'
+    HOUR_PLURAL: ساعات
+    DAY_PLURAL: أيام
+    WEEK_PLURAL: أسابيع
+    MONTH_PLURAL: أشهر
+    YEAR_PLURAL: سنوات
+    DECADE_PLURAL: عقود
+    SEC_PLURAL: ثواني
+    MIN_PLURAL: دقائق
+    HR_PLURAL: ساعات
+    WK_PLURAL: أسابيع
+    MO_PLURAL: أشهر
+    YR_PLURAL: سنوات
+    DEC_PLURAL: عقود
+  FORM:
+    VALIDATION_FAIL: <b>فشل التحقق من صحة:</b>
+    INVALID_INPUT: إدخال غير صحيح في
+    MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:'
+  MONTHS_OF_THE_YEAR:
+    - 'كانون الثاني'
+    - 'شباط'
+    - 'آذار/ مارس'
+    - 'نيسان'
+    - 'أيار'
+    - 'حزيران'
+    - 'تموز'
+    - 'آب'
+    - 'أيلول'
+    - 'تشرين الأول'
+    - 'تشرين الثاني'
+    - 'كانون الأول'
+  DAYS_OF_THE_WEEK:
+    - 'الاثنين'
+    - 'الثلاثاء'
+    - 'الأربعاء'
+    - 'الخميس'
+    - 'الجمعة'
+    - 'السبت'
+    - 'الأحد'

+ 62 - 0
system/languages/bg.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  NICETIME:
+    NO_DATE_PROVIDED: Не е въведена дата
+    BAD_DATE: Невалидна дата
+    AGO: преди
+    FROM_NOW: от сега
+    SECOND: секунда
+    MINUTE: минута
+    HOUR: час
+    DAY: ден
+    WEEK: седмица
+    MONTH: месец
+    YEAR: година
+    DECADE: десетилетие
+    SEC: сек
+    MIN: мин
+    HR: ч
+    WK: седм
+    MO: мес
+    YR: г
+    DEC: дстлт
+    SECOND_PLURAL: секунди
+    MINUTE_PLURAL: минути
+    HOUR_PLURAL: часа
+    DAY_PLURAL: дена
+    WEEK_PLURAL: седмици
+    MONTH_PLURAL: месеца
+    YEAR_PLURAL: години
+    DECADE_PLURAL: десетилетия
+    SEC_PLURAL: сек
+    MIN_PLURAL: мин
+    HR_PLURAL: ч
+    WK_PLURAL: седм
+    MO_PLURAL: мес
+    YR_PLURAL: г
+    DEC_PLURAL: дстлт
+  FORM:
+    VALIDATION_FAIL: <b>Неуспешна проверка:</b>
+    INVALID_INPUT: Невалидно въвеждане в
+    MISSING_REQUIRED_FIELD: 'Липсва задължително поле:'
+  MONTHS_OF_THE_YEAR:
+    - 'януари'
+    - 'февруари'
+    - 'март'
+    - 'април'
+    - 'май'
+    - 'юни'
+    - 'юли'
+    - 'август'
+    - 'септември'
+    - 'октомври'
+    - 'ноември'
+    - 'декември'
+  DAYS_OF_THE_WEEK:
+    - 'понеделник'
+    - 'вторник'
+    - 'сряда'
+    - 'четвъртък'
+    - 'петък'
+    - 'събота'
+    - 'неделя'

+ 61 - 74
system/languages/ca.yaml

@@ -1,75 +1,62 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # S'ha produït un error: Frontmatter invàlid
-  
-  Ruta: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: "No s'ha proporcionat data"
-  BAD_DATE: Data invàlida
-  AGO: abans
-  FROM_NOW: "des d'ara"
-  SECOND: segon
-  MINUTE: minut
-  HOUR: hora
-  DAY: dia
-  WEEK: setmana
-  MONTH: mes
-  YEAR: any
-  DECADE: dècada
-  SEC: s
-  MIN: min
-  HR: h
-  WK: setm.
-  MO: m.
-  YR: a.
-  DEC: dèc.
-  SECOND_PLURAL: segons
-  MINUTE_PLURAL: minuts
-  HOUR_PLURAL: hores
-  DAY_PLURAL: dies
-  WEEK_PLURAL: setmanes
-  MONTH_PLURAL: mesos
-  YEAR_PLURAL: anys
-  DECADE_PLURAL: dècades
-  SEC_PLURAL: s
-  MIN_PLURAL: min
-  HR_PLURAL: h
-  WK_PLURAL: setm.
-  MO_PLURAL: mesos
-  YR_PLURAL: anys
-  DEC_PLURAL: dèc.
-FORM:
-  VALIDATION_FAIL: '<b>Ha fallat la validació:</b>'
-  INVALID_INPUT: Entrada no vàlida a
-  MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
-MONTHS_OF_THE_YEAR:
-  - Gener
-  - Febrer
-  - Març
-  - Abril
-  - Maig
-  - Juny
-  - Juliol
-  - Agost
-  - Setembre
-  - Octubre
-  - Novembre
-  - Desembre
-DAYS_OF_THE_WEEK:
-  - Dilluns
-  - Dimarts
-  - Dimecres
-  - Dijous
-  - Divendres
-  - Dissabte
-  - Diumenge
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# S'ha produït un error: Frontmatter invàlid\n\nRuta: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: No s'ha proporcionat data
+    BAD_DATE: Data invàlida
+    AGO: abans
+    FROM_NOW: des d'ara
+    SECOND: segon
+    MINUTE: minut
+    HOUR: hora
+    DAY: dia
+    WEEK: setmana
+    MONTH: mes
+    YEAR: any
+    DECADE: dècada
+    SEC: s
+    HR: h
+    WK: setm.
+    MO: m.
+    YR: a.
+    DEC: dèc.
+    SECOND_PLURAL: segons
+    MINUTE_PLURAL: minuts
+    HOUR_PLURAL: hores
+    DAY_PLURAL: dies
+    WEEK_PLURAL: setmanes
+    MONTH_PLURAL: mesos
+    YEAR_PLURAL: anys
+    DECADE_PLURAL: dècades
+    SEC_PLURAL: s
+    MIN_PLURAL: min
+    HR_PLURAL: h
+    WK_PLURAL: setm.
+    MO_PLURAL: mesos
+    YR_PLURAL: anys
+    DEC_PLURAL: dèc.
+  FORM:
+    VALIDATION_FAIL: <b>Ha fallat la validació:</b>
+    INVALID_INPUT: Entrada no vàlida a
+    MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
+  MONTHS_OF_THE_YEAR:
+    - 'Gener'
+    - 'Febrer'
+    - 'Març'
+    - 'Abril'
+    - 'Maig'
+    - 'Juny'
+    - 'Juliol'
+    - 'Agost'
+    - 'Setembre'
+    - 'Octubre'
+    - 'Novembre'
+    - 'Desembre'
+  DAYS_OF_THE_WEEK:
+    - 'Dilluns'
+    - 'Dimarts'
+    - 'Dimecres'
+    - 'Dijous'
+    - 'Divendres'
+    - 'Dissabte'
+    - 'Diumenge'

+ 143 - 80
system/languages/cs.yaml

@@ -1,81 +1,144 @@
 ---
-INFLECTOR_UNCOUNTABLE:
-  - vybavení
-  - informace
-  - rýže
-  - peníze
-  - druhy
-  - série
-  - ryba
-  - ovce
-INFLECTOR_IRREGULAR:
-  person: lidé
-  man: muži
-  child: děti
-  sex: pohlaví
-  move: pohyby
-INFLECTOR_ORDINALS:
-  default: '.'
-  first: '.'
-  second: '.'
-  third: '.'
-NICETIME:
-  NO_DATE_PROVIDED: Datum nebylo vloženo
-  BAD_DATE: Chybné datum
-  AGO: zpět
-  FROM_NOW: od teď
-  SECOND: sekunda
-  MINUTE: minuta
-  HOUR: hodina
-  DAY: den
-  WEEK: týden
-  MONTH: měsíc
-  YEAR: rok
-  DECADE: dekáda
-  SEC: sek
-  MIN: min
-  HR: hod
-  WK: t
-  MO: m
-  YR: r
-  DEC: dek
-  SECOND_PLURAL: sekundy
-  MINUTE_PLURAL: minuty
-  HOUR_PLURAL: hodiny
-  DAY_PLURAL: dny
-  WEEK_PLURAL: týdny
-  MONTH_PLURAL: měsíce
-  YEAR_PLURAL: roky
-  DECADE_PLURAL: dekády
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: hod
-  WK_PLURAL: t
-  MO_PLURAL: m
-  YR_PLURAL: r
-  DEC_PLURAL: dek
-FORM:
-  VALIDATION_FAIL: '<b>Ověření se nezdařilo:</b>'
-  INVALID_INPUT: Neplatný vstup v
-  MISSING_REQUIRED_FIELD: 'Chybí požadované pole:'
-MONTHS_OF_THE_YEAR:
-  - ledna
-  - února
-  - března
-  - dubna
-  - května
-  - června
-  - července
-  - srpna
-  - září
-  - října
-  - listopadu
-  - prosince
-DAYS_OF_THE_WEEK:
-  - Pondělí
-  - Úterý
-  - Středa
-  - Čtvrtek
-  - Pátek
-  - Sobota
-  - Neděle
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Chyba: Chybný frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'vybavení'
+    - 'informace'
+    - 'rýže'
+    - 'peníze'
+    - 'druhy'
+    - 'série'
+    - 'ryba'
+    - 'ovce'
+  INFLECTOR_IRREGULAR:
+    'person': 'lidé'
+    'man': 'muži'
+    'child': 'děti'
+    'sex': 'pohlaví'
+    'move': 'pohyby'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Datum nebylo vloženo
+    BAD_DATE: Chybné datum
+    AGO: zpět
+    FROM_NOW: od teď
+    JUST_NOW: právě teď
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: hodina
+    DAY: den
+    WEEK: týden
+    MONTH: měsíc
+    YEAR: rok
+    DECADE: dekáda
+    SEC: sek
+    MIN: min
+    HR: hod
+    WK: t
+    MO: m
+    YR: r
+    DEC: dek
+    SECOND_PLURAL: sekundy
+    MINUTE_PLURAL: minuty
+    HOUR_PLURAL: hodiny
+    DAY_PLURAL: dny
+    WEEK_PLURAL: týdny
+    MONTH_PLURAL: měsíce
+    YEAR_PLURAL: roky
+    DECADE_PLURAL: dekády
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: hod
+    WK_PLURAL: t
+    MO_PLURAL: m
+    YR_PLURAL: r
+    DEC_PLURAL: dek
+  FORM:
+    VALIDATION_FAIL: <b>Ověření se nezdařilo:</b>
+    INVALID_INPUT: Neplatný vstup v
+    MISSING_REQUIRED_FIELD: 'Chybí požadované pole:'
+  MONTHS_OF_THE_YEAR:
+    - 'leden'
+    - 'únor'
+    - 'březen'
+    - 'duben'
+    - 'květen'
+    - 'červen'
+    - 'červenec'
+    - 'srpen'
+    - 'září'
+    - 'říjen'
+    - 'listopad'
+    - 'prosinec'
+  DAYS_OF_THE_WEEK:
+    - 'pondělí'
+    - 'úterý'
+    - 'středa'
+    - 'čtvrtek'
+    - 'pátek'
+    - 'sobota'
+    - 'neděle'
+  CRON:
+    EVERY: každý
+    EVERY_HOUR: každou hodinu
+    EVERY_MINUTE: každou minutu
+    EVERY_DAY_OF_WEEK: každý den v týdnu
+    EVERY_DAY_OF_MONTH: každý den v měsíci
+    EVERY_MONTH: každý měsíc
+    TEXT_PERIOD: Every <b />
+    TEXT_MINS: ' at <b /> minute(s) past the hour'
+    TEXT_TIME: ' at <b />:<b />'
+    TEXT_DOW: ' on <b />'
+    TEXT_MONTH: ' of <b />'
+    TEXT_DOM: ' on <b />'
+    ERROR1: Tag %s není podporován!
+    ERROR2: Chybný počet prvků
+    ERROR3: jquery_element musí být nastaven v nastaveních pro jqCron
+    ERROR4: Nerozpoznaný výraz

+ 61 - 74
system/languages/da.yaml

@@ -1,75 +1,62 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  Titel: %1$s
-  ---
-  
-  # Fejl: Ugyldigt frontmatter
-  
-  Sti: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Ingen dato angivet
-  BAD_DATE: Ugyldig dato
-  AGO: siden
-  FROM_NOW: fra nu
-  SECOND: sekund
-  MINUTE: minut
-  HOUR: time
-  DAY: dag
-  WEEK: uge
-  MONTH: måned
-  YEAR: år
-  DECADE: årti
-  SEC: sek
-  MIN: min
-  HR: t
-  WK: u
-  MO: md
-  YR: år
-  DEC: årti
-  SECOND_PLURAL: sekunder
-  MINUTE_PLURAL: minutter
-  HOUR_PLURAL: timer
-  DAY_PLURAL: dage
-  WEEK_PLURAL: uger
-  MONTH_PLURAL: måneder
-  YEAR_PLURAL: år
-  DECADE_PLURAL: årtier
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: timer
-  WK_PLURAL: uger
-  MO_PLURAL: mdr
-  YR_PLURAL: år
-  DEC_PLURAL: årtier
-FORM:
-  VALIDATION_FAIL: '<b>Validering mislykkedes:</b>'
-  INVALID_INPUT: Ugyldigt input i
-  MISSING_REQUIRED_FIELD: 'Mangler obligatorisk felt:'
-MONTHS_OF_THE_YEAR:
-  - Januar
-  - Februar
-  - Marts
-  - April
-  - Maj
-  - Juni
-  - Juli
-  - August
-  - September
-  - Oktober
-  - November
-  - December
-DAYS_OF_THE_WEEK:
-  - Mandag
-  - Tirsdag
-  - Onsdag
-  - Torsdag
-  - Fredag
-  - Lørdag
-  - Søndag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nTitel: %1$s\n---\n\n# Fejl: Ugyldigt frontmatter\n\nSti: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Ingen dato angivet
+    BAD_DATE: Ugyldig dato
+    AGO: siden
+    FROM_NOW: fra nu
+    SECOND: sekund
+    MINUTE: minut
+    HOUR: time
+    DAY: dag
+    WEEK: uge
+    MONTH: måned
+    YEAR: år
+    DECADE: årti
+    SEC: sek
+    HR: t
+    WK: u
+    MO: md
+    YR: år
+    DEC: årti
+    SECOND_PLURAL: sekunder
+    MINUTE_PLURAL: minutter
+    HOUR_PLURAL: timer
+    DAY_PLURAL: dage
+    WEEK_PLURAL: uger
+    MONTH_PLURAL: måneder
+    YEAR_PLURAL: år
+    DECADE_PLURAL: årtier
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: timer
+    WK_PLURAL: uger
+    MO_PLURAL: mdr
+    YR_PLURAL: år
+    DEC_PLURAL: årtier
+  FORM:
+    VALIDATION_FAIL: <b>Validering mislykkedes:</b>
+    INVALID_INPUT: Ugyldigt input i
+    MISSING_REQUIRED_FIELD: 'Mangler obligatorisk felt:'
+  MONTHS_OF_THE_YEAR:
+    - 'januar'
+    - 'februar'
+    - 'mars'
+    - 'april'
+    - 'mai'
+    - 'juni'
+    - 'juli'
+    - 'august'
+    - 'september'
+    - 'oktober'
+    - 'november'
+    - 'desember'
+  DAYS_OF_THE_WEEK:
+    - 'mandag'
+    - 'tirsdag'
+    - 'onsdag'
+    - 'torsdag'
+    - 'fredag'
+    - 'lørdag'
+    - 'søndag'

+ 143 - 88
system/languages/de.yaml

@@ -1,89 +1,144 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  # Fehler: Frontmatter enthält Fehler
-  
-  Pfad: `%2$s`
-  
-  **%3$s ** 
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_UNCOUNTABLE:
-  1: Informationen
-  2: Reis
-  3: Geld
-INFLECTOR_IRREGULAR:
-  person: Personen
-  man: Menschen
-  child: Kinder
-  sex: Geschlecht
-  move: Züge
-INFLECTOR_ORDINALS:
-  default: '.'
-  first: '.'
-  second: '.'
-  third: '.'
-NICETIME:
-  NO_DATE_PROVIDED: Kein Datum angegeben
-  BAD_DATE: Falsches Datum
-  AGO: her
-  FROM_NOW: ab jetzt
-  SECOND: Sekunde
-  MINUTE: Minute
-  HOUR: Stunde
-  DAY: Tag
-  WEEK: Woche
-  MONTH: Monat
-  YEAR: Jahr
-  DECADE: Jahrzehnt
-  SEC: Sek.
-  MIN: Min.
-  HR: Std.
-  WK: Wo.
-  MO: Mo.
-  YR: J.
-  DEC: Dek.
-  SECOND_PLURAL: Sekunden
-  MINUTE_PLURAL: Minuten
-  HOUR_PLURAL: Stunden
-  DAY_PLURAL: Tage
-  WEEK_PLURAL: Wochen
-  MONTH_PLURAL: Monate
-  YEAR_PLURAL: Jahre
-  DECADE_PLURAL: Jahrzehnte
-  SEC_PLURAL: Sekunden
-  MIN_PLURAL: Minuten
-  HR_PLURAL: Stunden
-  WK_PLURAL: Wochen
-  MO_PLURAL: Monate
-  YR_PLURAL: Jahre
-  DEC_PLURAL: Jahrzehnten
-FORM:
-  VALIDATION_FAIL: '<b>Überprüfung fehlgeschlagen:</b>'
-  INVALID_INPUT: Ungültige Eingabe in
-  MISSING_REQUIRED_FIELD: 'Erforderliches Feld fehlt:'
-MONTHS_OF_THE_YEAR:
-  - Januar
-  - Februar
-  - März
-  - April
-  - Mai
-  - Juni
-  - Juli
-  - August
-  - September
-  - Oktober
-  - November
-  - Dezember
-DAYS_OF_THE_WEEK:
-  - Montag
-  - Dienstag
-  - Mittwoch
-  - Donnerstag
-  - Freitag
-  - Samstag
-  - Sonntag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n# Fehler: Frontmatter enthält Fehler\n\nPfad: `%2$s`\n\n**%3$s ** \n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ice'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1ies'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2ves'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'Ausstattung'
+    - 'Informationen'
+    - 'Reis'
+    - 'Geld'
+    - 'Arten'
+    - 'Serie'
+    - 'Fisch'
+    - 'Schaf'
+  INFLECTOR_IRREGULAR:
+    'person': 'Personen'
+    'man': 'Menschen'
+    'child': 'Kinder'
+    'sex': 'Geschlecht'
+    'move': 'Züge'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Kein Datum angegeben
+    BAD_DATE: Falsches Datum
+    AGO: her
+    FROM_NOW: ab jetzt
+    JUST_NOW: jetzt gerade
+    SECOND: Sekunde
+    MINUTE: Minute
+    HOUR: Stunde
+    DAY: Tag
+    WEEK: Woche
+    MONTH: Monat
+    YEAR: Jahr
+    DECADE: Jahrzehnt
+    SEC: Sek.
+    MIN: Min.
+    HR: Std.
+    WK: Wo.
+    MO: Mo.
+    YR: J.
+    DEC: Dez
+    SECOND_PLURAL: Sekunden
+    MINUTE_PLURAL: Minuten
+    HOUR_PLURAL: Stunden
+    DAY_PLURAL: Tage
+    WEEK_PLURAL: Wochen
+    MONTH_PLURAL: Monate
+    YEAR_PLURAL: Jahre
+    DECADE_PLURAL: Jahrzehnte
+    SEC_PLURAL: Sekunden
+    MIN_PLURAL: Minuten
+    HR_PLURAL: Stunden
+    WK_PLURAL: Wochen
+    MO_PLURAL: Monate
+    YR_PLURAL: Jahre
+    DEC_PLURAL: Jahrzehnten
+  FORM:
+    VALIDATION_FAIL: <b>Überprüfung fehlgeschlagen:</b>
+    INVALID_INPUT: Ungültige Eingabe in
+    MISSING_REQUIRED_FIELD: 'Erforderliches Feld fehlt:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januar'
+    - 'Februar'
+    - 'März'
+    - 'April'
+    - 'Mai'
+    - 'Juni'
+    - 'Juli'
+    - 'August'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'Dezember'
+  DAYS_OF_THE_WEEK:
+    - 'Montag'
+    - 'Dienstag'
+    - 'Mittwoch'
+    - 'Donnerstag'
+    - 'Freitag'
+    - 'Samstag'
+    - 'Sonntag'
+  CRON:
+    EVERY: jede
+    EVERY_HOUR: jede Stunde
+    EVERY_MINUTE: Jede Minute
+    EVERY_DAY_OF_WEEK: jeden Tag der Woche
+    EVERY_DAY_OF_MONTH: jeden Tag des Monats
+    EVERY_MONTH: jeden Monat
+    TEXT_PERIOD: Alle <b />
+    TEXT_MINS: ' bei <b /> Minuten nach der vollen Stunde (n)'
+    TEXT_TIME: ' bei <b />:<b />'
+    TEXT_DOW: ' auf <b />'
+    TEXT_MONTH: ' von <b />'
+    TEXT_DOM: ' auf <b />'
+    ERROR1: Der Tag %s wird nicht unterstützt!
+    ERROR2: Ungültige Anzahl von Elementen
+    ERROR3: jquery_element sollte in den jqCron Einstellungen gesetzt werden
+    ERROR4: Unbekannter Ausdruck

+ 62 - 21
system/languages/el.yaml

@@ -1,22 +1,63 @@
 ---
-MONTHS_OF_THE_YEAR:
-  - Ιανουάριος
-  - Φεβρουάριος
-  - Μάρτιος
-  - Απρίλιος
-  - Μάιος
-  - Ιούνιος
-  - Ιούλιος
-  - Αύγουστος
-  - Σεπτέμβριος
-  - Οκτώβριος
-  - Νοέμβριος
-  - Δεκέμβριος
-DAYS_OF_THE_WEEK:
-  - Δευτέρα
-  - Τρίτη
-  - Τετάρτη
-  - Πέμπτη
-  - Παρασκευή
-  - Σάββατο
-  - Κυριακή
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nΤίτλος: %1$s\n---\n\n# Σφάλμα: Μη έγκυρη διαδρομή Frontmatter\n\nΔιαδρομή: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Δεν δόθηκε καμία ημερομηνία
+    BAD_DATE: Εσφαλμένη ημερομηνία
+    AGO: πρίν
+    FROM_NOW: από τώρα
+    SECOND: δευτερόλεπτο
+    MINUTE: λεπτό
+    HOUR: ώρα
+    DAY: ημέρα
+    WEEK: εβδομάδα
+    MONTH: μήνας
+    YEAR: έτος
+    DECADE: δεκαετία
+    SEC: δευτερόλεπτο
+    MIN: λεπτό
+    HR: ώρα
+    WK: εβδ
+    MO: μην
+    YR: έτος
+    DEC: δεκαετία
+    SECOND_PLURAL: δευτερόλεπτα
+    MINUTE_PLURAL: λεπτά
+    HOUR_PLURAL: ώρες
+    DAY_PLURAL: ημέρες
+    WEEK_PLURAL: εβδομάδες
+    MONTH_PLURAL: μήνες
+    YEAR_PLURAL: έτη
+    DECADE_PLURAL: δεκαετίες
+    SEC_PLURAL: δευτ.
+    MIN_PLURAL: λεπτά
+    HR_PLURAL: ώρες
+    WK_PLURAL: εβδομάδες
+    MO_PLURAL: μήνες
+    YR_PLURAL: έτη
+    DEC_PLURAL: δεκαετίες
+  FORM:
+    VALIDATION_FAIL: <b>Η επικύρωση απέτυχε:</b>
+    INVALID_INPUT: Μη έγκυρα δεδομένα σε
+    MISSING_REQUIRED_FIELD: 'Λείπει το απαιτούμενο πεδίο:'
+  MONTHS_OF_THE_YEAR:
+    - 'Ιανουάριος'
+    - 'Φεβρουάριος'
+    - 'Μάρτιος'
+    - 'Απρίλιος'
+    - 'Μάιος'
+    - 'Ιούνιος'
+    - 'Ιούλιος'
+    - 'Αύγουστος'
+    - 'Σεπτέμβριος'
+    - 'Οκτώβριος'
+    - 'Νοέμβριος'
+    - 'Δεκέμβριος'
+  DAYS_OF_THE_WEEK:
+    - 'Δευτέρα'
+    - 'Τρίτη'
+    - 'Τετάρτη'
+    - 'Πέμπτη'
+    - 'Παρασκευή'
+    - 'Σάββατο'
+    - 'Κυριακή'

+ 120 - 99
system/languages/en.yaml

@@ -1,99 +1,120 @@
-FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
-INFLECTOR_PLURALS:
-    '/(quiz)$/i': '\1zes'
-    '/^(ox)$/i': '\1en'
-    '/([m|l])ouse$/i': '\1ice'
-    '/(matr|vert|ind)ix|ex$/i': '\1ices'
-    '/(x|ch|ss|sh)$/i': '\1es'
-    '/([^aeiouy]|qu)ies$/i': '\1y'
-    '/([^aeiouy]|qu)y$/i': '\1ies'
-    '/(hive)$/i': '\1s'
-    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
-    '/sis$/i': 'ses'
-    '/([ti])um$/i': '\1a'
-    '/(buffal|tomat)o$/i': '\1oes'
-    '/(bu)s$/i': '\1ses'
-    '/(alias|status)/i': '\1es'
-    '/(octop|vir)us$/i': '\1i'
-    '/(ax|test)is$/i': '\1es'
-    '/s$/i': 's'
-    '/$/': 's'
-INFLECTOR_SINGULAR:
-    '/(quiz)zes$/i': '\1'
-    '/(matr)ices$/i': '\1ix'
-    '/(vert|ind)ices$/i': '\1ex'
-    '/^(ox)en/i': '\1'
-    '/(alias|status)es$/i': '\1'
-    '/([octop|vir])i$/i': '\1us'
-    '/(cris|ax|test)es$/i': '\1is'
-    '/(shoe)s$/i': '\1'
-    '/(o)es$/i': '\1'
-    '/(bus)es$/i': '\1'
-    '/([m|l])ice$/i': '\1ouse'
-    '/(x|ch|ss|sh)es$/i': '\1'
-    '/(m)ovies$/i': '\1ovie'
-    '/(s)eries$/i': '\1eries'
-    '/([^aeiouy]|qu)ies$/i': '\1y'
-    '/([lr])ves$/i': '\1f'
-    '/(tive)s$/i': '\1'
-    '/(hive)s$/i': '\1'
-    '/([^f])ves$/i': '\1fe'
-    '/(^analy)ses$/i': '\1sis'
-    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
-    '/([ti])a$/i': '\1um'
-    '/(n)ews$/i': '\1ews'
-    '/s$/i': ''
-INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep']
-INFLECTOR_IRREGULAR:
-    'person': 'people'
-    'man': 'men'
-    'child': 'children'
-    'sex': 'sexes'
-    'move': 'moves'
-INFLECTOR_ORDINALS:
-    'default': 'th'
-    'first': 'st'
-    'second': 'nd'
-    'third': 'rd'
-NICETIME:
-    NO_DATE_PROVIDED: No date provided
-    BAD_DATE: Bad date
-    AGO: ago
-    FROM_NOW: from now
-    JUST_NOW: just now
-    SECOND: second
-    MINUTE: minute
-    HOUR: hour
-    DAY: day
-    WEEK: week
-    MONTH: month
-    YEAR: year
-    DECADE: decade
-    SEC: sec
-    MIN: min
-    HR: hr
-    WK: wk
-    MO: mo
-    YR: yr
-    DEC: dec
-    SECOND_PLURAL: seconds
-    MINUTE_PLURAL: minutes
-    HOUR_PLURAL: hours
-    DAY_PLURAL: days
-    WEEK_PLURAL: weeks
-    MONTH_PLURAL: months
-    YEAR_PLURAL: years
-    DECADE_PLURAL: decades
-    SEC_PLURAL: secs
-    MIN_PLURAL: mins
-    HR_PLURAL: hrs
-    WK_PLURAL: wks
-    MO_PLURAL: mos
-    YR_PLURAL: yrs
-    DEC_PLURAL: decs
-FORM:
-    VALIDATION_FAIL: <b>Validation failed:</b>
-    INVALID_INPUT: Invalid input in
-    MISSING_REQUIRED_FIELD: Missing required field:
-MONTHS_OF_THE_YEAR: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
-DAYS_OF_THE_WEEK: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
+---
+GRAV:
+    FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+    INFLECTOR_PLURALS:
+        '/(quiz)$/i': '\1zes'
+        '/^(ox)$/i': '\1en'
+        '/([m|l])ouse$/i': '\1ice'
+        '/(matr|vert|ind)ix|ex$/i': '\1ices'
+        '/(x|ch|ss|sh)$/i': '\1es'
+        '/([^aeiouy]|qu)ies$/i': '\1y'
+        '/([^aeiouy]|qu)y$/i': '\1ies'
+        '/(hive)$/i': '\1s'
+        '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+        '/sis$/i': 'ses'
+        '/([ti])um$/i': '\1a'
+        '/(buffal|tomat)o$/i': '\1oes'
+        '/(bu)s$/i': '\1ses'
+        '/(alias|status)/i': '\1es'
+        '/(octop|vir)us$/i': '\1i'
+        '/(ax|test)is$/i': '\1es'
+        '/s$/i': 's'
+        '/$/': 's'
+    INFLECTOR_SINGULAR:
+        '/(quiz)zes$/i': '\1'
+        '/(matr)ices$/i': '\1ix'
+        '/(vert|ind)ices$/i': '\1ex'
+        '/^(ox)en/i': '\1'
+        '/(alias|status)es$/i': '\1'
+        '/([octop|vir])i$/i': '\1us'
+        '/(cris|ax|test)es$/i': '\1is'
+        '/(shoe)s$/i': '\1'
+        '/(o)es$/i': '\1'
+        '/(bus)es$/i': '\1'
+        '/([m|l])ice$/i': '\1ouse'
+        '/(x|ch|ss|sh)es$/i': '\1'
+        '/(m)ovies$/i': '\1ovie'
+        '/(s)eries$/i': '\1eries'
+        '/([^aeiouy]|qu)ies$/i': '\1y'
+        '/([lr])ves$/i': '\1f'
+        '/(tive)s$/i': '\1'
+        '/(hive)s$/i': '\1'
+        '/([^f])ves$/i': '\1fe'
+        '/(^analy)ses$/i': '\1sis'
+        '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+        '/([ti])a$/i': '\1um'
+        '/(n)ews$/i': '\1ews'
+        '/s$/i': ''
+    INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep']
+    INFLECTOR_IRREGULAR:
+        'person': 'people'
+        'man': 'men'
+        'child': 'children'
+        'sex': 'sexes'
+        'move': 'moves'
+    INFLECTOR_ORDINALS:
+        'default': 'th'
+        'first': 'st'
+        'second': 'nd'
+        'third': 'rd'
+    NICETIME:
+        NO_DATE_PROVIDED: No date provided
+        BAD_DATE: Bad date
+        AGO: ago
+        FROM_NOW: from now
+        JUST_NOW: just now
+        SECOND: second
+        MINUTE: minute
+        HOUR: hour
+        DAY: day
+        WEEK: week
+        MONTH: month
+        YEAR: year
+        DECADE: decade
+        SEC: sec
+        MIN: min
+        HR: hr
+        WK: wk
+        MO: mo
+        YR: yr
+        DEC: dec
+        SECOND_PLURAL: seconds
+        MINUTE_PLURAL: minutes
+        HOUR_PLURAL: hours
+        DAY_PLURAL: days
+        WEEK_PLURAL: weeks
+        MONTH_PLURAL: months
+        YEAR_PLURAL: years
+        DECADE_PLURAL: decades
+        SEC_PLURAL: secs
+        MIN_PLURAL: mins
+        HR_PLURAL: hrs
+        WK_PLURAL: wks
+        MO_PLURAL: mos
+        YR_PLURAL: yrs
+        DEC_PLURAL: decs
+    FORM:
+        VALIDATION_FAIL: <b>Validation failed:</b>
+        INVALID_INPUT: Invalid input in
+        MISSING_REQUIRED_FIELD: Missing required field:
+    MONTHS_OF_THE_YEAR: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
+    DAYS_OF_THE_WEEK: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
+    YES: "Yes"
+    NO: "No"
+    CRON:
+        EVERY: every
+        EVERY_HOUR: every hour
+        EVERY_MINUTE: every minute
+        EVERY_DAY_OF_WEEK: every day of the week
+        EVERY_DAY_OF_MONTH: every day of the month
+        EVERY_MONTH: every month
+        TEXT_PERIOD: Every <b />
+        TEXT_MINS: ' at <b /> minute(s) past the hour'
+        TEXT_TIME: ' at <b />:<b />'
+        TEXT_DOW: ' on <b />'
+        TEXT_MONTH: ' of <b />'
+        TEXT_DOM: ' on <b />'
+        ERROR1: The tag %s is not supported!
+        ERROR2: Bad number of elements
+        ERROR3: The jquery_element should be set into jqCron settings
+        ERROR4: Unrecognized expression

+ 94 - 89
system/languages/es.yaml

@@ -1,90 +1,95 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Error: Frontmatter Inválido
-  
-  Ruta: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_UNCOUNTABLE:
-  0: equipo
-  1: información
-  3: dinero
-  5: series
-  6: pescado
-  7: oveja
-INFLECTOR_IRREGULAR:
-  man: hombres
-  child: niños
-  sex: sexos
-INFLECTOR_ORDINALS:
-  first: ro
-  second: do
-  third: ro
-NICETIME:
-  NO_DATE_PROVIDED: No se proporcionó fecha
-  BAD_DATE: Fecha erronea
-  AGO: antes
-  FROM_NOW: desde ahora
-  SECOND: segundo
-  MINUTE: minuto
-  HOUR: hora
-  DAY: día
-  WEEK: semana
-  MONTH: mes
-  YEAR: año
-  DECADE: década
-  SEC: seg
-  MIN: min
-  HR: h
-  WK: sem
-  MO: mes
-  YR: año
-  DEC: dec
-  SECOND_PLURAL: segundos
-  MINUTE_PLURAL: minutos
-  HOUR_PLURAL: horas
-  DAY_PLURAL: días
-  WEEK_PLURAL: semanas
-  MONTH_PLURAL: meses
-  YEAR_PLURAL: años
-  DECADE_PLURAL: décadas
-  SEC_PLURAL: segs
-  MIN_PLURAL: mins
-  HR_PLURAL: hs
-  WK_PLURAL: sem
-  MO_PLURAL: mes
-  YR_PLURAL: años
-  DEC_PLURAL: décadas
-FORM:
-  VALIDATION_FAIL: '<b>Falló la validación. </b>'
-  INVALID_INPUT: 'Dato inválido en: '
-  MISSING_REQUIRED_FIELD: 'Falta el campo requerido: '
-MONTHS_OF_THE_YEAR:
-  - Enero
-  - Febrero
-  - Marzo
-  - Abril
-  - Mayo
-  - Junio
-  - Julio
-  - Agosto
-  - Septiembre
-  - Octubre
-  - Noviembre
-  - Diciembre
-DAYS_OF_THE_WEEK:
-  - Lunes
-  - Martes
-  - Miércoles
-  - Jueves
-  - Viernes
-  - Sábado
-  - Domingo
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntítulo: %1$s\n---\n\n# Error: Frontmatter no válido\n\nRuta: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipo'
+    - 'información'
+    - 'rice'
+    - 'dinero'
+    - 'species'
+    - 'series'
+    - 'pescado'
+    - 'oveja'
+  INFLECTOR_IRREGULAR:
+    'man': 'hombres'
+    'child': 'niños'
+    'sex': 'sexos'
+  INFLECTOR_ORDINALS:
+    'first': 'ro'
+    'second': 'do'
+    'third': 'ro'
+  NICETIME:
+    NO_DATE_PROVIDED: No se proporcionó fecha
+    BAD_DATE: Fecha errónea
+    AGO: antes
+    FROM_NOW: desde ahora
+    JUST_NOW: justo ahora
+    SECOND: segundo
+    MINUTE: minuto
+    HOUR: hora
+    DAY: día
+    WEEK: semana
+    MONTH: mes
+    YEAR: año
+    DECADE: década
+    SEC: seg
+    HR: h
+    WK: sem
+    MO: mes
+    YR: año
+    SECOND_PLURAL: segundos
+    MINUTE_PLURAL: minutos
+    HOUR_PLURAL: horas
+    DAY_PLURAL: días
+    WEEK_PLURAL: semanas
+    MONTH_PLURAL: meses
+    YEAR_PLURAL: años
+    DECADE_PLURAL: décadas
+    SEC_PLURAL: segs
+    HR_PLURAL: hs
+    WK_PLURAL: sem
+    MO_PLURAL: mes
+    YR_PLURAL: años
+    DEC_PLURAL: décadas
+  FORM:
+    VALIDATION_FAIL: '<b>Falló la validación: </b>'
+    INVALID_INPUT: 'Dato inválido en: '
+    MISSING_REQUIRED_FIELD: 'Falta el campo requerido: '
+  MONTHS_OF_THE_YEAR:
+    - 'Enero'
+    - 'Febrero'
+    - 'Marzo'
+    - 'Abril'
+    - 'Mayo'
+    - 'Junio'
+    - 'Julio'
+    - 'Agosto'
+    - 'Septiembre'
+    - 'Octubre'
+    - 'Noviembre'
+    - 'Diciembre'
+  DAYS_OF_THE_WEEK:
+    - 'Lunes'
+    - 'Martes'
+    - 'Miércoles'
+    - 'Jueves'
+    - 'Viernes'
+    - 'Sábado'
+    - 'Domingo'
+  CRON:
+    EVERY: cada
+    EVERY_HOUR: cada hora
+    EVERY_MINUTE: cada minuto
+    EVERY_DAY_OF_WEEK: cada día de la semana
+    EVERY_DAY_OF_MONTH: cada día del mes
+    EVERY_MONTH: cada mes
+    TEXT_PERIOD: Cada <b />
+    TEXT_MINS: ' a <b /> minuto(s) despues de la hora'
+    TEXT_TIME: ' a <b />:<b />'
+    TEXT_DOW: ' en <b />'
+    TEXT_MONTH: ' de<b />'
+    TEXT_DOM: ' en<b />'
+    ERROR1: La etiqueta %s no está soportada!
+    ERROR2: El número de elementos es erroneo
+    ERROR3: El jquery_element debería establecerse en la configuración del jqCron
+    ERROR4: Expresión no reconocida

+ 62 - 0
system/languages/et.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\npealkiri: %1$s\n---\n\n# Viga: vigane Frontmatter'i\n\nasukoht: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Kuupäev määramata
+    BAD_DATE: Vigane kuupäev
+    AGO: tagasi
+    FROM_NOW: praegusest
+    SECOND: sekund
+    MINUTE: minut
+    HOUR: tundi
+    DAY: päev
+    WEEK: nädal
+    MONTH: kuu
+    YEAR: aasta
+    DECADE: 10 aastat
+    SEC: sek
+    HR: t
+    WK: näd
+    MO: k.
+    YR: a.
+    DEC: dekaad
+    SECOND_PLURAL: sekundit
+    MINUTE_PLURAL: minutit
+    HOUR_PLURAL: tundi
+    DAY_PLURAL: päeva
+    WEEK_PLURAL: nädalat
+    MONTH_PLURAL: kuud
+    YEAR_PLURAL: aastat
+    DECADE_PLURAL: dekaadi
+    SEC_PLURAL: sekundit
+    MIN_PLURAL: min
+    HR_PLURAL: t
+    WK_PLURAL: näd
+    MO_PLURAL: kuud
+    YR_PLURAL: aastat
+    DEC_PLURAL: dek.
+  FORM:
+    VALIDATION_FAIL: <b>Kinnitamine nurjus:</b>
+    INVALID_INPUT: 'Vigane sisend:'
+    MISSING_REQUIRED_FIELD: 'Nõutud väli puudub:'
+  MONTHS_OF_THE_YEAR:
+    - 'jaanuar'
+    - 'veebruar'
+    - 'märts'
+    - 'aprill'
+    - 'mai'
+    - 'juuni'
+    - 'juuli'
+    - 'august'
+    - 'september'
+    - 'oktoober'
+    - 'november'
+    - 'detsember'
+  DAYS_OF_THE_WEEK:
+    - 'esmaspäev'
+    - 'teisipäev'
+    - 'kolmapäev'
+    - 'neljapäev'
+    - 'reede'
+    - 'laupäev'
+    - 'pühapäev'

+ 62 - 0
system/languages/eu.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "--- title: %1$s --- # Errorea: Baliogabeko Frontmatter Bidea: `%2$s` **%3$s** ``` %4$s ```"
+  NICETIME:
+    NO_DATE_PROVIDED: Ez da datarik ezarri
+    BAD_DATE: Okerreko data
+    AGO: ' duela'
+    FROM_NOW: oraindik aurrera
+    SECOND: segundo
+    MINUTE: minutu
+    HOUR: ordua
+    DAY: egun
+    WEEK: astea
+    MONTH: hilabetea
+    YEAR: urtea
+    DECADE: hamarkada
+    SEC: seg
+    HR: h
+    WK: ast
+    MO: hil
+    YR: urt
+    DEC: ham
+    SECOND_PLURAL: segundo
+    MINUTE_PLURAL: minutu
+    HOUR_PLURAL: ordu
+    DAY_PLURAL: egun
+    WEEK_PLURAL: aste
+    MONTH_PLURAL: hilabete
+    YEAR_PLURAL: urte
+    DECADE_PLURAL: hamarkada
+    SEC_PLURAL: segundo
+    MIN_PLURAL: minutu
+    HR_PLURAL: h
+    WK_PLURAL: ast
+    MO_PLURAL: hil
+    YR_PLURAL: urt
+    DEC_PLURAL: ham
+  FORM:
+    VALIDATION_FAIL: <b>Balidazioak huts egin du</b>
+    INVALID_INPUT: Baliogabeko sarrera
+    MISSING_REQUIRED_FIELD: 'Derrigorrezko eremua bete gabe:'
+  MONTHS_OF_THE_YEAR:
+    - 'Urtarrila'
+    - 'Otsaila'
+    - 'Martxoa'
+    - 'Apirila'
+    - 'Maiatza'
+    - 'Ekaina'
+    - 'Uztaila'
+    - 'Abuztua'
+    - 'Iraila'
+    - 'Urria'
+    - 'Azaroa'
+    - 'Abendua'
+  DAYS_OF_THE_WEEK:
+    - 'Astelehena'
+    - 'Asteartea'
+    - 'Azteazkena'
+    - 'Osteguna'
+    - 'Ostirala'
+    - 'Larunbata'
+    - 'Igandea'

+ 62 - 0
system/languages/fa.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nعنوان: %1$s\n---\n\n# خطا: Frontmatter غلط\n\nمسیر: %2$s\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: تاریخی ارائه نشده
+    BAD_DATE: تاریخ اشتباه
+    AGO: قبل
+    FROM_NOW: از حالا
+    SECOND: ثانیه
+    MINUTE: دقیقه
+    HOUR: ساعت
+    DAY: روز
+    WEEK: هفته
+    MONTH: ماه
+    YEAR: سال
+    DECADE: دهه
+    SEC: ثانیه
+    MIN: دقیقه
+    HR: ساعت
+    WK: هفته
+    MO: ماه
+    YR: سال
+    DEC: دهه
+    SECOND_PLURAL: ثانیه
+    MINUTE_PLURAL: دقیقه
+    HOUR_PLURAL: ساعت
+    DAY_PLURAL: روز
+    WEEK_PLURAL: هفته
+    MONTH_PLURAL: ماه
+    YEAR_PLURAL: سال
+    DECADE_PLURAL: دهه
+    SEC_PLURAL: ثانیه
+    MIN_PLURAL: دقیقه
+    HR_PLURAL: ساعت
+    WK_PLURAL: هفته
+    YR_PLURAL: سال
+    DEC_PLURAL: دهه
+  FORM:
+    VALIDATION_FAIL: <b>سنجش اعتبار ناموفق بود</b>
+    INVALID_INPUT: ورودی نامعتبر در
+    MISSING_REQUIRED_FIELD: 'قسمت ضروری جا افتاده:'
+  MONTHS_OF_THE_YEAR:
+    - 'ژانویه'
+    - 'فوریه'
+    - 'مارس'
+    - 'آوریل'
+    - 'می'
+    - 'ژوئن'
+    - 'ژوئیه'
+    - 'اوت'
+    - 'سپتامبر'
+    - 'اکتبر'
+    - 'نوامبر'
+    - 'دسامبر'
+  DAYS_OF_THE_WEEK:
+    - 'دوشنبه'
+    - 'سه‌ شنبه'
+    - 'چهارشنبه'
+    - 'پنج شنبه'
+    - 'جمعه'
+    - 'شنبه'
+    - 'یک‌شنبه'

+ 133 - 59
system/languages/fi.yaml

@@ -1,60 +1,134 @@
 ---
-NICETIME:
-  NO_DATE_PROVIDED: Päivämäärää ei annettu
-  BAD_DATE: Virheellinen päivämäärä
-  AGO: sitten
-  FROM_NOW: tästä lähtien
-  SECOND: sekunti
-  MINUTE: minuutti
-  HOUR: tunti
-  DAY: päivä
-  WEEK: viikko
-  MONTH: kuukausi
-  YEAR: vuosi
-  DECADE: vuosikymmen
-  SEC: sek
-  MIN: min
-  HR: h
-  WK: vk
-  MO: kk
-  YR: v
-  DEC: vuosikymmen
-  SECOND_PLURAL: sekuntia
-  MINUTE_PLURAL: minuuttia
-  HOUR_PLURAL: tuntia
-  DAY_PLURAL: päivää
-  WEEK_PLURAL: viikkoa
-  MONTH_PLURAL: kuukautta
-  YEAR_PLURAL: vuotta
-  DECADE_PLURAL: vuosikymmentä
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: h
-  WK_PLURAL: v
-  MO_PLURAL: kk
-  YR_PLURAL: v
-  DEC_PLURAL: vuosikymmentä
-FORM:
-  VALIDATION_FAIL: '<b>Vahvistus epäonnistui:</b>'
-  MISSING_REQUIRED_FIELD: 'Puuttuva pakollinen kenttä:'
-MONTHS_OF_THE_YEAR:
-  - Tammikuu
-  - Helmikuu
-  - Maaliskuu
-  - Huhtikuu
-  - Toukokuu
-  - Kesäkuuta
-  - Heinäkuu
-  - Elokuu
-  - Syyskuu
-  - Lokakuu
-  - Marraskuu
-  - Joulukuu
-DAYS_OF_THE_WEEK:
-  - Maanantai
-  - Tiistai
-  - Keskiviikko
-  - Torstai
-  - Perjantai
-  - Lauantai
-  - Sunnuntai
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\notsikko: %1$s\n---\n\n# Virhe: Virheellinen Frontmatter\n\nPolku: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'information'
+    - 'riisi'
+    - 'raha'
+    - 'lajit'
+    - 'series'
+    - 'kala'
+    - 'lammas'
+  INFLECTOR_IRREGULAR:
+    'person': 'ihmiset'
+    'man': 'miehet'
+    'child': 'lapset'
+    'sex': 'sukupuoli'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Päivämäärää ei annettu
+    BAD_DATE: Virheellinen päivämäärä
+    AGO: sitten
+    FROM_NOW: tästä lähtien
+    JUST_NOW: juuri nyt
+    SECOND: sekuntti
+    MINUTE: minuutti
+    HOUR: tunti
+    DAY: päivä
+    WEEK: viikko
+    MONTH: kuukausi
+    YEAR: vuosi
+    DECADE: vuosikymmen
+    SEC: sek
+    MIN: min
+    HR: h
+    WK: vk
+    MO: kk
+    YR: v
+    DEC: vuosikymmen
+    SECOND_PLURAL: sekuntia
+    MINUTE_PLURAL: minuuttia
+    HOUR_PLURAL: tuntia
+    DAY_PLURAL: päivää
+    WEEK_PLURAL: viikkoa
+    MONTH_PLURAL: kuukautta
+    YEAR_PLURAL: vuotta
+    DECADE_PLURAL: vuosikymmentä
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: h
+    WK_PLURAL: v
+    MO_PLURAL: kk
+    YR_PLURAL: v
+    DEC_PLURAL: vuosikymmentä
+  FORM:
+    VALIDATION_FAIL: <b>Vahvistus epäonnistui:</b>
+    INVALID_INPUT: Syöte ei kelpaa
+    MISSING_REQUIRED_FIELD: 'Puuttuva pakollinen kenttä:'
+  MONTHS_OF_THE_YEAR:
+    - 'Tammikuu'
+    - 'Helmikuu'
+    - 'Maaliskuu'
+    - 'Huhtikuu'
+    - 'Toukokuu'
+    - 'Kesäkuuta'
+    - 'Heinäkuu'
+    - 'Elokuu'
+    - 'Syyskuu'
+    - 'Lokakuu'
+    - 'Marraskuu'
+    - 'Joulukuu'
+  DAYS_OF_THE_WEEK:
+    - 'Maanantai'
+    - 'Tiistai'
+    - 'Keskiviikko'
+    - 'Torstai'
+    - 'Perjantai'
+    - 'Lauantai'
+    - 'Sunnuntai'
+  CRON:
+    EVERY: joka
+    EVERY_HOUR: joka tunti
+    EVERY_MINUTE: joka minuutti
+    EVERY_DAY_OF_WEEK: viikon jokaisena päivänä
+    EVERY_DAY_OF_MONTH: kuukauden jokaisena päivänä
+    EVERY_MONTH: joka kuukausi
+    TEXT_PERIOD: Joka <b />

+ 120 - 137
system/languages/fr.yaml

@@ -1,138 +1,121 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Erreur : Frontmatter invalide
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-  "/([m|l])ouse$/i": '\1ice'
-  /(matr|vert|ind)ix|ex$/i: '\1ices'
-  /(x|ch|ss|sh)$/i: '\1es'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([^aeiouy]|qu)y$/i": '\1ies'
-  /(hive)$/i: '\1s'
-  "/(?:([^f])fe|([lr])f)$/i": '\1\2ves'
-  /sis$/i: ses
-  "/([ti])um$/i": '\1a'
-  /(buffal|tomat)o$/i: '\1oes'
-  /(bu)s$/i: '\1ses'
-  /(alias|status)/i: '\1es'
-  /(octop|vir)us$/i: '\1i'
-  /(ax|test)is$/i: '\1es'
-  /s$/i: s
-  /$/: s
-INFLECTOR_SINGULAR:
-  /(quiz)zes$/i: '\1'
-  /(matr)ices$/i: '\1ix'
-  /(vert|ind)ices$/i: '\1ex'
-  /^(ox)en/i: '\1'
-  /(alias|status)es$/i: '\1'
-  "/([octop|vir])i$/i": '\1us'
-  /(cris|ax|test)es$/i: '\1is'
-  /(shoe)s$/i: '\1'
-  /(o)es$/i: '\1'
-  /(bus)es$/i: '\1'
-  "/([m|l])ice$/i": '\1ouse'
-  /(x|ch|ss|sh)es$/i: '\1'
-  /(m)ovies$/i: '\1ovie'
-  /(s)eries$/i: '\1eries'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([lr])ves$/i": '\1f'
-  /(tive)s$/i: '\1'
-  /(hive)s$/i: '\1'
-  "/([^f])ves$/i": '\1fe'
-  /(^analy)ses$/i: '\1sis'
-  /((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i: '\1\2sis'
-  "/([ti])a$/i": '\1um'
-  /(n)ews$/i: '\1ews'
-INFLECTOR_UNCOUNTABLE:
-  - équipement
-  - informations
-  - riz
-  - argent
-  - espèces
-  - séries
-  - poisson
-  - mouton
-INFLECTOR_IRREGULAR:
-  person: personnes
-  man: hommes
-  child: enfants
-  sex: sexes
-  move: déplacements
-INFLECTOR_ORDINALS:
-  default: ème
-  first: er
-  second: ème
-  third: ème
-NICETIME:
-  NO_DATE_PROVIDED: Aucune date fournie
-  BAD_DATE: Date erronée
-  AGO: plus tôt
-  FROM_NOW: à partir de maintenant
-  SECOND: seconde
-  MINUTE: minute
-  HOUR: heure
-  DAY: jour
-  WEEK: semaine
-  MONTH: mois
-  YEAR: année
-  DECADE: décennie
-  SEC: s
-  MIN: m
-  HR: h
-  WK: sem
-  MO: m
-  YR: an
-  DEC: déc
-  SECOND_PLURAL: secondes
-  MINUTE_PLURAL: minutes
-  HOUR_PLURAL: heures
-  DAY_PLURAL: jours
-  WEEK_PLURAL: semaines
-  MONTH_PLURAL: mois
-  YEAR_PLURAL: années
-  DECADE_PLURAL: décennies
-  SEC_PLURAL: s
-  MIN_PLURAL: m
-  HR_PLURAL: h
-  WK_PLURAL: sem
-  MO_PLURAL: mois
-  YR_PLURAL: a
-  DEC_PLURAL: décs
-FORM:
-  VALIDATION_FAIL: '<b>La validation a échoué :</b>'
-  INVALID_INPUT: Saisie non valide
-  MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
-MONTHS_OF_THE_YEAR:
-  - Janvier
-  - Février
-  - Mars
-  - Avril
-  - Mai
-  - Juin
-  - Juillet
-  - Août
-  - Septembre
-  - Octobre
-  - Novembre
-  - Décembre
-DAYS_OF_THE_WEEK:
-  - Lundi
-  - Mardi
-  - Mercredi
-  - Jeudi
-  - Vendredi
-  - Samedi
-  - Dimanche
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitre: %1$s\n---\n\n# Erreur : Frontmatter invalide\n\nChemin: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1es'
+    '/(ax|test)is$/i': '\1s'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+  INFLECTOR_UNCOUNTABLE:
+    - 'équipement'
+    - 'information'
+    - 'riz'
+    - 'argent'
+    - 'espèces'
+    - 'séries'
+    - 'poisson'
+    - 'mouton'
+  INFLECTOR_IRREGULAR:
+    'person': 'personnes'
+    'man': 'hommes'
+    'child': 'enfants'
+    'sex': 'sexes'
+    'move': 'déplacements'
+  INFLECTOR_ORDINALS:
+    'default': 'ème'
+    'first': 'er'
+    'second': 'ème'
+    'third': 'ème'
+  NICETIME:
+    NO_DATE_PROVIDED: Aucune date fournie
+    BAD_DATE: Date erronée
+    AGO: plus tôt
+    FROM_NOW: à partir de maintenant
+    JUST_NOW: à l'instant
+    SECOND: seconde
+    MINUTE: minute
+    HOUR: heure
+    DAY: jour
+    WEEK: semaine
+    MONTH: mois
+    YEAR: année
+    DECADE: décennie
+    SEC: s
+    MIN: m
+    HR: h
+    WK: sem
+    MO: m
+    YR: an
+    DEC: déc
+    SECOND_PLURAL: secondes
+    MINUTE_PLURAL: minutes
+    HOUR_PLURAL: heures
+    DAY_PLURAL: jours
+    WEEK_PLURAL: semaines
+    MONTH_PLURAL: mois
+    YEAR_PLURAL: années
+    DECADE_PLURAL: décennies
+    SEC_PLURAL: s
+    MIN_PLURAL: m
+    HR_PLURAL: h
+    WK_PLURAL: sem
+    MO_PLURAL: mois
+    YR_PLURAL: a
+    DEC_PLURAL: décs
+  FORM:
+    VALIDATION_FAIL: <b>La validation a échoué :</b>
+    INVALID_INPUT: Saisie non valide
+    MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
+  MONTHS_OF_THE_YEAR:
+    - 'Janvier'
+    - 'Février'
+    - 'Mars'
+    - 'Avril'
+    - 'Mai'
+    - 'Juin'
+    - 'Juillet'
+    - 'Août'
+    - 'Septembre'
+    - 'Octobre'
+    - 'Novembre'
+    - 'Décembre'
+  DAYS_OF_THE_WEEK:
+    - 'Lundi'
+    - 'Mardi'
+    - 'Mercredi'
+    - 'Jeudi'
+    - 'Vendredi'
+    - 'Samedi'
+    - 'Dimanche'
+  CRON:
+    EVERY: chaque
+    EVERY_HOUR: toutes les heures
+    EVERY_MINUTE: chaque minute
+    EVERY_DAY_OF_WEEK: tous les jours de la semaine
+    EVERY_DAY_OF_MONTH: tous les jours du mois
+    EVERY_MONTH: chaque mois
+    TEXT_PERIOD: Chaque<b/>
+    TEXT_MINS: ' à <b /> minute(s) après l''heure'
+    TEXT_TIME: ' à<b/>:<b/>'
+    TEXT_DOW: ' sur <b/>'
+    TEXT_MONTH: ' de <b />'
+    TEXT_DOM: ' sur <b/>'
+    ERROR1: La balise %s n'est pas supportée!
+    ERROR2: Nombre invalide d'éléments
+    ERROR3: L'élément jquery_element doit être défini dans les paramètres jqCron
+    ERROR4: Expression non reconnue

+ 63 - 0
system/languages/he.yaml

@@ -0,0 +1,63 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nכותרת: %1$s\n---\n# שגיאה: Fronmatter לא חוקי\nנתיב: `%2$s`\n**%3$s**\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: לא סופק תאריך
+    BAD_DATE: תאריך פגום
+    AGO: לפני
+    FROM_NOW: כרגע
+    SECOND: שנייה
+    MINUTE: דקה
+    HOUR: שעה
+    DAY: יום
+    WEEK: שבוע
+    MONTH: חודש
+    YEAR: שנה
+    DECADE: עשור
+    SEC: שנ'
+    MIN: דק'
+    HR: ש'
+    WK: שב'
+    MO: חו'
+    YR: שני'
+    DEC: עש'
+    SECOND_PLURAL: שניות
+    MINUTE_PLURAL: דקות
+    HOUR_PLURAL: שעות
+    DAY_PLURAL: ימים
+    WEEK_PLURAL: שבועות
+    MONTH_PLURAL: חודשים
+    YEAR_PLURAL: שנים
+    DECADE_PLURAL: עשורים
+    SEC_PLURAL: שנ'
+    MIN_PLURAL: דק'
+    HR_PLURAL: ש'
+    WK_PLURAL: שב'
+    MO_PLURAL: חו'
+    YR_PLURAL: שני'
+    DEC_PLURAL: עש'
+  FORM:
+    VALIDATION_FAIL: <b>האימות נכשל:</b>
+    INVALID_INPUT: קלט לא חוקי
+    MISSING_REQUIRED_FIELD: 'שדות חובה חסרים:'
+  MONTHS_OF_THE_YEAR:
+    - 'ינואר'
+    - 'פברואר'
+    - 'מרץ'
+    - 'אפריל'
+    - 'מאי'
+    - 'יוני'
+    - 'יולי'
+    - 'אוגוסט'
+    - 'ספטמבר'
+    - 'אוקטובר'
+    - 'נובמבר'
+    - 'דצמבר'
+  DAYS_OF_THE_WEEK:
+    - 'שני'
+    - 'שלישי'
+    - 'רביעי'
+    - 'חמישי'
+    - 'שישי'
+    - 'שבת'
+    - 'ראשון'

+ 75 - 74
system/languages/hr.yaml

@@ -1,75 +1,76 @@
 ---
-INFLECTOR_UNCOUNTABLE:
-  - oprema
-  - informacije
-  - riža
-  - novac
-  - vrsta
-  - serija
-  - riba
-  - ovca
-INFLECTOR_IRREGULAR:
-  person: osobe
-  man: ljudi
-  child: djeca
-  sex: spolovi
-  move: Pomakni
-NICETIME:
-  NO_DATE_PROVIDED: Datum nije upisan
-  BAD_DATE: Pogrešan datum
-  AGO: prije
-  FROM_NOW: od sada
-  SECOND: sekunda
-  MINUTE: minuta
-  HOUR: sat
-  DAY: dan
-  WEEK: tjedan
-  MONTH: mjesec
-  YEAR: godina
-  DECADE: desetljeće
-  SEC: sek
-  HR: sat
-  WK: t
-  MO: m
-  YR: g
-  DEC: des
-  SECOND_PLURAL: sekundi
-  MINUTE_PLURAL: minuta
-  HOUR_PLURAL: sati
-  DAY_PLURAL: dan
-  WEEK_PLURAL: tjedana
-  MONTH_PLURAL: mjeseci
-  YEAR_PLURAL: godina
-  DECADE_PLURAL: desetljeća
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: sat
-  WK_PLURAL: t
-  MO_PLURAL: m
-  YR_PLURAL: g
-  DEC_PLURAL: des
-FORM:
-  VALIDATION_FAIL: '<b>Validacija nije uspjela:</b>'
-  INVALID_INPUT: Pogrešan unos u
-  MISSING_REQUIRED_FIELD: 'Nedostaje obavezno polje:'
-MONTHS_OF_THE_YEAR:
-  - Siječanj
-  - Veljača
-  - Ožujak
-  - Travanj
-  - Svibanj
-  - Lipanj
-  - Srpanj
-  - Kolovoz
-  - Rujan
-  - Listopad
-  - Studeni
-  - Prosinac
-DAYS_OF_THE_WEEK:
-  - Ponedjeljak
-  - Utorak
-  - Srijeda
-  - Četvrtak
-  - Petak
-  - Subota
-  - Nedjelja
+GRAV:
+  INFLECTOR_UNCOUNTABLE:
+    - 'oprema'
+    - 'informacije'
+    - 'riža'
+    - 'novac'
+    - 'vrsta'
+    - 'serija'
+    - 'riba'
+    - 'ovca'
+  INFLECTOR_IRREGULAR:
+    'person': 'osobe'
+    'man': 'ljudi'
+    'child': 'djeca'
+    'sex': 'spolovi'
+    'move': 'Pomakni'
+  NICETIME:
+    NO_DATE_PROVIDED: Datum nije upisan
+    BAD_DATE: Pogrešan datum
+    AGO: prije
+    FROM_NOW: od sada
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: sat
+    DAY: dan
+    WEEK: tjedan
+    MONTH: mjesec
+    YEAR: godina
+    DECADE: desetljeće
+    SEC: sek
+    HR: sat
+    WK: t
+    MO: m
+    YR: g
+    DEC: des
+    SECOND_PLURAL: sekundi
+    MINUTE_PLURAL: minuta
+    HOUR_PLURAL: sati
+    DAY_PLURAL: dan
+    WEEK_PLURAL: tjedana
+    MONTH_PLURAL: mjeseci
+    YEAR_PLURAL: godina
+    DECADE_PLURAL: desetljeća
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: sat
+    WK_PLURAL: t
+    MO_PLURAL: m
+    YR_PLURAL: g
+    DEC_PLURAL: des
+  FORM:
+    VALIDATION_FAIL: <b>Validacija nije uspjela:</b>
+    INVALID_INPUT: Pogrešan unos u
+    MISSING_REQUIRED_FIELD: 'Nedostaje obavezno polje:'
+  MONTHS_OF_THE_YEAR:
+    - 'Siječanj'
+    - 'Veljača'
+    - 'Ožujak'
+    - 'Travanj'
+    - 'Svibanj'
+    - 'Lipanj'
+    - 'Srpanj'
+    - 'Kolovoz'
+    - 'Rujan'
+    - 'Listopad'
+    - 'Studeni'
+    - 'Prosinac'
+  DAYS_OF_THE_WEEK:
+    - 'Ponedjeljak'
+    - 'Utorak'
+    - 'Srijeda'
+    - 'Četvrtak'
+    - 'Petak'
+    - 'Subota'
+    - 'Nedjelja'

+ 96 - 137
system/languages/hu.yaml

@@ -1,138 +1,97 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  cím: %1$s
-  ---
-  
-  # Hiba: Érvénytelen Frontmatter
-  
-  Elérési út: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-  "/([m|l])ouse$/i": '\1ice'
-  /(matr|vert|ind)ix|ex$/i: '\1ices'
-  /(x|ch|ss|sh)$/i: '\1es'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([^aeiouy]|qu)y$/i": '\1ies'
-  /(hive)$/i: '\1s'
-  "/(?:([^f])fe|([lr])f)$/i": '\1\2ves'
-  /sis$/i: ses
-  "/([ti])um$/i": '\1a'
-  /(buffal|tomat)o$/i: '\1oes'
-  /(bu)s$/i: '\1ses'
-  /(alias|status)/i: '\1es'
-  /(octop|vir)us$/i: '\1i'
-  /(ax|test)is$/i: '\1es'
-  /s$/i: s
-  /$/: s
-INFLECTOR_SINGULAR:
-  /(quiz)zes$/i: '\1'
-  /(matr)ices$/i: '\1ix'
-  /(vert|ind)ices$/i: '\1ex'
-  /^(ox)en/i: '\1'
-  /(alias|status)es$/i: '\1'
-  "/([octop|vir])i$/i": '\1us'
-  /(cris|ax|test)es$/i: '\1is'
-  /(shoe)s$/i: '\1'
-  /(o)es$/i: '\1'
-  /(bus)es$/i: '\1'
-  "/([m|l])ice$/i": '\1ouse'
-  /(x|ch|ss|sh)es$/i: '\1'
-  /(m)ovies$/i: '\1ovie'
-  /(s)eries$/i: '\1eries'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([lr])ves$/i": '\1f'
-  /(tive)s$/i: '\1'
-  /(hive)s$/i: '\1'
-  "/([^f])ves$/i": '\1fe'
-  /(^analy)ses$/i: '\1sis'
-  /((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i: '\1\2sis'
-  "/([ti])a$/i": '\1um'
-  /(n)ews$/i: '\1ews'
-INFLECTOR_UNCOUNTABLE:
-  - felszerelés
-  - információ
-  - rizs
-  - pénz
-  - fajok
-  - sorozat
-  - hal
-  - juh
-INFLECTOR_IRREGULAR:
-  person: személyek
-  man: férfiak
-  child: gyerekek
-  sex: nemek
-  move: lépések
-INFLECTOR_ORDINALS:
-  default: '.'
-  first: '.'
-  second: '.'
-  third: '.'
-NICETIME:
-  NO_DATE_PROVIDED: Nincs dátum megadva
-  BAD_DATE: Hibás dátum
-  AGO: elteltével
-  FROM_NOW: mostantól
-  SECOND: másodperc
-  MINUTE: perc
-  HOUR: óra
-  DAY: nap
-  WEEK: hét
-  MONTH: hónap
-  YEAR: év
-  DECADE: évtized
-  SEC: mp
-  MIN: p
-  HR: ó
-  WK: hét
-  MO: hó
-  YR: év
-  DEC: évt
-  SECOND_PLURAL: másodperc
-  MINUTE_PLURAL: perc
-  HOUR_PLURAL: óra
-  DAY_PLURAL: nap
-  WEEK_PLURAL: hét
-  MONTH_PLURAL: hónap
-  YEAR_PLURAL: év
-  DECADE_PLURAL: évtized
-  SEC_PLURAL: mp
-  MIN_PLURAL: perc
-  HR_PLURAL: ó
-  WK_PLURAL: hét
-  MO_PLURAL: hó
-  YR_PLURAL: év
-  DEC_PLURAL: évt
-FORM:
-  VALIDATION_FAIL: '<b>A validáció hibát talált:</b>'
-  INVALID_INPUT: 'Az itt megadott érték érvénytelen:'
-  MISSING_REQUIRED_FIELD: 'Ez a kötelező mező nincs kitöltve:'
-MONTHS_OF_THE_YEAR:
-  - január
-  - február
-  - március
-  - április
-  - május
-  - június
-  - július
-  - augusztus
-  - szeptember
-  - október
-  - november
-  - december
-DAYS_OF_THE_WEEK:
-  - hétfő
-  - kedd
-  - szerda
-  - csütörtök
-  - péntek
-  - szombat
-  - vasárnap
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ncím: %1$s\n---\n\n# Hiba: Érvénytelen Frontmatter\n\nElérési út: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'felszerelés'
+    - 'információ'
+    - 'rizs'
+    - 'pénz'
+    - 'fajok'
+    - 'sorozat'
+    - 'hal'
+    - 'juh'
+  INFLECTOR_IRREGULAR:
+    'person': 'személyek'
+    'man': 'férfiak'
+    'child': 'gyerekek'
+    'sex': 'nemek'
+    'move': 'lépések'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Nincs dátum megadva
+    BAD_DATE: Hibás dátum
+    AGO: elteltével
+    FROM_NOW: mostantól
+    JUST_NOW: épp most
+    SECOND: másodperc
+    MINUTE: perc
+    HOUR: óra
+    DAY: nap
+    WEEK: hét
+    MONTH: hónap
+    YEAR: év
+    DECADE: évtized
+    SEC: mp
+    MIN: p
+    HR: ó
+    WK: hét
+    MO: hó
+    YR: év
+    DEC: évt
+    SECOND_PLURAL: másodperc
+    MINUTE_PLURAL: perc
+    HOUR_PLURAL: óra
+    DAY_PLURAL: nap
+    WEEK_PLURAL: hét
+    MONTH_PLURAL: hónap
+    YEAR_PLURAL: év
+    DECADE_PLURAL: évtized
+    SEC_PLURAL: mp
+    MIN_PLURAL: perc
+    HR_PLURAL: ó
+    WK_PLURAL: hét
+    MO_PLURAL: hó
+    YR_PLURAL: év
+    DEC_PLURAL: évt
+  FORM:
+    VALIDATION_FAIL: <b>Érvényesítés nem sikerült:</b>
+    INVALID_INPUT: 'A megadott érték érvénytelen:'
+    MISSING_REQUIRED_FIELD: 'Ez a kötelező mező nincs kitöltve:'
+  MONTHS_OF_THE_YEAR:
+    - 'január'
+    - 'február'
+    - 'március'
+    - 'április'
+    - 'május'
+    - 'június'
+    - 'július'
+    - 'augusztus'
+    - 'szeptember'
+    - 'október'
+    - 'november'
+    - 'december'
+  DAYS_OF_THE_WEEK:
+    - 'hétfő'
+    - 'kedd'
+    - 'szerda'
+    - 'csütörtök'
+    - 'péntek'
+    - 'szombat'
+    - 'vasárnap'
+  CRON:
+    EVERY: minden
+    EVERY_HOUR: óránként
+    EVERY_MINUTE: percenként
+    EVERY_DAY_OF_WEEK: a hét minden napján
+    EVERY_DAY_OF_MONTH: a hónap minden napján
+    EVERY_MONTH: minden hónapban
+    TEXT_PERIOD: Minden <b />
+    TEXT_MINS: '<b /> perccel az óra elteltével'
+    ERROR1: A %s címke nem engedélyezett!
+    ERROR2: Hibás elemszám
+    ERROR3: A jquery_element-et a jqCron beállítsokban kell meghatározni
+    ERROR4: Ismeretlen kifejezés

+ 95 - 0
system/languages/id.yaml

@@ -0,0 +1,95 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Frontmatter tidak valid\n\nLokasi: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'peralatan'
+    - 'informasi'
+    - 'nasi'
+    - 'uang'
+    - 'spesies'
+    - 'rangkaian'
+    - 'ikan'
+    - 'domba'
+  INFLECTOR_IRREGULAR:
+    'person': 'orang-orang'
+    'man': 'laki-laki'
+    'child': 'anak-anak'
+    'sex': 'jenis kelamin'
+    'move': 'pindahkan'
+  NICETIME:
+    NO_DATE_PROVIDED: Tanggal tidak tersedia
+    BAD_DATE: Format tanggal salah
+    AGO: yang lalu
+    FROM_NOW: dari saat ini
+    JUST_NOW: baru saja
+    SECOND: detik
+    MINUTE: menit
+    HOUR: jam
+    DAY: hari
+    WEEK: pekan
+    MONTH: bulan
+    YEAR: tahun
+    DECADE: dekade
+    SEC: dtk
+    MIN: mnt
+    HR: j
+    WK: mng
+    MO: bln
+    YR: thn
+    DEC: desimal
+    SECOND_PLURAL: detik
+    MINUTE_PLURAL: menit
+    HOUR_PLURAL: jam
+    DAY_PLURAL: hari
+    WEEK_PLURAL: pekan
+    MONTH_PLURAL: bulan
+    YEAR_PLURAL: tahun
+    DECADE_PLURAL: dekade
+    SEC_PLURAL: dtk
+    MIN_PLURAL: mnt
+    HR_PLURAL: j
+    WK_PLURAL: mgg
+    MO_PLURAL: bln
+    YR_PLURAL: thn
+    DEC_PLURAL: dekade
+  FORM:
+    VALIDATION_FAIL: <b>Validasi gagal:</b>
+    INVALID_INPUT: Input tidak valid di
+    MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januari'
+    - 'Februari'
+    - 'Maret'
+    - 'April'
+    - 'Mei'
+    - 'Juni'
+    - 'Juli'
+    - 'Agustus'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'Desember'
+  DAYS_OF_THE_WEEK:
+    - 'Senin'
+    - 'Selasa'
+    - 'Rabu'
+    - 'Kamis'
+    - 'Jumat'
+    - 'Sabtu'
+    - 'Minggu'
+  CRON:
+    EVERY: Setiap
+    EVERY_HOUR: Setiap jam
+    EVERY_MINUTE: Setiap menit
+    EVERY_DAY_OF_WEEK: Setiap hari selama seminggu
+    EVERY_DAY_OF_MONTH: pada tanggal setiap bulannya
+    EVERY_MONTH: setiap bulan
+    TEXT_PERIOD: Setiap <b />
+    TEXT_TIME: ' pada <b />:<b />'
+    TEXT_DOW: ' pada <b />'
+    TEXT_MONTH: ' pada <b />'
+    TEXT_DOM: ' pada <b />'
+    ERROR1: Tag %s tidak didukung!
+    ERROR2: Jumlah elemen tidak valid
+    ERROR3: jquery_element harus ditetapkan ke pengaturan jqCron
+    ERROR4: Ekspresi tidak dikenali

+ 69 - 0
system/languages/is.yaml

@@ -0,0 +1,69 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitill: %1$s\n---\n\n# Villa: Ógilt efni á forsíðu\n\nSlóð: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'upplýsingar'
+    - 'rice'
+    - 'money'
+    - 'species'
+    - 'series'
+    - 'fish'
+    - 'sheep'
+  NICETIME:
+    AGO: síðan
+    JUST_NOW: í þessu
+    SECOND: sekúndu
+    MINUTE: mínútu
+    HOUR: klukkustund
+    DAY: degi
+    WEEK: viku
+    MONTH: mánuði
+    YEAR: ári
+    DECADE: áratug
+    SEC: sek
+    MIN: mín
+    HR: klst
+    WK: vk
+    MO: mán
+    YR: ár
+    DEC: árat
+    SECOND_PLURAL: sekúndum
+    MINUTE_PLURAL: mínútum
+    HOUR_PLURAL: klukkustundum
+    DAY_PLURAL: dögum
+    WEEK_PLURAL: vikum
+    MONTH_PLURAL: mánuðum
+    YEAR_PLURAL: árum
+    DECADE_PLURAL: áratugum
+    SEC_PLURAL: sek
+    MIN_PLURAL: mín
+    HR_PLURAL: klst
+    WK_PLURAL: vik
+    MO_PLURAL: mán
+    YR_PLURAL: árum
+    DEC_PLURAL: árat
+  FORM:
+    VALIDATION_FAIL: <b>Sannvottun mistókst:</b>
+    MISSING_REQUIRED_FIELD: 'Vantar nauðsynlegan reit:'
+  MONTHS_OF_THE_YEAR:
+    - 'janúar'
+    - 'Febrúar'
+    - 'Mars'
+    - 'Apríl'
+    - 'Maí'
+    - 'Júní'
+    - 'Júlí'
+    - 'Ágúst'
+    - 'September'
+    - 'Október'
+    - 'Nóvember'
+    - 'Desember'
+  DAYS_OF_THE_WEEK:
+    - 'Mánudagur'
+    - 'Þriðjudagur'
+    - 'Miðvikudagur'
+    - 'Fimmtudagur'
+    - 'Föstudagur'
+    - 'Laugardagur'
+    - 'Sunnudagur'

+ 100 - 61
system/languages/it.yaml

@@ -1,62 +1,101 @@
 ---
-FRONTMATTER_ERROR_PAGE: "---Titolo: %1$s---# Errore: Frontmatter non valido: '%2$s' * *%3$s * * ' '%4$s ' '"
-NICETIME:
-  NO_DATE_PROVIDED: Nessuna data fornita
-  BAD_DATE: Data non valida
-  AGO: fa
-  FROM_NOW: da adesso
-  SECOND: secondo
-  MINUTE: minuto
-  HOUR: ora
-  DAY: giorno
-  WEEK: settimana
-  MONTH: mese
-  YEAR: anno
-  DECADE: decennio
-  SEC: sec
-  MIN: min
-  HR: ora
-  WK: settimana
-  MO: mese
-  YR: anno
-  DEC: decennio
-  SECOND_PLURAL: secondi
-  MINUTE_PLURAL: minuti
-  HOUR_PLURAL: ore
-  DAY_PLURAL: giorni
-  WEEK_PLURAL: settimane
-  MONTH_PLURAL: mesi
-  YEAR_PLURAL: anni
-  DECADE_PLURAL: decadi
-  SEC_PLURAL: secondi
-  MIN_PLURAL: minuti
-  HR_PLURAL: ore
-  WK_PLURAL: settimane
-  MO_PLURAL: mesi
-  YR_PLURAL: anni
-  DEC_PLURAL: decenni
-FORM:
-  VALIDATION_FAIL: '<b>Validazione fallita:</b>'
-  INVALID_INPUT: Input non valido in
-  MISSING_REQUIRED_FIELD: 'Campo richiesto mancante:'
-MONTHS_OF_THE_YEAR:
-  - Gennaio
-  - Febbraio
-  - Marzo
-  - Aprile
-  - Maggio
-  - Giugno
-  - Luglio
-  - Agosto
-  - Settembre
-  - Ottobre
-  - Novembre
-  - Dicembre
-DAYS_OF_THE_WEEK:
-  - Lunedì
-  - Martedì
-  - Mercoledì
-  - Giovedì
-  - Venerdì
-  - Sabato
-  - Domenica
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---Titolo: %1$s---# Errore: Frontmatter non valido: '%2$s' * *%3$s * * ' '%4$s ' '"
+  INFLECTOR_UNCOUNTABLE:
+    - 'dotazione'
+    - 'informazione'
+    - 'riso'
+    - 'denaro'
+    - 'specie'
+    - 'serie'
+    - 'pesce'
+    - 'pecora'
+  INFLECTOR_IRREGULAR:
+    'person': 'persone'
+    'man': 'uomini'
+    'child': 'bambino'
+    'sex': 'sessi'
+    'move': 'sposta'
+  INFLECTOR_ORDINALS:
+    'default': '°'
+    'first': '°'
+    'second': 'o'
+    'third': 'o'
+  NICETIME:
+    NO_DATE_PROVIDED: Nessuna data fornita
+    BAD_DATE: Data non valida
+    AGO: fa
+    FROM_NOW: da adesso
+    JUST_NOW: ora
+    SECOND: secondo
+    MINUTE: minuto
+    HOUR: ora
+    DAY: giorno
+    WEEK: settimana
+    MONTH: mese
+    YEAR: anno
+    DECADE: decennio
+    SEC: sec
+    MIN: min
+    HR: ora
+    WK: settimana
+    MO: mese
+    YR: anno
+    DEC: decennio
+    SECOND_PLURAL: secondi
+    MINUTE_PLURAL: minuti
+    HOUR_PLURAL: ore
+    DAY_PLURAL: giorni
+    WEEK_PLURAL: settimane
+    MONTH_PLURAL: mesi
+    YEAR_PLURAL: anni
+    DECADE_PLURAL: decadi
+    SEC_PLURAL: secondi
+    MIN_PLURAL: minuti
+    HR_PLURAL: ore
+    WK_PLURAL: settimane
+    MO_PLURAL: mesi
+    YR_PLURAL: anni
+    DEC_PLURAL: decenni
+  FORM:
+    VALIDATION_FAIL: <b>Validazione fallita:</b>
+    INVALID_INPUT: Input non valido in
+    MISSING_REQUIRED_FIELD: 'Campo richiesto mancante:'
+  MONTHS_OF_THE_YEAR:
+    - 'Gennaio'
+    - 'Febbraio'
+    - 'Marzo'
+    - 'Aprile'
+    - 'Maggio'
+    - 'Giugno'
+    - 'Luglio'
+    - 'Agosto'
+    - 'Settembre'
+    - 'Ottobre'
+    - 'Novembre'
+    - 'Dicembre'
+  DAYS_OF_THE_WEEK:
+    - 'Lunedì'
+    - 'Martedì'
+    - 'Mercoledì'
+    - 'Giovedì'
+    - 'Venerdì'
+    - 'Sabato'
+    - 'Domenica'
+  CRON:
+    EVERY: ogni
+    EVERY_HOUR: ogni ora
+    EVERY_MINUTE: ogni minuto
+    EVERY_DAY_OF_WEEK: ogni giorno della settimana
+    EVERY_DAY_OF_MONTH: ogni giorno del mese
+    EVERY_MONTH: ogni mese
+    TEXT_PERIOD: Ogni <b />
+    TEXT_MINS: ' a <b /> minuto(i) dall''inizio dell''ora'
+    TEXT_TIME: ' alle <b />:<b />'
+    TEXT_DOW: ' su <b />'
+    TEXT_MONTH: ' di <b />'
+    TEXT_DOM: ' di <b />'
+    ERROR1: Il tag %s non è supportato!
+    ERROR2: Numero di elementi non valido
+    ERROR3: Il jquery_element deve essere impostato nelle impostazioni di jqCron
+    ERROR4: Espressione non riconosciuta

+ 27 - 18
system/languages/ja.yaml

@@ -1,24 +1,15 @@
-FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
-INFLECTOR_PLURALS: []
-
-INFLECTOR_SINGULAR: []
-
-INFLECTOR_UNCOUNTABLE: []
-
-INFLECTOR_IRREGULAR:
+---
+GRAV:
+  INFLECTOR_IRREGULAR:
     'person': 'みんな'
     'man': '人'
     'child': '子供'
     'sex': '性別'
     'move': '移動'
-    
-INFLECTOR_ORDINALS: []
-
-NICETIME:
+  NICETIME:
     NO_DATE_PROVIDED: 日付が設定されていません
     BAD_DATE: 不正な日付
     AGO: 前
-    FROM_NOW: from now
     SECOND: 秒
     MINUTE: 分
     HOUR: 時
@@ -33,7 +24,6 @@ NICETIME:
     WK: 週
     MO: 月
     YR: 年
-    DEC: dec
     SECOND_PLURAL: 秒
     MINUTE_PLURAL: 分
     HOUR_PLURAL: 時
@@ -49,9 +39,28 @@ NICETIME:
     MO_PLURAL: 月
     YR_PLURAL: 年
     DEC_PLURAL: 10年
-FORM:
+  FORM:
     VALIDATION_FAIL: <b>バリデーション失敗 :</b>
     INVALID_INPUT: 不正な入力:
-    MISSING_REQUIRED_FIELD: 必須項目が入力されていません:
-MONTHS_OF_THE_YEAR: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
-DAYS_OF_THE_WEEK: ['月', '火', '水', '木', '金', '土', '日']
+    MISSING_REQUIRED_FIELD: '必須項目が入力されていません:'
+  MONTHS_OF_THE_YEAR:
+    - '1月'
+    - '2月'
+    - '3月'
+    - '4月'
+    - '5月'
+    - '6月'
+    - '7月'
+    - '8月'
+    - '9月'
+    - '10月'
+    - '11月'
+    - '12月'
+  DAYS_OF_THE_WEEK:
+    - '月'
+    - '火'
+    - '水'
+    - '木'
+    - '金'
+    - '土'
+    - '日'

+ 63 - 0
system/languages/ko.yaml

@@ -0,0 +1,63 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 오류: 무효의 Frontmatter\n\n경로: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: 제공된 날짜가 없습니다
+    BAD_DATE: 잘못된 날짜
+    AGO: 전
+    FROM_NOW: 후
+    SECOND: 초
+    MINUTE: 분
+    HOUR: 시간
+    DAY: 일
+    WEEK: 주
+    MONTH: 개월
+    YEAR: 년
+    DECADE: 년간
+    SEC: 초
+    MIN: 분
+    HR: 시간
+    WK: 주
+    MO: 개월
+    YR: 년
+    DEC: 년간
+    SECOND_PLURAL: 초
+    MINUTE_PLURAL: 분
+    HOUR_PLURAL: 시간
+    DAY_PLURAL: 일
+    WEEK_PLURAL: 주
+    MONTH_PLURAL: 개월
+    YEAR_PLURAL: 년
+    DECADE_PLURAL: 년간
+    SEC_PLURAL: 초
+    MIN_PLURAL: 분
+    HR_PLURAL: 시간
+    WK_PLURAL: 주
+    MO_PLURAL: 개월
+    YR_PLURAL: 년
+    DEC_PLURAL: 년간
+  FORM:
+    VALIDATION_FAIL: <b>유효성 검사 실패:</b>
+    INVALID_INPUT: 잘못된 입력
+    MISSING_REQUIRED_FIELD: '누락 된 필수 필드:'
+  MONTHS_OF_THE_YEAR:
+    - '일월'
+    - '이월'
+    - '삼월'
+    - '사월'
+    - '오월'
+    - '유월'
+    - '칠월'
+    - '팔월'
+    - '구월'
+    - '시월'
+    - '십일월'
+    - '십이월'
+  DAYS_OF_THE_WEEK:
+    - '월요일'
+    - '화요일'
+    - '수요일'
+    - '목요일'
+    - '금요일'
+    - '토요일'
+    - '일요일'

+ 77 - 68
system/languages/lt.yaml

@@ -1,69 +1,78 @@
 ---
-INFLECTOR_UNCOUNTABLE:
-  2: ryžiai
-  3: pinigai
-  4: prieskoniai
-  5: serijos
-  6: žuvis
-  7: avis
-INFLECTOR_IRREGULAR:
-  person: žmonės
-  man: žmogus
-  child: vaikai
-  sex: lytys
-  move: juda
-NICETIME:
-  NO_DATE_PROVIDED: Nenurodyta data
-  BAD_DATE: Neteisinga data
-  AGO: prieš
-  FROM_NOW: nuo dabar
-  SECOND: sekundė
-  MINUTE: minutė
-  HOUR: valanda
-  DAY: diena
-  WEEK: savaitė
-  MONTH: mėnuo
-  YEAR: metai
-  DECADE: dešimtmetis
-  SEC: sek
-  MIN: min
-  HR: val
-  WK: sav
-  MO: mėn
-  YR: m
-  MINUTE_PLURAL: minutės
-  HOUR_PLURAL: valandos
-  DAY_PLURAL: dienos
-  WEEK_PLURAL: savaitės
-  MONTH_PLURAL: mėnesiai
-  YEAR_PLURAL: metai
-  DECADE_PLURAL: dešimtmečiai
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: val
-  WK_PLURAL: sav
-  MO_PLURAL: mėn
-  YR_PLURAL: m
-FORM:
-  MISSING_REQUIRED_FIELD: 'Būtina užpildyti laukelį:'
-MONTHS_OF_THE_YEAR:
-  - Sausis
-  - Vasaris
-  - Kovas
-  - Balandis
-  - Gegužė
-  - Birželis
-  - Liepa
-  - Rugpjūtis
-  - Rugsėjis
-  - Spalis
-  - Lakpritis
-  - Gruodis
-DAYS_OF_THE_WEEK:
-  - Pirmadienis
-  - Antradienis
-  - Trečiadienis
-  - Ketvirtadienis
-  - Penktadienis
-  - Šeštadienis
-  - Sekmadienis
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Klaida: klaidinga įžanginė konfigūracija\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n %4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'information'
+    - 'ryžiai'
+    - 'pinigai'
+    - 'prieskoniai'
+    - 'serijos'
+    - 'žuvis'
+    - 'avis'
+  INFLECTOR_IRREGULAR:
+    'person': 'žmonės'
+    'man': 'žmogus'
+    'child': 'vaikai'
+    'sex': 'lytys'
+    'move': 'juda'
+  NICETIME:
+    NO_DATE_PROVIDED: Nenurodyta data
+    BAD_DATE: Neteisinga data
+    AGO: prieš
+    FROM_NOW: nuo dabar
+    SECOND: sekundė
+    MINUTE: minutė
+    HOUR: valanda
+    DAY: diena
+    WEEK: savaitė
+    MONTH: mėnuo
+    YEAR: metai
+    DECADE: dešimtmetis
+    SEC: sek.
+    MIN: min.
+    HR: val.
+    WK: sav.
+    MO: mėn.
+    YR: m.
+    DEC: dešimtmetis
+    SECOND_PLURAL: sekundės
+    MINUTE_PLURAL: minutės
+    HOUR_PLURAL: valandos
+    DAY_PLURAL: dienos
+    WEEK_PLURAL: savaitės
+    MONTH_PLURAL: mėnesiai
+    YEAR_PLURAL: metai
+    DECADE_PLURAL: dešimtmečiai
+    SEC_PLURAL: sek.
+    MIN_PLURAL: min.
+    HR_PLURAL: val.
+    WK_PLURAL: sav.
+    MO_PLURAL: mėn.
+    YR_PLURAL: m.
+    DEC_PLURAL: dešimtmečiai
+  FORM:
+    VALIDATION_FAIL: <b>Patvirtinimas nepavyko:</b>
+    INVALID_INPUT: Neteisingai įvesta į
+    MISSING_REQUIRED_FIELD: 'Būtina užpildyti laukelį:'
+  MONTHS_OF_THE_YEAR:
+    - 'Sausis'
+    - 'Vasaris'
+    - 'Kovas'
+    - 'Balandis'
+    - 'Gegužė'
+    - 'Birželis'
+    - 'Liepa'
+    - 'Rugpjūtis'
+    - 'Rugsėjis'
+    - 'Spalis'
+    - 'Lakpritis'
+    - 'Gruodis'
+  DAYS_OF_THE_WEEK:
+    - 'Pirmadienis'
+    - 'Antradienis'
+    - 'Trečiadienis'
+    - 'Ketvirtadienis'
+    - 'Penktadienis'
+    - 'Šeštadienis'
+    - 'Sekmadienis'

+ 4 - 2
system/languages/nb.yaml

@@ -1,2 +1,4 @@
-MONTHS_OF_THE_YEAR: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember']
-DAYS_OF_THE_WEEK: ['mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag', 'søndag']
+---
+GRAV:
+  MONTHS_OF_THE_YEAR: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember']
+  DAYS_OF_THE_WEEK: ['mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag', 'søndag']

+ 143 - 63
system/languages/nl.yaml

@@ -1,64 +1,144 @@
 ---
-INFLECTOR_IRREGULAR:
-  person: personen
-  man: mensen
-  child: kinderen
-  sex: geslacht
-  move: verplaatsen
-NICETIME:
-  NO_DATE_PROVIDED: geen datum opgegeven
-  BAD_DATE: Datumformaat onjuist
-  AGO: geleden
-  FROM_NOW: vanaf nu
-  SECOND: seconde
-  MINUTE: minuut
-  HOUR: uur
-  DAY: dag
-  WEEK: week
-  MONTH: maand
-  YEAR: jaar
-  DECADE: decenium
-  SEC: s
-  MIN: min
-  HR: u
-  MO: ma
-  YR: j
-  SECOND_PLURAL: seconden
-  MINUTE_PLURAL: minuten
-  HOUR_PLURAL: uren
-  DAY_PLURAL: dagen
-  WEEK_PLURAL: weken
-  MONTH_PLURAL: maanden
-  YEAR_PLURAL: jaren
-  DECADE_PLURAL: decennia
-  SEC_PLURAL: seconden
-  MIN_PLURAL: minuten
-  HR_PLURAL: uren
-  WK_PLURAL: weken
-  MO_PLURAL: maanden
-  YR_PLURAL: jaren
-FORM:
-  VALIDATION_FAIL: '<b>Validatie mislukt:</b>'
-  INVALID_INPUT: Ongeldige invoer in
-  MISSING_REQUIRED_FIELD: 'Verplicht veld ontbreekt:'
-MONTHS_OF_THE_YEAR:
-  - Januari
-  - Februari
-  - Maart
-  - april
-  - Mei
-  - Juni
-  - Juli
-  - Augustus
-  - september
-  - Oktober
-  - november
-  - december
-DAYS_OF_THE_WEEK:
-  - Maandag
-  - Dinsdag
-  - Woensdag
-  - Donderdag
-  - Vrijdag
-  - Zaterdag
-  - Zondag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitel: %1$s\n---\n\n# Fout: ongeldige frontmatter\n\nPad: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'uitrusting'
+    - 'informatie'
+    - 'rijst'
+    - 'geld'
+    - 'soorten'
+    - 'reeks'
+    - 'vis'
+    - 'schaap'
+  INFLECTOR_IRREGULAR:
+    'person': 'personen'
+    'man': 'mensen'
+    'child': 'kinderen'
+    'sex': 'geslacht'
+    'move': 'verplaatsen'
+  INFLECTOR_ORDINALS:
+    'default': 'th'
+    'first': 'st'
+    'second': 'nd'
+    'third': 'rd'
+  NICETIME:
+    NO_DATE_PROVIDED: geen datum opgegeven
+    BAD_DATE: Datumformaat onjuist
+    AGO: geleden
+    FROM_NOW: vanaf nu
+    JUST_NOW: zojuist
+    SECOND: seconde
+    MINUTE: minuut
+    HOUR: uur
+    DAY: dag
+    WEEK: week
+    MONTH: maand
+    YEAR: jaar
+    DECADE: decennium
+    SEC: s
+    MIN: min
+    HR: u
+    WK: week
+    MO: ma
+    YR: j
+    DEC: decennia
+    SECOND_PLURAL: seconden
+    MINUTE_PLURAL: minuten
+    HOUR_PLURAL: uren
+    DAY_PLURAL: dagen
+    WEEK_PLURAL: weken
+    MONTH_PLURAL: maanden
+    YEAR_PLURAL: jaren
+    DECADE_PLURAL: decennia
+    SEC_PLURAL: seconden
+    MIN_PLURAL: minuten
+    HR_PLURAL: uren
+    WK_PLURAL: weken
+    MO_PLURAL: maanden
+    YR_PLURAL: jaren
+    DEC_PLURAL: decennia
+  FORM:
+    VALIDATION_FAIL: <b>Validatie mislukt:</b>
+    INVALID_INPUT: Ongeldige invoer in
+    MISSING_REQUIRED_FIELD: 'Ontbrekend verplicht veld:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januari'
+    - 'Februari'
+    - 'Maart'
+    - 'April'
+    - 'Mei'
+    - 'Juni'
+    - 'Juli'
+    - 'Augustus'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Maandag'
+    - 'Dinsdag'
+    - 'Woensdag'
+    - 'Donderdag'
+    - 'Vrijdag'
+    - 'Zaterdag'
+    - 'Zondag'
+  CRON:
+    EVERY: elke
+    EVERY_HOUR: elk uur
+    EVERY_MINUTE: elke minuut
+    EVERY_DAY_OF_WEEK: elke dag van de week
+    EVERY_DAY_OF_MONTH: elke dag van de maand
+    EVERY_MONTH: elke maand
+    TEXT_PERIOD: Elke <b />
+    TEXT_MINS: ' <b /> minuten te laat'
+    TEXT_TIME: ' op <b />:<b />'
+    TEXT_DOW: ' op <b />'
+    TEXT_MONTH: ' van <b />'
+    TEXT_DOM: ' op <b />'
+    ERROR1: De tag %s wordt niet ondersteund!
+    ERROR2: Slecht aantal elementen
+    ERROR3: Het jquery_element moet ingesteld worden in de jqCron instellingen
+    ERROR4: Onbekende expressie

+ 76 - 92
system/languages/no.yaml

@@ -1,93 +1,77 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  Tittel: %1$s
-  ---
-  
-  # Feilmelding: Ugyldig Frontmatter
-  
-  Pane: '%2$s'
-  
-  **%3$s **
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-INFLECTOR_UNCOUNTABLE:
-  - utstyr
-  - informasjon
-  - ris
-  - penger
-  - arter
-  - serier
-  - fisk
-  - sau
-INFLECTOR_IRREGULAR:
-  person: folk
-  man: menn
-  child: barn
-  sex: kjønn
-  move: trekk
-NICETIME:
-  NO_DATE_PROVIDED: Ingen dato gitt
-  BAD_DATE: Dårlig dato
-  AGO: siden
-  FROM_NOW: fra nå
-  SECOND: sekund
-  MINUTE: minutt
-  HOUR: time
-  DAY: dag
-  WEEK: uke
-  MONTH: måned
-  YEAR: år
-  DECADE: tiår
-  SEC: sek
-  MIN: min
-  HR: t
-  WK: uke
-  MO: må
-  YR: år
-  DEC: des
-  SECOND_PLURAL: sekunder
-  MINUTE_PLURAL: minutter
-  HOUR_PLURAL: timer
-  DAY_PLURAL: dager
-  WEEK_PLURAL: uker
-  MONTH_PLURAL: måneder
-  YEAR_PLURAL: år
-  DECADE_PLURAL: tiår
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: timer
-  WK_PLURAL: uker
-  MO_PLURAL: mdr
-  YR_PLURAL: år
-  DEC_PLURAL: årtier
-FORM:
-  VALIDATION_FAIL: '<b>Validering mislyktes:</b>'
-  INVALID_INPUT: Ugyldig innhold i
-  MISSING_REQUIRED_FIELD: 'Mangler påkrevd felt:'
-MONTHS_OF_THE_YEAR:
-  - januar
-  - februar
-  - mars
-  - april
-  - mai
-  - juni
-  - juli
-  - august
-  - september
-  - oktober
-  - november
-  - desember
-DAYS_OF_THE_WEEK:
-  - mandag
-  - tirsdag
-  - onsdag
-  - torsdag
-  - fredag
-  - lørdag
-  - søndag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nTittel: %1$s\n---\n\n# Feilmelding: Ugyldig Frontmatter\n\nSti: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'utstyr'
+    - 'informasjon'
+    - 'ris'
+    - 'penger'
+    - 'arter'
+    - 'serier'
+    - 'fisk'
+    - 'sau'
+  INFLECTOR_IRREGULAR:
+    'person': 'folk'
+    'man': 'menn'
+    'child': 'barn'
+    'sex': 'kjønn'
+    'move': 'trekk'
+  NICETIME:
+    NO_DATE_PROVIDED: Ingen dato gitt
+    BAD_DATE: Ugyldig dato
+    AGO: siden
+    FROM_NOW: fra nå
+    SECOND: sekund
+    MINUTE: minutt
+    HOUR: time
+    DAY: dag
+    WEEK: uke
+    MONTH: måned
+    YEAR: år
+    DECADE: tiår
+    SEC: sek
+    HR: t
+    WK: uke
+    MO: må
+    YR: år
+    DEC: tiår
+    SECOND_PLURAL: sekunder
+    MINUTE_PLURAL: minutter
+    HOUR_PLURAL: timer
+    DAY_PLURAL: dager
+    WEEK_PLURAL: uker
+    MONTH_PLURAL: måneder
+    YEAR_PLURAL: år
+    DECADE_PLURAL: tiår
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: timer
+    WK_PLURAL: uker
+    MO_PLURAL: md
+    YR_PLURAL: år
+    DEC_PLURAL: årtier
+  FORM:
+    VALIDATION_FAIL: <b>Godkjenning mislyktes:</b>
+    INVALID_INPUT: Ugyldig innhold i
+    MISSING_REQUIRED_FIELD: 'Mangler påkrevd felt:'
+  MONTHS_OF_THE_YEAR:
+    - 'januar'
+    - 'februar'
+    - 'mars'
+    - 'april'
+    - 'mai'
+    - 'juni'
+    - 'juli'
+    - 'august'
+    - 'september'
+    - 'oktober'
+    - 'november'
+    - 'desember'
+  DAYS_OF_THE_WEEK:
+    - 'mandag'
+    - 'tirsdag'
+    - 'onsdag'
+    - 'torsdag'
+    - 'fredag'
+    - 'lørdag'
+    - 'søndag'

+ 61 - 74
system/languages/pl.yaml

@@ -1,75 +1,62 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Error: Nieprawidłowy Frontmatter
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Nie podano daty
-  BAD_DATE: Zła data
-  AGO: temu
-  FROM_NOW: od teraz
-  SECOND: sekunda
-  MINUTE: minuta
-  HOUR: godzina
-  DAY: dzień
-  WEEK: tydzień
-  MONTH: miesiąc
-  YEAR: rok
-  DECADE: dekada
-  SEC: sek
-  MIN: min
-  HR: godz
-  WK: tydz
-  MO: m-c
-  YR: rok
-  DEC: dekada
-  SECOND_PLURAL: sekund
-  MINUTE_PLURAL: minut
-  HOUR_PLURAL: godzin
-  DAY_PLURAL: dni
-  WEEK_PLURAL: tygodnie
-  MONTH_PLURAL: miesięcy
-  YEAR_PLURAL: lat
-  DECADE_PLURAL: dekad
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: godz
-  WK_PLURAL: tyg
-  MO_PLURAL: m-ce
-  YR_PLURAL: lat
-  DEC_PLURAL: dekad
-FORM:
-  VALIDATION_FAIL: '<b>Weryfikacja nie powiodła się:</b>'
-  INVALID_INPUT: Nieprawidłowe dane wejściowe
-  MISSING_REQUIRED_FIELD: 'Opuszczono wymagane pole:'
-MONTHS_OF_THE_YEAR:
-  - Styczeń
-  - Luty
-  - Marzec
-  - Kwiecień
-  - Maj
-  - Czerwiec
-  - Lipiec
-  - Sierpień
-  - Wrzesień
-  - Październik
-  - Listopad
-  - Grudzień
-DAYS_OF_THE_WEEK:
-  - Poniedziałek
-  - Wtorek
-  - Środa
-  - Czwartek
-  - Piątek
-  - Sobota
-  - Niedziela
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Nieprawidłowy Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Nie podano daty
+    BAD_DATE: Zła data
+    AGO: temu
+    FROM_NOW: od teraz
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: godzina
+    DAY: dzień
+    WEEK: tydzień
+    MONTH: miesiąc
+    YEAR: rok
+    DECADE: dekada
+    SEC: sek
+    HR: godz
+    WK: tydz
+    MO: m-c
+    YR: rok
+    DEC: dekada
+    SECOND_PLURAL: sekund
+    MINUTE_PLURAL: minut
+    HOUR_PLURAL: godzin
+    DAY_PLURAL: dni
+    WEEK_PLURAL: tygodnie
+    MONTH_PLURAL: miesięcy
+    YEAR_PLURAL: lat
+    DECADE_PLURAL: dekad
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: godz
+    WK_PLURAL: tyg
+    MO_PLURAL: m-ce
+    YR_PLURAL: lat
+    DEC_PLURAL: dekad
+  FORM:
+    VALIDATION_FAIL: <b>Weryfikacja nie powiodła się:</b>
+    INVALID_INPUT: Nieprawidłowe dane wejściowe
+    MISSING_REQUIRED_FIELD: 'Opuszczono wymagane pole:'
+  MONTHS_OF_THE_YEAR:
+    - 'Styczeń'
+    - 'Luty'
+    - 'Marzec'
+    - 'Kwiecień'
+    - 'Maj'
+    - 'Czerwiec'
+    - 'Lipiec'
+    - 'Sierpień'
+    - 'Wrzesień'
+    - 'Październik'
+    - 'Listopad'
+    - 'Grudzień'
+  DAYS_OF_THE_WEEK:
+    - 'Poniedziałek'
+    - 'Wtorek'
+    - 'Środa'
+    - 'Czwartek'
+    - 'Piątek'
+    - 'Sobota'
+    - 'Niedziela'

+ 36 - 78
system/languages/pt.yaml

@@ -1,79 +1,37 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  título: %1$s
-  ---
-  
-  # Erro: Frontmatter inválida
-  
-  Caminho: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_UNCOUNTABLE:
-  1: informação
-  2: arroz
-  3: dinheiro
-INFLECTOR_IRREGULAR:
-  man: homens
-  sex: sexos
-NICETIME:
-  NO_DATE_PROVIDED: Não foi fornecida data
-  BAD_DATE: Data inválida
-  AGO: atrás
-  FROM_NOW: a partir de agora
-  SECOND: segundo
-  MINUTE: minuto
-  HOUR: hora
-  DAY: dia
-  WEEK: semana
-  MONTH: mês
-  YEAR: ano
-  DECADE: década
-  SEC: seg
-  MIN: mín
-  HR: h
-  WK: sem
-  MO: m
-  YR: a
-  DEC: dec
-  SECOND_PLURAL: segundos
-  MINUTE_PLURAL: minutos
-  HOUR_PLURAL: horas
-  DAY_PLURAL: dias
-  WEEK_PLURAL: semanas
-  MONTH_PLURAL: meses
-  YEAR_PLURAL: anos
-  DECADE_PLURAL: décadas
-  SEC_PLURAL: seg
-  MIN_PLURAL: mins
-  HR_PLURAL: hrs
-  WK_PLURAL: sems
-  YR_PLURAL: anos
-FORM:
-  VALIDATION_FAIL: '<b>Validação falhada: </b>'
-  MISSING_REQUIRED_FIELD: 'Campo obrigatório ausente:'
-MONTHS_OF_THE_YEAR:
-  - Janeiro
-  - Fevereiro
-  - Março
-  - Abril
-  - Maio
-  - Junho
-  - Julho
-  - Agosto
-  - Setembro
-  - Outubro
-  - Novembro
-  - Dezembro
-DAYS_OF_THE_WEEK:
-  - Segunda
-  - Terça
-  - Quarta
-  - Quinta
-  - Sexta
-  - Sábado
-  - Domingo
+GRAV:
+  NICETIME:
+    NO_DATE_PROVIDED: Nenhuma data fornecida
+    AGO: atrás
+    SECOND: segundo
+    MINUTE: minuto
+    HOUR: hora
+    DAY: dia
+    WEEK: semana
+    MONTH: mês
+    YEAR: ano
+    DECADE: década
+    SEC: segundos
+    MIN: minutos
+    MINUTE_PLURAL: minutos
+    DAY_PLURAL: dias
+    WEEK_PLURAL: semanas
+    MONTH_PLURAL: meses
+    YEAR_PLURAL: anos
+    DECADE_PLURAL: decadas
+  FORM:
+    VALIDATION_FAIL: <b>Falha na validação!</b>
+    MISSING_REQUIRED_FIELD: 'Campo obrigatório requerido:'
+  MONTHS_OF_THE_YEAR:
+    - 'Janeiro'
+    - 'Fevereiro'
+    - 'Março'
+    - 'Abril'
+    - 'Maio'
+    - 'Junho'
+    - 'Julho'
+    - 'Agosto'
+    - 'Setembro'
+    - 'Outubro'
+    - 'Novembro'
+    - 'Dezembro'

+ 95 - 100
system/languages/ro.yaml

@@ -1,101 +1,96 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  Titlu: %1$s
-  ---
-  # Eroare: Frontmatter este invalid
-  
-  Calea: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-  "/([m|l])ouse$/i": '\1ice'
-  /(matr|vert|ind)ix|ex$/i: '\1ices'
-  /(x|ch|ss|sh)$/i: '\1es'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([^aeiouy]|qu)y$/i": '\1ies'
-  /(hive)$/i: '\1s'
-  "/(?:([^f])fe|([lr])f)$/i": '\1\2ves'
-  /sis$/i: ses
-  "/([ti])um$/i": '\1a'
-  /(buffal|tomat)o$/i: '\1oes'
-INFLECTOR_UNCOUNTABLE:
-  - echipament
-  - informaţie
-  - orez
-  - bani
-  - specii
-  - serii
-  - peşte
-  - oaie
-INFLECTOR_IRREGULAR:
-  person: persoane
-  man: bărbați
-  child: copii
-  sex: sexe
-  move: mutări
-NICETIME:
-  NO_DATE_PROVIDED: Nu există o dată prevăzută
-  BAD_DATE: Dată incorectă
-  AGO: în urmă
-  FROM_NOW: de acum
-  SECOND: secundă
-  MINUTE: minut
-  HOUR: oră
-  DAY: zi
-  WEEK: săptămână
-  MONTH: lună
-  YEAR: an
-  DECADE: decadă
-  SEC: sec
-  MIN: min
-  HR: oră
-  WK: săpt
-  MO: lună
-  YR: an
-  DEC: decadă
-  SECOND_PLURAL: secunde
-  MINUTE_PLURAL: minute
-  HOUR_PLURAL: ore
-  DAY_PLURAL: zile
-  WEEK_PLURAL: săptămâni
-  MONTH_PLURAL: luni
-  YEAR_PLURAL: ani
-  DECADE_PLURAL: decade
-  SEC_PLURAL: sec
-  MIN_PLURAL: min
-  HR_PLURAL: ore
-  WK_PLURAL: săpt
-  MO_PLURAL: luni
-  YR_PLURAL: ani
-  DEC_PLURAL: decenii
-FORM:
-  VALIDATION_FAIL: '<b>Validare nereușită</b>'
-  INVALID_INPUT: Date incorecte în
-  MISSING_REQUIRED_FIELD: 'Câmp obligatoriu lipsă:'
-MONTHS_OF_THE_YEAR:
-  - Ianuarie
-  - Februarie
-  - Martie
-  - Aprilie
-  - Mai
-  - Iunie
-  - Iulie
-  - August
-  - Septembrie
-  - Octombrie
-  - Noiembrie
-  - Decembrie
-DAYS_OF_THE_WEEK:
-  - Luni
-  - Marți
-  - Miercuri
-  - Joi
-  - Vineri
-  - Sâmbătă
-  - Duminică
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nTitlu: %1$s\n---\n# Eroare: Frontmatter este invalid\n\nCalea: `%2$s`\n\n**%3$s**\n\n```\n%4$s"
+  INFLECTOR_UNCOUNTABLE:
+    - 'echipament'
+    - 'informaţie'
+    - 'orez'
+    - 'bani'
+    - 'specii'
+    - 'serii'
+    - 'peşte'
+    - 'oaie'
+  INFLECTOR_IRREGULAR:
+    'person': 'persoane'
+    'man': 'bărbați'
+    'child': 'copii'
+    'sex': 'sexe'
+    'move': 'mutări'
+  NICETIME:
+    NO_DATE_PROVIDED: Nu există o dată prevăzută
+    BAD_DATE: Dată incorectă
+    AGO: în urmă
+    FROM_NOW: de acum
+    JUST_NOW: chiar acum
+    SECOND: secundă
+    MINUTE: minut
+    HOUR: oră
+    DAY: zi
+    WEEK: săptămână
+    MONTH: lună
+    YEAR: an
+    DECADE: decadă
+    SEC: secunde
+    MIN: minute
+    HR: oră
+    WK: săpt
+    MO: lună
+    YR: an
+    DEC: decadă
+    SECOND_PLURAL: secunde
+    MINUTE_PLURAL: minute
+    HOUR_PLURAL: ore
+    DAY_PLURAL: zile
+    WEEK_PLURAL: săptămâni
+    MONTH_PLURAL: luni
+    YEAR_PLURAL: ani
+    DECADE_PLURAL: decade
+    SEC_PLURAL: sec
+    MIN_PLURAL: min
+    HR_PLURAL: ore
+    WK_PLURAL: săpt
+    MO_PLURAL: luni
+    YR_PLURAL: ani
+    DEC_PLURAL: decenii
+  FORM:
+    VALIDATION_FAIL: <b>Validare nereușită</b>
+    INVALID_INPUT: Date incorecte în
+    MISSING_REQUIRED_FIELD: 'Câmp obligatoriu lipsă:'
+  MONTHS_OF_THE_YEAR:
+    - 'Ianuarie'
+    - 'Februarie'
+    - 'Martie'
+    - 'Aprilie'
+    - 'Mai'
+    - 'Iunie'
+    - 'Iulie'
+    - 'August'
+    - 'Septembrie'
+    - 'Octombrie'
+    - 'Noiembrie'
+    - 'Decembrie'
+  DAYS_OF_THE_WEEK:
+    - 'Luni'
+    - 'Marți'
+    - 'Miercuri'
+    - 'Joi'
+    - 'Vineri'
+    - 'Sâmbătă'
+    - 'Duminică'
+  CRON:
+    EVERY: la fiecare
+    EVERY_HOUR: la fiecare oră
+    EVERY_MINUTE: la fiecare minut
+    EVERY_DAY_OF_WEEK: fiecare zi a săptămânii
+    EVERY_DAY_OF_MONTH: fiecare zi a lunii
+    EVERY_MONTH: fiecare lună
+    TEXT_PERIOD: Fiecare <b />
+    TEXT_MINS: ' la <b /> minut(e) ale fiecărei ore'
+    TEXT_TIME: ' la <b />:<b />'
+    TEXT_DOW: ' pe <b />'
+    TEXT_MONTH: 'al(e) <b />'
+    TEXT_DOM: ' pe <b />'
+    ERROR1: Eticheta %s nu este acceptată!
+    ERROR2: Număr nevalid de elemente
+    ERROR3: jquery_element ar trebui setat în opțiunile jqCron
+    ERROR4: Expresie necunoscută

+ 100 - 80
system/languages/ru.yaml

@@ -1,81 +1,101 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Ошибка: Недопустимое содержимое
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_IRREGULAR:
-  person: люди
-  man: человек
-  child: ребенок
-  sex: пол
-  move: движется
-NICETIME:
-  NO_DATE_PROVIDED: Дата не указана
-  BAD_DATE: Неверная дата
-  AGO: назад
-  FROM_NOW: теперь
-  SECOND: секунда
-  MINUTE: минута
-  HOUR: час
-  DAY: д
-  WEEK: неделя
-  MONTH: месяц
-  YEAR: год
-  DECADE: десятилетие
-  SEC: с
-  MIN: мин
-  HR: ч
-  WK: нед.
-  MO: мес.
-  YR: г.
-  DEC: гг.
-  SECOND_PLURAL: секунды
-  MINUTE_PLURAL: минуты
-  HOUR_PLURAL: часы
-  DAY_PLURAL: д
-  WEEK_PLURAL: недели
-  MONTH_PLURAL: месяцы
-  YEAR_PLURAL: годы
-  DECADE_PLURAL: десятилетия
-  SEC_PLURAL: с
-  MIN_PLURAL: мин
-  HR_PLURAL: ч
-  WK_PLURAL: нед
-  MO_PLURAL: мес
-  YR_PLURAL: г.
-  DEC_PLURAL: гг.
-FORM:
-  VALIDATION_FAIL: '<b>Проверка не удалась:</b>'
-  INVALID_INPUT: Неверный ввод в
-  MISSING_REQUIRED_FIELD: 'Отсутствует необходимое поле:'
-MONTHS_OF_THE_YEAR:
-  - Январь
-  - Февраль
-  - Март
-  - Апрель
-  - Май
-  - Июнь
-  - Июль
-  - Август
-  - Сентябрь
-  - Октябрь
-  - Ноябрь
-  - Декабрь
-DAYS_OF_THE_WEEK:
-  - Понедельник
-  - Вторник
-  - Среда
-  - Четверг
-  - Пятница
-  - Суббота
-  - Воскресенье
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Ошибка: недопустимое содержимое Frontmatter\n\nПуть: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'экипировка'
+    - 'информация'
+    - 'рис'
+    - 'деньги'
+    - 'виды'
+    - 'серии'
+    - 'рыба'
+    - 'овца'
+  INFLECTOR_IRREGULAR:
+    'person': 'люди'
+    'man': 'человек'
+    'child': 'ребенок'
+    'sex': 'пол'
+    'move': 'движется'
+  INFLECTOR_ORDINALS:
+    'default': 'й'
+    'first': 'й'
+    'second': 'й'
+    'third': 'й'
+  NICETIME:
+    NO_DATE_PROVIDED: Дата не указана
+    BAD_DATE: Неверная дата
+    AGO: назад
+    FROM_NOW: теперь
+    JUST_NOW: только что
+    SECOND: секунда
+    MINUTE: минута
+    HOUR: час
+    DAY: день
+    WEEK: неделя
+    MONTH: месяц
+    YEAR: год
+    DECADE: десятилетие
+    SEC: сек
+    MIN: мин
+    HR: ч
+    WK: нед
+    MO: мес
+    YR: г
+    DEC: дстлт
+    SECOND_PLURAL: сек
+    MINUTE_PLURAL: мин
+    HOUR_PLURAL: ч
+    DAY_PLURAL: д
+    WEEK_PLURAL: нед
+    MONTH_PLURAL: мес
+    YEAR_PLURAL: г
+    DECADE_PLURAL: дстлт
+    SEC_PLURAL: сек
+    MIN_PLURAL: мин
+    HR_PLURAL: ч
+    WK_PLURAL: нед
+    MO_PLURAL: мес
+    YR_PLURAL: г
+    DEC_PLURAL: дстлт
+  FORM:
+    VALIDATION_FAIL: <b>Проверка не удалась:</b>
+    INVALID_INPUT: Неверный ввод в
+    MISSING_REQUIRED_FIELD: 'Отсутствует необходимое поле:'
+  MONTHS_OF_THE_YEAR:
+    - 'январь'
+    - 'февраль'
+    - 'март'
+    - 'апрель'
+    - 'май'
+    - 'июнь'
+    - 'Июль'
+    - 'Август'
+    - 'Сентябрь'
+    - 'Октябрь'
+    - 'Ноябрь'
+    - 'Декабрь'
+  DAYS_OF_THE_WEEK:
+    - 'понедельник'
+    - 'вторник'
+    - 'среда'
+    - 'четверг'
+    - 'пятница'
+    - 'суббота'
+    - 'воскресенье'
+  CRON:
+    EVERY: раз в
+    EVERY_HOUR: раз в час
+    EVERY_MINUTE: раз в минуту
+    EVERY_DAY_OF_WEEK: каждый день недели
+    EVERY_DAY_OF_MONTH: каждый день недели
+    EVERY_MONTH: раз в месяц
+    TEXT_PERIOD: Каждый <b />
+    TEXT_MINS: ' в <b /> минуте(ах) за час'
+    TEXT_TIME: ' в <b />:<b />'
+    TEXT_DOW: ' на <b />'
+    TEXT_MONTH: ' из <b />'
+    TEXT_DOM: ' на <b />'
+    ERROR1: Тег %s не поддерживается!
+    ERROR2: Неверное количество элементов
+    ERROR3: jquery_element должен быть установлен в настройки jqCron
+    ERROR4: Выражение не распознано

+ 59 - 41
system/languages/sk.yaml

@@ -1,42 +1,60 @@
 ---
-NICETIME:
-  NO_DATE_PROVIDED: Neposkytnutý žiaden dátum
-  BAD_DATE: Nesprávny dátum
-  AGO: pred
-  FROM_NOW: odteraz
-  SECOND: sekunda
-  MINUTE: minúta
-  HOUR: hodina
-  DAY: deň
-  WEEK: týždeň
-  MONTH: mesiac
-  YEAR: rok
-  DECADE: desaťročie
-  SEC: sek
-  MIN: min
-  HR: hod
-FORM:
-  VALIDATION_FAIL: '<b>Overenie zlyhalo:</b>'
-  INVALID_INPUT: Neplatný vstup v
-  MISSING_REQUIRED_FIELD: 'Chýba vyžadované pole:'
-MONTHS_OF_THE_YEAR:
-  - Január
-  - Február
-  - Marec
-  - Apríl
-  - Máj
-  - Jún
-  - Júl
-  - August
-  - September
-  - Október
-  - November
-  - December
-DAYS_OF_THE_WEEK:
-  - Pondelok
-  - Utorok
-  - Streda
-  - Štvrtok
-  - Piatok
-  - Sobota
-  - Nedeľa
+GRAV:
+  NICETIME:
+    NO_DATE_PROVIDED: Neposkytnutý žiaden dátum
+    BAD_DATE: Nesprávny dátum
+    AGO: pred
+    FROM_NOW: odteraz
+    SECOND: sekunda
+    MINUTE: minúta
+    HOUR: hodina
+    DAY: deň
+    WEEK: týždeň
+    MONTH: mesiac
+    YEAR: rok
+    DECADE: desaťročie
+    SEC: sek
+    HR: hod
+    WK: t
+    MO: m
+    YR: r
+    SECOND_PLURAL: sekúnd
+    MINUTE_PLURAL: minút
+    HOUR_PLURAL: hodín
+    DAY_PLURAL: dní
+    WEEK_PLURAL: týždňov
+    MONTH_PLURAL: mesiacov
+    YEAR_PLURAL: rokov
+    DECADE_PLURAL: dekád
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: hod
+    WK_PLURAL: t
+    MO_PLURAL: mes.
+    YR_PLURAL: rokov
+    DEC_PLURAL: dekád
+  FORM:
+    VALIDATION_FAIL: <b>Overenie zlyhalo:</b>
+    INVALID_INPUT: Neplatný vstup v
+    MISSING_REQUIRED_FIELD: 'Chýba vyžadované pole:'
+  MONTHS_OF_THE_YEAR:
+    - 'Január'
+    - 'Február'
+    - 'Marec'
+    - 'Apríl'
+    - 'Máj'
+    - 'Jún'
+    - 'Júl'
+    - 'August'
+    - 'September'
+    - 'Október'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Pondelok'
+    - 'Utorok'
+    - 'Streda'
+    - 'Štvrtok'
+    - 'Piatok'
+    - 'Sobota'
+    - 'Nedeľa'

+ 62 - 0
system/languages/sl.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Napaka: Neveljavna Frontmatter\n\nPath: `%2$s`\n\n**%3$s ** \n\n```\n%4$s \n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Datum ni na voljo
+    BAD_DATE: Neveljaven datum
+    AGO: pred
+    FROM_NOW: od zdaj
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: ura
+    DAY: dan
+    WEEK: teden
+    MONTH: mesec
+    YEAR: leto
+    DECADE: desetletje
+    SEC: sek
+    HR: ur
+    WK: T.
+    MO: m
+    YR: l
+    DEC: des
+    SECOND_PLURAL: sekund
+    MINUTE_PLURAL: minut
+    HOUR_PLURAL: ure
+    DAY_PLURAL: dnevi
+    WEEK_PLURAL: tednov
+    MONTH_PLURAL: mesecev
+    YEAR_PLURAL: leta
+    DECADE_PLURAL: desetletja
+    SEC_PLURAL: s
+    MIN_PLURAL: min
+    HR_PLURAL: ur
+    WK_PLURAL: t
+    MO_PLURAL: m
+    YR_PLURAL: l
+    DEC_PLURAL: des
+  FORM:
+    VALIDATION_FAIL: <b>Preverjanje veljavnosti ni uspelo:</b>
+    INVALID_INPUT: Neveljaven vnos v
+    MISSING_REQUIRED_FIELD: 'Manjka obvezno polje:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januar'
+    - 'Februar'
+    - 'Marec'
+    - 'April'
+    - 'Maj'
+    - 'Junij'
+    - 'Julij'
+    - 'Avgust'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Ponedeljek'
+    - 'Torek'
+    - 'Sreda'
+    - 'Četrtek'
+    - 'Petek'
+    - 'Sobota'
+    - 'Nedelja'

+ 60 - 61
system/languages/sv.yaml

@@ -1,62 +1,61 @@
 ---
-FRONTMATTER_ERROR_PAGE: '--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```'
-NICETIME:
-  NO_DATE_PROVIDED: Inget datum har angivits
-  BAD_DATE: Ogiltigt datum
-  AGO: sedan
-  FROM_NOW: från nu
-  SECOND: sekund
-  MINUTE: minut
-  HOUR: timme
-  DAY: dag
-  WEEK: vecka
-  MONTH: månad
-  YEAR: år
-  DECADE: årtionde
-  SEC: sek
-  MIN: min
-  HR: t
-  WK: v
-  MO: m
-  YR: år
-  DEC: dec
-  SECOND_PLURAL: sekunder
-  MINUTE_PLURAL: minuter
-  HOUR_PLURAL: timmar
-  DAY_PLURAL: dagar
-  WEEK_PLURAL: veckor
-  MONTH_PLURAL: månader
-  YEAR_PLURAL: år
-  DECADE_PLURAL: årtionden
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: t
-  WK_PLURAL: v
-  MO_PLURAL: må
-  YR_PLURAL: år
-  DEC_PLURAL: dec
-FORM:
-  VALIDATION_FAIL: '<b>Kontrollen misslyckades:</b>'
-  INVALID_INPUT: Ogiltig indata i
-  MISSING_REQUIRED_FIELD: 'Obligatoriskt fält måste fyllas i:'
-MONTHS_OF_THE_YEAR:
-  - Januari
-  - Februrari
-  - Mars
-  - April
-  - Maj
-  - Juni
-  - Juli
-  - Augusti
-  - September
-  - Oktober
-  - November
-  - December
-DAYS_OF_THE_WEEK:
-  - Måndag
-  - Tisdag
-  - Onsdag
-  - Torsdag
-  - Fredag
-  - Lördag
-  - Söndag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```"
+  NICETIME:
+    NO_DATE_PROVIDED: Inget datum har angivits
+    BAD_DATE: Ogiltigt datum
+    AGO: sedan
+    FROM_NOW: fr.o.m nu
+    SECOND: sekund
+    MINUTE: minut
+    HOUR: timme
+    DAY: dag
+    WEEK: vecka
+    MONTH: månad
+    YEAR: år
+    DECADE: årtionde
+    SEC: sek
+    HR: t
+    WK: v
+    MO: m
+    YR: år
+    SECOND_PLURAL: sekunder
+    MINUTE_PLURAL: minuter
+    HOUR_PLURAL: timmar
+    DAY_PLURAL: dagar
+    WEEK_PLURAL: veckor
+    MONTH_PLURAL: månader
+    YEAR_PLURAL: år
+    DECADE_PLURAL: årtionden
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: t
+    WK_PLURAL: v
+    MO_PLURAL: må
+    YR_PLURAL: år
+    DEC_PLURAL: dec
+  FORM:
+    VALIDATION_FAIL: <b>Kontrollen misslyckades:</b>
+    INVALID_INPUT: Ogiltig indata i
+    MISSING_REQUIRED_FIELD: 'Obligatoriskt fält måste fyllas i:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januari'
+    - 'Februari'
+    - 'Mars'
+    - 'April'
+    - 'Maj'
+    - 'Juni'
+    - 'Juli'
+    - 'Augusti'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Måndag'
+    - 'Tisdag'
+    - 'Onsdag'
+    - 'Torsdag'
+    - 'Fredag'
+    - 'Lördag'
+    - 'Söndag'

+ 55 - 74
system/languages/th.yaml

@@ -1,75 +1,56 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  ชื่อเรื่อง: %1$s
-  ---
-  
-  # ข้อผิดพลาด: Invalid Frontmatter
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: ไม่มีวันที่ให้
-  BAD_DATE: รูปแบบวันที่ผิด
-  AGO: ที่ผ่านมา
-  FROM_NOW: จากตอนนี้
-  SECOND: วินาที
-  MINUTE: นาที
-  HOUR: ชั่วโมง
-  DAY: วัน
-  WEEK: สัปดาห์
-  MONTH: เดือน
-  YEAR: ปี
-  DECADE: ทศวรรษที่ผ่านมา
-  SEC: วิ
-  MIN: นาที
-  HR: ชม.
-  WK: wk
-  MO: mo
-  YR: yr
-  DEC: dec
-  SECOND_PLURAL: วินาที
-  MINUTE_PLURAL: นาที
-  HOUR_PLURAL: ชั่วโมง
-  DAY_PLURAL: วัน
-  WEEK_PLURAL: สัปดาห์
-  MONTH_PLURAL: เดือน
-  YEAR_PLURAL: ปี
-  DECADE_PLURAL: ทศวรรษที่ผ่านมา
-  SEC_PLURAL: วินาที
-  MIN_PLURAL: นาที
-  HR_PLURAL: ชั่วโมง
-  WK_PLURAL: wks
-  MO_PLURAL: mos
-  YR_PLURAL: ปี
-  DEC_PLURAL: decs
-FORM:
-  VALIDATION_FAIL: '<b>ตรวจสอบล้มเหลว: </b>'
-  INVALID_INPUT: ป้อนข้อมูลไม่ถูกต้องใน
-  MISSING_REQUIRED_FIELD: 'ขาดข้อมูลที่จำเป็น:'
-MONTHS_OF_THE_YEAR:
-  - มกราคม
-  - กุมภาพันธ์
-  - มีนาคม
-  - เมษายน
-  - พฤษภาคม
-  - มิถุนายน
-  - กรกฏาคม
-  - สิงหาคม
-  - กันยายน
-  - ตุลาคม
-  - พฤศจิกายน
-  - ธันวาคม
-DAYS_OF_THE_WEEK:
-  - จันทร์
-  - อังคาร
-  - พุธ
-  - พฤหัสบดี
-  - ศุกร์
-  - เสาร์
-  - อาทิตย์
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nชื่อเรื่อง: %1$s\n---\n\n# ข้อผิดพลาด: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: ไม่มีวันที่ให้
+    BAD_DATE: รูปแบบวันที่ผิด
+    AGO: ที่ผ่านมา
+    FROM_NOW: จากตอนนี้
+    SECOND: วินาที
+    MINUTE: นาที
+    HOUR: ชั่วโมง
+    DAY: วัน
+    WEEK: สัปดาห์
+    MONTH: เดือน
+    YEAR: ปี
+    DECADE: ทศวรรษที่ผ่านมา
+    SEC: วิ
+    MIN: นาที
+    HR: ชม.
+    SECOND_PLURAL: วินาที
+    MINUTE_PLURAL: นาที
+    HOUR_PLURAL: ชั่วโมง
+    DAY_PLURAL: วัน
+    WEEK_PLURAL: สัปดาห์
+    MONTH_PLURAL: เดือน
+    YEAR_PLURAL: ปี
+    DECADE_PLURAL: ทศวรรษที่ผ่านมา
+    SEC_PLURAL: วินาที
+    MIN_PLURAL: นาที
+    HR_PLURAL: ชั่วโมง
+    YR_PLURAL: ปี
+  FORM:
+    VALIDATION_FAIL: '<b>ตรวจสอบล้มเหลว: </b>'
+    INVALID_INPUT: ป้อนข้อมูลไม่ถูกต้องใน
+    MISSING_REQUIRED_FIELD: 'ขาดข้อมูลที่จำเป็น:'
+  MONTHS_OF_THE_YEAR:
+    - 'มกราคม'
+    - 'กุมภาพันธ์'
+    - 'มีนาคม'
+    - 'เมษายน'
+    - 'พฤษภาคม'
+    - 'มิถุนายน'
+    - 'กรกฏาคม'
+    - 'สิงหาคม'
+    - 'กันยายน'
+    - 'ตุลาคม'
+    - 'พฤศจิกายน'
+    - 'ธันวาคม'
+  DAYS_OF_THE_WEEK:
+    - 'จันทร์'
+    - 'อังคาร'
+    - 'พุธ'
+    - 'พฤหัสบดี'
+    - 'ศุกร์'
+    - 'เสาร์'
+    - 'อาทิตย์'

+ 62 - 58
system/languages/tr.yaml

@@ -1,59 +1,63 @@
 ---
-NICETIME:
-  NO_DATE_PROVIDED: Tarih yok
-  BAD_DATE: Yanlış tarih
-  AGO: önce
-  FROM_NOW: (şimdiden)
-  SECOND: saniye
-  MINUTE: dakika
-  HOUR: saat
-  DAY: gün
-  WEEK: hafta
-  MONTH: ay
-  YEAR: yıl
-  DECADE: onyıl
-  SEC: sn
-  MIN: dk
-  HR: sa
-  WK: hft
-  MO: ay
-  YR: yl
-  DEC: onyl
-  SECOND_PLURAL: saniye
-  MINUTE_PLURAL: dakika
-  HOUR_PLURAL: saat
-  DAY_PLURAL: gün
-  WEEK_PLURAL: hafta
-  MONTH_PLURAL: ay
-  YEAR_PLURAL: yıl
-  DECADE_PLURAL: onyıl
-  SEC_PLURAL: sn
-  MIN_PLURAL: dk
-  HR_PLURAL: sa
-  WK_PLURAL: hft
-  MO_PLURAL: ay
-  YR_PLURAL: yl
-  DEC_PLURAL: onyl
-FORM:
-  VALIDATION_FAIL: '<b>Doğrulama başarısız:</b>'
-MONTHS_OF_THE_YEAR:
-  - Ocak
-  - Şubat
-  - Mart
-  - Nisan
-  - Mayıs
-  - Haziran
-  - Temmuz
-  - Ağustos
-  - Eylül
-  - Ekim
-  - Kasım
-  - Aralık
-DAYS_OF_THE_WEEK:
-  - Pazartesi
-  - Salı
-  - Çarşamba
-  - Perşembe
-  - Cuma
-  - Cumartesi
-  - Pazar
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nBaşlık: %1$s\n---\n\n# Hata: Geçersiz Önbölüm\n\nYol: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Sağlanan tarih yok
+    BAD_DATE: Yanlış tarih
+    AGO: önce
+    FROM_NOW: şu andan itibaren
+    SECOND: saniye
+    MINUTE: dakika
+    HOUR: saat
+    DAY: gün
+    WEEK: hafta
+    MONTH: ay
+    YEAR: yıl
+    DECADE: onyıl
+    SEC: sn
+    MIN: dk
+    HR: sa
+    WK: hft
+    MO: ay
+    YR: yl
+    DEC: onyl
+    SECOND_PLURAL: saniye
+    MINUTE_PLURAL: dakika
+    HOUR_PLURAL: saat
+    DAY_PLURAL: gün
+    WEEK_PLURAL: hafta
+    MONTH_PLURAL: ay
+    YEAR_PLURAL: yıl
+    DECADE_PLURAL: onyıl
+    SEC_PLURAL: sn
+    MIN_PLURAL: dk
+    HR_PLURAL: sa
+    WK_PLURAL: hft
+    MO_PLURAL: ay
+    YR_PLURAL: yıl
+    DEC_PLURAL: onyl
+  FORM:
+    VALIDATION_FAIL: <b>Doğrulama başarısız:</b>
+    INVALID_INPUT: Geçersiz bilgi girişi
+    MISSING_REQUIRED_FIELD: 'Gerekli alan eksik:'
+  MONTHS_OF_THE_YEAR:
+    - 'Ocak'
+    - 'Şubat'
+    - 'Mart'
+    - 'Nisan'
+    - 'Mayıs'
+    - 'Haziran'
+    - 'Temmuz'
+    - 'Ağustos'
+    - 'Eylül'
+    - 'Ekim'
+    - 'Kasım'
+    - 'Aralık'
+  DAYS_OF_THE_WEEK:
+    - 'Pazartesi'
+    - 'Salı'
+    - 'Çarşamba'
+    - 'Perşembe'
+    - 'Cuma'
+    - 'Cumartesi'
+    - 'Pazar'

+ 62 - 74
system/languages/uk.yaml

@@ -1,75 +1,63 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Помилка: Недопустимий вміст
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Не вказана дата
-  BAD_DATE: Невірна дата
-  AGO: назад
-  FROM_NOW: відтепер
-  SECOND: секунда
-  MINUTE: хвилина
-  HOUR: година
-  DAY: день
-  WEEK: тиждень
-  MONTH: місяць
-  YEAR: рік
-  DECADE: десятиріччя
-  SEC: с
-  MIN: хв
-  HR: год
-  WK: тиж.
-  MO: міс.
-  YR: р.
-  DEC: рр.
-  SECOND_PLURAL: секунди
-  MINUTE_PLURAL: хвилини
-  HOUR_PLURAL: години
-  DAY_PLURAL: дні
-  WEEK_PLURAL: тижні
-  MONTH_PLURAL: місяці
-  YEAR_PLURAL: роки
-  DECADE_PLURAL: десятиріччя
-  SEC_PLURAL: с
-  MIN_PLURAL: хв
-  HR_PLURAL: год
-  WK_PLURAL: тиж.
-  MO_PLURAL: міс.
-  YR_PLURAL: рр.
-  DEC_PLURAL: рр.
-FORM:
-  VALIDATION_FAIL: '<b>Перевірка не вдалася:</b>'
-  INVALID_INPUT: Невірне введення в
-  MISSING_REQUIRED_FIELD: 'Відсутнє необхідне поле:'
-MONTHS_OF_THE_YEAR:
-  - Січень
-  - Лютий
-  - Березень
-  - Квітень
-  - Травень
-  - Червень
-  - Липень
-  - Серпень
-  - Вересень
-  - Жовтень
-  - Листопад
-  - Грудень
-DAYS_OF_THE_WEEK:
-  - Понеділок
-  - Вівторок
-  - Середа
-  - Четвер
-  - "П'ятниця"
-  - Субота
-  - Неділя
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Помилка: Недопустимий вміст\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Не вказана дата
+    BAD_DATE: Невірна дата
+    AGO: назад
+    FROM_NOW: відтепер
+    SECOND: секунда
+    MINUTE: хвилина
+    HOUR: година
+    DAY: день
+    WEEK: тиждень
+    MONTH: місяць
+    YEAR: рік
+    DECADE: десятиріччя
+    SEC: с
+    MIN: хв
+    HR: год
+    WK: тиж.
+    MO: міс.
+    YR: р.
+    DEC: рр.
+    SECOND_PLURAL: секунди
+    MINUTE_PLURAL: хвилини
+    HOUR_PLURAL: години
+    DAY_PLURAL: дні
+    WEEK_PLURAL: тижні
+    MONTH_PLURAL: місяці
+    YEAR_PLURAL: роки
+    DECADE_PLURAL: десятиріччя
+    SEC_PLURAL: с
+    MIN_PLURAL: хв
+    HR_PLURAL: год
+    WK_PLURAL: тиж.
+    MO_PLURAL: міс.
+    YR_PLURAL: рр.
+    DEC_PLURAL: рр.
+  FORM:
+    VALIDATION_FAIL: <b>Перевірка не вдалася:</b>
+    INVALID_INPUT: Невірне введення в
+    MISSING_REQUIRED_FIELD: 'Відсутнє обов''язкове поле:'
+  MONTHS_OF_THE_YEAR:
+    - 'Січень'
+    - 'Лютий'
+    - 'Березень'
+    - 'Квітень'
+    - 'Травень'
+    - 'Червень'
+    - 'Липень'
+    - 'Серпень'
+    - 'Вересень'
+    - 'Жовтень'
+    - 'Листопад'
+    - 'Грудень'
+  DAYS_OF_THE_WEEK:
+    - 'Понеділок'
+    - 'Вівторок'
+    - 'Середа'
+    - 'Четвер'
+    - 'П''ятниця'
+    - 'Субота'
+    - 'Неділя'

+ 62 - 74
system/languages/vi.yaml

@@ -1,75 +1,63 @@
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Error: Invalid Frontmatter
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Không có ngày được cung cấp
-  BAD_DATE: Ngày không hợp lệ
-  AGO: cách đây
-  FROM_NOW: từ bây giờ
-  SECOND: giây
-  MINUTE: phút
-  HOUR: giờ
-  DAY: ngày
-  WEEK: tuần
-  MONTH: tháng
-  YEAR: năm
-  DECADE: thập kỷ
-  SEC: giây
-  MIN: phút
-  HR: giờ
-  WK: tuần
-  MO: tháng
-  YR: năm
-  DEC: thập kỷ
-  SECOND_PLURAL: giây
-  MINUTE_PLURAL: phút
-  HOUR_PLURAL: giờ
-  DAY_PLURAL: ngày
-  WEEK_PLURAL: tuần
-  MONTH_PLURAL: tháng
-  YEAR_PLURAL: năm
-  DECADE_PLURAL: thập kỷ
-  SEC_PLURAL: giây
-  MIN_PLURAL: phút
-  HR_PLURAL: giờ
-  WK_PLURAL: tuần
-  MO_PLURAL: tháng
-  YR_PLURAL: năm
-  DEC_PLURAL: thập kỷ
-FORM:
-  VALIDATION_FAIL: '<b>Xác nhận thất bại:</b>'
-  INVALID_INPUT: Dữ liệu nhập không hợp lệ cho
-  MISSING_REQUIRED_FIELD: 'Thiếu trường bắt buộc:'
-MONTHS_OF_THE_YEAR:
-  - Tháng 1
-  - Tháng 2
-  - Tháng 3
-  - Tháng 4
-  - Tháng 5
-  - Tháng 6
-  - Tháng 7
-  - Tháng 8
-  - Tháng 9
-  - Tháng 10
-  - Tháng Mười 11
-  - Tháng 12
-DAYS_OF_THE_WEEK:
-  - Thứ 2
-  - Thứ 3
-  - Thứ 4
-  - Thứ 5
-  - Thứ 6
-  - Thứ 7
-  - Chủ Nhật
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntiêu đề: %1$s\n---\n\n# Error: Trang không hợp lệ\n\nĐường dẫn: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Không có ngày được cung cấp
+    BAD_DATE: Ngày không hợp lệ
+    AGO: cách đây
+    FROM_NOW: từ bây giờ
+    SECOND: giây
+    MINUTE: phút
+    HOUR: giờ
+    DAY: ngày
+    WEEK: tuần
+    MONTH: tháng
+    YEAR: năm
+    DECADE: thập kỷ
+    SEC: giây
+    MIN: phút
+    HR: giờ
+    WK: tuần
+    MO: tháng
+    YR: năm
+    DEC: thập kỷ
+    SECOND_PLURAL: giây
+    MINUTE_PLURAL: phút
+    HOUR_PLURAL: giờ
+    DAY_PLURAL: ngày
+    WEEK_PLURAL: tuần
+    MONTH_PLURAL: tháng
+    YEAR_PLURAL: năm
+    DECADE_PLURAL: thập kỷ
+    SEC_PLURAL: giây
+    MIN_PLURAL: phút
+    HR_PLURAL: giờ
+    WK_PLURAL: tuần
+    MO_PLURAL: tháng
+    YR_PLURAL: năm
+    DEC_PLURAL: thập kỷ
+  FORM:
+    VALIDATION_FAIL: <b>Xác nhận thất bại:</b>
+    INVALID_INPUT: Dữ liệu nhập không hợp lệ cho
+    MISSING_REQUIRED_FIELD: 'Thiếu trường bắt buộc:'
+  MONTHS_OF_THE_YEAR:
+    - 'Tháng 1'
+    - 'Tháng 2'
+    - 'Tháng 3'
+    - 'Tháng 4'
+    - 'Tháng 5'
+    - 'Tháng 6'
+    - 'Tháng 7'
+    - 'Tháng 8'
+    - 'Tháng 9'
+    - 'Tháng 10'
+    - 'Tháng 11'
+    - 'Tháng 12'
+  DAYS_OF_THE_WEEK:
+    - 'Thứ 2'
+    - 'Thứ 3'
+    - 'Thứ 4'
+    - 'Thứ 5'
+    - 'Thứ 6'
+    - 'Thứ 7'
+    - 'Chủ Nhật'

+ 42 - 0
system/languages/zh-tw.yaml

@@ -0,0 +1,42 @@
+---
+GRAV:
+  NICETIME:
+    SECOND: 秒
+    MINUTE: 分
+    HOUR: 小時
+    SECOND_PLURAL: 秒
+    MINUTE_PLURAL: 分
+    HOUR_PLURAL: 時
+    DAY_PLURAL: 日
+    WEEK_PLURAL: 周
+    MONTH_PLURAL: 月
+    YEAR_PLURAL: 年
+    DECADE_PLURAL: 十年
+    SEC_PLURAL: 秒
+    MIN_PLURAL: 分
+    HR_PLURAL: 時
+    WK_PLURAL: 周
+    MO_PLURAL: 月
+    YR_PLURAL: 年
+    DEC_PLURAL: 十年
+  MONTHS_OF_THE_YEAR:
+    - '一月'
+    - '二月'
+    - '三月'
+    - '四月'
+    - '五月'
+    - '六月'
+    - '七月'
+    - '八月'
+    - '九月'
+    - '十月'
+    - '十一月'
+    - '十二月'
+  DAYS_OF_THE_WEEK:
+    - '星期一'
+    - '星期二'
+    - '星期三'
+    - '星期四'
+    - '星期五'
+    - '星期六'
+    - '星期日'

+ 63 - 0
system/languages/zh.yaml

@@ -0,0 +1,63 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 錯誤: 不正確的 Frontmatter\n\n路徑: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: 沒有提供日期
+    BAD_DATE: 錯誤日期
+    AGO: 之前
+    FROM_NOW: 之後
+    JUST_NOW: 剛剛
+    SECOND: 秒
+    MINUTE: 分
+    HOUR: 小時
+    DAY: 天
+    WEEK: 週
+    MONTH: 月
+    YEAR: 年
+    DECADE: 十年
+    SEC: 秒
+    MIN: 分
+    HR: 小時
+    WK: 週
+    MO: 月
+    YR: 年
+    DEC: 十年
+    SECOND_PLURAL: 秒
+    MINUTE_PLURAL: 分
+    HOUR_PLURAL: 小時
+    DAY_PLURAL: 天
+    WEEK_PLURAL: 週
+    MONTH_PLURAL: 月
+    YEAR_PLURAL: 年
+    DECADE_PLURAL: 十年
+    SEC_PLURAL: 秒
+    MIN_PLURAL: 分
+    HR_PLURAL: 時
+    WK_PLURAL: 週
+    MO_PLURAL: 月
+    YR_PLURAL: 年
+    DEC_PLURAL: 十年
+  FORM:
+    MISSING_REQUIRED_FIELD: 遺漏必填欄位:
+  MONTHS_OF_THE_YEAR:
+    - '一月'
+    - '二月'
+    - '三月'
+    - '四月'
+    - '五月'
+    - '六月'
+    - '七月'
+    - '八月'
+    - '九月'
+    - '十月'
+    - '十一月'
+    - '十二月'
+  DAYS_OF_THE_WEEK:
+    - '星期一'
+    - '星期二'
+    - '星期三'
+    - '星期四'
+    - '星期五'
+    - '星期六'
+    - '星期日'
+

+ 6 - 5
system/router.php

@@ -1,13 +1,14 @@
 <?php
+
 /**
- * @package    Grav.Core
+ * @package    Grav\Core
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 if (PHP_SAPI !== 'cli-server') {
-    exit('This script cannot be run from browser. Run it from a CLI.');
+    die('This script cannot be run from browser. Run it from a CLI.');
 }
 
 $_SERVER['PHP_CLI_ROUTER'] = true;
@@ -21,6 +22,6 @@ $_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR .
 $_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR . 'index.php';
 $_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR . 'index.php';
 
-require 'index.php';
-
 error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4);
+
+require 'index.php';

+ 188 - 1257
system/src/Grav/Common/Assets.php

@@ -1,27 +1,33 @@
 <?php
+
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 namespace Grav\Common;
 
-use Closure;
-use Exception;
-use FilesystemIterator;
+use Grav\Common\Assets\Pipeline;
+use Grav\Common\Assets\Traits\LegacyAssetsTrait;
+use Grav\Common\Assets\Traits\TestingAssetsTrait;
 use Grav\Common\Config\Config;
-use RecursiveDirectoryIterator;
-use RecursiveIteratorIterator;
-use RegexIterator;
+use Grav\Framework\Object\PropertyObject;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
-define('CSS_ASSET', true);
-define('JS_ASSET', false);
-
-class Assets
+class Assets extends PropertyObject
 {
+    use TestingAssetsTrait;
+    use LegacyAssetsTrait;
+
+    const CSS_COLLECTION = 'assets_css';
+    const JS_COLLECTION = 'assets_js';
+    const CSS_TYPE = 'Css';
+    const JS_TYPE = 'Js';
+    const INLINE_CSS_TYPE = 'InlineCss';
+    const INLINE_JS_TYPE = 'InlineJs';
+
     /** @const Regex to match CSS and JavaScript files */
     const DEFAULT_REGEX = '/.\.(css|js)$/i';
 
@@ -31,79 +37,50 @@ class Assets
     /** @const Regex to match JavaScript files */
     const JS_REGEX = '/.\.js$/i';
 
-    /** @const Regex to match CSS urls */
-    const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';
+    protected $assets_dir;
+    protected $assets_url;
 
-    /** @const Regex to match CSS sourcemap comments */
-    const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
+    protected $assets_css = [];
+    protected $assets_js = [];
 
-    /** @const Regex to match CSS import content */
-    const CSS_IMPORT_REGEX = '{@import(.*?);}';
+    // Config Options
+    protected $css_pipeline;
+    protected $css_pipeline_include_externals;
+    protected $css_pipeline_before_excludes;
+    protected $js_pipeline;
+    protected $js_pipeline_include_externals;
+    protected $js_pipeline_before_excludes;
+    protected $pipeline_options = [];
 
-    /**
-     * @const Regex to match <script> or <style> tag when adding inline style/script. Note that this only supports a
-     * single tag, so the check is greedy to avoid issues in JS.
-     */
-    const HTML_TAG_REGEX = '#(<([A-Z][A-Z0-9]*)>)+(.*)(<\/\2>)#is';
+
+    protected $fetch_command;
+    protected $autoload;
+    protected $enable_asset_timestamp;
+    protected $collections;
+    protected $timestamp;
 
 
     /**
-     * Closure used by the pipeline to fetch assets.
-     *
-     * Useful when file_get_contents() function is not available in your PHP
-     * installation or when you want to apply any kind of preprocessing to
-     * your assets before they get pipelined.
-     *
-     * The closure will receive as the only parameter a string with the path/URL of the asset and
-     * it should return the content of the asset file as a string.
-     *
-     * @var Closure
+     * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
      */
-    protected $fetch_command;
+    public function init()
+    {
+        $grav = Grav::instance();
+        /** @var Config $config */
+        $config = $grav['config'];
 
-    // Configuration toggles to enable/disable the pipelining feature
-    protected $css_pipeline = false;
-    protected $css_pipeline_include_externals = true;
-    protected $css_pipeline_before_excludes = true;
-    protected $js_pipeline = false;
-    protected $js_pipeline_include_externals = true;
-    protected $js_pipeline_before_excludes = true;
-
-    // The asset holding arrays
-    protected $collections = [];
-    protected $css = [];
-    protected $js = [];
-    protected $inline_css = [];
-    protected $inline_js = [];
-    protected $imports = [];
-
-    // Some configuration variables
-    protected $config;
-    protected $base_url;
-    protected $timestamp = '';
-    protected $assets_dir;
-    protected $assets_url;
+        $asset_config = (array)$config->get('system.assets');
 
-    // Default values for pipeline settings
-    protected $css_minify = true;
-    protected $css_minify_windows = false;
-    protected $css_rewrite = true;
-    protected $js_minify = true;
+        /** @var UniformResourceLocator $locator */
+        $locator = $grav['locator'];
+        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_url = $locator->findResource('asset://', false);
 
-    // Arrays to hold assets that should NOT be pipelined
-    protected $css_no_pipeline = [];
-    protected $js_no_pipeline = [];
+        $this->config($asset_config);
 
-    /**
-     * Assets constructor.
-     *
-     * @param array $options
-     */
-    public function __construct(array $options = [])
-    {
-        // Forward config options
-        if ($options) {
-            $this->config((array)$options);
+        // Register any preconfigured collections
+        foreach ((array) $this->collections as $name => $collection) {
+            $this->registerCollection($name, (array)$collection);
         }
     }
 
@@ -117,140 +94,60 @@ class Assets
      * @param  array $config Configurable options.
      *
      * @return $this
-     * @throws \Exception
      */
     public function config(array $config)
     {
-        // Set pipeline modes
-        if (isset($config['css_pipeline'])) {
-            $this->css_pipeline = $config['css_pipeline'];
-        }
-
-        if (isset($config['css_pipeline_include_externals'])) {
-            $this->css_pipeline_include_externals = $config['css_pipeline_include_externals'];
-        }
-
-        if (isset($config['css_pipeline_before_excludes'])) {
-            $this->css_pipeline_before_excludes = $config['css_pipeline_before_excludes'];
-        }
-
-        if (isset($config['js_pipeline'])) {
-            $this->js_pipeline = $config['js_pipeline'];
-        }
-
-        if (isset($config['js_pipeline_include_externals'])) {
-            $this->js_pipeline_include_externals = $config['js_pipeline_include_externals'];
-        }
-
-        if (isset($config['js_pipeline_before_excludes'])) {
-            $this->js_pipeline_before_excludes = $config['js_pipeline_before_excludes'];
-        }
-
-        // Pipeline requires public dir
-        if (($this->js_pipeline || $this->css_pipeline) && !is_dir($this->assets_dir)) {
-            throw new \Exception('Assets: Public dir not found');
-        }
-
-        // Set custom pipeline fetch command
-        if (isset($config['fetch_command']) && ($config['fetch_command'] instanceof Closure)) {
-            $this->fetch_command = $config['fetch_command'];
-        }
-
-        // Set CSS Minify state
-        if (isset($config['css_minify'])) {
-            $this->css_minify = $config['css_minify'];
-        }
-
-        if (isset($config['css_minify_windows'])) {
-            $this->css_minify_windows = $config['css_minify_windows'];
-        }
-
-        if (isset($config['css_rewrite'])) {
-            $this->css_rewrite = $config['css_rewrite'];
-        }
-
-        // Set JS Minify state
-        if (isset($config['js_minify'])) {
-            $this->js_minify = $config['js_minify'];
-        }
-
-        // Set collections
-        if (isset($config['collections']) && is_array($config['collections'])) {
-            $this->collections = $config['collections'];
-        }
-
-        // Autoload assets
-        if (isset($config['autoload']) && is_array($config['autoload'])) {
-            foreach ($config['autoload'] as $asset) {
-                $this->add($asset);
+        foreach ($config as $key => $value) {
+            if ($this->hasProperty($key)) {
+                $this->setProperty($key, $value);
+            } elseif (Utils::startsWith($key, 'css_') || Utils::startsWith($key, 'js_')) {
+                $this->pipeline_options[$key] = $value;
             }
         }
 
-        // Set timestamp
-        if (isset($config['enable_asset_timestamp']) && $config['enable_asset_timestamp'] === true) {
+        // Add timestamp if it's enabled
+        if ($this->enable_asset_timestamp) {
             $this->timestamp = Grav::instance()['cache']->getKey();
         }
 
         return $this;
     }
 
-    /**
-     * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
-     */
-    public function init()
-    {
-        $grav = Grav::instance();
-        /** @var Config $config */
-        $config = $grav['config'];
-        $base_url = $grav['base_url'];
-        $asset_config = (array)$config->get('system.assets');
-
-        /** @var UniformResourceLocator $locator */
-        $locator = $grav['locator'];
-        $this->assets_dir = $locator->findResource('asset://') . DS;
-        $this->assets_url = $locator->findResource('asset://', false);
-
-        $this->config($asset_config);
-        $this->base_url = ($config->get('system.absolute_urls') ? '' : '/') . ltrim(ltrim($base_url, '/') . '/', '/');
-
-        // Register any preconfigured collections
-        foreach ($config->get('system.assets.collections', []) as $name => $collection) {
-            $this->registerCollection($name, (array)$collection);
-        }
-    }
-
     /**
      * Add an asset or a collection of assets.
      *
      * It automatically detects the asset type (JavaScript, CSS or collection).
      * You may add more than one asset passing an array as argument.
      *
-     * @param  mixed $asset
-     * @param  int   $priority the priority, bigger comes first
-     * @param  bool  $pipeline false if this should not be pipelined
-     *
+     * @param array|string $asset
      * @return $this
      */
-    public function add($asset, $priority = null, $pipeline = true)
+    public function add($asset)
     {
+        $args = \func_get_args();
+
         // More than one asset
-        if (is_array($asset)) {
+        if (\is_array($asset)) {
             foreach ($asset as $a) {
-                $this->add($a, $priority, $pipeline);
+                array_shift($args);
+                $args = array_merge([$a], $args);
+                \call_user_func_array([$this, 'add'], $args);
             }
         } elseif (isset($this->collections[$asset])) {
-            $this->add($this->collections[$asset], $priority, $pipeline);
+            array_shift($args);
+            $args = array_merge([$this->collections[$asset]], $args);
+            \call_user_func_array([$this, 'add'], $args);
         } else {
             // Get extension
             $extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
 
             // JavaScript or CSS
-            if (strlen($extension) > 0) {
+            if (\strlen($extension) > 0) {
                 $extension = strtolower($extension);
                 if ($extension === 'css') {
-                    $this->addCss($asset, $priority, $pipeline);
+                    \call_user_func_array([$this, 'addCss'], $args);
                 } elseif ($extension === 'js') {
-                    $this->addJs($asset, $priority, $pipeline);
+                    \call_user_func_array([$this, 'addJs'], $args);
                 }
             }
         }
@@ -258,1187 +155,221 @@ class Assets
         return $this;
     }
 
-    /**
-     * Add an asset to its assembly.
-     *
-     * It checks for duplicates.
-     * You may add more than one asset passing an array as argument.
-     * The third argument may alternatively contain an array of options which take precedence over positional
-     * arguments.
-     *
-     * @param  array   $assembly the array assembling the assets
-     * @param  mixed   $asset
-     * @param  int     $priority the priority, bigger comes first
-     * @param  bool    $pipeline false if this should not be pipelined
-     * @param  string  $loading  how the asset is loaded (async/defer/inline, for CSS: only inline)
-     * @param  string  $group    name of the group
-     *
-     * @return $this
-     */
-    public function addTo(&$assembly, $asset, $priority = null, $pipeline = true, $loading = null, $group = null)
+    protected function addType($collection, $type, $asset, $options)
     {
-        if (is_array($asset)) {
+        if (\is_array($asset)) {
             foreach ($asset as $a) {
-                $this->addTo($assembly, $a, $priority, $pipeline, $loading, $group);
+                $this->addType($collection, $type, $a, $options);
             }
-
             return $this;
-        } elseif (isset($this->collections[$asset])) {
-            $this->addTo($assembly, $this->collections[$asset], $priority, $pipeline, $loading, $group);
+        }
 
+        if (($type === $this::CSS_TYPE || $type === $this::JS_TYPE) && isset($this->collections[$asset])) {
+            $this->addType($collection, $type, $this->collections[$asset], $options);
             return $this;
         }
 
-        $query = [];
-
-        $modified = false;
-        $remote = $this->isRemoteLink($asset);
-        if (!$remote) {
+        // If pipeline disabled, set to position if provided, else after
+        if (isset($options['pipeline'])) {
+            if ($options['pipeline'] === false) {
+                $exclude_type = ($type === $this::JS_TYPE || $type === $this::INLINE_JS_TYPE) ? $this::JS_TYPE : $this::CSS_TYPE;
+                $excludes = strtolower($exclude_type . '_pipeline_before_excludes');
+                if ($this->{$excludes}) {
+                    $default = 'after';
+                } else {
+                    $default = 'before';
+                }
 
-            $asset_parts = parse_url($asset);
-            if (isset($asset_parts['query'])) {
-                $query[] = $asset_parts['query'];
-                unset($asset_parts['query']);
-                $asset = Uri::buildUrl($asset_parts);
+                $options['position'] = $options['position'] ?? $default;
             }
 
-
-            $modified = $this->getLastModificationTime($asset);
-            $asset = $this->buildLocalLink($asset);
+            unset($options['pipeline']);
         }
 
-        // Check for existence
-        if ($asset === false) {
-            return $this;
-        }
+        // Add timestamp
+        $options['timestamp'] = $this->timestamp;
 
-        $data = [
-            'asset'    => $asset,
-            'remote'   => $remote,
-            'priority' => intval($priority ?: 10),
-            'order'    => count($assembly),
-            'pipeline' => (bool) $pipeline,
-            'loading'  => $loading ?: '',
-            'group'    => $group ?: 'head',
-            'modified' => $modified,
-            'query'    => implode('&', $query),
-        ];
-
-        // check for dynamic array and merge with defaults
-        if (func_num_args() > 2) {
-            $dynamic_arg = func_get_arg(2);
-            if (is_array($dynamic_arg)) {
-                $data = array_merge($data, $dynamic_arg);
-            }
-        }
+        // Set order
+        $options['order'] = \count($this->$collection);
 
-        $key = md5($asset);
-        if ($asset) {
-            $assembly[$key] = $data;
+        // Create asset of correct type
+        $asset_class = "\\Grav\\Common\\Assets\\{$type}";
+        $asset_object = new $asset_class();
+
+        // If exists
+        if ($asset_object->init($asset, $options)) {
+            $this->$collection[md5($asset)] = $asset_object;
         }
 
         return $this;
-    }
 
-    /**
-     * Add a CSS asset.
-     *
-     * It checks for duplicates.
-     * You may add more than one asset passing an array as argument.
-     * The second argument may alternatively contain an array of options which take precedence over positional
-     * arguments.
-     *
-     * @param  mixed   $asset
-     * @param  int     $priority the priority, bigger comes first
-     * @param  bool    $pipeline false if this should not be pipelined
-     * @param  string  $group
-     * @param  string  $loading  how the asset is loaded (async/defer/inline, for CSS: only inline)
-     *
-     * @return $this
-     */
-    public function addCss($asset, $priority = null, $pipeline = true, $group = null, $loading = null)
-    {
-        return $this->addTo($this->css, $asset, $priority, $pipeline, $loading, $group);
     }
 
     /**
-     * Add a JavaScript asset.
-     *
-     * It checks for duplicates.
-     * You may add more than one asset passing an array as argument.
-     * The second argument may alternatively contain an array of options which take precedence over positional
-     * arguments.
-     *
-     * @param  mixed  $asset
-     * @param  int    $priority the priority, bigger comes first
-     * @param  bool   $pipeline false if this should not be pipelined
-     * @param  string $loading  how the asset is loaded (async/defer)
-     * @param  string $group    name of the group
+     * Add a CSS asset or a collection of assets.
      *
      * @return $this
      */
-    public function addJs($asset, $priority = null, $pipeline = true, $loading = null, $group = null)
+    public function addCss($asset)
     {
-        return $this->addTo($this->js, $asset, $priority, $pipeline, $loading, $group);
+        return $this->addType(Assets::CSS_COLLECTION,Assets::CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::CSS_TYPE));
     }
 
     /**
-     * Convenience wrapper for async loading of JavaScript
-     *
-     * @param        $asset
-     * @param int    $priority
-     * @param bool   $pipeline
-     * @param string $group name of the group
+     * Add an Inline CSS asset or a collection of assets.
      *
-     * @deprecated Please use dynamic method with ['loading' => 'async']
-     *
-     * @return \Grav\Common\Assets
+     * @return $this
      */
-    public function addAsyncJs($asset, $priority = null, $pipeline = true, $group = null)
+    public function addInlineCss($asset)
     {
-        return $this->addJs($asset, $priority, $pipeline, 'async', $group);
+        return $this->addType(Assets::CSS_COLLECTION, Assets::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_CSS_TYPE));
     }
 
     /**
-     * Convenience wrapper for deferred loading of JavaScript
-     *
-     * @param        $asset
-     * @param int    $priority
-     * @param bool   $pipeline
-     * @param string $group name of the group
-     *
-     * @deprecated Please use dynamic method with ['loading' => 'defer']
+     * Add a JS asset or a collection of assets.
      *
-     * @return \Grav\Common\Assets
+     * @return $this
      */
-    public function addDeferJs($asset, $priority = null, $pipeline = true, $group = null)
+    public function addJs($asset)
     {
-        return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
+        return $this->addType(Assets::JS_COLLECTION, Assets::JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::JS_TYPE));
     }
 
     /**
-     * Add an inline CSS asset.
-     *
-     * It checks for duplicates.
-     * For adding chunks of string-based inline CSS
-     *
-     * @param  mixed $asset
-     * @param  int   $priority the priority, bigger comes first
-     * @param null   $group
+     * Add an Inline JS asset or a collection of assets.
      *
      * @return $this
      */
-    public function addInlineCss($asset, $priority = null, $group = null)
+    public function addInlineJs($asset)
     {
-        $asset = trim($asset);
-
-        if (is_a($asset, 'Twig_Markup')) {
-            preg_match(self::HTML_TAG_REGEX, $asset, $matches);
-            if (isset($matches[3])) {
-                $asset = $matches[3];
-            }
-        }
-
-        $data = [
-            'priority' => intval($priority ?: 10),
-            'order'    => count($this->inline_css),
-            'asset'    => $asset,
-            'group'    => $group ?: 'head'
-        ];
-
-        // check for dynamic array and merge with defaults
-        if (func_num_args() == 2) {
-            $dynamic_arg = func_get_arg(1);
-            if (is_array($dynamic_arg)) {
-                $data = array_merge($data, $dynamic_arg);
-            }
-        }
-
-        $key = md5($asset);
-        if ($asset && is_string($asset) && !array_key_exists($key, $this->inline_css)) {
-            $this->inline_css[$key] = $data;
-        }
-
-        return $this;
+        return $this->addType(Assets::JS_COLLECTION, Assets::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_JS_TYPE));
     }
 
+
     /**
-     * Add an inline JS asset.
-     *
-     * It checks for duplicates.
-     * For adding chunks of string-based inline JS
+     * Add/replace collection.
      *
-     * @param  mixed $asset
-     * @param  int $priority the priority, bigger comes first
-     * @param string $group name of the group
-     * @param null $attributes
+     * @param  string $collectionName
+     * @param  array  $assets
+     * @param bool    $overwrite
      *
      * @return $this
      */
-    public function addInlineJs($asset, $priority = null, $group = null, $attributes = null)
+    public function registerCollection($collectionName, Array $assets, $overwrite = false)
     {
-        $asset = trim($asset);
-
-        if (is_a($asset, 'Twig_Markup')) {
-            preg_match(self::HTML_TAG_REGEX, $asset, $matches);
-            if (isset($matches[3])) {
-                $asset = $matches[3];
-            }
-        }
-
-        $data = [
-            'asset'    => $asset,
-            'priority' => intval($priority ?: 10),
-            'order'    => count($this->js),
-            'group'    => $group ?: 'head',
-            'type'     => $attributes ?: '',
-        ];
-
-        // check for dynamic array and merge with defaults
-        if (func_num_args() == 2) {
-            $dynamic_arg = func_get_arg(1);
-            if (is_array($dynamic_arg)) {
-                $data = array_merge($data, $dynamic_arg);
-            }
-        }
-
-        $key = md5($asset);
-        if ($asset && is_string($asset) && !array_key_exists($key, $this->inline_js)) {
-            $this->inline_js[$key] = $data;
+        if ($overwrite || !isset($this->collections[$collectionName])) {
+            $this->collections[$collectionName] = $assets;
         }
 
         return $this;
     }
 
-    /**
-     * Build the CSS link tags.
-     *
-     * @param  string $group name of the group
-     * @param  array  $attributes
-     *
-     * @return string
-     */
-    public function css($group = 'head', $attributes = [])
+    protected function filterAssets($assets, $key, $value, $sort = false)
     {
-        if (!$this->css && !$this->inline_css) {
-            return null;
-        }
-
-        // Sort array by priorities (larger priority first)
-        if (Grav::instance()) {
-            uasort($this->css, array($this, 'sortAssetsByPriorityThenOrder'));
-            uasort($this->inline_css, array($this, 'sortAssetsByPriorityThenOrder'));
-        }
-
-        $inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline';
+        $results = array_filter($assets, function($asset) use ($key, $value) {
 
-        $attributes = $this->attributes(array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes));
+            if ($key === 'position' && $value === 'pipeline') {
 
-        $output = '';
-        $inline_css = '';
+                $type = $asset->getType();
 
-        if ($this->css_pipeline) {
-            $pipeline_result = $this->pipelineCss($group, !$inlineGroup);
-            $pipeline_html = ($inlineGroup ? '' : '<link href="' . $pipeline_result . '"' . $attributes . ' />' . "\n");
-
-            if ($this->css_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_css .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
-            foreach ($this->css_no_pipeline as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($file['loading'] === 'inline') {
-                        $inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n";
-                    }
-                    else {
-                        $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
-                        $output .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\n";
-                    }
-                }
-            }
-            if (!$this->css_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_css .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
-        } else {
-            foreach ($this->css as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($inlineGroup || $file['loading'] === 'inline') {
-                        $inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n";
-                    }
-                    else {
-                        $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
-                        $output .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\n";
+                if ($asset->getRemote() && $this->{$type . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline' ) {
+                    if ($this->{$type . '_pipeline_before_excludes'}) {
+                        $asset->setPosition('after');
+                    } else {
+                        $asset->setPosition('before');
                     }
+                    return false;
                 }
-            }
-        }
 
-        // Render Inline CSS
-        foreach ($this->inline_css as $inline) {
-            if ($group && $inline['group'] == $group) {
-                $inline_css .= $inline['asset'] . "\n";
             }
-        }
 
-        if ($inline_css) {
-            $output .= "\n<style>\n" . $inline_css . "\n</style>\n";
+            if ($asset[$key] === $value) return true;
+            return false;
+        });
+
+        if ($sort && !empty($results)) {
+            $results = $this->sortAssets($results);
         }
 
 
-        return $output;
+        return $results;
     }
 
-    /**
-     * Build the JavaScript script tags.
-     *
-     * @param  string $group name of the group
-     * @param  array  $attributes
-     *
-     * @return string
-     */
-    public function js($group = 'head', $attributes = [])
+    protected function sortAssets($assets)
     {
-        if (!$this->js && !$this->inline_js) {
-            return null;
-        }
-
-        // Sort array by priorities (larger priority first)
-        uasort($this->js, array($this, 'sortAssetsByPriorityThenOrder'));
-        uasort($this->inline_js, array($this, 'sortAssetsByPriorityThenOrder'));
+        uasort ($assets, function($a, $b) {
+            if ($a['priority'] == $b['priority']) {
+                return $a['order'] - $b['order'];
+            }
+            return $b['priority'] - $a['priority'];
+        });
+        return $assets;
+    }
 
-        $inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline';
+    public function render($type, $group = 'head', $attributes = [])
+    {
+        $before_output = '';
+        $pipeline_output = '';
+        $after_output = '';
 
-        $attributes = $this->attributes($attributes);
+        $assets = 'assets_' . $type;
+        $pipeline_enabled = $type . '_pipeline';
+        $render_pipeline = 'render' . ucfirst($type);
 
-        $output = '';
-        $inline_js = '';
+        $group_assets = $this->filterAssets($this->$assets, 'group', $group);
+        $pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true);
+        $before_assets = $this->filterAssets($group_assets, 'position', 'before', true);
+        $after_assets = $this->filterAssets($group_assets, 'position', 'after', true);
 
-        if ($this->js_pipeline) {
-            $pipeline_result = $this->pipelineJs($group, !$inlineGroup);
-            $pipeline_html = ($inlineGroup ? '' : '<script src="' . $pipeline_result . '"' . $attributes . ' ></script>' . "\n");
+        // Pipeline
+        if ($this->{$pipeline_enabled}) {
+            $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
 
-            if ($this->js_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_js .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
-            foreach ($this->js_no_pipeline as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($file['loading'] === 'inline') {
-                        $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n";
-                    }
-                    else {
-                        $output .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
-                    }
-                }
-            }
-            if (!$this->js_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_js .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
+            $pipeline = new Pipeline($options);
+            $pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes);
         } else {
-            foreach ($this->js as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($inlineGroup || $file['loading'] === 'inline') {
-                        $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n";
-                    }
-                    else {
-                        $output .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
-                    }
-                }
+            foreach ($pipeline_assets as $asset) {
+                $pipeline_output .= $asset->render();
             }
         }
 
-        // Render Inline JS
-        foreach ($this->inline_js as $inline) {
-            if ($group && $inline['group'] == $group) {
-                $inline_js .= $inline['asset'] . "\n";
-            }
+        // Before Pipeline
+        foreach ($before_assets as $asset) {
+            $before_output .= $asset->render();
         }
 
-        if ($inline_js) {
-            $attribute_string = isset($inline) && $inline['type'] ? " type=\"" . $inline['type'] . "\"" : '';
-            $output .= "\n<script" . $attribute_string . ">\n" . $inline_js . "\n</script>\n";
+        // After Pipeline
+        foreach ($after_assets as $asset) {
+            $after_output .= $asset->render();
         }
 
-        return $output;
+        return $before_output . $pipeline_output . $after_output;
     }
 
+
     /**
-     * Minify and concatenate CSS
+     * Build the CSS link tags.
      *
-     * @param string $group
-     * @param bool $returnURL  true if pipeline should return the URL, otherwise the content
+     * @param  string $group name of the group
+     * @param  array  $attributes
      *
-     * @return bool|string     URL or generated content if available, else false
+     * @return string
      */
-    protected function pipelineCss($group = 'head', $returnURL = true)
+    public function css($group = 'head', $attributes = [])
     {
-        // temporary list of assets to pipeline
-        $temp_css = [];
-
-        // clear no-pipeline assets lists
-        $this->css_no_pipeline = [];
-
-        // Compute uid based on assets and timestamp
-        $uid = md5(json_encode($this->css) . $this->css_minify . $this->css_rewrite . $group);
-        $file =  $uid . '.css';
-        $inline_file = $uid . '-inline.css';
-
-        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
-
-        // If inline files exist set them on object
-        if (file_exists($this->assets_dir . $inline_file)) {
-            $this->css_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true);
-        }
-
-        // If pipeline exist return its URL or content
-        if (file_exists($this->assets_dir . $file)) {
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return file_get_contents($this->assets_dir . $file) . "\n";
-            }
-        }
-
-        // Remove any non-pipeline files
-        foreach ($this->css as $id => $asset) {
-            if ($asset['group'] == $group) {
-                if (!$asset['pipeline'] ||
-                    ($asset['remote'] && $this->css_pipeline_include_externals === false)) {
-                    $this->css_no_pipeline[$id] = $asset;
-                } else {
-                    $temp_css[$id] = $asset;
-                }
-            }
-        }
-
-        //if nothing found get out of here!
-        if (count($temp_css) == 0) {
-            return false;
-        }
-
-        // Write non-pipeline files out
-        if (!empty($this->css_no_pipeline)) {
-            file_put_contents($this->assets_dir . $inline_file, json_encode($this->css_no_pipeline));
-        }
-
-
-        $css_minify = $this->css_minify;
-
-        // If this is a Windows server, and minify_windows is false (default value) skip the
-        // minification process because it will cause Apache to die/crash due to insufficient
-        // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
-        if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN' && !$this->css_minify_windows) {
-            $css_minify = false;
-        }
-
-        // Concatenate files
-        $buffer = $this->gatherLinks($temp_css, CSS_ASSET);
-        if ($css_minify) {
-            $minifier = new \MatthiasMullie\Minify\CSS();
-            $minifier->add($buffer);
-            $buffer = $minifier->minify();
-        }
-
-        // Write file
-        if (strlen(trim($buffer)) > 0) {
-            file_put_contents($this->assets_dir . $file, $buffer);
-
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return $buffer . "\n";
-            }
-        } else {
-            return false;
-        }
+        return $this->render('css', $group, $attributes);
     }
 
     /**
-     * Minify and concatenate JS files.
+     * Build the JavaScript script tags.
      *
-     * @param string $group
-     * @param bool $returnURL  true if pipeline should return the URL, otherwise the content
+     * @param  string $group name of the group
+     * @param  array  $attributes
      *
-     * @return bool|string     URL or generated content if available, else false
+     * @return string
      */
-    protected function pipelineJs($group = 'head', $returnURL = true)
+    public function js($group = 'head', $attributes = [])
     {
-        // temporary list of assets to pipeline
-        $temp_js = [];
-
-        // clear no-pipeline assets lists
-        $this->js_no_pipeline = [];
-
-        // Compute uid based on assets and timestamp
-        $uid = md5(json_encode($this->js) . $this->js_minify . $group);
-        $file =  $uid . '.js';
-        $inline_file = $uid . '-inline.js';
-
-        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
-
-        // If inline files exist set them on object
-        if (file_exists($this->assets_dir . $inline_file)) {
-            $this->js_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true);
-        }
-
-        // If pipeline exist return its URL or content
-        if (file_exists($this->assets_dir . $file)) {
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return file_get_contents($this->assets_dir . $file) . "\n";
-            }
-        }
-
-        // Remove any non-pipeline files
-        foreach ($this->js as $id => $asset) {
-            if ($asset['group'] == $group) {
-                if (!$asset['pipeline'] ||
-                    ($asset['remote'] && $this->js_pipeline_include_externals === false)) {
-                    $this->js_no_pipeline[] = $asset;
-                } else {
-                    $temp_js[$id] = $asset;
-                }
-            }
-        }
-
-        //if nothing found get out of here!
-        if (count($temp_js) == 0) {
-            return false;
-        }
-
-        // Write non-pipeline files out
-        if (!empty($this->js_no_pipeline)) {
-            file_put_contents($this->assets_dir . $inline_file, json_encode($this->js_no_pipeline));
-        }
-
-        // Concatenate files
-        $buffer = $this->gatherLinks($temp_js, JS_ASSET);
-        if ($this->js_minify) {
-            $minifier = new \MatthiasMullie\Minify\JS();
-            $minifier->add($buffer);
-            $buffer = $minifier->minify();
-        }
-
-        // Write file
-        if (strlen(trim($buffer)) > 0) {
-            file_put_contents($this->assets_dir . $file, $buffer);
-
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return $buffer . "\n";
-            }
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Return the array of all the registered CSS assets
-     * If a $key is provided, it will try to return only that asset
-     * else it will return null
-     *
-     * @param null|string $key the asset key
-     * @return array
-     */
-    public function getCss($key = null)
-    {
-        if (!empty($key)) {
-            $asset_key = md5($key);
-            if (isset($this->css[$asset_key])) {
-                return $this->css[$asset_key];
-            } else {
-                return null;
-            }
-        }
-
-        return $this->css;
-    }
-
-    /**
-     * Return the array of all the registered JS assets
-     * If a $key is provided, it will try to return only that asset
-     * else it will return null
-     *
-     * @param null|string $key the asset key
-     * @return array
-     */
-    public function getJs($key = null)
-    {
-        if (!empty($key)) {
-            $asset_key = md5($key);
-            if (isset($this->js[$asset_key])) {
-                return $this->js[$asset_key];
-            } else {
-                return null;
-            }
-        }
-
-        return $this->js;
-    }
-
-    /**
-     * Set the whole array of CSS assets
-     *
-     * @param $css
-     */
-    public function setCss($css)
-    {
-        $this->css = $css;
-    }
-
-    /**
-     * Set the whole array of JS assets
-     *
-     * @param $js
-     */
-    public function setJs($js)
-    {
-        $this->js = $js;
-    }
-
-    /**
-     * Removes an item from the CSS array if set
-     *
-     * @param string $key  The asset key
-     */
-    public function removeCss($key)
-    {
-        $asset_key = md5($key);
-        if (isset($this->css[$asset_key])) {
-            unset($this->css[$asset_key]);
-        }
-    }
-
-    /**
-     * Removes an item from the JS array if set
-     *
-     * @param string $key  The asset key
-     */
-    public function removeJs($key)
-    {
-        $asset_key = md5($key);
-        if (isset($this->js[$asset_key])) {
-            unset($this->js[$asset_key]);
-        }
-    }
-
-    /**
-     * Return the array of all the registered collections
-     *
-     * @return array
-     */
-    public function getCollections()
-    {
-        return $this->collections;
-    }
-
-    /**
-     * Set the array of collections explicitly
-     *
-     * @param $collections
-     */
-    public function setCollection($collections)
-    {
-        $this->collections = $collections;
-    }
-
-    /**
-     * Determines if an asset exists as a collection, CSS or JS reference
-     *
-     * @param $asset
-     *
-     * @return bool
-     */
-    public function exists($asset)
-    {
-        if (isset($this->collections[$asset]) || isset($this->css[$asset]) || isset($this->js[$asset])) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Add/replace collection.
-     *
-     * @param  string $collectionName
-     * @param  array  $assets
-     * @param bool    $overwrite
-     *
-     * @return $this
-     */
-    public function registerCollection($collectionName, Array $assets, $overwrite = false)
-    {
-        if ($overwrite || !isset($this->collections[$collectionName])) {
-            $this->collections[$collectionName] = $assets;
-        }
-
-        return $this;
+        return $this->render('js', $group, $attributes);
     }
-
-    /**
-     * Reset all assets.
-     *
-     * @return $this
-     */
-    public function reset()
-    {
-        return $this->resetCss()->resetJs();
-    }
-
-    /**
-     * Reset JavaScript assets.
-     *
-     * @return $this
-     */
-    public function resetJs()
-    {
-        $this->js = [];
-        $this->inline_js = [];
-
-        return $this;
-    }
-
-    /**
-     * Reset CSS assets.
-     *
-     * @return $this
-     */
-    public function resetCss()
-    {
-        $this->css = [];
-        $this->inline_css = [];
-
-        return $this;
-    }
-
-    /**
-     * Add all JavaScript assets within $directory
-     *
-     * @param  string $directory Relative to the Grav root path, or a stream identifier
-     *
-     * @return $this
-     */
-    public function addDirJs($directory)
-    {
-        return $this->addDir($directory, self::JS_REGEX);
-    }
-
-    /**
-     * Add all CSS assets within $directory
-     *
-     * @param  string $directory Relative to the Grav root path, or a stream identifier
-     *
-     * @return $this
-     */
-    public function addDirCss($directory)
-    {
-        return $this->addDir($directory, self::CSS_REGEX);
-    }
-
-    /**
-     * Add all assets matching $pattern within $directory.
-     *
-     * @param  string $directory Relative to the Grav root path, or a stream identifier
-     * @param  string $pattern   (regex)
-     *
-     * @return $this
-     * @throws Exception
-     */
-    public function addDir($directory, $pattern = self::DEFAULT_REGEX)
-    {
-        $root_dir = rtrim(ROOT_DIR, '/');
-
-        // Check if $directory is a stream.
-        if (strpos($directory, '://')) {
-            $directory = Grav::instance()['locator']->findResource($directory, null);
-        }
-
-        // Get files
-        $files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/');
-
-        // No luck? Nothing to do
-        if (!$files) {
-            return $this;
-        }
-
-        // Add CSS files
-        if ($pattern === self::CSS_REGEX) {
-            foreach ($files as $file) {
-                $this->addCss($file);
-            }
-
-            return $this;
-        }
-
-        // Add JavaScript files
-        if ($pattern === self::JS_REGEX) {
-            foreach ($files as $file) {
-                $this->addJs($file);
-            }
-
-            return $this;
-        }
-
-        // Unknown pattern.
-        foreach ($files as $asset) {
-            $this->add($asset);
-        }
-
-        return $this;
-    }
-
-    /**
-     * Determine whether a link is local or remote.
-     *
-     * Understands both "http://" and "https://" as well as protocol agnostic links "//"
-     *
-     * @param  string $link
-     *
-     * @return bool
-     */
-    protected function isRemoteLink($link)
-    {
-        $base = Grav::instance()['uri']->rootUrl(true);
-
-        // sanity check for local URLs with absolute URL's enabled
-        if (Utils::startsWith($link, $base)) {
-            return false;
-        }
-
-        return ('http://' === substr($link, 0, 7) || 'https://' === substr($link, 0, 8) || '//' === substr($link, 0,
-                2));
-    }
-
-    /**
-     * Build local links including grav asset shortcodes
-     *
-     * @param  string $asset    the asset string reference
-     * @param  bool   $absolute build absolute asset link
-     *
-     * @return string           the final link url to the asset
-     */
-    protected function buildLocalLink($asset, $absolute = false)
-    {
-        try {
-            $asset = Grav::instance()['locator']->findResource($asset, $absolute);
-        } catch (\Exception $e) {
-        }
-
-        $uri = $absolute ? $asset : $this->base_url . ltrim($asset, '/');
-        return $asset ? $uri : false;
-    }
-
-    /**
-     * Get the last modification time of asset
-     *
-     * @param  string $asset    the asset string reference
-     *
-     * @return string           the last modifcation time or false on error
-     */
-    protected function getLastModificationTime($asset)
-    {
-        $file = GRAV_ROOT . $asset;
-        if (Grav::instance()['locator']->isStream($asset)) {
-            $file = $this->buildLocalLink($asset, true);
-        }
-
-        return file_exists($file) ? filemtime($file) : false;
-    }
-
-    /**
-     * Build an HTML attribute string from an array.
-     *
-     * @param  array $attributes
-     *
-     * @return string
-     */
-    protected function attributes(array $attributes)
-    {
-        $html = '';
-        $no_key = ['loading'];
-
-        foreach ($attributes as $key => $value) {
-            // For numeric keys we will assume that the key and the value are the same
-            // as this will convert HTML attributes such as "required" to a correct
-            // form like required="required" instead of using incorrect numerics.
-            if (is_numeric($key)) {
-                $key = $value;
-            }
-            if (is_array($value)) {
-                $value = implode(' ', $value);
-            }
-
-            if (in_array($key, $no_key)) {
-                $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
-            } else {
-                $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
-            }
-
-            $html .= ' ' . $element;
-        }
-
-        return $html;
-    }
-
-    /**
-     * Download and concatenate the content of several links.
-     *
-     * @param  array $links
-     * @param  bool  $css
-     *
-     * @return string
-     */
-    protected function gatherLinks(array $links, $css = true)
-    {
-        $buffer = '';
-
-
-        foreach ($links as $asset) {
-            $relative_dir = '';
-            $local = true;
-
-            $link = $asset['asset'];
-            $relative_path = $link;
-
-            if ($this->isRemoteLink($link)) {
-                $local = false;
-                if ('//' === substr($link, 0, 2)) {
-                    $link = 'http:' . $link;
-                }
-            } else {
-                // Fix to remove relative dir if grav is in one
-                if (($this->base_url != '/') && (strpos($this->base_url, $link) == 0)) {
-                    $base_url = '#' . preg_quote($this->base_url, '#') . '#';
-                    $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
-                }
-
-                $relative_dir = dirname($relative_path);
-                $link = ROOT_DIR . $relative_path;
-            }
-
-            $file = ($this->fetch_command instanceof Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
-
-            // No file found, skip it...
-            if ($file === false) {
-                continue;
-            }
-
-            // Double check last character being
-            if (!$css) {
-                $file = rtrim($file, ' ;') . ';';
-            }
-
-            // If this is CSS + the file is local + rewrite enabled
-            if ($css && $local && $this->css_rewrite) {
-                $file = $this->cssRewrite($file, $relative_dir);
-            }
-
-            $file = rtrim($file) . PHP_EOL;
-            $buffer .= $file;
-        }
-
-        // Pull out @imports and move to top
-        if ($css) {
-            $buffer = $this->moveImports($buffer);
-        }
-
-        return $buffer;
-    }
-
-    /**
-     * Finds relative CSS urls() and rewrites the URL with an absolute one
-     *
-     * @param string $file          the css source file
-     * @param string $relative_path relative path to the css file
-     *
-     * @return mixed
-     */
-    protected function cssRewrite($file, $relative_path)
-    {
-        // Strip any sourcemap comments
-        $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
-
-        // Find any css url() elements, grab the URLs and calculate an absolute path
-        // Then replace the old url with the new one
-        $file = preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($relative_path) {
-
-            $old_url = $matches[2];
-
-            // Ensure link is not rooted to webserver, a data URL, or to a remote host
-            if (Utils::startsWith($old_url, '/') || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
-                return $matches[0];
-            }
-
-            $new_url = $this->base_url . ltrim(Utils::normalizePath($relative_path . '/' . $old_url), '/');
-
-            return str_replace($old_url, $new_url, $matches[0]);
-        }, $file);
-
-        return $file;
-    }
-
-    /**
-     * Moves @import statements to the top of the file per the CSS specification
-     *
-     * @param  string $file the file containing the combined CSS files
-     *
-     * @return string       the modified file with any @imports at the top of the file
-     */
-    protected function moveImports($file)
-    {
-        $this->imports = [];
-
-        $file = preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) {
-            $this->imports[] = $matches[0];
-
-            return '';
-        }, $file);
-
-        return implode("\n", $this->imports) . "\n\n" . $file;
-    }
-
-    /**
-     * Recursively get files matching $pattern within $directory.
-     *
-     * @param  string $directory
-     * @param  string $pattern (regex)
-     * @param  string $ltrim   Will be trimmed from the left of the file path
-     *
-     * @return array
-     */
-    protected function rglob($directory, $pattern, $ltrim = null)
-    {
-        $iterator = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory,
-            FilesystemIterator::SKIP_DOTS)), $pattern);
-        $offset = strlen($ltrim);
-        $files = [];
-
-        foreach ($iterator as $file) {
-            $files[] = substr($file->getPathname(), $offset);
-        }
-
-        return $files;
-    }
-
-    /**
-     * Sets the state of CSS Pipeline
-     *
-     * @param boolean $value
-     */
-    public function setCssPipeline($value)
-    {
-        $this->css_pipeline = (bool)$value;
-    }
-
-    /**
-     * Sets the state of JS Pipeline
-     *
-     * @param boolean $value
-     */
-    public function setJsPipeline($value)
-    {
-        $this->js_pipeline = (bool)$value;
-    }
-
-    /**
-     * Explicitly set's a timestamp for assets
-     *
-     * @param $value
-     */
-    public function setTimestamp($value)
-    {
-        $this->timestamp = $value;
-    }
-
-    /**
-     * Get the timestamp for assets
-     *
-     * @return string
-     */
-    public function getTimestamp($include_join = true)
-    {
-        if ($this->timestamp) {
-            $timestamp = $include_join ? '?' . $this->timestamp : $this->timestamp;
-            return $timestamp;
-        }
-        return;
-    }
-
-    /**
-     *
-     *
-     * @param $asset
-     * @return string
-     */
-    public function getQuerystring($asset)
-    {
-        $querystring = '';
-
-        if (!empty($asset['query'])) {
-            if (Utils::contains($asset['asset'], '?')) {
-                $querystring .=  '&' . $asset['query'];
-            } else {
-               $querystring .= '?' . $asset['query'];
-            }
-        }
-
-        if ($this->timestamp) {
-            if (Utils::contains($asset['asset'], '?') || $querystring) {
-                $querystring .=  '&' . $this->timestamp;
-            } else {
-                $querystring .= '?' . $this->timestamp;
-            }
-        }
-
-        return $querystring;
-    }
-
-    /**
-     * @return string
-     */
-    public function __toString()
-    {
-        return '';
-    }
-
-    /**
-     * @param $a
-     * @param $b
-     *
-     * @return mixed
-     */
-    protected function sortAssetsByPriorityThenOrder($a, $b)
-    {
-        if ($a['priority'] == $b['priority']) {
-            return $a['order'] - $b['order'];
-        }
-
-        return $b['priority'] - $a['priority'];
-    }
-
 }

+ 198 - 0
system/src/Grav/Common/Assets/BaseAsset.php

@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Assets\Traits\AssetUtilsTrait;
+use Grav\Common\Grav;
+use Grav\Common\Uri;
+use Grav\Common\Utils;
+use Grav\Framework\Object\PropertyObject;
+
+abstract class BaseAsset extends PropertyObject
+{
+    use AssetUtilsTrait;
+
+    protected const CSS_ASSET = true;
+    protected const JS_ASSET = false;
+
+    /** @const Regex to match CSS import content */
+    protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
+
+    protected $asset;
+
+    protected $asset_type;
+    protected $order;
+    protected $group;
+    protected $position;
+    protected $priority;
+    protected $attributes = [];
+
+
+    protected $timestamp;
+    protected $modified;
+    protected $remote;
+    protected $query = '';
+
+    // Private Bits
+    private $base_url;
+    private $fetch_command;
+    private $css_rewrite = false;
+    private $css_minify = false;
+
+    abstract function render();
+
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_config = [
+            'group' => 'head',
+            'position' => 'pipeline',
+            'priority' => 10,
+            'modified' => null,
+            'asset' => null
+        ];
+
+        // Merge base defaults
+        $elements = array_merge($base_config, $elements);
+
+        parent::__construct($elements, $key);
+    }
+
+    public function init($asset, $options)
+    {
+        $config = Grav::instance()['config'];
+        $uri = Grav::instance()['uri'];
+
+        // set attributes
+        foreach ($options as $key => $value) {
+            if ($this->hasProperty($key)) {
+                $this->setProperty($key, $value);
+            } else {
+                $this->attributes[$key] = $value;
+            }
+        }
+
+        // Force priority to be an int
+        $this->priority = (int) $this->priority;
+
+        // Do some special stuff for CSS/JS (not inline)
+        if (!Utils::startsWith($this->getType(), 'inline')) {
+            $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
+            $this->remote = static::isRemoteLink($asset);
+
+            // Move this to render?
+            if (!$this->remote) {
+
+                $asset_parts = parse_url($asset);
+                if (isset($asset_parts['query'])) {
+                    $this->query = $asset_parts['query'];
+                    unset($asset_parts['query']);
+                    $asset = Uri::buildUrl($asset_parts);
+                }
+
+                $locator = Grav::instance()['locator'];
+
+                if ($locator->isStream($asset)) {
+                    $path = $locator->findResource($asset, true);
+                } else {
+                    $path = GRAV_ROOT . $asset;
+                }
+
+                // If local file is missing return
+                if ($path === false) {
+                    return false;
+                }
+
+                $file = new \SplFileInfo($path);
+
+                $asset = $this->buildLocalLink($file->getPathname());
+
+                $this->modified = $file->isFile() ? $file->getMTime() : false;
+            }
+        }
+
+        $this->asset = $asset;
+
+        return $this;
+    }
+
+    public function getAsset()
+    {
+        return $this->asset;
+    }
+
+    public function getRemote()
+    {
+        return $this->remote;
+    }
+
+    public function setPosition($position)
+    {
+        $this->position = $position;
+        return $this;
+    }
+
+
+    /**
+     *
+     * Get the last modification time of asset
+     *
+     * @param  string $asset    the asset string reference
+     *
+     * @return string           the last modifcation time or false on error
+     */
+//    protected function getLastModificationTime($asset)
+//    {
+//        $file = GRAV_ROOT . $asset;
+//        if (Grav::instance()['locator']->isStream($asset)) {
+//            $file = $this->buildLocalLink($asset, true);
+//        }
+//
+//        return file_exists($file) ? filemtime($file) : false;
+//    }
+
+    /**
+     *
+     * Build local links including grav asset shortcodes
+     *
+     * @param  string $asset    the asset string reference
+     *
+     * @return string           the final link url to the asset
+     */
+    protected function buildLocalLink($asset)
+    {
+        if ($asset) {
+            return $this->base_url . ltrim(Utils::replaceFirstOccurrence(GRAV_ROOT, '', $asset), '/');
+        }
+        return false;
+    }
+
+
+    /**
+     * Implements JsonSerializable interface.
+     *
+     * @return array
+     */
+    public function jsonSerialize()
+    {
+        return ['type' => $this->getType(), 'elements' => $this->getElements()];
+    }
+
+    /**
+     * Placeholder for AssetUtilsTrait method
+     *
+     * @param string $file
+     * @param string $dir
+     * @param bool $local
+     */
+    protected function cssRewrite($file, $dir, $local)
+    {
+        return;
+    }
+}

+ 40 - 0
system/src/Grav/Common/Assets/Css.php

@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class Css extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'css',
+            'attributes' => [
+                'type' => 'text/css',
+                'rel' => 'stylesheet'
+            ]
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
+            $buffer = $this->gatherLinks( [$this], self::CSS_ASSET);
+            return "<style>\n" . trim($buffer) . "\n</style>\n";
+        }
+
+        return '<link href="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . ">\n";
+    }
+}

+ 32 - 0
system/src/Grav/Common/Assets/InlineCss.php

@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class InlineCss extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'css',
+            'position' => 'after'
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        return '<style' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</style>\n";
+    }
+}

+ 32 - 0
system/src/Grav/Common/Assets/InlineJs.php

@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class InlineJs extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'js',
+            'position' => 'after'
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        return '<script' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</script>\n";
+    }
+}

+ 36 - 0
system/src/Grav/Common/Assets/Js.php

@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class Js extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'js',
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
+            $buffer = $this->gatherLinks( [$this], self::JS_ASSET);
+            return '<script' . $this->renderAttributes() . ">\n" . trim($buffer) . "\n</script>\n";
+        }
+
+        return '<script src="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . "></script>\n";
+    }
+}

+ 273 - 0
system/src/Grav/Common/Assets/Pipeline.php

@@ -0,0 +1,273 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Assets\Traits\AssetUtilsTrait;
+use Grav\Common\Config\Config;
+use Grav\Common\Grav;
+use Grav\Common\Uri;
+use Grav\Common\Utils;
+use Grav\Framework\Object\PropertyObject;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+class Pipeline extends PropertyObject
+{
+    use AssetUtilsTrait;
+
+    protected const CSS_ASSET = true;
+    protected const JS_ASSET = false;
+
+    /** @const Regex to match CSS urls */
+    protected const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';
+
+    /** @const Regex to match CSS sourcemap comments */
+    protected const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
+
+    /** @const Regex to match CSS import content */
+    protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
+
+    protected const FIRST_FORWARDSLASH_REGEX = '{^\/{1}\w}';
+
+    protected $css_minify;
+    protected $css_minify_windows;
+    protected $css_rewrite;
+
+    protected $js_minify;
+    protected $js_minify_windows;
+
+    protected $base_url;
+    protected $assets_dir;
+    protected $assets_url;
+    protected $timestamp;
+    protected $attributes;
+    protected $query;
+    protected $asset;
+
+    /**
+     * Closure used by the pipeline to fetch assets.
+     *
+     * Useful when file_get_contents() function is not available in your PHP
+     * installation or when you want to apply any kind of preprocessing to
+     * your assets before they get pipelined.
+     *
+     * The closure will receive as the only parameter a string with the path/URL of the asset and
+     * it should return the content of the asset file as a string.
+     *
+     * @var \Closure
+     */
+    protected $fetch_command;
+
+    public function __construct(array $elements = [], ?string $key = null)
+    {
+        parent::__construct($elements, $key);
+
+        /** @var UniformResourceLocator $locator */
+        $locator = Grav::instance()['locator'];
+
+        /** @var Config $config */
+        $config = Grav::instance()['config'];
+
+        /** @var Uri $uri */
+        $uri = Grav::instance()['uri'];
+
+        $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
+        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_url = $locator->findResource('asset://', false);
+    }
+
+    /**
+     * Minify and concatenate CSS
+     *
+     * @param array $assets
+     * @param string $group
+     * @param array $attributes
+     *
+     * @return bool|string     URL or generated content if available, else false
+     */
+    public function renderCss($assets, $group, $attributes = [])
+    {
+        // temporary list of assets to pipeline
+        $inline_group = false;
+
+        if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
+            $inline_group = true;
+            unset($attributes['loading']);
+        }
+
+        // Store Attributes
+        $this->attributes = array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes);
+
+        // Compute uid based on assets and timestamp
+        $json_assets = json_encode($assets);
+        $uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group);
+        $file = $uid . '.css';
+        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
+
+        $buffer = null;
+
+        if (file_exists($this->assets_dir . $file)) {
+            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        } else {
+            //if nothing found get out of here!
+            if (empty($assets)) {
+                return false;
+            }
+
+            // Concatenate files
+            $buffer = $this->gatherLinks($assets, self::CSS_ASSET);
+
+            // Minify if required
+            if ($this->shouldMinify('css')) {
+                $minifier = new \MatthiasMullie\Minify\CSS();
+                $minifier->add($buffer);
+                $buffer = $minifier->minify();
+            }
+
+            // Write file
+            if (trim($buffer) !== '') {
+                file_put_contents($this->assets_dir . $file, $buffer);
+            }
+        }
+
+        if ($inline_group) {
+            $output = "<style>\n" . $buffer . "\n</style>\n";
+        } else {
+            $this->asset = $relative_path;
+            $output = '<link href="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . ">\n";
+        }
+
+        return $output;
+    }
+
+    /**
+     * Minify and concatenate JS files.
+     *
+     * @param array $assets
+     * @param string $group
+     * @param array $attributes
+     *
+     * @return bool|string     URL or generated content if available, else false
+     */
+    public function renderJs($assets, $group, $attributes = [])
+    {
+        // temporary list of assets to pipeline
+        $inline_group = false;
+
+        if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
+            $inline_group = true;
+            unset($attributes['loading']);
+        }
+
+        // Store Attributes
+        $this->attributes = $attributes;
+
+        // Compute uid based on assets and timestamp
+        $json_assets = json_encode($assets);
+        $uid = md5($json_assets . $this->js_minify . $group);
+        $file = $uid . '.js';
+        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
+
+        $buffer = null;
+
+        if (file_exists($this->assets_dir . $file)) {
+            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        } else {
+            //if nothing found get out of here!
+            if (empty($assets)) {
+                return false;
+            }
+
+            // Concatenate files
+            $buffer = $this->gatherLinks($assets, self::JS_ASSET);
+
+            // Minify if required
+            if ($this->shouldMinify('js')) {
+                $minifier = new \MatthiasMullie\Minify\JS();
+                $minifier->add($buffer);
+                $buffer = $minifier->minify();
+            }
+
+            // Write file
+            if (trim($buffer) !== '') {
+                file_put_contents($this->assets_dir . $file, $buffer);
+            }
+        }
+
+        if ($inline_group) {
+            $output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
+        } else {
+            $this->asset = $relative_path;
+            $output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . "></script>\n";
+        }
+
+        return $output;
+    }
+
+
+    /**
+     * Finds relative CSS urls() and rewrites the URL with an absolute one
+     *
+     * @param string $file the css source file
+     * @param string $dir , $local relative path to the css file
+     * @param bool $local is this a local or remote asset
+     *
+     * @return mixed
+     */
+    protected function cssRewrite($file, $dir, $local)
+    {
+        // Strip any sourcemap comments
+        $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
+
+        // Find any css url() elements, grab the URLs and calculate an absolute path
+        // Then replace the old url with the new one
+        $file = (string)preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($dir, $local) {
+
+            $old_url = $matches[2];
+
+            // Ensure link is not rooted to web server, a data URL, or to a remote host
+            if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
+                return $matches[0];
+            }
+
+            // clean leading /
+            $old_url = Utils::normalizePath($dir . '/' . $old_url);
+            if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) {
+                $old_url = ltrim($old_url, '/');
+            }
+
+            $new_url = ($local ? $this->base_url: '') . $old_url;
+
+            $fixed = str_replace($matches[2], $new_url, $matches[0]);
+
+            return $fixed;
+        }, $file);
+
+        return $file;
+    }
+
+
+
+    private function shouldMinify($type = 'css')
+    {
+        $check = $type . '_minify';
+        $win_check = $type . '_minify_windows';
+
+        $minify = (bool) $this->$check;
+
+        // If this is a Windows server, and minify_windows is false (default value) skip the
+        // minification process because it will cause Apache to die/crash due to insufficient
+        // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
+        if (stripos(php_uname('s'), 'WIN') === 0 && !$this->{$win_check}) {
+            $minify = false;
+        }
+
+        return $minify;
+    }
+}

+ 182 - 0
system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php

@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets\Traits
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets\Traits;
+
+use Grav\Common\Grav;
+use Grav\Common\Utils;
+
+trait AssetUtilsTrait
+{
+    /**
+     * Determine whether a link is local or remote.
+     * Understands both "http://" and "https://" as well as protocol agnostic links "//"
+     *
+     * @param  string $link
+     * @return bool
+     */
+    public static function isRemoteLink($link)
+    {
+        $base = Grav::instance()['uri']->rootUrl(true);
+
+        // Sanity check for local URLs with absolute URL's enabled
+        if (Utils::startsWith($link, $base)) {
+            return false;
+        }
+
+        return (0 === strpos($link, 'http://') || 0 === strpos($link, 'https://') || 0 === strpos($link, '//'));
+    }
+
+    /**
+     * Download and concatenate the content of several links.
+     *
+     * @param  array $assets
+     * @param  bool $css
+     *
+     * @return string
+     */
+    protected function gatherLinks(array $assets, $css = true)
+    {
+        $buffer = '';
+
+
+        foreach ($assets as $id => $asset) {
+            $local = true;
+
+            $link = $asset->getAsset();
+            $relative_path = $link;
+
+            if (static::isRemoteLink($link)) {
+                $local = false;
+                if (0 === strpos($link, '//')) {
+                    $link = 'http:' . $link;
+                }
+                $relative_dir = \dirname($relative_path);
+            } else {
+                // Fix to remove relative dir if grav is in one
+                if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
+                    $base_url = '#' . preg_quote($this->base_url, '#') . '#';
+                    $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
+                }
+
+                $relative_dir = \dirname($relative_path);
+                $link = ROOT_DIR . $relative_path;
+            }
+
+            $file = ($this->fetch_command instanceof \Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
+
+            // No file found, skip it...
+            if ($file === false) {
+                continue;
+            }
+
+            // Double check last character being
+            if (!$css) {
+                $file = rtrim($file, ' ;') . ';';
+            }
+
+            // If this is CSS + the file is local + rewrite enabled
+            if ($css && $this->css_rewrite) {
+                $file = $this->cssRewrite($file, $relative_dir, $local);
+            }
+
+            $file = rtrim($file) . PHP_EOL;
+            $buffer .= $file;
+        }
+
+        // Pull out @imports and move to top
+        if ($css) {
+            $buffer = $this->moveImports($buffer);
+        }
+
+        return $buffer;
+    }
+
+    /**
+     * Moves @import statements to the top of the file per the CSS specification
+     *
+     * @param  string $file the file containing the combined CSS files
+     *
+     * @return string       the modified file with any @imports at the top of the file
+     */
+    protected function moveImports($file)
+    {
+        $imports = [];
+
+        $file = (string)preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) use (&$imports) {
+            $imports[] = $matches[0];
+
+            return '';
+        }, $file);
+
+        return implode("\n", $imports) . "\n\n" . $file;
+    }
+
+    /**
+     *
+     * Build an HTML attribute string from an array.
+     *
+     * @return string
+     */
+    protected function renderAttributes()
+    {
+        $html = '';
+        $no_key = ['loading'];
+
+        foreach ($this->attributes as $key => $value) {
+            if (is_numeric($key)) {
+                $key = $value;
+            }
+            if (\is_array($value)) {
+                $value = implode(' ', $value);
+            }
+
+            if (\in_array($key, $no_key, true)) {
+                $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
+            } else {
+                $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
+            }
+
+            $html .= ' ' . $element;
+        }
+
+        return $html;
+    }
+
+    /**
+     * Render Querystring
+     *
+     * @param string $asset
+     * @return string
+     */
+    protected function renderQueryString($asset = null)
+    {
+        $querystring = '';
+
+        $asset = $asset ?? $this->asset;
+
+        if (!empty($this->query)) {
+            if (Utils::contains($asset, '?')) {
+                $querystring .=  '&' . $this->query;
+            } else {
+                $querystring .= '?' . $this->query;
+            }
+        }
+
+        if ($this->timestamp) {
+            if (Utils::contains($asset, '?') || $querystring) {
+                $querystring .=  '&' . $this->timestamp;
+            } else {
+                $querystring .= '?' . $this->timestamp;
+            }
+        }
+
+        return $querystring;
+    }
+}

+ 118 - 0
system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php

@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets\Traits
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets\Traits;
+
+use Grav\Common\Assets;
+
+trait LegacyAssetsTrait
+{
+
+    /**
+     * @param array $args
+     * @param string $type
+     * @return array
+     */
+    protected function unifyLegacyArguments($args, $type = Assets::CSS_TYPE)
+    {
+        // First argument is always the asset
+        array_shift($args);
+
+        if (\count($args) === 0) {
+            return [];
+        }
+        if (\count($args) === 1 && \is_array($args[0])) {
+            return $args[0];
+        }
+
+        switch ($type) {
+            case(Assets::INLINE_CSS_TYPE):
+                $defaults = ['priority' => null, 'group' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+                break;
+
+            case(Assets::JS_TYPE):
+                $defaults = ['priority' => null, 'pipeline' => true, 'loading' => null, 'group' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+                break;
+
+            case(Assets::INLINE_JS_TYPE):
+                $defaults = ['priority' => null, 'group' => null, 'attributes' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+
+                // special case to handle old attributes being passed in
+                if (isset($arguments['attributes'])) {
+                    $old_attributes = $arguments['attributes'];
+                    $arguments = array_merge($arguments, $old_attributes);
+                }
+                unset($arguments['attributes']);
+
+                break;
+
+            default:
+            case(Assets::CSS_TYPE):
+                $defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+        }
+
+        return $arguments;
+    }
+
+    protected function createArgumentsFromLegacy(array $args, array $defaults)
+    {
+        // Remove arguments with old default values.
+        $arguments = [];
+        foreach ($args as $arg) {
+            $default = current($defaults);
+            if ($arg !== $default) {
+                $arguments[key($defaults)] = $arg;
+            }
+            next($defaults);
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Convenience wrapper for async loading of JavaScript
+     *
+     * @param string|array  $asset
+     * @param int           $priority
+     * @param bool          $pipeline
+     * @param string        $group name of the group
+     *
+     * @return \Grav\Common\Assets
+     * @deprecated Please use dynamic method with ['loading' => 'async'].
+     */
+    public function addAsyncJs($asset, $priority = 10, $pipeline = true, $group = 'head')
+    {
+        user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'async\']', E_USER_DEPRECATED);
+
+        return $this->addJs($asset, $priority, $pipeline, 'async', $group);
+    }
+
+    /**
+     * Convenience wrapper for deferred loading of JavaScript
+     *
+     * @param string|array  $asset
+     * @param int           $priority
+     * @param bool          $pipeline
+     * @param string        $group name of the group
+     *
+     * @return \Grav\Common\Assets
+     * @deprecated Please use dynamic method with ['loading' => 'defer'].
+     */
+    public function addDeferJs($asset, $priority = 10, $pipeline = true, $group = 'head')
+    {
+        user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'defer\']', E_USER_DEPRECATED);
+
+        return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
+    }
+
+}

+ 343 - 0
system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php

@@ -0,0 +1,343 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets\Traits
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets\Traits;
+
+use Grav\Common\Grav;
+
+trait TestingAssetsTrait
+{
+    /**
+     * Determines if an asset exists as a collection, CSS or JS reference
+     *
+     * @param string $asset
+     *
+     * @return bool
+     */
+    public function exists($asset)
+    {
+        return isset($this->collections[$asset]) || isset($this->assets_css[$asset]) || isset($this->assets_js[$asset]);
+    }
+
+    /**
+     * Return the array of all the registered collections
+     *
+     * @return array
+     */
+    public function getCollections()
+    {
+        return $this->collections;
+    }
+
+    /**
+     * Set the array of collections explicitly
+     *
+     * @param array $collections
+     *
+     * @return $this
+     */
+    public function setCollection($collections)
+    {
+        $this->collections = $collections;
+
+        return $this;
+    }
+
+    /**
+     * Return the array of all the registered CSS assets
+     * If a $key is provided, it will try to return only that asset
+     * else it will return null
+     *
+     * @param null|string $key the asset key
+     * @return array
+     */
+    public function getCss($key = null)
+    {
+        if (null !== $key) {
+            $asset_key = md5($key);
+
+            return $this->assets_css[$asset_key] ?? null;
+        }
+
+        return $this->assets_css;
+    }
+
+    /**
+     * Return the array of all the registered JS assets
+     * If a $key is provided, it will try to return only that asset
+     * else it will return null
+     *
+     * @param null|string $key the asset key
+     * @return array
+     */
+    public function getJs($key = null)
+    {
+        if (null !== $key) {
+            $asset_key = md5($key);
+
+            return $this->assets_js[$asset_key] ?? null;
+        }
+
+        return $this->assets_js;
+    }
+
+    /**
+     * Set the whole array of CSS assets
+     *
+     * @param array $css
+     *
+     * @return $this
+     */
+    public function setCss($css)
+    {
+        $this->assets_css = $css;
+
+        return $this;
+    }
+
+    /**
+     * Set the whole array of JS assets
+     *
+     * @param array $js
+     *
+     * @return $this
+     */
+    public function setJs($js)
+    {
+        $this->assets_js = $js;
+
+        return $this;
+    }
+
+    /**
+     * Removes an item from the CSS array if set
+     *
+     * @param string $key  The asset key
+     *
+     * @return $this
+     */
+    public function removeCss($key)
+    {
+        $asset_key = md5($key);
+        if (isset($this->assets_css[$asset_key])) {
+            unset($this->assets_css[$asset_key]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Removes an item from the JS array if set
+     *
+     * @param string $key  The asset key
+     *
+     * @return $this
+     */
+    public function removeJs($key)
+    {
+        $asset_key = md5($key);
+        if (isset($this->assets_js[$asset_key])) {
+            unset($this->assets_js[$asset_key]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Sets the state of CSS Pipeline
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function setCssPipeline($value)
+    {
+        $this->css_pipeline = (bool)$value;
+
+        return $this;
+    }
+
+    /**
+     * Sets the state of JS Pipeline
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function setJsPipeline($value)
+    {
+        $this->js_pipeline = (bool)$value;
+
+        return $this;
+    }
+
+    /**
+     * Reset all assets.
+     *
+     * @return $this
+     */
+    public function reset()
+    {
+        $this->resetCss();
+        $this->resetJs();
+        $this->setCssPipeline(false);
+        $this->setJsPipeline(false);
+
+        return $this;
+    }
+
+    /**
+     * Reset JavaScript assets.
+     *
+     * @return $this
+     */
+    public function resetJs()
+    {
+        $this->assets_js = [];
+
+        return $this;
+    }
+
+    /**
+     * Reset CSS assets.
+     *
+     * @return $this
+     */
+    public function resetCss()
+    {
+        $this->assets_css = [];
+
+        return $this;
+    }
+
+    /**
+     * Explicitly set's a timestamp for assets
+     *
+     * @param string|int $value
+     */
+    public function setTimestamp($value)
+    {
+        $this->timestamp = $value;
+    }
+
+    /**
+     * Get the timestamp for assets
+     *
+     * @param  bool  $include_join
+     * @return string
+     */
+    public function getTimestamp($include_join = true)
+    {
+        if ($this->timestamp) {
+            return $include_join ? '?' . $this->timestamp : $this->timestamp;
+        }
+
+        return null;
+    }
+
+    /**
+     * Add all assets matching $pattern within $directory.
+     *
+     * @param  string $directory Relative to the Grav root path, or a stream identifier
+     * @param  string $pattern   (regex)
+     *
+     * @return $this
+     */
+    public function addDir($directory, $pattern = self::DEFAULT_REGEX)
+    {
+        $root_dir = rtrim(ROOT_DIR, '/');
+
+        // Check if $directory is a stream.
+        if (strpos($directory, '://')) {
+            $directory = Grav::instance()['locator']->findResource($directory, null);
+        }
+
+        // Get files
+        $files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/');
+
+        // No luck? Nothing to do
+        if (!$files) {
+            return $this;
+        }
+
+        // Add CSS files
+        if ($pattern === self::CSS_REGEX) {
+            foreach ($files as $file) {
+                $this->addCss($file);
+            }
+
+            return $this;
+        }
+
+        // Add JavaScript files
+        if ($pattern === self::JS_REGEX) {
+            foreach ($files as $file) {
+                $this->addJs($file);
+            }
+
+            return $this;
+        }
+
+        // Unknown pattern.
+        foreach ($files as $asset) {
+            $this->add($asset);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add all JavaScript assets within $directory
+     *
+     * @param  string $directory Relative to the Grav root path, or a stream identifier
+     *
+     * @return $this
+     */
+    public function addDirJs($directory)
+    {
+        return $this->addDir($directory, self::JS_REGEX);
+    }
+
+    /**
+     * Add all CSS assets within $directory
+     *
+     * @param  string $directory Relative to the Grav root path, or a stream identifier
+     *
+     * @return $this
+     */
+    public function addDirCss($directory)
+    {
+        return $this->addDir($directory, self::CSS_REGEX);
+    }
+
+    /**
+     * Recursively get files matching $pattern within $directory.
+     *
+     * @param  string $directory
+     * @param  string $pattern (regex)
+     * @param  string $ltrim   Will be trimmed from the left of the file path
+     *
+     * @return array
+     */
+    protected function rglob($directory, $pattern, $ltrim = null)
+    {
+        $iterator = new \RegexIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory,
+            \FilesystemIterator::SKIP_DOTS)), $pattern);
+        $offset = \strlen($ltrim);
+        $files = [];
+
+        foreach ($iterator as $file) {
+            $files[] = substr($file->getPathname(), $offset);
+        }
+
+        return $files;
+    }
+
+
+}

+ 261 - 0
system/src/Grav/Common/Backup/Backups.php

@@ -0,0 +1,261 @@
+<?php
+
+/**
+ * @package    Grav\Common\Backup
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Backup;
+
+use Grav\Common\Filesystem\Archiver;
+use Grav\Common\Filesystem\Folder;
+use Grav\Common\Inflector;
+use Grav\Common\Scheduler\Job;
+use Grav\Common\Scheduler\Scheduler;
+use Grav\Common\Utils;
+use Grav\Common\Grav;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\Event\EventDispatcher;
+use RocketTheme\Toolbox\File\JsonFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+class Backups
+{
+    protected const BACKUP_FILENAME_REGEXZ = "#(.*)--(\d*).zip#";
+
+    protected const BACKUP_DATE_FORMAT = 'YmdHis';
+
+    protected static $backup_dir;
+
+    protected static $backups = null;
+
+    public function init()
+    {
+        /** @var EventDispatcher $dispatcher */
+        $dispatcher = Grav::instance()['events'];
+        $dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
+        Grav::instance()->fireEvent('onBackupsInitialized', new Event(['backups' => $this]));
+    }
+
+    public function setup()
+    {
+        if (null === static::$backup_dir) {
+            static::$backup_dir = Grav::instance()['locator']->findResource('backup://', true, true);
+            Folder::create(static::$backup_dir);
+        }
+    }
+
+    public function onSchedulerInitialized(Event $event)
+    {
+        /** @var Scheduler $scheduler */
+        $scheduler = $event['scheduler'];
+
+        /** @var Inflector $inflector */
+        $inflector = Grav::instance()['inflector'];
+
+        foreach (static::getBackupProfiles() as $id => $profile) {
+            $at = $profile['schedule_at'];
+            $name = $inflector::hyphenize($profile['name']);
+            $logs = 'logs/backup-' . $name . '.out';
+            /** @var Job $job */
+            $job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name );
+            $job->at($at);
+            $job->output($logs);
+            $job->backlink('/tools/backups');
+        }
+    }
+
+    public function getBackupDownloadUrl($backup, $base_url)
+    {
+        $param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':');
+        $download = urlencode(base64_encode($backup));
+        $url      = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim($base_url,
+                '/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
+
+        return $url;
+    }
+
+    public static function getBackupProfiles()
+    {
+        return Grav::instance()['config']->get('backups.profiles');
+    }
+
+    public static function getPurgeConfig()
+    {
+        return Grav::instance()['config']->get('backups.purge');
+    }
+
+    public function getBackupNames()
+    {
+        return array_column(static::getBackupProfiles(), 'name');
+    }
+
+    public static function getTotalBackupsSize()
+    {
+        $backups = static::getAvailableBackups();
+        $size = array_sum(array_column($backups, 'size'));
+
+        return $size ?? 0;
+    }
+
+    public static function getAvailableBackups($force = false)
+    {
+        if ($force || null === static::$backups) {
+            static::$backups = [];
+            $backups_itr = new \GlobIterator(static::$backup_dir . '/*.zip', \FilesystemIterator::KEY_AS_FILENAME);
+            $inflector = Grav::instance()['inflector'];
+            $long_date_format = DATE_RFC2822;
+
+            /**
+             * @var string $name
+             * @var \SplFileInfo $file
+             */
+            foreach ($backups_itr as $name => $file) {
+
+                if (preg_match(static::BACKUP_FILENAME_REGEXZ, $name, $matches)) {
+                    $date = \DateTime::createFromFormat(static::BACKUP_DATE_FORMAT, $matches[2]);
+                    $timestamp = $date->getTimestamp();
+                    $backup = new \stdClass();
+                    $backup->title = $inflector->titleize($matches[1]);
+                    $backup->time = $date;
+                    $backup->date = $date->format($long_date_format);
+                    $backup->filename = $name;
+                    $backup->path = $file->getPathname();
+                    $backup->size = $file->getSize();
+                    static::$backups[$timestamp] = $backup;
+                }
+            }
+            // Reverse Key Sort to get in reverse date order
+            krsort(static::$backups);
+        }
+
+        return static::$backups;
+    }
+
+    /**
+     * Backup
+     *
+     * @param int   $id
+     * @param callable|null $status
+     *
+     * @return null|string
+     */
+    public static function backup($id = 0, callable $status = null)
+    {
+        $profiles = static::getBackupProfiles();
+        /** @var UniformResourceLocator $locator */
+        $locator = Grav::instance()['locator'];
+
+        if (isset($profiles[$id])) {
+            $backup = (object) $profiles[$id];
+        } else {
+            throw new \RuntimeException('No backups defined...');
+        }
+
+        $name = Grav::instance()['inflector']->underscorize($backup->name);
+        $date = date(static::BACKUP_DATE_FORMAT, time());
+        $filename = trim($name, '_') . '--' . $date . '.zip';
+        $destination = static::$backup_dir . DS . $filename;
+        $max_execution_time = ini_set('max_execution_time', 600);
+        $backup_root = $backup->root;
+
+        if ($locator->isStream($backup_root)) {
+            $backup_root = $locator->findResource($backup_root);
+        } else {
+            $backup_root = rtrim(GRAV_ROOT . $backup_root, '/');
+        }
+
+        if (!file_exists($backup_root)) {
+            throw new \RuntimeException("Backup location: {$backup_root} does not exist...");
+        }
+
+        $options = [
+            'exclude_files' => static::convertExclude($backup->exclude_files ?? ''),
+            'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''),
+        ];
+
+        /** @var Archiver $archiver */
+        $archiver = Archiver::create('zip');
+        $archiver->setArchive($destination)->setOptions($options)->compress($backup_root, $status)->addEmptyFolders($options['exclude_paths'], $status);
+
+        $status && $status([
+            'type' => 'message',
+            'message' => 'Done...',
+        ]);
+
+        $status && $status([
+            'type' => 'progress',
+            'complete' => true
+        ]);
+
+        if ($max_execution_time !== false) {
+            ini_set('max_execution_time', $max_execution_time);
+        }
+
+        // Log the backup
+        Grav::instance()['log']->notice('Backup Created: ' . $destination);
+
+        // Fire Finished event
+        Grav::instance()->fireEvent('onBackupFinished', new Event(['backup' => $destination]));
+
+        // Purge anything required
+        static::purge();
+
+        // Log
+        $log = JsonFile::instance(Grav::instance()['locator']->findResource("log://backup.log", true, true));
+        $log->content([
+            'time'     => time(),
+            'location' => $destination
+        ]);
+        $log->save();
+
+        return $destination;
+    }
+
+    public static function purge()
+    {
+        $purge_config = static::getPurgeConfig();
+        $trigger = $purge_config['trigger'];
+        $backups = static::getAvailableBackups(true);
+
+        switch ($trigger)
+        {
+            case 'number':
+                $backups_count = count($backups);
+                if ($backups_count > $purge_config['max_backups_count']) {
+                    $last = end($backups);
+                    unlink ($last->path);
+                    static::purge();
+                }
+                break;
+
+            case 'time':
+                $last = end($backups);
+                $now = new \DateTime();
+                $interval = $now->diff($last->time);
+                if ($interval->days > $purge_config['max_backups_time']) {
+                    unlink($last->path);
+                    static::purge();
+                }
+                break;
+
+            default:
+                $used_space = static::getTotalBackupsSize();
+                $max_space = $purge_config['max_backups_space'] * 1024 * 1024 *  1024;
+                if ($used_space > $max_space) {
+                    $last = end($backups);
+                    unlink($last->path);
+                    static::purge();
+                }
+                break;
+        }
+    }
+
+    protected static function convertExclude($exclude)
+    {
+        $lines = preg_split("/[\s,]+/", $exclude);
+        return array_map('trim', $lines, array_fill(0, \count($lines), '/'));
+    }
+}

+ 0 - 144
system/src/Grav/Common/Backup/ZipBackup.php

@@ -1,144 +0,0 @@
-<?php
-/**
- * @package    Grav.Common.Backup
- *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
- * @license    MIT License; see LICENSE file for details.
- */
-
-namespace Grav\Common\Backup;
-
-use Grav\Common\Grav;
-use Grav\Common\Inflector;
-
-class ZipBackup
-{
-    protected static $ignorePaths = [
-        'backup',
-        'cache',
-        'images',
-        'logs',
-        'tmp'
-    ];
-
-    protected static $ignoreFolders = [
-        '.git',
-        '.svn',
-        '.hg',
-        '.idea',
-        'node_modules'
-    ];
-
-    /**
-     * Backup
-     *
-     * @param string|null   $destination
-     * @param callable|null $messager
-     *
-     * @return null|string
-     */
-    public static function backup($destination = null, callable $messager = null)
-    {
-        if (!$destination) {
-            $destination = Grav::instance()['locator']->findResource('backup://', true);
-
-            if (!$destination) {
-                throw new \RuntimeException('The backup folder is missing.');
-            }
-        }
-
-        $name = substr(strip_tags(Grav::instance()['config']->get('site.title', basename(GRAV_ROOT))), 0, 20);
-
-        $inflector = new Inflector();
-
-        if (is_dir($destination)) {
-            $date = date('YmdHis', time());
-            $filename = trim($inflector->hyphenize($name), '-') . '-' . $date . '.zip';
-            $destination = rtrim($destination, DS) . DS . $filename;
-        }
-
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => 'Creating new Backup "' . $destination . '"'
-        ]);
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => ''
-        ]);
-
-        $zip = new \ZipArchive();
-        $zip->open($destination, \ZipArchive::CREATE);
-
-        $max_execution_time = ini_set('max_execution_time', 600);
-
-        static::folderToZip(GRAV_ROOT, $zip, strlen(rtrim(GRAV_ROOT, DS) . DS), $messager);
-
-        $messager && $messager([
-            'type' => 'progress',
-            'percentage' => false,
-            'complete' => true
-        ]);
-
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => ''
-        ]);
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => 'Saving and compressing archive...'
-        ]);
-
-        $zip->close();
-
-        if ($max_execution_time !== false) {
-            ini_set('max_execution_time', $max_execution_time);
-        }
-
-        return $destination;
-    }
-
-    /**
-     * @param $folder
-     * @param $zipFile
-     * @param $exclusiveLength
-     * @param $messager
-     */
-    private static function folderToZip($folder, \ZipArchive $zipFile, $exclusiveLength, callable $messager = null)
-    {
-        $handle = opendir($folder);
-        while (false !== $f = readdir($handle)) {
-            if ($f !== '.' && $f !== '..') {
-                $filePath = "$folder/$f";
-                // Remove prefix from file path before add to zip.
-                $localPath = substr($filePath, $exclusiveLength);
-
-                if (in_array($f, static::$ignoreFolders)) {
-                    continue;
-                }
-                if (in_array($localPath, static::$ignorePaths)) {
-                    $zipFile->addEmptyDir($f);
-                    continue;
-                }
-
-                if (is_file($filePath)) {
-                    $zipFile->addFile($filePath, $localPath);
-
-                    $messager && $messager([
-                        'type' => 'progress',
-                        'percentage' => false,
-                        'complete' => false
-                    ]);
-                } elseif (is_dir($filePath)) {
-                    // Add sub-directory.
-                    $zipFile->addEmptyDir($localPath);
-                    static::folderToZip($filePath, $zipFile, $exclusiveLength, $messager);
-                }
-            }
-        }
-        closedir($handle);
-    }
-}

+ 15 - 3
system/src/Grav/Common/Browser.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -113,7 +114,7 @@ class Browser
     {
         $version = explode('.', $this->getLongVersion());
 
-        return intval($version[0]);
+        return (int)$version[0];
     }
 
     /**
@@ -134,4 +135,15 @@ class Browser
 
         return true;
     }
+    
+    /**
+     * Determine if “Do Not Track” is set by browser
+     * @see https://www.w3.org/TR/tracking-dnt/
+     *
+     * @return bool
+     */
+    public function isTrackable(): bool
+    {
+        return !(isset($_SERVER['HTTP_DNT']) && $_SERVER['HTTP_DNT'] === '1');
+    }
 }

+ 204 - 60
system/src/Grav/Common/Cache.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -11,15 +12,16 @@ namespace Grav\Common;
 use \Doctrine\Common\Cache as DoctrineCache;
 use Grav\Common\Config\Config;
 use Grav\Common\Filesystem\Folder;
+use Grav\Common\Scheduler\Scheduler;
+use Psr\SimpleCache\CacheInterface;
 use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\Event\EventDispatcher;
 
 /**
  * The GravCache object is used throughout Grav to store and retrieve cached data.
  * It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
  *
  * APCu
- * APC
- * XCache
  * RedisCache
  * MemCache
  * MemCacheD
@@ -43,6 +45,11 @@ class Cache extends Getters
      */
     protected $driver;
 
+    /**
+     * @var CacheInterface
+     */
+    protected $simpleCache;
+
     protected $driver_name;
 
     protected $driver_setting;
@@ -117,37 +124,77 @@ class Cache extends Getters
         $this->config = $grav['config'];
         $this->now = time();
 
-        $this->cache_dir = $grav['locator']->findResource('cache://doctrine', true, true);
+        if (null === $this->enabled) {
+            $this->enabled = (bool)$this->config->get('system.cache.enabled');
+        }
 
         /** @var Uri $uri */
         $uri = $grav['uri'];
 
         $prefix = $this->config->get('system.cache.prefix');
+        $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
 
-        if (is_null($this->enabled)) {
-            $this->enabled = (bool)$this->config->get('system.cache.enabled');
+        // Cache key allows us to invalidate all cache on configuration changes.
+        $this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
+        $this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
+        $this->driver_setting = $this->config->get('system.cache.driver');
+        $this->driver = $this->getCacheDriver();
+        $this->driver->setNamespace($this->key);
+
+        /** @var EventDispatcher $dispatcher */
+        $dispatcher = Grav::instance()['events'];
+        $dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
+    }
+
+    /**
+     * @return CacheInterface
+     */
+    public function getSimpleCache()
+    {
+        if (null === $this->simpleCache) {
+            $cache = new \Grav\Framework\Cache\Adapter\DoctrineCache($this->driver, '', $this->getLifetime());
+
+            // Disable cache key validation.
+            $cache->setValidation(false);
+
+            $this->simpleCache = $cache;
         }
 
-        // Cache key allows us to invalidate all cache on configuration changes.
-        $this->key = ($prefix ? $prefix : 'g') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION),
-                2, 8);
+        return $this->simpleCache;
+    }
 
-        $this->driver_setting = $this->config->get('system.cache.driver');
+    /**
+     * Deletes the old out of date file-based caches
+     *
+     * @return int
+     */
+    public function purgeOldCache()
+    {
+        $cache_dir = dirname($this->cache_dir);
+        $current = basename($this->cache_dir);
+        $count = 0;
+
+        foreach (new \DirectoryIterator($cache_dir) as $file) {
+            $dir = $file->getBasename();
+            if ($dir === $current || $file->isDot() || $file->isFile()) {
+                continue;
+            }
 
-        $this->driver = $this->getCacheDriver();
+            Folder::delete($file->getPathname());
+            $count++;
+        }
 
-        // Set the cache namespace to our unique key
-        $this->driver->setNamespace($this->key);
+        return $count;
     }
 
     /**
      * Public accessor to set the enabled state of the cache
      *
-     * @param $enabled
+     * @param bool|int $enabled
      */
     public function setEnabled($enabled)
     {
-        $this->enabled = (bool) $enabled;
+        $this->enabled = (bool)$enabled;
     }
 
     /**
@@ -184,19 +231,15 @@ class Cache extends Getters
 
         // CLI compatibility requires a non-volatile cache driver
         if ($this->config->get('system.cache.cli_compatibility') && (
-            $setting == 'auto' || $this->isVolatileDriver($setting))) {
+            $setting === 'auto' || $this->isVolatileDriver($setting))) {
             $setting = $driver_name;
         }
 
-        if (!$setting || $setting == 'auto') {
+        if (!$setting || $setting === 'auto') {
             if (extension_loaded('apcu')) {
                 $driver_name = 'apcu';
-            } elseif (extension_loaded('apc')) {
-                $driver_name = 'apc';
             } elseif (extension_loaded('wincache')) {
                 $driver_name = 'wincache';
-            } elseif (extension_loaded('xcache')) {
-                $driver_name = 'xcache';
             }
         } else {
             $driver_name = $setting;
@@ -206,9 +249,6 @@ class Cache extends Getters
 
         switch ($driver_name) {
             case 'apc':
-                $driver = new DoctrineCache\ApcCache();
-                break;
-
             case 'apcu':
                 $driver = new DoctrineCache\ApcuCache();
                 break;
@@ -217,45 +257,53 @@ class Cache extends Getters
                 $driver = new DoctrineCache\WinCacheCache();
                 break;
 
-            case 'xcache':
-                $driver = new DoctrineCache\XcacheCache();
-                break;
-
             case 'memcache':
-                $memcache = new \Memcache();
-                $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
-                    $this->config->get('system.cache.memcache.port', 11211));
-                $driver = new DoctrineCache\MemcacheCache();
-                $driver->setMemcache($memcache);
+                if (extension_loaded('memcache')) {
+                    $memcache = new \Memcache();
+                    $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
+                        $this->config->get('system.cache.memcache.port', 11211));
+                    $driver = new DoctrineCache\MemcacheCache();
+                    $driver->setMemcache($memcache);
+                } else {
+                    throw new \LogicException('Memcache PHP extension has not been installed');
+                }
                 break;
 
             case 'memcached':
-                $memcached = new \Memcached();
-                $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
-                    $this->config->get('system.cache.memcached.port', 11211));
-                $driver = new DoctrineCache\MemcachedCache();
-                $driver->setMemcached($memcached);
+                if (extension_loaded('memcached')) {
+                    $memcached = new \Memcached();
+                    $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
+                        $this->config->get('system.cache.memcached.port', 11211));
+                    $driver = new DoctrineCache\MemcachedCache();
+                    $driver->setMemcached($memcached);
+                } else {
+                    throw new \LogicException('Memcached PHP extension has not been installed');
+                }
                 break;
 
             case 'redis':
-                $redis = new \Redis();
-                $socket = $this->config->get('system.cache.redis.socket', false);
-                $password = $this->config->get('system.cache.redis.password', false);
+                if (extension_loaded('redis')) {
+                    $redis = new \Redis();
+                    $socket = $this->config->get('system.cache.redis.socket', false);
+                    $password = $this->config->get('system.cache.redis.password', false);
+
+                    if ($socket) {
+                        $redis->connect($socket);
+                    } else {
+                        $redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
+                            $this->config->get('system.cache.redis.port', 6379));
+                    }
 
-                if ($socket) {
-                    $redis->connect($socket);
-                } else {
-                    $redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
-                    $this->config->get('system.cache.redis.port', 6379));
-                }
+                    // Authenticate with password if set
+                    if ($password && !$redis->auth($password)) {
+                        throw new \RedisException('Redis authentication failed');
+                    }
 
-                // Authenticate with password if set
-                if ($password && !$redis->auth($password)) {
-                    throw new \RedisException('Redis authentication failed');
+                    $driver = new DoctrineCache\RedisCache();
+                    $driver->setRedis($redis);
+                } else {
+                    throw new \LogicException('Redis PHP extension has not been installed');
                 }
-
-                $driver = new DoctrineCache\RedisCache();
-                $driver->setRedis($redis);
                 break;
 
             default:
@@ -277,9 +325,9 @@ class Cache extends Getters
     {
         if ($this->enabled) {
             return $this->driver->fetch($id);
-        } else {
-            return false;
         }
+
+        return false;
     }
 
     /**
@@ -310,6 +358,21 @@ class Cache extends Getters
         if ($this->enabled) {
             return $this->driver->delete($id);
         }
+
+        return false;
+    }
+
+    /**
+     * Deletes all cache
+     *
+     * @return bool
+     */
+    public function deleteAll()
+    {
+        if ($this->enabled) {
+            return $this->driver->deleteAll();
+        }
+
         return false;
     }
 
@@ -324,6 +387,7 @@ class Cache extends Getters
         if ($this->enabled) {
             return $this->driver->contains(($id));
         }
+
         return false;
     }
 
@@ -382,6 +446,12 @@ class Cache extends Getters
 
         }
 
+        // Delete entries in the doctrine cache if required
+        if (in_array($remove, ['all', 'standard'])) {
+            $cache = Grav::instance()['cache'];
+            $cache->driver->deleteAll();
+        }
+
         // Clearing cache event to add paths to clear
         Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
 
@@ -422,7 +492,7 @@ class Cache extends Getters
 
         $output[] = '';
 
-        if (($remove == 'all' || $remove == 'standard') && file_exists($user_config)) {
+        if (($remove === 'all' || $remove === 'standard') && file_exists($user_config)) {
             touch($user_config);
 
             $output[] = '<red>Touched: </red>' . $user_config;
@@ -440,6 +510,24 @@ class Cache extends Getters
         return $output;
     }
 
+    public static function invalidateCache()
+    {
+        $user_config = USER_DIR . 'config/system.yaml';
+
+        if (file_exists($user_config)) {
+            touch($user_config);
+        }
+
+        // Clear stat cache
+        @clearstatcache();
+
+        // Clear opcache
+        if (function_exists('opcache_reset')) {
+            @opcache_reset();
+        }
+
+    }
+
 
     /**
      * Set the cache lifetime programmatically
@@ -496,15 +584,71 @@ class Cache extends Getters
     /**
      * is this driver a volatile driver in that it resides in PHP process memory
      *
-     * @param $setting
+     * @param string $setting
      * @return bool
      */
     public function isVolatileDriver($setting)
     {
         if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
             return true;
-        } else {
-            return false;
         }
+
+        return false;
     }
+
+    /**
+     * Static function to call as a scheduled Job to purge old Doctrine files
+     */
+    public static function purgeJob()
+    {
+        /** @var Cache $cache */
+        $cache = Grav::instance()['cache'];
+        $deleted_folders = $cache->purgeOldCache();
+
+        echo 'Purged ' . $deleted_folders . ' old cache folders...';
+    }
+
+    /**
+     * Static function to call as a scheduled Job to clear Grav cache
+     *
+     * @param string $type
+     */
+    public static function clearJob($type)
+    {
+        $result = static::clearCache($type);
+        static::invalidateCache();
+
+        echo strip_tags(implode("\n", $result));
+    }
+
+    public function onSchedulerInitialized(Event $event)
+    {
+        /** @var Scheduler $scheduler */
+        $scheduler = $event['scheduler'];
+        $config = Grav::instance()['config'];
+
+        // File Cache Purge
+        $at = $config->get('system.cache.purge_at');
+        $name = 'cache-purge';
+        $logs = 'logs/' . $name . '.out';
+
+        $job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [], $name );
+        $job->at($at);
+        $job->output($logs);
+        $job->backlink('/config/system#caching');
+
+        // Cache Clear
+        $at = $config->get('system.cache.clear_at');
+        $clear_type = $config->get('system.cache.clear_job_type');
+        $name = 'cache-clear';
+        $logs = 'logs/' . $name . '.out';
+
+        $job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name );
+        $job->at($at);
+        $job->output($logs);
+        $job->backlink('/config/system#caching');
+
+    }
+
+
 }

+ 7 - 6
system/src/Grav/Common/Composer.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -11,7 +12,7 @@ namespace Grav\Common;
 class Composer
 {
     /** @const Default composer location */
-    const DEFAULT_PATH = "bin/composer.phar";
+    const DEFAULT_PATH = 'bin/composer.phar';
 
     /**
      * Returns the location of composer.
@@ -20,12 +21,12 @@ class Composer
      */
     public static function getComposerLocation()
     {
-        if (!function_exists('shell_exec') || strtolower(substr(PHP_OS, 0, 3)) === 'win') {
+        if (!\function_exists('shell_exec') || stripos(PHP_OS, 'win') === 0) {
             return self::DEFAULT_PATH;
         }
 
         // check for global composer install
-        $path = trim(shell_exec("command -v composer"));
+        $path = trim(shell_exec('command -v composer'));
 
         // fall back to grav bundled composer
         if (!$path || !preg_match('/(composer|composer\.phar)$/', $path)) {
@@ -46,7 +47,7 @@ class Composer
         $composer = static::getComposerLocation();
 
         if ($composer !== static::DEFAULT_PATH && is_executable($composer)) {
-            $file = fopen($composer, 'r');
+            $file = fopen($composer, 'rb');
             $firstLine = fgets($file);
             fclose($file);
 

+ 9 - 10
system/src/Grav/Common/Config/CompiledBase.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -128,7 +129,7 @@ abstract class CompiledBase
      */
     public function checksum()
     {
-        if (!isset($this->checksum)) {
+        if (null === $this->checksum) {
             $this->checksum = md5(json_encode($this->files) . $this->version);
         }
 
@@ -197,11 +198,9 @@ abstract class CompiledBase
 
         $cache = include $filename;
         if (
-            !is_array($cache)
-            || !isset($cache['checksum'])
-            || !isset($cache['data'])
-            || !isset($cache['@class'])
-            || $cache['@class'] != get_class($this)
+            !\is_array($cache)
+            || !isset($cache['checksum'], $cache['data'], $cache['@class'])
+            || $cache['@class'] !== \get_class($this)
         ) {
             return false;
         }
@@ -212,7 +211,7 @@ abstract class CompiledBase
         }
 
         $this->createObject($cache['data']);
-        $this->timestamp = isset($cache['timestamp']) ? $cache['timestamp'] : 0;
+        $this->timestamp = $cache['timestamp'] ?? 0;
 
         $this->finalizeObject();
 
@@ -243,7 +242,7 @@ abstract class CompiledBase
         }
 
         $cache = [
-            '@class' => get_class($this),
+            '@class' => \get_class($this),
             'timestamp' => time(),
             'checksum' => $this->checksum(),
             'files' => $this->files,

+ 13 - 10
system/src/Grav/Common/Config/CompiledBlueprints.php

@@ -1,27 +1,30 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
+
 namespace Grav\Common\Config;
 
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Data\BlueprintSchema;
 use Grav\Common\Grav;
 
+/**
+ * Class CompiledBlueprints
+ * @package Grav\Common\Config
+ */
 class CompiledBlueprints extends CompiledBase
 {
-    /**
-     * @var int Version number for the compiled file.
-     */
-    public $version = 2;
+    public function __construct($cacheFolder, array $files, $path)
+    {
+        parent::__construct($cacheFolder, $files, $path);
 
-    /**
-     * @var BlueprintSchema  Blueprints object.
-     */
-    protected $object;
+        $this->version = 2;
+    }
 
     /**
      * Returns checksum from the configuration files.

+ 11 - 13
system/src/Grav/Common/Config/CompiledConfig.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -12,16 +13,6 @@ use Grav\Common\File\CompiledYamlFile;
 
 class CompiledConfig extends CompiledBase
 {
-    /**
-     * @var int Version number for the compiled file.
-     */
-    public $version = 1;
-
-    /**
-     * @var Config  Configuration object.
-     */
-    protected $object;
-
     /**
      * @var callable  Blueprints loader.
      */
@@ -32,6 +23,13 @@ class CompiledConfig extends CompiledBase
      */
     protected $withDefaults;
 
+    public function __construct($cacheFolder, array $files, $path)
+    {
+        parent::__construct($cacheFolder, $files, $path);
+
+        $this->version = 1;
+    }
+
     /**
      * Set blueprints for the configuration.
      *
@@ -63,7 +61,7 @@ class CompiledConfig extends CompiledBase
      */
     protected function createObject(array $data = [])
     {
-        if ($this->withDefaults && empty($data) && is_callable($this->callable)) {
+        if ($this->withDefaults && empty($data) && \is_callable($this->callable)) {
             $blueprints = $this->callable;
             $data = $blueprints()->getDefaults();
         }

+ 8 - 10
system/src/Grav/Common/Config/CompiledLanguages.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -12,15 +13,12 @@ use Grav\Common\File\CompiledYamlFile;
 
 class CompiledLanguages extends CompiledBase
 {
-    /**
-     * @var int Version number for the compiled file.
-     */
-    public $version = 1;
+    public function __construct($cacheFolder, array $files, $path)
+    {
+        parent::__construct($cacheFolder, $files, $path);
 
-    /**
-     * @var Languages  Configuration object.
-     */
-    protected $object;
+        $this->version = 1;
+    }
 
     /**
      * Create configuration object.

+ 19 - 9
system/src/Grav/Common/Config/Config.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -16,14 +17,24 @@ use Grav\Common\Utils;
 
 class Config extends Data
 {
+    public $environment;
+
+    /** @var string */
+    protected $key;
     /** @var string */
     protected $checksum;
-    protected $modified = false;
+    /** @var int */
     protected $timestamp = 0;
+    /** @var bool */
+    protected $modified = false;
 
     public function key()
     {
-        return $this->checksum();
+        if (null === $this->key) {
+            $this->key = md5($this->checksum . $this->timestamp);
+        }
+
+        return $this->key;
     }
 
     public function checksum($checksum = null)
@@ -90,7 +101,7 @@ class Config extends Data
     {
         $setup = Grav::instance()['setup']->toArray();
         foreach ($setup as $key => $value) {
-            if ($key === 'streams' || !is_array($value)) {
+            if ($key === 'streams' || !\is_array($value)) {
                 // Optimized as streams and simple values are fully defined in setup.
                 $this->items[$key] = $value;
             } else {
@@ -98,14 +109,13 @@ class Config extends Data
             }
         }
 
-        // Override the media.upload_limit based on PHP values
-        $upload_limit = Utils::getUploadLimit();
-        $this->items['system']['media']['upload_limit'] = $upload_limit > 0 ? $upload_limit : 1024*1024*1024;
+        // Legacy value - Override the media.upload_limit based on PHP values
+        $this->items['system']['media']['upload_limit'] = Utils::getUploadLimit();
     }
 
     /**
      * @return mixed
-     * @deprecated
+     * @deprecated 1.5 Use Grav::instance()['languages'] instead.
      */
     public function getLanguages()
     {

+ 3 - 2
system/src/Grav/Common/Config/ConfigFileFinder.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 30 - 2
system/src/Grav/Common/Config/Languages.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -13,6 +14,22 @@ use Grav\Common\Utils;
 
 class Languages extends Data
 {
+    /**
+     * @var string|null
+     */
+    protected $checksum;
+
+    /**
+     * @var string|null
+     */
+    protected $modified;
+
+    /**
+     * @var string|null
+     */
+    protected $timestamp;
+
+
     public function checksum($checksum = null)
     {
         if ($checksum !== null) {
@@ -52,4 +69,15 @@ class Languages extends Data
     {
         $this->items = Utils::arrayMergeRecursiveUnique($this->items, $data);
     }
+
+    public function flattenByLang($lang)
+    {
+        $language = $this->items[$lang];
+        return Utils::arrayFlattenDotNotation($language);
+    }
+
+    public function unflatten($array)
+    {
+        return Utils::arrayUnflattenDotNotation($array);
+    }
 }

+ 57 - 19
system/src/Grav/Common/Config/Setup.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -12,11 +13,23 @@ use Grav\Common\File\CompiledYamlFile;
 use Grav\Common\Data\Data;
 use Grav\Common\Utils;
 use Pimple\Container;
-use RocketTheme\Toolbox\File\YamlFile;
+use Psr\Http\Message\ServerRequestInterface;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 class Setup extends Data
 {
+    /**
+     * @var array Environment aliases normalized to lower case.
+     */
+    public static $environments = [
+        '' => 'unknown',
+        '127.0.0.1' => 'localhost',
+        '::1' => 'localhost'
+    ];
+
+    /**
+     * @var string Current environment normalized to lower case.
+     */
     public static $environment;
 
     protected $streams = [
@@ -38,7 +51,7 @@ class Setup extends Data
             // If not defined, environment will be set up in the constructor.
         ],
         'asset' => [
-            'type' => 'ReadOnlyStream',
+            'type' => 'Stream',
             'prefixes' => [
                 '' => ['assets'],
             ]
@@ -109,7 +122,7 @@ class Setup extends Data
             ]
         ],
         'image' => [
-            'type' => 'ReadOnlyStream',
+            'type' => 'Stream',
             'prefixes' => [
                 '' => ['user://images', 'system://images']
             ]
@@ -120,6 +133,13 @@ class Setup extends Data
                 '' => ['user://pages']
             ]
         ],
+        'user-data' => [
+            'type' => 'Stream',
+            'force' => true,
+            'prefixes' => [
+                '' => ['user://data']
+            ]
+        ],
         'account' => [
             'type' => 'ReadOnlyStream',
             'prefixes' => [
@@ -133,12 +153,26 @@ class Setup extends Data
      */
     public function __construct($container)
     {
-        $environment = null !== static::$environment ? static::$environment : ($container['uri']->environment() ?: 'localhost');
+        // If no environment is set, make sure we get one (CLI or hostname).
+        if (!static::$environment) {
+            if (\defined('GRAV_CLI')) {
+                static::$environment = 'cli';
+            } else {
+                /** @var ServerRequestInterface $request */
+                $request = $container['request'];
+                $host = $request->getUri()->getHost();
+
+                static::$environment = Utils::substrToString($host, ':');
+            }
+        }
+
+        // Resolve server aliases to the proper environment.
+        $environment = $this->environments[static::$environment] ?? static::$environment;
 
         // Pre-load setup.php which contains our initial configuration.
         // Configuration may contain dynamic parts, which is why we need to always load it.
-        // If "GRAVE_SETUP_PATH" has been defined, use it, otherwise use defaults.
-        $file = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH :  GRAV_ROOT . '/setup.php';
+        // If "GRAV_SETUP_PATH" has been defined, use it, otherwise use defaults.
+        $file = \defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH :  GRAV_ROOT . '/setup.php';
         $setup = is_file($file) ? (array) include $file : [];
 
         // Add default streams defined in beginning of the class.
@@ -151,8 +185,8 @@ class Setup extends Data
         parent::__construct($setup);
 
         // Set up environment.
-        $this->def('environment', $environment ?: 'cli');
-        $this->def('streams.schemes.environment.prefixes', ['' => $environment ? ["user://{$this->environment}"] : []]);
+        $this->def('environment', $environment);
+        $this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->get('environment')}"]]);
     }
 
     /**
@@ -212,8 +246,8 @@ class Setup extends Data
                 $locator->addPath($scheme, '', $config['paths']);
             }
 
-            $override = isset($config['override']) ? $config['override'] : false;
-            $force = isset($config['force']) ? $config['force'] : false;
+            $override = $config['override'] ?? false;
+            $force = $config['force'] ?? false;
 
             if (isset($config['prefixes'])) {
                 foreach ((array)$config['prefixes'] as $prefix => $paths) {
@@ -232,7 +266,7 @@ class Setup extends Data
     {
         $schemes = [];
         foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
-            $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
+            $type = $config['type'] ?? 'ReadOnlyStream';
             if ($type[0] !== '\\') {
                 $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
             }
@@ -251,8 +285,8 @@ class Setup extends Data
      */
     protected function check(UniformResourceLocator $locator)
     {
-        $streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null;
-        if (!is_array($streams)) {
+        $streams = $this->items['streams']['schemes'] ?? null;
+        if (!\is_array($streams)) {
             throw new \InvalidArgumentException('Configuration is missing streams.schemes!');
         }
         $diff = array_keys(array_diff_key($this->streams, $streams));
@@ -271,10 +305,14 @@ class Setup extends Data
 
             // Create security.yaml if it doesn't exist.
             $filename = $locator->findResource('config://security.yaml', true, true);
-            $file = YamlFile::instance($filename);
-            if (!$file->exists()) {
-                $file->save(['salt' => Utils::generateRandomString(14)]);
-                $file->free();
+            $security_file = CompiledYamlFile::instance($filename);
+            $security_content = (array)$security_file->content();
+
+            if (!isset($security_content['salt'])) {
+                $security_content = array_merge($security_content, ['salt' => Utils::generateRandomString(14)]);
+                $security_file->content($security_content);
+                $security_file->save();
+                $security_file->free();
             }
         } catch (\RuntimeException $e) {
             throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);

+ 202 - 26
system/src/Grav/Common/Data/Blueprint.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -10,21 +11,37 @@ namespace Grav\Common\Data;
 
 use Grav\Common\File\CompiledYamlFile;
 use Grav\Common\Grav;
+use Grav\Common\User\Interfaces\UserInterface;
 use RocketTheme\Toolbox\Blueprints\BlueprintForm;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 class Blueprint extends BlueprintForm
 {
-    /**
-     * @var string
-     */
+    /** @var string */
     protected $context = 'blueprints://';
 
-    /**
-     * @var BlueprintSchema
-     */
+    protected $scope;
+
+    /** @var BlueprintSchema */
     protected $blueprintSchema;
 
+    /** @var array */
+    protected $defaults;
+
+    protected $handlers = [];
+
+    public function __clone()
+    {
+        if ($this->blueprintSchema) {
+            $this->blueprintSchema = clone $this->blueprintSchema;
+        }
+    }
+
+    public function setScope($scope)
+    {
+        $this->scope = $scope;
+    }
+
     /**
      * Set default values for field types.
      *
@@ -51,7 +68,60 @@ class Blueprint extends BlueprintForm
     {
         $this->initInternals();
 
-        return $this->blueprintSchema->getDefaults();
+        if (null === $this->defaults) {
+            $this->defaults = $this->blueprintSchema->getDefaults();
+        }
+
+        return $this->defaults;
+    }
+
+    /**
+     * Initialize blueprints with its dynamic fields.
+     *
+     * @return $this
+     */
+    public function init()
+    {
+        foreach ($this->dynamic as $key => $data) {
+            // Locate field.
+            $path = explode('/', $key);
+            $current = &$this->items;
+
+            foreach ($path as $field) {
+                if (\is_object($current)) {
+                    // Handle objects.
+                    if (!isset($current->{$field})) {
+                        $current->{$field} = [];
+                    }
+
+                    $current = &$current->{$field};
+                } else {
+                    // Handle arrays and scalars.
+                    if (!\is_array($current)) {
+                        $current = [$field => []];
+                    } elseif (!isset($current[$field])) {
+                        $current[$field] = [];
+                    }
+
+                    $current = &$current[$field];
+                }
+            }
+
+            // Set dynamic property.
+            foreach ($data as $property => $call) {
+                $action = $call['action'];
+                $method = 'dynamic' . ucfirst($action);
+
+                if (isset($this->handlers[$action])) {
+                    $callable = $this->handlers[$action];
+                    $callable($current, $property, $call);
+                } elseif (method_exists($this, $method)) {
+                    $this->{$method}($current, $property, $call);
+                }
+            }
+        }
+
+        return $this;
     }
 
     /**
@@ -70,6 +140,20 @@ class Blueprint extends BlueprintForm
         return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator);
     }
 
+    /**
+     * Process data coming from a form.
+     *
+     * @param array $data
+     * @param array $toggles
+     * @return array
+     */
+    public function processForm(array $data, array $toggles = [])
+    {
+        $this->initInternals();
+
+        return $this->blueprintSchema->processForm($data, $toggles);
+    }
+
     /**
      * Return data fields that do not exist in blueprints.
      *
@@ -101,15 +185,32 @@ class Blueprint extends BlueprintForm
      * Filter data by using blueprints.
      *
      * @param  array $data
+     * @param  bool $missingValuesAsNull
+     * @param  bool $keepEmptyValues
      * @return array
      */
-    public function filter(array $data)
+    public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false)
     {
         $this->initInternals();
 
-        return $this->blueprintSchema->filter($data);
+        return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues);
     }
 
+
+    /**
+     * Flatten data by using blueprints.
+     *
+     * @param  array $data
+     * @return array
+     */
+    public function flattenData(array $data)
+    {
+        $this->initInternals();
+
+        return $this->blueprintSchema->flattenData($data);
+    }
+
+
     /**
      * Return blueprint data schema.
      *
@@ -122,20 +223,28 @@ class Blueprint extends BlueprintForm
         return $this->blueprintSchema;
     }
 
+    public function addDynamicHandler(string $name, callable $callable): void
+    {
+        $this->handlers[$name] = $callable;
+    }
+
     /**
      * Initialize validator.
      */
     protected function initInternals()
     {
-        if (!isset($this->blueprintSchema)) {
+        if (null === $this->blueprintSchema) {
             $types = Grav::instance()['plugins']->formFieldTypes;
 
             $this->blueprintSchema = new BlueprintSchema;
+
             if ($types) {
                 $this->blueprintSchema->setTypes($types);
             }
+
             $this->blueprintSchema->embed('', $this->items);
             $this->blueprintSchema->init();
+            $this->defaults = null;
         }
     }
 
@@ -162,17 +271,19 @@ class Blueprint extends BlueprintForm
         /** @var UniformResourceLocator $locator */
         $locator = Grav::instance()['locator'];
 
-        if (is_string($path) && !$locator->isStream($path)) {
+        if (\is_string($path) && !$locator->isStream($path)) {
             // Find path overrides.
-            $paths = isset($this->overrides[$path]) ? (array) $this->overrides[$path] : [];
+            $paths = (array) ($this->overrides[$path] ?? null);
 
             // Add path pointing to default context.
             if ($context === null) {
                 $context = $this->context;
             }
-            if ($context && $context[strlen($context)-1] !== '/') {
+
+            if ($context && $context[\strlen($context)-1] !== '/') {
                 $context .= '/';
             }
+
             $path = $context . $path;
 
             if (!preg_match('/\.yaml$/', $path)) {
@@ -186,7 +297,7 @@ class Blueprint extends BlueprintForm
 
         $files = [];
         foreach ($paths as $lookup) {
-            if (is_string($lookup) && strpos($lookup, '://')) {
+            if (\is_string($lookup) && strpos($lookup, '://')) {
                 $files = array_merge($files, $locator->findResources($lookup));
             } else {
                 $files[] = $lookup;
@@ -205,27 +316,29 @@ class Blueprint extends BlueprintForm
     {
         $params = $call['params'];
 
-        if (is_array($params)) {
+        if (\is_array($params)) {
             $function = array_shift($params);
         } else {
             $function = $params;
             $params = [];
         }
 
-        list($o, $f) = preg_split('/::/', $function, 2);
+        [$o, $f] = explode('::', $function, 2);
+
+        $data = null;
         if (!$f) {
-            if (function_exists($o)) {
-                $data = call_user_func_array($o, $params);
+            if (\function_exists($o)) {
+                $data = \call_user_func_array($o, $params);
             }
         } else {
             if (method_exists($o, $f)) {
-                $data = call_user_func_array(array($o, $f), $params);
+                $data = \call_user_func_array([$o, $f], $params);
             }
         }
 
         // If function returns a value,
-        if (isset($data)) {
-            if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
+        if (null !== $data) {
+            if (\is_array($data) && isset($field[$property]) && \is_array($field[$property])) {
                 // Combine field and @data-field together.
                 $field[$property] += $data;
             } else {
@@ -243,12 +356,75 @@ class Blueprint extends BlueprintForm
     protected function dynamicConfig(array &$field, $property, array &$call)
     {
         $value = $call['params'];
-
-        $default = isset($field[$property]) ? $field[$property] : null;
+        $default = $field[$property] ?? null;
         $config = Grav::instance()['config']->get($value, $default);
 
-        if (!is_null($config)) {
+        if (null !== $config) {
             $field[$property] = $config;
         }
     }
+
+    /**
+     * @param array $field
+     * @param string $property
+     * @param array $call
+     */
+    protected function dynamicSecurity(array &$field, $property, array &$call)
+    {
+        if ($property || !empty($field['validate']['ignore'])) {
+            return;
+        }
+
+        $grav = Grav::instance();
+        $actions = (array)$call['params'];
+
+        /** @var UserInterface $user */
+        if (isset($grav['user'])) {
+            $user = Grav::instance()['user'];
+            foreach ($actions as $action) {
+                if (!$user->authorize($action)) {
+                    $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * @param array $field
+     * @param string $property
+     * @param array $call
+     */
+    protected function dynamicScope(array &$field, $property, array &$call)
+    {
+        if ($property && $property !== 'ignore') {
+            return;
+        }
+
+        $scopes = (array)$call['params'];
+        $matches = \in_array($this->scope, $scopes, true);
+        if ($this->scope && $property !== 'ignore') {
+            $matches = !$matches;
+        }
+
+        if ($matches) {
+            $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
+            return;
+        }
+    }
+
+    protected function addPropertyRecursive(array &$field, $property, $value)
+    {
+        if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
+            $field[$property] = array_merge_recursive($field[$property], $value);
+        } else {
+            $field[$property] = $value;
+        }
+
+        if (!empty($field['fields'])) {
+            foreach ($field['fields'] as $key => &$child) {
+                $this->addPropertyRecursive($child, $property, $value);
+            }
+        }
+    }
 }

+ 172 - 24
system/src/Grav/Common/Data/BlueprintSchema.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -26,6 +27,23 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         'fields' => true
     ];
 
+    /**
+     * @return array
+     */
+    public function getTypes()
+    {
+        return $this->types;
+    }
+
+    /**
+     * @param string $name
+     * @return array
+     */
+    public function getType($name)
+    {
+        return $this->types[$name] ?? [];
+    }
+
     /**
      * Validate data against blueprints.
      *
@@ -46,36 +64,91 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         }
     }
 
+    /**
+     * @param array $data
+     * @param array $toggles
+     * @return array
+     */
+    public function processForm(array $data, array $toggles = [])
+    {
+        return $this->processFormRecursive($data, $toggles, $this->nested);
+    }
+
     /**
      * Filter data by using blueprints.
      *
-     * @param  array $data
+     * @param  array $data                  Incoming data, for example from a form.
+     * @param  bool  $missingValuesAsNull   Include missing values as nulls.
+     * @param bool   $keepEmptyValues       Include empty values.
      * @return array
      */
-    public function filter(array $data)
+    public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false)
     {
-        return $this->filterArray($data, $this->nested);
+        return $this->filterArray($data, $this->nested, $missingValuesAsNull, $keepEmptyValues);
+    }
+
+    /**
+     * Flatten data by using blueprints.
+     *
+     * @param  array $data                  Data to be flattened.
+     * @return array
+     */
+    public function flattenData(array $data)
+    {
+        return $this->flattenArray($data, $this->nested, '');
     }
 
     /**
      * @param array $data
      * @param array $rules
-     * @returns array
+     * @param string $prefix
+     * @return array
+     */
+    protected function flattenArray(array $data, array $rules, string $prefix)
+    {
+        $array = [];
+
+        foreach ($data as $key => $field) {
+            $val = $rules[$key] ?? $rules['*'] ?? null;
+            $rule = is_string($val) ? $this->items[$val] : null;
+
+            if ($rule || isset($val['*'])) {
+                // Item has been defined in blueprints.
+                $array[$prefix.$key] = $field;
+            } elseif (is_array($field) && is_array($val)) {
+                // Array has been defined in blueprints.
+                $array += $this->flattenArray($field, $val, $prefix . $key . '.');
+            } else {
+                // Undefined/extra item.
+                $array[$prefix.$key] = $field;
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * @param array $data
+     * @param array $rules
+     * @return array
      * @throws \RuntimeException
-     * @internal
      */
     protected function validateArray(array $data, array $rules)
     {
         $messages = $this->checkRequired($data, $rules);
 
         foreach ($data as $key => $field) {
-            $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
-            $rule = is_string($val) ? $this->items[$val] : null;
+            $val = $rules[$key] ?? $rules['*'] ?? null;
+            $rule = \is_string($val) ? $this->items[$val] : null;
 
             if ($rule) {
                 // Item has been defined in blueprints.
+                if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
+                    // Skip validation in the ignored field.
+                    continue;
+                }
+
                 $messages += Validation::validate($field, $rule);
-            } elseif (is_array($field) && is_array($val)) {
+            } elseif (\is_array($field) && \is_array($val)) {
                 // Array has been defined in blueprints.
                 $messages += $this->validateArray($field, $val);
             } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
@@ -90,32 +163,99 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
     /**
      * @param array $data
      * @param array $rules
+     * @param bool  $missingValuesAsNull
+     * @param bool $keepEmptyValues
      * @return array
-     * @internal
      */
-    protected function filterArray(array $data, array $rules)
+    protected function filterArray(array $data, array $rules, $missingValuesAsNull, $keepEmptyValues)
     {
-        $results = array();
+        $results = [];
+
+        if ($missingValuesAsNull) {
+            // First pass is to fill up all the fields with null. This is done to lock the ordering of the fields.
+            foreach ($rules as $key => $rule) {
+                if ($key && !isset($results[$key])) {
+                    $val = $rules[$key] ?? $rules['*'] ?? null;
+                    $rule = \is_string($val) ? $this->items[$val] : null;
+
+                    if (empty($rule['disabled']) && empty($rule['validate']['ignore'])) {
+                        continue;
+                    }
+                }
+            }
+        }
+
         foreach ($data as $key => $field) {
-            $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
-            $rule = is_string($val) ? $this->items[$val] : null;
+            $val = $rules[$key] ?? $rules['*'] ?? null;
+            $rule = \is_string($val) ? $this->items[$val] : null;
 
             if ($rule) {
                 // Item has been defined in blueprints.
+                if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
+                    // Skip any data in the ignored field.
+                    unset($results[$key]);
+                    continue;
+                }
+
                 $field = Validation::filter($field, $rule);
-            } elseif (is_array($field) && is_array($val)) {
+            } elseif (\is_array($field) && \is_array($val)) {
                 // Array has been defined in blueprints.
-                $field = $this->filterArray($field, $val);
+                $field = $this->filterArray($field, $val, $missingValuesAsNull, $keepEmptyValues);
+
             } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
-                $field = null;
+                // Skip any extra data.
+                continue;
             }
 
-            if (isset($field) && (!is_array($field) || !empty($field))) {
+            if ($keepEmptyValues || (null !== $field && (!\is_array($field) || !empty($field)))) {
                 $results[$key] = $field;
             }
         }
 
-        return $results;
+        return $results ?: null;
+    }
+
+    /**
+     * @param array|null $data
+     * @param array $toggles
+     * @param array $nested
+     * @return array|null
+     */
+    protected function processFormRecursive(?array $data, array $toggles, array $nested)
+    {
+        foreach ($nested as $key => $value) {
+            if ($key === '') {
+                continue;
+            }
+            if ($key === '*') {
+                // TODO: Add support to collections.
+                continue;
+            }
+            if (is_array($value)) {
+                // Recursively fetch the items.
+                $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggles[$key] ?? [], $value);
+            } else {
+                $field = $this->get($value);
+                // Do not add the field if:
+                if (
+                    // Not an input field
+                    !$field
+                    // Field has been disabled
+                    || !empty($field['disabled'])
+                    // Field validation is set to be ignored
+                    || !empty($field['validate']['ignore'])
+                    // Field is toggleable and the toggle is turned off
+                    || (!empty($field['toggleable']) && empty($toggles[$key]))
+                ) {
+                    continue;
+                }
+                if (!isset($data[$key])) {
+                    $data[$key] = null;
+                }
+            }
+        }
+
+        return $data;
     }
 
     /**
@@ -128,10 +268,18 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         $messages = [];
 
         foreach ($fields as $name => $field) {
-            if (!is_string($field)) {
+            if (!\is_string($field)) {
                 continue;
             }
+
             $field = $this->items[$field];
+
+            // Skip ignored field, it will not be required.
+            if (!empty($field['disabled']) || !empty($field['validate']['ignore'])) {
+                continue;
+            }
+
+            // Check if required.
             if (isset($field['validate']['required'])
                 && $field['validate']['required'] === true) {
 
@@ -142,9 +290,9 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
                     continue;
                 }
 
-                $value = isset($field['label']) ? $field['label'] : $field['name'];
+                $value = $field['label'] ?? $field['name'];
                 $language = Grav::instance()['language'];
-                $message  = sprintf($language->translate('FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
+                $message  = sprintf($language->translate('GRAV.FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
                 $messages[$field['name']][] = $message;
             }
         }
@@ -161,7 +309,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
     {
         $value = $call['params'];
 
-        $default = isset($field[$property]) ? $field[$property] : null;
+        $default = $field[$property] ?? null;
         $config = Grav::instance()['config']->get($value, $default);
 
         if (null !== $config) {

+ 8 - 4
system/src/Grav/Common/Data/Blueprints.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -13,8 +14,11 @@ use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 class Blueprints
 {
+    /** @var array|string */
     protected $search;
+    /** @var array */
     protected $types;
+    /** @var array */
     protected $instances = [];
 
     /**
@@ -49,7 +53,7 @@ class Blueprints
     public function types()
     {
         if ($this->types === null) {
-            $this->types = array();
+            $this->types = [];
 
             $grav = Grav::instance();
 
@@ -87,7 +91,7 @@ class Blueprints
     {
         $blueprint = new Blueprint($name);
 
-        if (is_array($this->search) || is_object($this->search)) {
+        if (\is_array($this->search) || \is_object($this->search)) {
             // Page types.
             $blueprint->setOverrides($this->search);
             $blueprint->setContext('blueprints://pages');

+ 33 - 21
system/src/Grav/Common/Data/Data.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -15,28 +16,27 @@ use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
 use RocketTheme\Toolbox\File\File;
 use RocketTheme\Toolbox\File\FileInterface;
 
-class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
+class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable, ExportInterface
 {
     use NestedArrayAccessWithGetters, Countable, Export;
 
+    /** @var string */
     protected $gettersVariable = 'items';
+
+    /** @var array */
     protected $items;
 
-    /**
-     * @var Blueprints
-     */
+    /** @var Blueprint */
     protected $blueprints;
 
-    /**
-     * @var File
-     */
+    /** @var File */
     protected $storage;
 
     /**
      * @param array $items
      * @param Blueprint|callable $blueprints
      */
-    public function __construct(array $items = array(), $blueprints = null)
+    public function __construct(array $items = [], $blueprints = null)
     {
         $this->items = $items;
         $this->blueprints = $blueprints;
@@ -70,14 +70,16 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
     {
         $old = $this->get($name, null, $separator);
         if ($old !== null) {
-            if (!is_array($old)) {
+            if (!\is_array($old)) {
                 throw new \RuntimeException('Value ' . $old);
             }
-            if (is_object($value)) {
+
+            if (\is_object($value)) {
                 $value = (array) $value;
-            } elseif (!is_array($value)) {
+            } elseif (!\is_array($value)) {
                 throw new \RuntimeException('Value ' . $value);
             }
+
             $value = $this->blueprints()->mergeData($old, $value, $name, $separator);
         }
 
@@ -108,9 +110,10 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
      */
     public function joinDefaults($name, $value, $separator = '.')
     {
-        if (is_object($value)) {
+        if (\is_object($value)) {
             $value = (array) $value;
         }
+
         $old = $this->get($name, null, $separator);
         if ($old !== null) {
             $value = $this->blueprints()->mergeData($value, $old, $name, $separator);
@@ -125,16 +128,16 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
      * Get value from the configuration and join it with given data.
      *
      * @param string  $name       Dot separated path to the requested value.
-     * @param array   $value      Value to be joined.
+     * @param array|object $value      Value to be joined.
      * @param string  $separator  Separator, defaults to '.'
      * @return array
      * @throws \RuntimeException
      */
     public function getJoined($name, $value, $separator = '.')
     {
-        if (is_object($value)) {
+        if (\is_object($value)) {
             $value = (array) $value;
-        } elseif (!is_array($value)) {
+        } elseif (!\is_array($value)) {
             throw new \RuntimeException('Value ' . $value);
         }
 
@@ -145,7 +148,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
             return $value;
         }
 
-        if (!is_array($old)) {
+        if (!\is_array($old)) {
             throw new \RuntimeException('Value ' . $old);
         }
 
@@ -195,11 +198,14 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
 
     /**
      * @return $this
-     * Filter all items by using blueprints.
      */
     public function filter()
     {
-        $this->items = $this->blueprints()->filter($this->items);
+        $args = func_get_args();
+        $missingValuesAsNull = (bool)(array_shift($args) ?: false);
+        $keepEmptyValues = (bool)(array_shift($args) ?: false);
+
+        $this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull, $keepEmptyValues);
 
         return $this;
     }
@@ -223,7 +229,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
     {
         if (!$this->blueprints){
             $this->blueprints = new Blueprint;
-        } elseif (is_callable($this->blueprints)) {
+        } elseif (\is_callable($this->blueprints)) {
             // Lazy load blueprints.
             $blueprints = $this->blueprints;
             $this->blueprints = $blueprints();
@@ -282,6 +288,12 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface
         if ($storage) {
             $this->storage = $storage;
         }
+
         return $this->storage;
     }
+
+    public function jsonSerialize()
+    {
+        return $this->items;
+    }
 }

+ 3 - 2
system/src/Grav/Common/Data/DataInterface.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 132 - 104
system/src/Grav/Common/Data/Validation.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -11,62 +12,58 @@ namespace Grav\Common\Data;
 use Grav\Common\Grav;
 use Grav\Common\Utils;
 use Grav\Common\Yaml;
-use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYaml;
 
 class Validation
 {
     /**
      * Validate value against a blueprint field definition.
      *
-     * @param $value
+     * @param mixed $value
      * @param array $field
      * @return array
      */
     public static function validate($value, array $field)
     {
-        $messages = [];
-
-        $validate = isset($field['validate']) ? (array) $field['validate'] : [];
-        // Validate type with fallback type text.
-        $type = (string) isset($validate['type']) ? $validate['type'] : $field['type'];
-        $method = 'type'.strtr($type, '-', '_');
-
-        // If value isn't required, we will stop validation if empty value is given.
-        if ((empty($validate['required']) || (isset($validate['required']) && $validate['required'] !== true)) && ($value === null || $value === '' || (($field['type'] === 'checkbox' || $field['type'] === 'switch') && $value == false))) {
-            return $messages;
-        }
-
         if (!isset($field['type'])) {
             $field['type'] = 'text';
         }
+        $type = $validate['type'] ?? $field['type'];
+        $validate = (array)($field['validate'] ?? null);
+        $required = $validate['required'] ?? false;
+
+        // If value isn't required, we will stop validation if empty value is given.
+        if ($required !== true && ($value === null || $value === '' || (($field['type'] === 'checkbox' || $field['type'] === 'switch') && $value == false))
+        ) {
+            return [];
+        }
 
         // Get language class.
         $language = Grav::instance()['language'];
 
-        $name = ucfirst(isset($field['label']) ? $field['label'] : $field['name']);
+        $name = ucfirst($field['label'] ?? $field['name']);
         $message = (string) isset($field['validate']['message'])
             ? $language->translate($field['validate']['message'])
-            : $language->translate('FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"';
+            : $language->translate('GRAV.FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"';
+
 
+        // Validate type with fallback type text.
+        $method = 'type' . str_replace('-', '_', $type);
 
         // If this is a YAML field validate/filter as such
-        if ($type != 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
+        if (isset($field['yaml']) && $field['yaml'] === true) {
             $method = 'typeYaml';
         }
 
-        if (method_exists(__CLASS__, $method)) {
-            $success = self::$method($value, $validate, $field);
-        } else {
-            $success = true;
-        }
+        $messages = [];
 
+        $success = method_exists(__CLASS__, $method) ? self::$method($value, $validate, $field) : true;
         if (!$success) {
             $messages[$field['name']][] = $message;
         }
 
         // Check individual rules.
         foreach ($validate as $rule => $params) {
-            $method = 'validate' . ucfirst(strtr($rule, '-', '_'));
+            $method = 'validate' . ucfirst(str_replace('-', '_', $rule));
 
             if (method_exists(__CLASS__, $method)) {
                 $success = self::$method($value, $params);
@@ -89,29 +86,27 @@ class Validation
      */
     public static function filter($value, array $field)
     {
-        $validate = isset($field['validate']) ? (array) $field['validate'] : [];
+        $validate = (array)($field['filter'] ?? $field['validate'] ?? null);
 
         // If value isn't required, we will return null if empty value is given.
-        if (empty($validate['required']) && ($value === null || $value === '')) {
+        if (($value === null || $value === '') && empty($validate['required'])) {
             return null;
         }
 
         if (!isset($field['type'])) {
             $field['type'] = 'text';
         }
+        $type = $field['filter']['type'] ?? $field['validate']['type'] ?? $field['type'];
 
-
-        // Validate type with fallback type text.
-        $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
-        $method = 'filter' . ucfirst(strtr($type, '-', '_'));
+        $method = 'filter' . ucfirst(str_replace('-', '_', $type));
 
         // If this is a YAML field validate/filter as such
-        if ($type !== 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
+        if (isset($field['yaml']) && $field['yaml'] === true) {
             $method = 'filterYaml';
         }
 
         if (!method_exists(__CLASS__, $method)) {
-            $method = 'filterText';
+            $method = isset($field['array']) && $field['array'] === true ? 'filterArray' : 'filterText';
         }
 
         return self::$method($value, $validate, $field);
@@ -127,22 +122,26 @@ class Validation
      */
     public static function typeText($value, array $params, array $field)
     {
-        if (!is_string($value) && !is_numeric($value)) {
+        if (!\is_string($value) && !is_numeric($value)) {
             return false;
         }
 
         $value = (string)$value;
 
-        if (isset($params['min']) && strlen($value) < $params['min']) {
+        if (!empty($params['trim'])) {
+            $value = trim($value);
+        }
+
+        if (isset($params['min']) && \strlen($value) < $params['min']) {
             return false;
         }
 
-        if (isset($params['max']) && strlen($value) > $params['max']) {
+        if (isset($params['max']) && \strlen($value) > $params['max']) {
             return false;
         }
 
-        $min = isset($params['min']) ? $params['min'] : 0;
-        if (isset($params['step']) && (strlen($value) - $min) % $params['step'] == 0) {
+        $min = $params['min'] ?? 0;
+        if (isset($params['step']) && (\strlen($value) - $min) % $params['step'] === 0) {
             return false;
         }
 
@@ -155,17 +154,25 @@ class Validation
 
     protected static function filterText($value, array $params, array $field)
     {
+        if (!\is_string($value) && !is_numeric($value)) {
+            return '';
+        }
+
+        if (!empty($params['trim'])) {
+            $value = trim($value);
+        }
+
         return (string) $value;
     }
 
     protected static function filterCommaList($value, array $params, array $field)
     {
-        return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
+        return \is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
     }
 
     public static function typeCommaList($value, array $params, array $field)
     {
-        return is_array($value) ? true : self::typeText($value, $params, $field);
+        return \is_array($value) ? true : self::typeText($value, $params, $field);
     }
 
     protected static function filterLower($value, array $params)
@@ -234,6 +241,7 @@ class Validation
     {
         // Set multiple: true so checkboxes can easily use min/max counts to control number of options required
         $field['multiple'] = true;
+
         return self::typeArray((array) $value, $params, $field);
     }
 
@@ -252,16 +260,10 @@ class Validation
      */
     public static function typeCheckbox($value, array $params, array $field)
     {
-        $value = (string) $value;
-
-        if (!isset($field['value'])) {
-            $field['value'] = 1;
-        }
-        if (isset($value) && $value != $field['value']) {
-            return false;
-        }
+        $value = (string)$value;
+        $field_value = (string)($field['value'] ?? '1');
 
-        return true;
+        return $value === $field_value;
     }
 
     /**
@@ -287,6 +289,10 @@ class Validation
      */
     public static function typeToggle($value, array $params, array $field)
     {
+        if (\is_bool($value)) {
+            $value = (int)$value;
+        }
+
         return self::typeArray((array) $value, $params, $field);
     }
 
@@ -300,12 +306,12 @@ class Validation
      */
     public static function typeFile($value, array $params, array $field)
     {
-        return self::typeArray((array) $value, $params, $field);
+        return self::typeArray((array)$value, $params, $field);
     }
 
     protected static function filterFile($value, array $params, array $field)
     {
-        return (array) $value;
+        return (array)$value;
     }
 
     /**
@@ -343,12 +349,9 @@ class Validation
             return false;
         }
 
-        $min = isset($params['min']) ? $params['min'] : 0;
-        if (isset($params['step']) && fmod($value - $min, $params['step']) == 0) {
-            return false;
-        }
+        $min = $params['min'] ?? 0;
 
-        return true;
+        return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
     }
 
     protected static function filterNumber($value, array $params, array $field)
@@ -408,10 +411,10 @@ class Validation
      */
     public static function typeEmail($value, array $params, array $field)
     {
-        $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
+        $values = !\is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value;
 
-        foreach ($values as $value) {
-            if (!(self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_EMAIL))) {
+        foreach ($values as $val) {
+            if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) {
                 return false;
             }
         }
@@ -445,9 +448,11 @@ class Validation
     {
         if ($value instanceof \DateTime) {
             return true;
-        } elseif (!is_string($value)) {
+        }
+        if (!\is_string($value)) {
             return false;
-        } elseif (!isset($params['format'])) {
+        }
+        if (!isset($params['format'])) {
             return false !== strtotime($value);
         }
 
@@ -479,10 +484,10 @@ class Validation
      */
     public static function typeDate($value, array $params, array $field)
     {
-        $params = array($params);
         if (!isset($params['format'])) {
             $params['format'] = 'Y-m-d';
         }
+
         return self::typeDatetime($value, $params, $field);
     }
 
@@ -496,10 +501,10 @@ class Validation
      */
     public static function typeTime($value, array $params, array $field)
     {
-        $params = array($params);
         if (!isset($params['format'])) {
             $params['format'] = 'H:i';
         }
+
         return self::typeDatetime($value, $params, $field);
     }
 
@@ -513,10 +518,10 @@ class Validation
      */
     public static function typeMonth($value, array $params, array $field)
     {
-        $params = array($params);
         if (!isset($params['format'])) {
             $params['format'] = 'Y-m';
         }
+
         return self::typeDatetime($value, $params, $field);
     }
 
@@ -533,6 +538,7 @@ class Validation
         if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) {
             return false;
         }
+
         return self::typeDatetime($value, $params, $field);
     }
 
@@ -546,72 +552,80 @@ class Validation
      */
     public static function typeArray($value, array $params, array $field)
     {
-        if (!is_array($value)) {
+        if (!\is_array($value)) {
             return false;
         }
 
         if (isset($field['multiple'])) {
-            if (isset($params['min']) && count($value) < $params['min']) {
+            if (isset($params['min']) && \count($value) < $params['min']) {
                 return false;
             }
 
-            if (isset($params['max']) && count($value) > $params['max']) {
+            if (isset($params['max']) && \count($value) > $params['max']) {
                 return false;
             }
 
-            $min = isset($params['min']) ? $params['min'] : 0;
-            if (isset($params['step']) && (count($value) - $min) % $params['step'] == 0) {
+            $min = $params['min'] ?? 0;
+            if (isset($params['step']) && (\count($value) - $min) % $params['step'] === 0) {
                 return false;
             }
         }
 
-        $options = isset($field['options']) ? array_keys($field['options']) : array();
-        $values = isset($field['use']) && $field['use'] == 'keys' ? array_keys($value) : $value;
-        if ($options && array_diff($values, $options)) {
-            return false;
+        $options = $field['options'] ?? [];
+        $use = $field['use'] ?? 'values';
+
+        if (empty($field['selectize']) || empty($field['multiple'])) {
+            $options = array_keys($options);
+        }
+        if ($use === 'keys') {
+            $value = array_keys($value);
         }
 
-        return true;
+        return !($options && array_diff($value, $options));
     }
 
     protected static function filterArray($value, $params, $field)
     {
         $values = (array) $value;
-        $options = isset($field['options']) ? array_keys($field['options']) : array();
-        $multi = isset($field['multiple']) ? $field['multiple'] : false;
+        $options = isset($field['options']) ? array_keys($field['options']) : [];
+        $multi = $field['multiple'] ?? false;
 
-        if (count($values) == 1 && isset($values[0]) && $values[0] == '') {
+        if (\count($values) === 1 && isset($values[0]) && $values[0] === '') {
             return null;
         }
 
 
         if ($options) {
-            $useKey = isset($field['use']) && $field['use'] == 'keys';
-            foreach ($values as $key => $value) {
-                $values[$key] = $useKey ? (bool) $value : $value;
+            $useKey = isset($field['use']) && $field['use'] === 'keys';
+            foreach ($values as $key => $val) {
+                $values[$key] = $useKey ? (bool) $val : $val;
             }
         }
 
         if ($multi) {
-            foreach ($values as $key => $value) {
-                if (is_array($value)) {
-                    $value = implode(',', $value);
-                    $values[$key] =  array_map('trim', explode(',', $value));
+            foreach ($values as $key => $val) {
+                if (\is_array($val)) {
+                    $val = implode(',', $val);
+                    $values[$key] =  array_map('trim', explode(',', $val));
                 } else {
-                    $values[$key] =  trim($value);
+                    $values[$key] =  trim($val);
                 }
             }
         }
 
         if (isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty'])) {
-            foreach ($values as $key => $value) {
-                foreach ($value as $inner_key => $inner_value) {
-                    if ($inner_value == '') {
-                        unset($value[$inner_key]);
+            foreach ($values as $key => $val) {
+                if ($val === '') {
+                    unset($values[$key]);
+                } elseif (\is_array($val)) {
+                    foreach ($val as $inner_key => $inner_value) {
+                        if ($inner_value === '') {
+                            unset($val[$inner_key]);
+                        }
                     }
                 }
 
-                $values[$key] = $value;
+                $values[$key] = $val;
             }
         }
 
@@ -620,7 +634,7 @@ class Validation
 
     public static function typeList($value, array $params, array $field)
     {
-        if (!is_array($value)) {
+        if (!\is_array($value)) {
             return false;
         }
 
@@ -628,7 +642,7 @@ class Validation
             foreach ($value as $key => $item) {
                 foreach ($field['fields'] as $subKey => $subField) {
                     $subKey = trim($subKey, '.');
-                    $subValue = isset($item[$subKey]) ? $item[$subKey] : null;
+                    $subValue = $item[$subKey] ?? null;
                     self::validate($subValue, $subField);
                 }
             }
@@ -644,7 +658,7 @@ class Validation
 
     public static function filterYaml($value, $params)
     {
-        if (!is_string($value)) {
+        if (!\is_string($value)) {
             return $value;
         }
 
@@ -670,6 +684,23 @@ class Validation
         return $value;
     }
 
+    /**
+     * Input value which can be ignored.
+     *
+     * @param  mixed  $value   Value to be validated.
+     * @param  array  $params  Validation parameters.
+     * @param  array  $field   Blueprint for the field.
+     * @return bool   True if validation succeeded.
+     */
+    public static function typeUnset($value, array $params, array $field)
+    {
+        return true;
+    }
+
+    public static function filterUnset($value, array $params, array $field)
+    {
+        return null;
+    }
 
     // HTML5 attributes (min, max and range are handled inside the types)
 
@@ -677,9 +708,9 @@ class Validation
     {
         if (is_scalar($value)) {
             return (bool) $params !== true || $value !== '';
-        } else {
-            return (bool) $params !== true || !empty($value);
         }
+
+        return (bool) $params !== true || !empty($value);
     }
 
     public static function validatePattern($value, $params)
@@ -702,12 +733,12 @@ class Validation
 
     public static function typeBool($value, $params)
     {
-        return is_bool($value) || $value == 1 || $value == 0;
+        return \is_bool($value) || $value == 1 || $value == 0;
     }
 
     public static function validateBool($value, $params)
     {
-        return is_bool($value) || $value == 1 || $value == 0;
+        return \is_bool($value) || $value == 1 || $value == 0;
     }
 
     protected static function filterBool($value, $params)
@@ -722,7 +753,7 @@ class Validation
 
     public static function validateFloat($value, $params)
     {
-        return is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
+        return \is_float(filter_var($value, FILTER_VALIDATE_FLOAT));
     }
 
     protected static function filterFloat($value, $params)
@@ -737,20 +768,17 @@ class Validation
 
     public static function validateInt($value, $params)
     {
-        return is_numeric($value) && (int) $value == $value;
+        return is_numeric($value) && (int)$value == $value;
     }
 
     protected static function filterInt($value, $params)
     {
-        return (int) $value;
+        return (int)$value;
     }
 
     public static function validateArray($value, $params)
     {
-        return is_array($value)
-        || ($value instanceof \ArrayAccess
-            && $value instanceof \Traversable
-            && $value instanceof \Countable);
+        return \is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable && $value instanceof \Countable);
     }
 
     public static function filterItem_List($value, $params)

+ 4 - 3
system/src/Grav/Common/Data/ValidationException.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -18,7 +19,7 @@ class ValidationException extends \RuntimeException
         $this->messages = $messages;
 
         $language = Grav::instance()['language'];
-        $this->message = $language->translate('FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message;
+        $this->message = $language->translate('GRAV.FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message;
 
         foreach ($messages as $variable => &$list) {
             $list = array_unique($list);

+ 229 - 66
system/src/Grav/Common/Debugger.php

@@ -1,18 +1,29 @@
 <?php
+
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
 namespace Grav\Common;
 
 use DebugBar\DataCollector\ConfigCollector;
+use DebugBar\DataCollector\DataCollectorInterface;
+use DebugBar\DataCollector\ExceptionsCollector;
+use DebugBar\DataCollector\MemoryCollector;
 use DebugBar\DataCollector\MessagesCollector;
+use DebugBar\DataCollector\PhpInfoCollector;
+use DebugBar\DataCollector\RequestDataCollector;
+use DebugBar\DataCollector\TimeDataCollector;
+use DebugBar\DebugBar;
 use DebugBar\JavascriptRenderer;
 use DebugBar\StandardDebugBar;
 use Grav\Common\Config\Config;
+use Grav\Common\Processors\ProcessorInterface;
+use Twig\Template;
+use Twig\TemplateWrapper;
 
 class Debugger
 {
@@ -28,13 +39,18 @@ class Debugger
     /** @var StandardDebugBar $debugbar */
     protected $debugbar;
 
+    /** @var bool */
     protected $enabled;
 
+    protected $initialized = false;
+
+    /** @var array */
     protected $timers = [];
 
-    /** @var string[] $deprecations */
+    /** @var array $deprecations */
     protected $deprecations = [];
 
+    /** @var callable */
     protected $errorHandler;
 
     /**
@@ -42,11 +58,26 @@ class Debugger
      */
     public function __construct()
     {
+        $currentTime = microtime(true);
+
+        if (!\defined('GRAV_REQUEST_TIME')) {
+            \define('GRAV_REQUEST_TIME', $currentTime);
+        }
+
         // Enable debugger until $this->init() gets called.
         $this->enabled = true;
 
-        $this->debugbar = new StandardDebugBar();
-        $this->debugbar['time']->addMeasure('Loading', $this->debugbar['time']->getRequestStartTime(), microtime(true));
+        $debugbar = new DebugBar();
+        $debugbar->addCollector(new PhpInfoCollector());
+        $debugbar->addCollector(new MessagesCollector());
+        $debugbar->addCollector(new RequestDataCollector());
+        $debugbar->addCollector(new TimeDataCollector($_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME));
+
+        $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
+        $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $currentTime);
+        $debugbar['time']->addMeasure('Debugger', $currentTime, microtime(true));
+
+        $this->debugbar = $debugbar;
 
         // Set deprecation collector.
         $this->setErrorHandler();
@@ -60,21 +91,28 @@ class Debugger
      */
     public function init()
     {
+        if ($this->initialized) {
+            return $this;
+        }
+
         $this->grav = Grav::instance();
         $this->config = $this->grav['config'];
 
         // Enable/disable debugger based on configuration.
-        $this->enabled = $this->config->get('system.debugger.enabled');
+        $this->enabled = (bool)$this->config->get('system.debugger.enabled');
 
         if ($this->enabled()) {
+            $this->initialized = true;
 
             $plugins_config = (array)$this->config->get('plugins');
 
             ksort($plugins_config);
 
-
-            $this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
-            $this->debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
+            $debugbar = $this->debugbar;
+            $debugbar->addCollector(new MemoryCollector());
+            $debugbar->addCollector(new ExceptionsCollector());
+            $debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
+            $debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
             $this->addMessage('Grav v' . GRAV_VERSION);
         }
 
@@ -86,12 +124,12 @@ class Debugger
      *
      * @param bool $state If null, the method returns the enabled value. If set, the method sets the enabled state
      *
-     * @return null
+     * @return bool
      */
     public function enabled($state = null)
     {
         if ($state !== null) {
-            $this->enabled = $state;
+            $this->enabled = (bool)$state;
         }
 
         return $this->enabled;
@@ -147,7 +185,7 @@ class Debugger
     /**
      * Adds a data collector
      *
-     * @param $collector
+     * @param DataCollectorInterface $collector
      *
      * @return $this
      * @throws \DebugBar\DebugBarException
@@ -162,9 +200,9 @@ class Debugger
     /**
      * Returns a data collector
      *
-     * @param $collector
+     * @param DataCollectorInterface $collector
      *
-     * @return \DebugBar\DataCollector\DataCollectorInterface
+     * @return DataCollectorInterface
      * @throws \DebugBar\DebugBarException
      */
     public function getCollector($collector)
@@ -229,14 +267,14 @@ class Debugger
     /**
      * Start a timer with an associated name and description
      *
-     * @param             $name
+     * @param string      $name
      * @param string|null $description
      *
      * @return $this
      */
     public function startTimer($name, $description = null)
     {
-        if ($name[0] === '_' || $this->enabled()) {
+        if (strpos($name, '_') === 0 || $this->enabled()) {
             $this->debugbar['time']->startMeasure($name, $description);
             $this->timers[] = $name;
         }
@@ -253,7 +291,7 @@ class Debugger
      */
     public function stopTimer($name)
     {
-        if (in_array($name, $this->timers, true) && ($name[0] === '_' || $this->enabled())) {
+        if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) {
             $this->debugbar['time']->stopMeasure($name);
         }
 
@@ -263,7 +301,7 @@ class Debugger
     /**
      * Dump variables into the Messages tab of the Debug Bar
      *
-     * @param        $message
+     * @param mixed  $message
      * @param string $label
      * @param bool   $isString
      *
@@ -286,7 +324,7 @@ class Debugger
      */
     public function addException(\Exception $e)
     {
-        if ($this->enabled()) {
+        if ($this->initialized && $this->enabled()) {
             $this->debugbar['exceptions']->addException($e);
         }
 
@@ -321,57 +359,183 @@ class Debugger
             return true;
         }
 
-        $backtrace = debug_backtrace(false);
+        // Figure out error scope from the error.
+        $scope = 'unknown';
+        if (stripos($errstr, 'grav') !== false) {
+            $scope = 'grav';
+        } elseif (strpos($errfile, '/twig/') !== false) {
+            $scope = 'twig';
+        } elseif (stripos($errfile, '/yaml/') !== false) {
+            $scope = 'yaml';
+        } elseif (strpos($errfile, '/vendor/') !== false) {
+            $scope = 'vendor';
+        }
+
+        // Clean up backtrace to make it more useful.
+        $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
 
         // Skip current call.
         array_shift($backtrace);
 
-        // Skip vendor libraries and the method where error was triggered.
-        while ($current = array_shift($backtrace)) {
-            if (isset($current['file']) && strpos($current['file'], 'vendor') !== false) {
-                continue;
-            }
-            if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
-                $current = array_shift($backtrace);
+        // Find yaml file where the error happened.
+        if ($scope === 'yaml') {
+            foreach ($backtrace as $current) {
+                if (isset($current['args'])) {
+                    foreach ($current['args'] as $arg) {
+                        if ($arg instanceof \SplFileInfo) {
+                            $arg = $arg->getPathname();
+                        }
+                        if (\is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {
+                            $errfile = $arg;
+                            $errline = 0;
+
+                            break 2;
+                        }
+                    }
+                }
             }
-
-            break;
         }
 
-        // Add back last call.
-        array_unshift($backtrace, $current);
-
         // Filter arguments.
-        foreach ($backtrace as &$current) {
+        $cut = 0;
+        $previous = null;
+        foreach ($backtrace as $i => &$current) {
             if (isset($current['args'])) {
                 $args = [];
                 foreach ($current['args'] as $arg) {
                     if (\is_string($arg)) {
-                        $args[] = "'" . $arg . "'";
+                        $arg = "'" . $arg . "'";
+                        if (mb_strlen($arg) > 100) {
+                            $arg = 'string';
+                        }
                     } elseif (\is_bool($arg)) {
-                        $args[] = $arg ? 'true' : 'false';
+                        $arg = $arg ? 'true' : 'false';
                     } elseif (\is_scalar($arg)) {
-                        $args[] = $arg;
+                        $arg = $arg;
                     } elseif (\is_object($arg)) {
-                        $args[] = get_class($arg) . ' $object';
+                        $arg = get_class($arg) . ' $object';
                     } elseif (\is_array($arg)) {
-                        $args[] = '$array';
+                        $arg = '$array';
                     } else {
-                        $args[] = '$object';
+                        $arg = '$object';
                     }
+
+                    $args[] = $arg;
                 }
                 $current['args'] = $args;
             }
+
+            $object = $current['object'] ?? null;
+            unset($current['object']);
+
+            $reflection = null;
+            if ($object instanceof TemplateWrapper) {
+                $reflection = new \ReflectionObject($object);
+                $property = $reflection->getProperty('template');
+                $property->setAccessible(true);
+                $object = $property->getValue($object);
+            }
+
+            if ($object instanceof Template) {
+                $file = $current['file'] ?? null;
+
+                if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) {
+                    $current = null;
+                    continue;
+                }
+
+                $debugInfo = $object->getDebugInfo();
+
+                $line = 1;
+                if (!$reflection) {
+                    foreach ($debugInfo as $codeLine => $templateLine) {
+                        if ($codeLine <= $current['line']) {
+                            $line = $templateLine;
+                            break;
+                        }
+                    }
+                }
+
+                $src = $object->getSourceContext();
+                //$code = preg_split('/\r\n|\r|\n/', $src->getCode());
+                //$current['twig']['twig'] = trim($code[$line - 1]);
+                $current['twig']['file'] = $src->getPath();
+                $current['twig']['line'] = $line;
+
+                $prevFile = $previous['file'] ?? null;
+                if ($prevFile && $file === $prevFile) {
+                    $prevLine = $previous['line'];
+
+                    $line = 1;
+                    foreach ($debugInfo as $codeLine => $templateLine) {
+                        if ($codeLine <= $prevLine) {
+                            $line = $templateLine;
+                            break;
+                        }
+                    }
+
+                    //$previous['twig']['twig'] = trim($code[$line - 1]);
+                    $previous['twig']['file'] = $src->getPath();
+                    $previous['twig']['line'] = $line;
+                }
+
+                $cut = $i;
+            } elseif ($object instanceof ProcessorInterface) {
+                $cut = $cut ?: $i;
+                break;
+            }
+
+            $previous = &$backtrace[$i];
         }
         unset($current);
 
-        $this->deprecations[] = [
+        if ($cut) {
+            $backtrace = array_slice($backtrace, 0, $cut + 1);
+        }
+        $backtrace = array_values(array_filter($backtrace));
+
+        // Skip vendor libraries and the method where error was triggered.
+        foreach ($backtrace as $i => $current) {
+            if (!isset($current['file'])) {
+                continue;
+            }
+            if (strpos($current['file'], '/vendor/') !== false) {
+                $cut = $i + 1;
+                continue;
+            }
+            if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
+                $cut = $i + 1;
+                continue;
+            }
+
+            break;
+        }
+
+        if ($cut) {
+            $backtrace = array_slice($backtrace, $cut);
+        }
+        $backtrace = array_values(array_filter($backtrace));
+
+        $current = reset($backtrace);
+
+        // If the issue happened inside twig file, change the file and line to match that file.
+        $file = $current['twig']['file'] ?? '';
+        if ($file) {
+            $errfile = $file;
+            $errline = $current['twig']['line'] ?? 0;
+        }
+
+        $deprecation = [
+            'scope' => $scope,
             'message' => $errstr,
             'file' => $errfile,
             'line' => $errline,
             'trace' => $backtrace,
+            'count' => 1
         ];
 
+        $this->deprecations[] = $deprecation;
+
         // Do not pass forward.
         return true;
     }
@@ -396,38 +560,37 @@ class Debugger
 
     protected function getDepracatedMessage($deprecated)
     {
-        $scope = 'unknown';
-        if (stripos($deprecated['message'], 'grav') !== false) {
-            $scope = 'grav';
-        } elseif (!isset($deprecated['file'])) {
-            $scope = 'unknown';
-        } elseif (stripos($deprecated['file'], 'twig') !== false) {
-            $scope = 'twig';
-        } elseif (stripos($deprecated['file'], 'yaml') !== false) {
-            $scope = 'yaml';
-        } elseif (stripos($deprecated['file'], 'vendor') !== false) {
-            $scope = 'vendor';
-        }
+        $scope = $deprecated['scope'];
 
         $trace = [];
-        foreach ($deprecated['trace'] as $current) {
-            $class = isset($current['class']) ? $current['class'] : '';
-            $type = isset($current['type']) ? $current['type'] : '';
-            $function = $this->getFunction($current);
-            if (isset($current['file'])) {
-                $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
-            }
+        if (isset($deprecated['trace'])) {
+            foreach ($deprecated['trace'] as $current) {
+                $class = $current['class'] ?? '';
+                $type = $current['type'] ?? '';
+                $function = $this->getFunction($current);
+                if (isset($current['file'])) {
+                    $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
+                }
 
-            unset($current['class'], $current['type'], $current['function'], $current['args']);
+                unset($current['class'], $current['type'], $current['function'], $current['args']);
 
-            $trace[] = ['call' => $class . $type . $function] + $current;
+                if (isset($current['twig'])) {
+                    $trace[] = $current['twig'];
+                } else {
+                    $trace[] = ['call' => $class . $type . $function] + $current;
+                }
+            }
         }
 
+        $array = [
+            'message' => $deprecated['message'],
+            'file' => $deprecated['file'],
+            'line' => $deprecated['line'],
+            'trace' => $trace
+        ];
+
         return [
-            [
-                'message' => $deprecated['message'],
-                'trace' => $trace
-            ],
+            array_filter($array),
             $scope
         ];
     }
@@ -438,6 +601,6 @@ class Debugger
             return '';
         }
 
-        return $trace['function'] . '(' . implode(', ', $trace['args']) . ')';
+        return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')';
     }
 }

+ 3 - 2
system/src/Grav/Common/Errors/BareHandler.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Errors
+ * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

+ 8 - 16
system/src/Grav/Common/Errors/Errors.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Errors
+ * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -17,11 +18,11 @@ class Errors
     {
         $grav = Grav::instance();
         $config = $grav['config']->get('system.errors');
-        $jsonRequest = $_SERVER && isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] == 'application/json';
+        $jsonRequest = $_SERVER && isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json';
 
         // Setup Whoops-based error handler
         $system = new SystemFacade;
-        $whoops = new \Whoops\Run($system);
+        $whoops = new Whoops\Run($system);
 
         $verbosity = 1;
 
@@ -49,17 +50,8 @@ class Errors
                 break;
         }
 
-        if (method_exists('Whoops\Util\Misc', 'isAjaxRequest')) { //Whoops 2.0
-            if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) {
-                $whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
-            }
-        } elseif (function_exists('Whoops\isAjaxRequest')) { //Whoops 2.0.0-alpha
-            if (Whoops\isAjaxRequest() || $jsonRequest) {
-                $whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
-            }
-        } else { //Whoops 1.x
-            $json_page = new Whoops\Handler\JsonResponseHandler;
-            $json_page->onlyForAjaxRequests(true);
+        if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) {
+            $whoops->pushHandler(new Whoops\Handler\JsonResponseHandler);
         }
 
         if (isset($config['log']) && $config['log']) {
@@ -70,7 +62,7 @@ class Errors
                 } catch (\Exception $e) {
                     echo $e;
                 }
-            }, 'log');
+            });
         }
 
         $whoops->register();

+ 11 - 10
system/src/Grav/Common/Errors/SimplePageHandler.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Errors
+ * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 
@@ -20,7 +21,7 @@ class SimplePageHandler extends Handler
     public function __construct()
     {
         // Add the default, local resource search path:
-        $this->searchPaths[] = __DIR__ . "/Resources";
+        $this->searchPaths[] = __DIR__ . '/Resources';
     }
 
     /**
@@ -31,8 +32,8 @@ class SimplePageHandler extends Handler
         $inspector = $this->getInspector();
 
         $helper = new TemplateHelper();
-        $templateFile = $this->getResource("layout.html.php");
-        $cssFile      = $this->getResource("error.css");
+        $templateFile = $this->getResource('layout.html.php');
+        $cssFile      = $this->getResource('error.css');
 
         $code = $inspector->getException()->getCode();
         if ( ($code >= 400) && ($code < 600) )
@@ -46,9 +47,9 @@ class SimplePageHandler extends Handler
         }
 
         $vars = array(
-            "stylesheet" => file_get_contents($cssFile),
-            "code"        => $code,
-            "message"     => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING),
+            'stylesheet' => file_get_contents($cssFile),
+            'code'        => $code,
+            'message'     => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING),
         );
 
         $helper->setVariables($vars);
@@ -58,7 +59,7 @@ class SimplePageHandler extends Handler
     }
 
     /**
-     * @param $resource
+     * @param string $resource
      *
      * @return string
      * @throws \RuntimeException
@@ -74,7 +75,7 @@ class SimplePageHandler extends Handler
         // Search through available search paths, until we find the
         // resource we're after:
         foreach ($this->searchPaths as $path) {
-            $fullPath = $path . "/$resource";
+            $fullPath = "{$path}/{$resource}";
 
             if (is_file($fullPath)) {
                 // Cache the result:

+ 3 - 2
system/src/Grav/Common/Errors/SystemFacade.php

@@ -1,8 +1,9 @@
 <?php
+
 /**
- * @package    Grav.Common.Errors
+ * @package    Grav\Common\Errors
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  */
 

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