Support parsing and removing a YAML metadata block in markdown.

This commit is contained in:
Sean Harvey 2017-03-21 16:33:57 +13:00
parent d474be2841
commit 484e57e404
7 changed files with 152 additions and 123 deletions

View File

@ -218,7 +218,7 @@ class DocumentationParser
! !
\[ \[
(.*?) # image title (non greedy) (.*?) # image title (non greedy)
\] \]
\( \(
(.*?) # image url (non greedy) (.*?) # image url (non greedy)
\) \)
@ -432,18 +432,18 @@ class DocumentationParser
public static function rewrite_relative_links($md, $page) public static function rewrite_relative_links($md, $page)
{ {
$baselink = $page->getEntity()->Link(); $baselink = $page->getEntity()->Link();
$re = '/ $re = '/
([^\!]?) # exclude image format ([^\!]?) # exclude image format
\[ \[
(.*?) # link title (non greedy) (.*?) # link title (non greedy)
\] \]
\( \(
(.*?) # link url (non greedy) (.*?) # link url (non greedy)
\) \)
/x'; /x';
preg_match_all($re, $md, $matches); preg_match_all($re, $md, $matches);
// relative path (relative to module base folder), without the filename. // relative path (relative to module base folder), without the filename.
// For "sapphire/en/current/topics/templates", this would be "templates" // For "sapphire/en/current/topics/templates", this would be "templates"
$relativePath = DocumentationHelper::normalizePath(dirname($page->getRelativePath())); $relativePath = DocumentationHelper::normalizePath(dirname($page->getRelativePath()));
@ -522,36 +522,4 @@ class DocumentationParser
return $md; return $md;
} }
/**
* Strips out the metadata for a page
*
* @param DocumentationPage
*/
public static function retrieve_meta_data(DocumentationPage $page)
{
$md = $page->getMarkdown();
if ($md) {
// get the text up to the first empty line
$extPattern = "/^(.+)\n\r*\n/Uis";
$matches = preg_match($extPattern, $md, $block);
if ($matches && $block[1]) {
// find the key/value pairs
$lines = preg_split('/\v+/', $block[1]);
$key = '';
$value = '';
foreach ($lines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(':', $line, 2);
$key = trim($key);
$value = trim($value);
} else {
$value .= ' ' . trim($line);
}
$page->setMetaData($key, $value);
}
}
}
}
} }

View File

@ -172,13 +172,6 @@ class DocumentationPage extends ViewableData
return false; return false;
} }
public function setMetaData($key, $value)
{
$key = strtolower($key);
$this->$key = $value;
}
/** /**
* @return string * @return string
*/ */
@ -280,37 +273,66 @@ class DocumentationPage extends ViewableData
*/ */
public function populateMetaDataFromText(&$md, $removeMetaData = false) public function populateMetaDataFromText(&$md, $removeMetaData = false)
{ {
if ($md) { if (!$md) {
// get the text up to the first empty line return;
$extPattern = "/^(.+)\n\r*\n/Uis"; }
$matches = preg_match($extPattern, $md, $block);
if ($matches && $block[1]) { // See if there is YAML metadata block at the top of the document. e.g.
$metaDataFound = false; // ---
// property: value
// find the key/value pairs // another: value
$lines = preg_split('/\v+/', $block[1]); // ---
$key = ''; //
$value = ''; // If we found one, then we'll use a YAML parser to extract the
foreach ($lines as $line) { // data out and then remove the whole block from the markdown string.
if (strpos($line, ':') !== false) { $parser = new \Mni\FrontYAML\Parser();
list($key, $value) = explode(':', $line, 2); $document = $parser->parse($md, false);
$key = trim($key); $yaml = $document->getYAML();
$value = trim($value); if ($yaml) {
} else { foreach ($yaml as $key => $value) {
$value .= ' ' . trim($line); if (!property_exists(get_class($this), $key)) {
} continue;
if (property_exists(get_class(), $key)) {
$this->$key = $value;
$metaDataFound = true;
}
} }
$this->$key = $value;
}
if ($removeMetaData) {
$md = $document->getContent();
}
return;
}
// optionally remove the metadata block (only on the page that // this is the alternative way of parsing the properties out that don't contain
// is displayed) // a YAML block declared with ---
if ($metaDataFound && $removeMetaData) { //
$md = preg_replace($extPattern, '', $md); // get the text up to the first empty line
$extPattern = "/^(.+)\n\r*\n/Uis";
$matches = preg_match($extPattern, $md, $block);
if ($matches && $block[1]) {
$metaDataFound = false;
// find the key/value pairs
$lines = preg_split('/\v+/', $block[1]);
$key = '';
$value = '';
foreach ($lines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(':', $line, 2);
$key = trim($key);
$value = trim($value);
} else {
$value .= ' ' . trim($line);
} }
if (property_exists(get_class(), $key)) {
$this->$key = $value;
$metaDataFound = true;
}
}
// optionally remove the metadata block (only on the page that
// is displayed)
if ($metaDataFound && $removeMetaData) {
$md = preg_replace($extPattern, '', $md);
} }
} }
} }
@ -328,3 +350,4 @@ class DocumentationPage extends ViewableData
return sprintf(get_class($this) .': %s)', $this->getPath()); return sprintf(get_class($this) .': %s)', $this->getPath());
} }
} }

View File

@ -1,24 +1,25 @@
{ {
"name": "silverstripe/docsviewer", "name": "silverstripe/docsviewer",
"description": "Documentation viewer module for SilverStripe", "description": "Documentation viewer module for SilverStripe",
"type": "silverstripe-module", "type": "silverstripe-module",
"keywords": ["silverstripe", "documentation"], "keywords": ["silverstripe", "documentation"],
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"authors": [{ "authors": [{
"name": "Will Rossiter", "name": "Will Rossiter",
"homepage": "http://wilr.github.io", "homepage": "http://wilr.github.io",
"email": "will@fullscreen.io" "email": "will@fullscreen.io"
}], }],
"support": { "support": {
"email": "will@fullscreen.io", "email": "will@fullscreen.io",
"irc": "irc://irc.freenode.org/silverstripe" "irc": "irc://irc.freenode.org/silverstripe"
}, },
"require": { "require": {
"silverstripe/framework": "~3.1", "silverstripe/framework": "~3.1",
"erusev/parsedown-extra": "0.2.2", "erusev/parsedown-extra": "0.2.2",
"erusev/parsedown": "~1.1.0" "erusev/parsedown": "~1.1.0",
}, "mnapoli/front-yaml": "^1.5"
"suggest": { },
"silverstripe/staticpublisher": "Allows publishing documentation as HTML" "suggest": {
} "silverstripe/staticpublisher": "Allows publishing documentation as HTML"
}
} }

View File

@ -11,7 +11,7 @@ class DocumentationParserTest extends SapphireTest
public function tearDown() public function tearDown()
{ {
parent::tearDown(); parent::tearDown();
Config::unnest(); Config::unnest();
} }
@ -21,7 +21,7 @@ class DocumentationParserTest extends SapphireTest
Config::nest(); Config::nest();
// explicitly use dev/docs. Custom paths should be tested separately // explicitly use dev/docs. Custom paths should be tested separately
Config::inst()->update( Config::inst()->update(
'DocumentationViewer', 'link_base', 'dev/docs/' 'DocumentationViewer', 'link_base', 'dev/docs/'
); );
@ -48,7 +48,7 @@ class DocumentationParserTest extends SapphireTest
'subpage.md', 'subpage.md',
DOCSVIEWER_PATH. '/tests/docs/en/subfolder/subpage.md' DOCSVIEWER_PATH. '/tests/docs/en/subfolder/subpage.md'
); );
$this->subSubPage = new DocumentationPage( $this->subSubPage = new DocumentationPage(
$this->entity, $this->entity,
'subsubpage.md', 'subsubpage.md',
@ -128,13 +128,12 @@ with multiple
lines lines
and tab indent and tab indent
and escaped < brackets and escaped < brackets
``` ```
Normal text after code block Normal text after code block
HTML; HTML;
$this->assertContains($expected, $result, 'Custom code blocks with ::: prefix'); $this->assertContains($expected, $result, 'Custom code blocks with ::: prefix');
$expected = <<<HTML $expected = <<<HTML
``` ```
code block code block
@ -149,7 +148,7 @@ Fenced code block
``` ```
HTML; HTML;
$this->assertContains($expected, $result, 'Backtick code blocks'); $this->assertContains($expected, $result, 'Backtick code blocks');
$expected = <<<HTML $expected = <<<HTML
```php ```php
Fenced box with Fenced box with
@ -171,7 +170,7 @@ HTML;
$this->indexPage->getMarkdown(), $this->indexPage->getMarkdown(),
$this->indexPage $this->indexPage
); );
$this->assertContains( $this->assertContains(
'[link: subfolder index](dev/docs/en/documentationparsertest/2.4/subfolder/)', '[link: subfolder index](dev/docs/en/documentationparsertest/2.4/subfolder/)',
$result $result
@ -183,7 +182,7 @@ HTML;
$this->page->getMarkdown(), $this->page->getMarkdown(),
$this->page $this->page
); );
$this->assertContains( $this->assertContains(
'[link: subfolder index](dev/docs/en/documentationparsertest/2.4/subfolder/)', '[link: subfolder index](dev/docs/en/documentationparsertest/2.4/subfolder/)',
$result $result
@ -217,7 +216,7 @@ HTML;
'[link: relative](dev/docs/en/documentationparsertest/2.4/subfolder/subpage.md/)', '[link: relative](dev/docs/en/documentationparsertest/2.4/subfolder/subpage.md/)',
$result $result
); );
$this->assertContains( $this->assertContains(
'[link: absolute index](dev/docs/en/documentationparsertest/2.4/)', '[link: absolute index](dev/docs/en/documentationparsertest/2.4/)',
$result $result
@ -233,22 +232,22 @@ HTML;
'[link: relative index](dev/docs/en/documentationparsertest/2.4/)', '[link: relative index](dev/docs/en/documentationparsertest/2.4/)',
$result $result
); );
$this->assertContains( $this->assertContains(
'[link: relative parent page](dev/docs/en/documentationparsertest/2.4/test/)', '[link: relative parent page](dev/docs/en/documentationparsertest/2.4/test/)',
$result $result
); );
$this->assertContains( $this->assertContains(
'[link: absolute parent page](dev/docs/en/documentationparsertest/2.4/test/)', '[link: absolute parent page](dev/docs/en/documentationparsertest/2.4/test/)',
$result $result
); );
$result = DocumentationParser::rewrite_relative_links( $result = DocumentationParser::rewrite_relative_links(
$this->subSubPage->getMarkdown(), $this->subSubPage->getMarkdown(),
$this->subSubPage $this->subSubPage
); );
$this->assertContains( $this->assertContains(
'[link: absolute index](dev/docs/en/documentationparsertest/2.4/)', '[link: absolute index](dev/docs/en/documentationparsertest/2.4/)',
$result $result
@ -286,7 +285,7 @@ HTML;
} }
public function testImageRewrites() public function testImageRewrites()
{ {
$result = DocumentationParser::rewrite_image_links( $result = DocumentationParser::rewrite_image_links(
@ -311,7 +310,7 @@ HTML;
), ),
$result $result
); );
$expected = Controller::join_links( $expected = Controller::join_links(
Director::absoluteBaseURL(), DOCSVIEWER_DIR, '/tests/docs/en/_images/image.png' Director::absoluteBaseURL(), DOCSVIEWER_DIR, '/tests/docs/en/_images/image.png'
); );
@ -321,7 +320,7 @@ HTML;
$result $result
); );
} }
public function testApiLinks() public function testApiLinks()
{ {
@ -357,14 +356,14 @@ HTML;
} }
} }
public function testHeadlineAnchors() public function testHeadlineAnchors()
{ {
$result = DocumentationParser::rewrite_heading_anchors( $result = DocumentationParser::rewrite_heading_anchors(
$this->page->getMarkdown(), $this->page->getMarkdown(),
$this->page $this->page
); );
/* /*
# Heading one {#Heading-one} # Heading one {#Heading-one}
@ -379,7 +378,7 @@ HTML;
## Heading duplicate {#Heading-duplicate-2} ## Heading duplicate {#Heading-duplicate-2}
## Heading duplicate {#Heading-duplicate-3} ## Heading duplicate {#Heading-duplicate-3}
*/ */
$this->assertContains('# Heading one {#heading-one}', $result); $this->assertContains('# Heading one {#heading-one}', $result);
@ -395,14 +394,41 @@ HTML;
public function testRetrieveMetaData() public function testRetrieveMetaData()
{ {
DocumentationParser::retrieve_meta_data($this->metaDataPage); $this->metaDataPage->getMarkdown(true);
$this->assertEquals('Foo Bar\'s Test page.', $this->metaDataPage->getTitle());
$this->assertEquals('Dr. Foo Bar.', $this->metaDataPage->author); $this->assertEquals('A long intro that splits over many lines', $this->metaDataPage->getIntroduction());
$this->assertEquals("Foo Bar's Test page.", $this->metaDataPage->getTitle()); $this->assertEquals('Foo Bar Test page description', $this->metaDataPage->getSummary());
$this->assertEquals("Foo Bar's Test page.", $this->metaDataPage->getTitle());
$this->assertEquals("A long intro that splits over many lines", $this->metaDataPage->getIntroduction()); $parsed = DocumentationParser::parse($this->metaDataPage);
$expected = <<<HTML
<h1 id="content">Content</h1>
HTML;
$this->assertEquals($parsed, $expected, 'Metadata block removed, parsed correctly');
} }
public function testRetrieveMetaDataYamlBlock()
{
$page = new DocumentationPage(
$this->entityAlt,
'MetaDataYamlBlockTest.md',
DOCSVIEWER_PATH . '/tests/docs-parser/en/MetaDataYamlBlockTest.md'
);
$page->getMarkdown(true);
$this->assertEquals('Foo Bar\'s Test page.', $page->getTitle());
$this->assertEquals('This is the page\'s description', $page->getSummary());
$parsed = DocumentationParser::parse($page);
$expected = <<<HTML
<h2 id="content-2">Content</h2>
<p>Content goes here.</p>
<hr />
<h2>randomblock: ignored</h2>
HTML;
$this->assertEquals($parsed, $expected, 'YAML metadata block removed, parsed correctly');
}
public function testRewritingRelativeLinksToFiles() public function testRewritingRelativeLinksToFiles()
{ {
$parsed = DocumentationParser::parse($this->filePage); $parsed = DocumentationParser::parse($this->filePage);
@ -411,7 +437,7 @@ HTML;
DOCSVIEWER_DIR .'/tests/docs-parser/en/_images/external_link.png', DOCSVIEWER_DIR .'/tests/docs-parser/en/_images/external_link.png',
$parsed $parsed
); );
$this->assertContains( $this->assertContains(
DOCSVIEWER_DIR .'/tests/docs-parser/en/_images/test.tar.gz', DOCSVIEWER_DIR .'/tests/docs-parser/en/_images/test.tar.gz',
$parsed $parsed

View File

@ -1,7 +1,6 @@
Title: Foo Bar's Test page. title: Foo Bar's Test page.
Author: Dr. Foo Bar. summary: Foo Bar Test page description
Another: Test. introduction: A long intro that
Introduction: A long intro that
splits over splits over
many lines many lines

View File

@ -0,0 +1,13 @@
---
title: "Foo Bar's Test page."
summary: "This is the page's description"
---
## Content
Content goes here.
---
randomblock: ignored
---

View File

@ -29,7 +29,6 @@ test
lines lines
and tab indent and tab indent
and escaped < brackets and escaped < brackets
Normal text after code block Normal text after code block