<?php
/**
* @package Grav\Common\Helpers
*
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
use Exception;
use Grav\Common\Grav;
use Grav\Common\Utils;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use RocketTheme\Toolbox\File\MarkdownFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Yaml\Yaml;
/**
* Class YamlLinter
* @package Grav\Common\Helpers
*/
class YamlLinter
{
/**
* @param string|null $folder
* @return array
*/
public static function lint(string $folder = null)
{
if (null !== $folder) {
$folder = $folder ?: GRAV_ROOT;
return static::recurseFolder($folder);
}
return array_merge(static::lintConfig(), static::lintPages(), static::lintBlueprints());
}
/**
* @return array
*/
public static function lintPages()
{
return static::recurseFolder('page://');
}
/**
* @return array
*/
public static function lintConfig()
{
return static::recurseFolder('config://');
}
/**
* @return array
*/
public static function lintBlueprints()
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$current_theme = Grav::instance()['config']->get('system.pages.theme');
$theme_path = 'themes://' . $current_theme . '/blueprints';
$locator->addPath('blueprints', '', [$theme_path]);
return static::recurseFolder('blueprints://');
}
/**
* @param string $path
* @param string $extensions
* @return array
*/
public static function recurseFolder($path, $extensions = '(md|yaml)')
{
$lint_errors = [];
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$flags = RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new RecursiveDirectoryIterator($path, $flags);
}
$recursive = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
$iterator = new RegexIterator($recursive, '/^.+\.'.$extensions.'$/ui');
/** @var RecursiveDirectoryIterator $file */
foreach ($iterator as $filepath => $file) {
try {
Yaml::parse(static::extractYaml($filepath));
} catch (Exception $e) {
$lint_errors[str_replace(GRAV_ROOT, '', $filepath)] = $e->getMessage();
}
}
return $lint_errors;
}
/**
* @param string $path
* @return string
*/
protected static function extractYaml($path)
{
$extension = Utils::pathinfo($path, PATHINFO_EXTENSION);
if ($extension === 'md') {
$file = MarkdownFile::instance($path);
$contents = $file->frontmatter();
$file->free();
} else {
$contents = file_get_contents($path);
}
return $contents;
}
}