* array( * 'en/someniceurl/' => array( * 'filepath' => '/path/to/docs/en/SomeniceFile.md', * 'title' => 'Some nice URL', * 'summary' => 'Summary Text', * 'basename' => 'SomeniceFile.md', * 'type' => 'DocumentationPage' * ) * ) * * * URL format is in the following structures: * * {lang}/{path} * {lang}/{module}/{path} * {lang}/{module}/{version}/{/path} * * @package framework * @subpackage manifest */ class DocumentationManifest { /** * @config * * @var boolean $automatic_registration */ private static $automatic_registration = true; /** * @config * * @var array $registered_entities */ private static $register_entities = array(); protected $cache; protected $cacheKey; protected $inited; protected $forceRegen; /** * @var array $pages */ protected $pages = array(); /** * @var DocumentationEntity */ private $entity; /** * @var ArrayList */ private $registeredEntities; /** * Constructs a new template manifest. The manifest is not actually built * or loaded from cache until needed. * * @param bool $includeTests Include tests in the manifest. * @param bool $forceRegen Force the manifest to be regenerated. */ public function __construct($forceRegen = false) { $this->cacheKey = 'manifest'; $this->forceRegen = $forceRegen; $this->registeredEntities = new ArrayList(); $this->cache = SS_Cache::factory('DocumentationManifest', 'Core', array( 'automatic_serialization' => true, 'lifetime' => null )); $this->setupEntities(); } /** * Sets up the top level entities. * * Either manually registered through the YAML syntax or automatically * loaded through investigating the file system for `docs` folder. */ public function setupEntities() { if(Config::inst()->get('DocumentationManifest', 'automatic_registration')) { $this->populateEntitiesFromInstall(); } $registered = Config::inst()->get('DocumentationManifest', 'register_entities'); foreach($registered as $details) { // validate the details provided through the YAML configuration $required = array('Path', 'Title'); foreach($required as $require) { if(!isset($details[$require])) { throw new Exception("$require is a required key in DocumentationManifest.register_entities"); } } // if path is not an absolute value then assume it is relative from // the BASE_PATH. $path = $this->getRealPath($details['Path']); $key = (isset($details['Key'])) ? $details['Key'] : $details['Title']; if(!is_dir($path)) { throw new Exception($path . ' is not a valid documentation directory'); } $version = (isset($details['Version'])) ? $details['Version'] : ''; $langs = scandir($path); if($langs) { $possible = i18n::get_common_languages(true); foreach($langs as $k => $lang) { if(isset($possible[$lang])) { $entity = Injector::inst()->create( 'DocumentationEntity', $key ); $entity->setPath(Controller::join_links($path, $lang, '/')); $entity->setTitle($details['Title']); $entity->setLanguage($lang); $entity->setVersion($version); if(isset($details['Stable'])) { $entity->setIsStable($details['Stable']); } if(isset($details['DefaultEntity'])) { $entity->setIsDefaultEntity($details['DefaultEntity']); } $this->registeredEntities->push($entity); } } } } } public function getRealPath($path) { if(substr($path, 0, 1) != '/') { $path = realpath(Controller::join_links(BASE_PATH, $path)); } return $path; } /** * @return ArrayList */ public function getEntities() { return $this->registeredEntities; } /** * Scans the current installation and picks up all the SilverStripe modules * that contain a `docs` folder. * * @return void */ public function populateEntitiesFromInstall() { $entities = array(); foreach(scandir(BASE_PATH) as $key => $entity) { if($key == "themes") { continue; } $dir = is_dir(Controller::join_links(BASE_PATH, $entity)); if($dir) { // check to see if it has docs $docs = Controller::join_links($dir, 'docs'); if(is_dir($docs)) { $entities[] = array( 'BasePath' => $entity, 'Folder' => $key, 'Version' => 'master', 'Stable' => true ); } } } Config::inst()->update( 'DocumentationManifest', 'registered_entities', $entities ); } /** * */ protected function init() { if (!$this->forceRegen && $data = $this->cache->load($this->cacheKey)) { $this->pages = $data; $this->inited = true; } else { $this->regenerate(); } } /** * Returns a map of all documentation pages. * * @return array */ public function getPages() { if (!$this->inited) { $this->init(); } return $this->pages; } /** * Returns a particular page for the requested URL. * * @return DocumentationPage */ public function getPage($url) { $pages = $this->getPages(); $url = $this->normalizeUrl($url); if(!isset($pages[$url])) { return null; } $record = $pages[$url]; foreach($this->getEntities() as $entity) { if(strpos($record['filepath'], $entity->getPath()) !== false) { $page = Injector::inst()->create( $record['type'], $entity, $record['basename'], $record['filepath'] ); return $page; } } } /** * Regenerates the manifest by scanning the base path. * * @param bool $cache */ public function regenerate($cache = true) { $finder = new DocumentationManifestFileFinder(); $finder->setOptions(array( 'dir_callback' => array($this, 'handleFolder'), 'file_callback' => array($this, 'handleFile') )); foreach($this->getEntities() as $entity) { $this->entity = $entity; $this->handleFolder('', $this->entity->getPath(), 0); $finder->find($this->entity->getPath()); } // groupds $grouped = array(); foreach($this->pages as $url => $page) { if(!isset($grouped[$page['entitypath']])) { $grouped[$page['entitypath']] = array(); } $grouped[$page['entitypath']][$url] = $page; } $this->pages = array(); foreach($grouped as $entity) { uasort($entity, function($a, $b) { // ensure parent directories are first $a['filepath'] = str_replace('index.md', '', $a['filepath']); $b['filepath'] = str_replace('index.md', '', $b['filepath']); if(strpos($b['filepath'], $a['filepath']) === 0) { return -1; } if ($a['filepath'] == $b['filepath']) { return 0; } return ($a['filepath'] < $b['filepath']) ? -1 : 1; }); $this->pages = array_merge($this->pages, $entity); } if ($cache) { $this->cache->save($this->pages, $this->cacheKey); } $this->inited = true; } /** * */ public function handleFolder($basename, $path, $depth) { $folder = Injector::inst()->create( 'DocumentationFolder', $this->entity, $basename, $path ); $link = ltrim(str_replace( Config::inst()->get('DocumentationViewer', 'link_base'), '', $folder->Link() ), '/'); $this->pages[$link] = array( 'title' => $folder->getTitle(), 'basename' => $basename, 'filepath' => $path, 'type' => 'DocumentationFolder', 'entitypath' => $this->entity->getPath(), 'summary' => '' ); } /** * Individual files can optionally provide a nice title and a better URL * through the use of markdown meta data. This creates a new * {@link DocumentationPage} instance for the file. * * If the markdown does not specify the title in the meta data it falls back * to using the file name. * * @param string $basename * @param string $path * @param int $depth */ public function handleFile($basename, $path, $depth) { $page = Injector::inst()->create( 'DocumentationPage', $this->entity, $basename, $path ); // populate any meta data $page->getMarkdown(); $link = ltrim(str_replace( Config::inst()->get('DocumentationViewer', 'link_base'), '', $page->Link() ), '/'); $this->pages[$link] = array( 'title' => $page->getTitle(), 'filepath' => $path, 'entitypath' => $this->entity->getPath(), 'basename' => $basename, 'type' => 'DocumentationPage', 'summary' => $page->getSummary() ); } /** * Generate an {@link ArrayList} of the pages to the given page. * * @param DocumentationPage * @param DocumentationEntityLanguage * * @return ArrayList */ public function generateBreadcrumbs($record, $base) { $output = new ArrayList(); $parts = explode('/', trim($record->getRelativeLink(), '/')); // Add the base link. $output->push(new ArrayData(array( 'Link' => $base->Link(), 'Title' => $base->Title ))); $progress = $base->Link(); foreach($parts as $part) { if($part) { $progress = Controller::join_links($progress, $part, '/'); $output->push(new ArrayData(array( 'Link' => $progress, 'Title' => DocumentationHelper::clean_page_name($part) ))); } } return $output; } /** * Determine the next page from the given page. * * Relies on the fact when the manifest was built, it was generated in * order. * * @param string $filepath * @param string $entityBase * * @return ArrayData */ public function getNextPage($filepath, $entityBase) { $grabNext = false; $fallback = null; foreach($this->getPages() as $url => $page) { if($grabNext && strpos($page['filepath'], $entityBase) !== false) { return new ArrayData(array( 'Link' => $url, 'Title' => $page['title'] )); } if($filepath == $page['filepath']) { $grabNext = true; } else if(!$fallback && strpos($page['filepath'], $filepath) !== false) { $fallback = new ArrayData(array( 'Link' => $url, 'Title' => $page['title'], 'Fallback' => true )); } } if(!$grabNext) { return $fallback; } return null; } /** * Determine the previous page from the given page. * * Relies on the fact when the manifest was built, it was generated in * order. * * @param string $filepath * @param string $entityBase * * @return ArrayData */ public function getPreviousPage($filepath, $entityPath) { $previousUrl = $previousPage = null; foreach($this->getPages() as $url => $page) { if($filepath == $page['filepath']) { if($previousUrl) { return new ArrayData(array( 'Link' => $previousUrl, 'Title' => $previousPage['title'] )); } } if(strpos($page['filepath'], $entityPath) !== false) { $previousUrl = $url; $previousPage = $page; } } return null; } /** * @param string * * @return string */ public function normalizeUrl($url) { $url = trim($url, '/') .'/'; // if the page is the index page then hide it from the menu if(strpos(strtolower($url), '/index.md/')) { $url = substr($url, 0, strpos($url, "index.md/")); } return $url; } /** * Return the children of the provided record path. * * Looks for any pages in the manifest which have one more slash attached. * * @param string $path * * @return ArrayList */ public function getChildrenFor($entityPath, $recordPath = null) { if(!$recordPath) { $recordPath = $entityPath; } $output = new ArrayList(); $base = Config::inst()->get('DocumentationViewer', 'link_base'); $entityPath = $this->normalizeUrl($entityPath); $recordPath = $this->normalizeUrl($recordPath); $depth = substr_count($entityPath, '/'); foreach($this->getPages() as $url => $page) { $pagePath = $this->normalizeUrl($page['filepath']); // check to see if this page is under the given path if(strpos($pagePath, $entityPath) === false) { continue; } // only pull it up if it's one more level depth if(substr_count($pagePath, DIRECTORY_SEPARATOR) == ($depth + 1)) { $mode = (strpos($recordPath, $pagePath) !== false) ? 'current' : 'link'; $children = new ArrayList(); if($mode == 'current') { $children = $this->getChildrenFor($pagePath, $recordPath); } $output->push(new ArrayData(array( 'Link' => Controller::join_links($base, $url, '/'), 'Title' => $page['title'], 'LinkingMode' => $mode, 'Summary' => $page['summary'], 'Children' => $children ))); } } return $output; } /** * @param DocumentationEntity * * @return ArrayList */ public function getAllVersionsOfEntity(DocumentationEntity $entity) { $all = new ArrayList(); foreach($this->getEntities() as $check) { if($check->getKey() == $entity->getKey()) { if($check->getLanguage() == $entity->getLanguage()) { $all->push($check); } } } return $all; } /** * @param DocumentationEntity * * @return DocumentationEntity */ public function getStableVersion(DocumentationEntity $entity) { foreach($this->getEntities() as $check) { if($check->getKey() == $entity->getKey()) { if($check->getLanguage() == $entity->getLanguage()) { if($check->getIsStable()) { return $check; } } } } return $entity; } /** * @param DocumentationEntity * * @return ArrayList */ public function getVersions($entity) { if(!$entity) { return null; } $output = new ArrayList(); foreach($this->getEntities() as $check) { if($check->getKey() == $entity->getKey()) { if($check->getLanguage() == $entity->getLanguage()) { $same = ($check->getVersion() == $entity->getVersion()); $output->push(new ArrayList(array( 'Title' => $entity->getTitle(), 'Link' => $entity->getLink(), 'LinkingMode' => ($same) ? 'current' : 'link' ))); } } } return $output; } /** * Returns a sorted array of all the unique versions registered */ public function getAllVersions() { $versions = array(); foreach($this->getEntities() as $entity) { if($entity->getVersion()) { $versions[$entity->getVersion()] = $entity->getVersion(); } else { $versions['0.0'] = _t('DocumentationManifest.MASTER', 'Master'); } } asort($versions); return $versions; } }