Unverified Commit f0462354 authored by Lauri Timmanee's avatar Lauri Timmanee
Browse files

Issue #3064854 by markcarver, andypost, johnwebdev, lauriii, ridhimaabrol24,...

Issue #3064854 by markcarver, andypost, johnwebdev, lauriii, ridhimaabrol24, Hardik_Patel_12, Charlie ChX Negyesi, jhodgdon, aleksip, alexpott, Fabianx, Chi, Wim Leers: Allow Twig templates to use front matter for metadata support
parent ba28c93e
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -486,7 +486,7 @@
            "dist": {
                "type": "path",
                "url": "core",
                "reference": "5bd6798a64831fa08a343a14a0ee47127c4cb99f"
                "reference": "deeb3ec5ad5b0a9b0aa65505ab74e2bde5255abd"
            },
            "require": {
                "asm89/stack-cors": "^1.1",
+1 −0
Original line number Diff line number Diff line
@@ -85,6 +85,7 @@
        "drupal/core-file-cache": "self.version",
        "drupal/core-file-security": "self.version",
        "drupal/core-filesystem": "self.version",
        "drupal/core-front-matter": "self.version",
        "drupal/core-gettext": "self.version",
        "drupal/core-graph": "self.version",
        "drupal/core-http-foundation": "self.version",
+65 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Component\FrontMatter\Exception;

use Drupal\Component\Serialization\Exception\InvalidDataTypeException;

/**
 * Defines a class for front matter parsing exceptions.
 */
class FrontMatterParseException extends InvalidDataTypeException {

  /**
   * The line number of where the parse error occurred.
   *
   * This line number is in relation to where the parse error occurred in the
   * source front matter content. It is different from \Exception::getLine()
   * which is populated with the line number of where this exception was
   * thrown in PHP.
   *
   * @var int
   */
  protected $sourceLine;

  /**
   * Constructs a new FrontMatterParseException instance.
   *
   * @param \Drupal\Component\Serialization\Exception\InvalidDataTypeException $exception
   *   The exception thrown when attempting to parse front matter data.
   */
  public function __construct(InvalidDataTypeException $exception) {
    $this->sourceLine = 1;

    // Attempt to extract the line number from the serializer error. This isn't
    // a very stable way to do this, however it is the only way given that
    // \Drupal\Component\Serialization\SerializationInterface does not have
    // methods for accessing this kind of information reliably.
    $message = 'An error occurred when attempting to parse front matter data';
    if ($exception) {
      preg_match('/line:?\s?(\d+)/i', $exception->getMessage(), $matches);
      if (!empty($matches[1])) {
        $message .= ' on line %d';
        // Add any matching line count to the existing source line so it
        // increases it by 1 to account for the front matter separator (---).
        $this->sourceLine += (int) $matches[1];
      }
    }
    parent::__construct(sprintf($message, $this->sourceLine), 0, $exception);
  }

  /**
   * Retrieves the line number where the parse error occurred.
   *
   * This line number is in relation to where the parse error occurred in the
   * source front matter content. It is different from \Exception::getLine()
   * which is populated with the line number of where this exception was
   * thrown in PHP.
   *
   * @return int
   *   The source line number.
   */
  public function getSourceLine(): int {
    return $this->sourceLine;
  }

}
+200 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Component\FrontMatter;

use Drupal\Component\FrontMatter\Exception\FrontMatterParseException;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\SerializationInterface;

/**
 * Component for parsing front matter from a source.
 *
 * This component allows for an easy and convenient way to parse
 * @link https://jekyllrb.com/docs/front-matter/ front matter @endlink
 * from a source.
 *
 * Front matter is used as a way to provide additional static data associated
 * with a source without affecting the contents of the source. Typically this
 * is used in templates to denote special handling or categorization.
 *
 * Front matter must be the first thing in the source and must take the form of
 * valid YAML set in between triple-hyphen lines:
 *
 * source.md:
 * @code
 * ---
 * important: true
 * ---
 * My content
 * @endcode
 *
 * example.php:
 * @code
 * use Drupal\Component\FrontMatter\FrontMatter;
 *
 * $frontMatter = FrontMatter::create(file_get_contents('source.md'));
 * $data = $frontMatter->getData(); // ['important' => TRUE]
 * $content = $frontMatter->getContent(); // 'My content'
 * $line => $frontMatter->getLine(); // 4, line where content actually starts.
 * @endcode
 *
 * @ingroup utility
 */
class FrontMatter {

  /**
   * The separator used to indicate front matter data.
   *
   * @var string
   */
  const SEPARATOR = '---';

  /**
   * The regular expression used to extract the YAML front matter content.
   *
   * @var string
   */
  const REGEXP = '/\A(' . self::SEPARATOR . '(.*?)?\R' . self::SEPARATOR . ')(\R.*)?\Z/s';

  /**
   * The parsed source.
   *
   * @var array
   */
  protected $parsed;

  /**
   * A serializer.
   *
   * @var string
   */
  protected $serializer;

  /**
   * The source.
   *
   * @var string
   */
  protected $source;

  /**
   * FrontMatter constructor.
   *
   * @param string $source
   *   A string source.
   * @param string $serializer
   *   The name of a class that implements
   *   \Drupal\Component\Serialization\SerializationInterface.
   */
  public function __construct(string $source, string $serializer = '\Drupal\Component\Serialization\Yaml') {
    assert(is_subclass_of($serializer, SerializationInterface::class), sprintf('The $serializer parameter must reference a class that implements %s.', SerializationInterface::class));
    $this->serializer = $serializer;
    $this->source = $source;
  }

  /**
   * Creates a new FrontMatter instance.
   *
   * @param string $source
   *   A string source.
   * @param string $serializer
   *   The name of a class that implements
   *   \Drupal\Component\Serialization\SerializationInterface.
   *
   * @return static
   */
  public static function create(string $source, string $serializer = '\Drupal\Component\Serialization\Yaml') {
    return new static($source, $serializer);
  }

  /**
   * Parses the source.
   *
   * @return array
   *   An associative array containing:
   *   - content: The real content.
   *   - data: The front matter data extracted and decoded.
   *   - line: The line number where the real content starts.
   *
   * @throws \Drupal\Component\FrontMatter\Exception\FrontMatterParseException
   */
  protected function parse(): array {
    if (!$this->parsed) {
      $content = $this->source;
      $data = [];
      $line = 1;

      // Parse front matter data.
      if (preg_match(static::REGEXP, $content, $matches)) {
        // Extract the source content.
        $content = !empty($matches[3]) ? trim($matches[3]) : '';

        // Extract the front matter data and typecast to an array to ensure
        // top level scalars are in an array.
        $raw = !empty($matches[2]) ? trim($matches[2]) : '';
        if ($raw) {
          try {
            $data = (array) $this->serializer::decode($raw);
          }
          catch (InvalidDataTypeException $exception) {
            // Rethrow a specific front matter parse exception.
            throw new FrontMatterParseException($exception);
          }
        }

        // Determine the real source line by counting all newlines in the first
        // match (which includes the front matter separators) and append a new
        // line to denote that the content should start after it.
        if (!empty($matches[1])) {
          $line += preg_match_all('/\R/', $matches[1] . "\n");
        }
      }

      // Set the parsed data.
      $this->parsed = [
        'content' => $content,
        'data' => $data,
        'line' => $line,
      ];
    }

    return $this->parsed;
  }

  /**
   * Retrieves the extracted source content.
   *
   * @return string
   *   The extracted source content.
   *
   * @throws \Drupal\Component\FrontMatter\Exception\FrontMatterParseException
   */
  public function getContent(): string {
    return $this->parse()['content'];
  }

  /**
   * Retrieves the extracted front matter data.
   *
   * @return array
   *   The extracted front matter data.
   *
   * @throws \Drupal\Component\FrontMatter\Exception\FrontMatterParseException
   */
  public function getData(): array {
    return $this->parse()['data'];
  }

  /**
   * Retrieves the line where the source content starts, after any data.
   *
   * @return int
   *   The source content line.
   *
   * @throws \Drupal\Component\FrontMatter\Exception\FrontMatterParseException
   */
  public function getLine(): int {
    return $this->parse()['line'];
  }

}
+339 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading