Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #30, bug where the package cannot properly parse a file that contains Markdown code blocks with front matter in them #38

Merged
merged 12 commits into from
Apr 6, 2022
99 changes: 99 additions & 0 deletions src/ComplexMarkdownParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Spatie\YamlFrontMatter;

use Symfony\Component\Yaml\Yaml;

class ComplexMarkdownParser
{
protected $content;
protected $lines;

public $frontMatterStartLine;
public $frontMatterEndLine;

public function __construct(string $content)
{
$this->content = $content;
$this->lines = explode("\n", $this->content);
freekmurze marked this conversation as resolved.
Show resolved Hide resolved
freekmurze marked this conversation as resolved.
Show resolved Hide resolved
}

public function parse(): Document
{
$this->findFrontMatterStartAndEndLineNumbers();

if (!$this->hasFrontMatter()) {
return new Document([], $this->content);
}

$matter = $this->getFrontMatter();
$body = $this->getBody();

$matter = Yaml::parse($matter);

return new Document($matter, $body);
}

protected function findFrontMatterStartAndEndLineNumbers()
{
foreach ($this->lines as $lineNumber => $lineContents) {
if ($this->isFrontMatterControlBlock($lineContents)) {
$this->setFrontMatterLineNumber($lineNumber);
}
}
}

protected function setFrontMatterLineNumber(int $lineNumber)
{
if (!isset($this->frontMatterStartLine)) {
$this->frontMatterStartLine = $lineNumber;
return;
}
caendesilva marked this conversation as resolved.
Show resolved Hide resolved
caendesilva marked this conversation as resolved.
Show resolved Hide resolved

if (!isset($this->frontMatterEndLine)) {
$this->frontMatterEndLine = $lineNumber;
}
}

protected function getFrontMatter(): string
{
$matter = [];
foreach ($this->lines as $lineNumber => $lineContents) {
if ($lineNumber <= $this->frontMatterEndLine) {
if (!$this->isFrontMatterControlBlock($lineContents)) {
$matter[] = $lineContents;
}
}
}
return implode("\n", $matter);
freekmurze marked this conversation as resolved.
Show resolved Hide resolved
freekmurze marked this conversation as resolved.
Show resolved Hide resolved
}

protected function getBody(): string
{
$body = [];
foreach ($this->lines as $lineNumber => $lineContents) {
if ($lineNumber > $this->frontMatterEndLine) {
$body[] = $lineContents;
}
}
return implode("\n", $this->trimBody($body));
}

protected function trimBody(array $body): array
{
if (trim($body[0]) === '') {
unset($body[0]);
}
return $body;
}

protected function hasFrontMatter(): bool
{
return ($this->frontMatterStartLine !== null) && ($this->frontMatterEndLine !== null);
}

protected function isFrontMatterControlBlock(string $line): bool
{
return substr($line, 0, 3) === '---';
}
}
8 changes: 8 additions & 0 deletions src/YamlFrontMatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public static function parse(string $content): Document
return new Document($matter, $body);
}

/**
* A parser that can handle Markdown that contains Markdown.
*/
public static function markdownCompatibleParse(string $content): Document
caendesilva marked this conversation as resolved.
Show resolved Hide resolved
caendesilva marked this conversation as resolved.
Show resolved Hide resolved
{
return (new ComplexMarkdownParser($content))->parse();
}

public static function parseFile(string $path): Document
{
return static::parse(
Expand Down
100 changes: 100 additions & 0 deletions tests/YamlFrontMatterMarkdownCompatibleParseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace Spatie\YamlFrontMatter\Tests;

use Spatie\YamlFrontMatter\Document;
use Spatie\YamlFrontMatter\YamlFrontMatter;
use PHPUnit\Framework\TestCase;

class YamlFrontMatterMarkdownCompatibleParseTest extends TestCase
{
/** @test */
public function it_can_parse_simple_front_matter_from_a_file()
{
$document = YamlFrontMatter::markdownCompatibleParse(
file_get_contents(__DIR__.'/document.md')
);

$this->assertInstanceOf(Document::class, $document);
$this->assertEquals(['foo' => 'bar'], $document->matter());
$this->assertStringContainsString('Lorem ipsum.', $document->body());
}

/** @test */
public function it_can_parse_complex_front_matter_from_a_file()
{
$document = YamlFrontMatter::markdownCompatibleParse(
file_get_contents(__DIR__.'/meta-document.md')
);

$this->assertInstanceOf(Document::class, $document);
$this->assertEquals(['foo' => 'bar'], $document->matter());
$this->assertStringContainsString('Lorem ipsum.', $document->body());
}

/** @test */
public function it_separates_the_front_matter_from_the_body()
{
$document = YamlFrontMatter::markdownCompatibleParse(
"---\ntitle: Front Matter\n---\n\nLorem ipsum."
);

$this->assertInstanceOf(Document::class, $document);

// This implicitly asserts that the front matter does not contain any markdown
$this->assertEquals(['title' => 'Front Matter'], $document->matter());
// This implicitly asserts that the body does not contain any front matter remnants
$this->assertEquals('Lorem ipsum.', $document->body());
}

/** @test */
public function it_leaves_string_without_front_matter_intact()
{
$document = YamlFrontMatter::markdownCompatibleParse(
"Lorem ipsum."
);

$this->assertInstanceOf(Document::class, $document);
$this->assertEmpty($document->matter());
$this->assertEquals('Lorem ipsum.', $document->body());
}

/** @test */
public function it_can_parse_a_file_partial_front_matter()
{
// If there is only one YAML control block, (---) the front matter is invalid
// and the document should be interpreted as having no front matter.

$document = YamlFrontMatter::markdownCompatibleParse(
"---\ntitle: Front Matter\n\nLorem ipsum."
);

$this->assertInstanceOf(Document::class, $document);
$this->assertEmpty($document->matter());
$this->assertEquals("---\ntitle: Front Matter\n\nLorem ipsum.", $document->body());
}

/** @test */
public function it_can_parse_a_string_with_unix_line_endings()
{
$document = YamlFrontMatter::markdownCompatibleParse(
"---\nfoo: bar\n---\n\nLorem ipsum."
);

$this->assertInstanceOf(Document::class, $document);
$this->assertEquals(['foo' => 'bar'], $document->matter());
$this->assertStringContainsString('Lorem ipsum.', $document->body());
}

/** @test */
public function it_can_parse_a_string_with_windows_line_endings()
{
$document = YamlFrontMatter::markdownCompatibleParse(
"---\r\nfoo: bar\r\n---\r\n\r\nLorem ipsum."
);

$this->assertInstanceOf(Document::class, $document);
$this->assertEquals(['foo' => 'bar'], $document->matter());
$this->assertStringContainsString('Lorem ipsum.', $document->body());
}
}
17 changes: 17 additions & 0 deletions tests/meta-document.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
foo: bar
---

Lorem ipsum.

A paragraph in a Markdown Post.
This file contains a Markdown code block that the original parser does not handle.
See https://github.com/spatie/yaml-front-matter/discussions/30.

```
---
title: The Title of Markdown Code
---

A paragraph in Markdown Code
```