FEATURE: initial cut of Zend_Lucene_Search backend for documentation search.

This commit is contained in:
Will Rossiter 2010-12-21 09:42:44 +00:00
parent 30b5134b80
commit 3d90271b31
16 changed files with 724 additions and 441 deletions

View File

@ -23,22 +23,22 @@ class DocumentationEntity extends ViewableData {
);
/**
* @var String $module folder name
* @var string $module folder name
*/
private $moduleFolder;
/**
* @var String $title nice title
* @var string $title nice title
*/
private $title;
/**
* @var Array $version version numbers and the paths to each
* @var array $version version numbers and the paths to each
*/
private $versions = array();
/**
* @var Array
* @var array
*/
private $currentVersion;
@ -51,13 +51,14 @@ class DocumentationEntity extends ViewableData {
* Constructor. You do not need to pass the langs to this as
* it will work out the languages from the filesystem
*
* @param String $module name of module
* @param String $version version of this module
* @param String $path Absolute path to this module (excluding language folders)
* @param string $module name of module
* @param string $version version of this module
* @param string $path Absolute path to this module (excluding language folders)
* @param string $title
*/
function __construct($module, $version = '', $path, $title = false) {
function __construct($module, $version, $path, $title = false) {
$this->addVersion($version, $path);
$this->title = (!$title) ? $this->module : $title;
$this->title = (!$title) ? $module : $title;
$this->moduleFolder = $module;
}
@ -224,6 +225,9 @@ class DocumentationEntity extends ViewableData {
return rtrim($path, '/') . '/' . rtrim($lang, '/') .'/';
}
/**
* @return string
*/
function __toString() {
return sprintf('DocumentationEntity: %s)', $this->getPath());
}

View File

@ -1,9 +1,8 @@
<?php
/**
* A specific page within a {@link DocumentationEntity}.
* Has to represent an actual file, please use {@link DocumentationViewer}
* to generate "virtual" index views.
* A specific page within a {@link DocumentationEntity}. Maps 1 to 1 to a file on the
* filesystem.
*
* @package sapphiredocs
*/
@ -18,17 +17,24 @@ class DocumentationPage extends ViewableData {
* @var String
*/
protected $relativePath;
protected $fullPath; // needed for the search
/**
* @var String
*/
protected $lang = 'en';
/**
* @var string
*/
protected $title;
/**
* @var String
*/
protected $version;
/**
* @return DocumentationEntity
*/
@ -36,18 +42,23 @@ class DocumentationPage extends ViewableData {
return $this->entity;
}
/**
* @param DocumentationEntity
*/
function setEntity($entity) {
$this->entity = $entity;
}
/**
* @return String Relative path to file or folder within the entity (including file extension),
* but excluding version or language folders.
* @return string
*/
function getRelativePath() {
return $this->relativePath;
}
/**
* @param string
*/
function setRelativePath($path) {
$this->relativePath = $path;
}
@ -55,21 +66,60 @@ class DocumentationPage extends ViewableData {
/**
* Absolute path including version and lang folder.
*
* @return String
* @throws InvalidArgumentException
*
* @return string
*/
function getPath() {
$path = realpath(rtrim($this->entity->getPath($this->version, $this->lang), '/') . '/' . trim($this->getRelativePath(), '/'));
if($this->fullPath) {
return $this->fullPath;
}
elseif($this->entity) {
$path = realpath(rtrim($this->entity->getPath($this->version, $this->lang), '/') . '/' . trim($this->getRelativePath(), '/'));
if(!file_exists($path)) {
throw new InvalidArgumentException(sprintf(
'Path could not be found. Module path: %s, file path: %s',
$this->entity->getPath(),
$this->relativePath
));
if(!file_exists($path)) {
throw new InvalidArgumentException(sprintf(
'Path could not be found. Module path: %s, file path: %s',
$this->entity->getPath(),
$this->relativePath
));
}
}
else {
$path = $this->relativePath;
}
return $path;
}
/**
* Absolute path including version and lang to the file to read
* off the file system. In the case of a folder this is the index.md file
*
* @return string
*/
function getFilePath() {
$path = $this->getPath();
if(!is_dir($path)) return $path;
if($entity = $this->getEntity()) {
if($relative = $this->getRelativePath()) {
return DocumentationService::find_page($entity, explode($relative, '/'));
}
else {
$parts = str_replace($entity->getPath($this->version, $this->lang), '', $this->fullPath);
return DocumentationService::find_page($entity, explode($parts, '/'));
}
}
return rtrim($path, '/') . '/index.md';
}
function setFullPath($path) {
$this->fullPath = $path;
}
function getLang() {
return $this->lang;
@ -86,14 +136,22 @@ class DocumentationPage extends ViewableData {
function setVersion($version) {
$this->version = $version;
}
function setTitle($title) {
$this->title = $title;
}
function getTitle() {
return $this->title;
}
/**
* @return String
*/
function getMarkdown() {
try {
$path = $this->getPath();
$path = $this->getFilePath();
return file_get_contents($path);
}
catch(InvalidArgumentException $e) {}
@ -106,6 +164,7 @@ class DocumentationPage extends ViewableData {
* @return String
*/
function getHTML($baselink = null) {
return DocumentationParser::parse($this, $baselink);
// if this is not a directory then we can to parse the file
return DocumentationParser::parse($this->getFilePath(), $baselink);
}
}

View File

@ -1,15 +1,11 @@
<?php
/**
* Parser wrapping the Markdown Extra parser (see http://michelf.com/projects/php-markdown/extra/).
* As it parses files in context
*
* @todo Abstract code that doesn't require DocumentationPage context into standalone parser
* extending the Markdown_Extra class.
*
* Parser wrapping the Markdown Extra parser (see http://michelf.com/projects/php-markdown/extra/).
*
* @package sapphiredocs
*/
class DocumentationParser {
/**
* @var String Rewriting of api links in the format "[api:MyClass]" or "[api:MyClass::$my_property]".
*/
@ -35,19 +31,21 @@ class DocumentationParser {
* @return String
*/
public static function parse(DocumentationPage $page, $baselink = null) {
$md = $page->getMarkdown();
// Pre-processing
$md = self::rewrite_image_links($md, $page);
$md = self::rewrite_relative_links($md, $page, $baselink);
$md = self::rewrite_api_links($md, $page);
$md = self::rewrite_heading_anchors($md, $page);
// $md = self::rewrite_code_blocks($md, $page);
require_once('../sapphiredocs/thirdparty/markdown.php');
$html = Markdown($md);
if(!$page || (!$page instanceof DocumentationPage)) return false;
return $html;
$md = $page->getMarkdown();
// Pre-processing
$md = self::rewrite_image_links($md, $page);
$md = self::rewrite_relative_links($md, $page, $baselink);
$md = self::rewrite_api_links($md, $page);
$md = self::rewrite_heading_anchors($md, $page);
// $md = self::rewrite_code_blocks($md, $page);
require_once('../sapphiredocs/thirdparty/markdown.php');
$html = Markdown($md);
return $html;
}
/*
@ -157,15 +155,17 @@ class DocumentationParser {
`?
/x';
preg_match_all($re, $md, $linksWithTitles);
if($linksWithTitles) foreach($linksWithTitles[0] as $i => $match) {
$title = $linksWithTitles[1][$i];
$subject = $linksWithTitles[2][$i];
$url = sprintf(self::$api_link_base, $subject, $page->getVersion(), $page->getEntity()->getModuleFolder());
$md = str_replace(
$match,
sprintf('<code>[%s](%s)</code>', $title, $url),
$md
);
if($linksWithTitles) {
foreach($linksWithTitles[0] as $i => $match) {
$title = $linksWithTitles[1][$i];
$subject = $linksWithTitles[2][$i];
$url = sprintf(self::$api_link_base, $subject, $page->getVersion(), $page->getEntity()->getModuleFolder());
$md = str_replace(
$match,
sprintf('<code>[%s](%s)</code>', $title, $url),
$md
);
}
}
// Bare links
@ -177,14 +177,16 @@ class DocumentationParser {
`?
/x';
preg_match_all($re, $md, $links);
if($links) foreach($links[0] as $i => $match) {
$subject = $links[1][$i];
$url = sprintf(self::$api_link_base, $subject, $page->getVersion(), $page->getEntity()->getModuleFolder());
$md = str_replace(
$match,
sprintf('<code>[%s](%s)</code>', $subject, $url),
$md
);
if($links) {
foreach($links[0] as $i => $match) {
$subject = $links[1][$i];
$url = sprintf(self::$api_link_base, $subject, $page->getVersion(), $page->getEntity()->getModuleFolder());
$md = str_replace(
$match,
sprintf('<code>[%s](%s)</code>', $subject, $url),
$md
);
}
}
return $md;
@ -259,187 +261,42 @@ class DocumentationParser {
$relativePath = dirname($page->getRelativePath());
if($relativePath == '.') $relativePath = '';
if($matches) foreach($matches[0] as $i => $match) {
$title = $matches[2][$i];
$url = $matches[3][$i];
if($matches) {
foreach($matches[0] as $i => $match) {
$title = $matches[2][$i];
$url = $matches[3][$i];
// Don't process API links
if(preg_match('/^api:/', $url)) continue;
// Don't process API links
if(preg_match('/^api:/', $url)) continue;
// Don't process absolute links (based on protocol detection)
$urlParts = parse_url($url);
if($urlParts && isset($urlParts['scheme'])) continue;
// Don't process absolute links (based on protocol detection)
$urlParts = parse_url($url);
if($urlParts && isset($urlParts['scheme'])) continue;
// Rewrite URL (relative or absolute)
if(preg_match('/^\//', $url)) {
$relativeUrl = $baselink . $url;
} else {
$relativeUrl = $baselink . '/' . $relativePath . '/' . $url;
// Rewrite URL (relative or absolute)
if(preg_match('/^\//', $url)) {
$relativeUrl = $baselink . $url;
} else {
$relativeUrl = $baselink . '/' . $relativePath . '/' . $url;
}
// Resolve relative paths
while(strpos($relativeUrl, '..') !== FALSE) {
$relativeUrl = preg_replace('/\w+\/\.\.\//', '', $relativeUrl);
}
// Replace any double slashes (apart from protocol)
$relativeUrl = preg_replace('/([^:])\/{2,}/', '$1/', $relativeUrl);
// Replace in original content
$md = str_replace(
$match,
sprintf('%s[%s](%s)', $matches[1][$i], $title, $relativeUrl),
$md
);
}
// Resolve relative paths
while(strpos($relativeUrl, '..') !== FALSE) {
$relativeUrl = preg_replace('/\w+\/\.\.\//', '', $relativeUrl);
}
// Replace any double slashes (apart from protocol)
$relativeUrl = preg_replace('/([^:])\/{2,}/', '$1/', $relativeUrl);
// Replace in original content
$md = str_replace(
$match,
sprintf('%s[%s](%s)', $matches[1][$i], $title, $relativeUrl),
$md
);
}
return $md;
}
/**
* Find a documentation page given a path and a file name. It ignores the extensions
* and simply compares the title.
*
* Name may also be a path /install/foo/bar.
*
* @param String $modulePath Absolute path to the entity
* @param Array $path path to the file in the entity
*
* @return String|false - File path
*/
static function find_page($modulePath, $path) {
return self::find_page_recursive($modulePath, $path);
}
/**
* Recursive function for finding the goal
*/
private static function find_page_recursive($base, $goal) {
$handle = opendir($base);
$name = strtolower(array_shift($goal));
if(!$name) $name = 'index';
if($handle) {
$extensions = DocumentationService::get_valid_extensions();
while (false !== ($file = readdir($handle))) {
if(in_array($file, DocumentationService::get_valid_extensions())) continue;
$formatted = strtolower($file);
// if the name has a . then take the substr
$formatted = ($pos = strrpos($formatted, '.')) ? substr($formatted, 0, $pos) : $formatted;
$name = ($dot = strrpos($name, '.')) ? substr($name, 0, $dot) : $name;
// the folder is the one that we are looking for.
if(strtolower($name) == strtolower($formatted)) {
if(is_dir($base . $file)) {
// if this is a directory check that there is any more states to get
// to in the goal. If none then what we want is the 'index.md' file
if(count($goal) > 0) {
return self::find_page_recursive($base . $file, $goal);
}
else {
// recurse but check for an index.md file next time around
return self::find_page_recursive($base . $file, array('index'));
}
}
else {
// goal state. End of recursion
$result = $base .'/'. $file;
return $result;
}
}
}
}
closedir($handle);
}
/**
* String helper for cleaning a file name to a readable version.
*
* @param String $name to convert
*
* @return String $name output
*/
public static function clean_page_name($name) {
// remove dashs and _
$name = str_replace(array('-', '_'), ' ', $name);
// remove extension
$hasExtension = strrpos($name, '.');
if($hasExtension !== false && $hasExtension > 0) {
$name = substr($name, 0, $hasExtension);
}
// convert first letter
return ucfirst(trim($name));
}
/**
* Return the children from a given module sorted by Title using natural ordering.
* It is used for building the tree of the page.
*
* @param String module name
* @param bool Recursive search
* @param DataObjectSet set of pages matched so far
*
* @throws Exception
* @return DataObjectSet
*/
public static function get_pages_from_folder($folder, $recursive = false, &$pages = false) {
$output = new DataObjectSet();
if(!$pages) $pages = new DataObjectSet();
if(!is_dir($folder)) throw new Exception(sprintf('%s is not a folder', $folder));
$handle = opendir($folder);
if($handle) {
$extensions = DocumentationService::get_valid_extensions();
$ignore = DocumentationService::get_ignored_files();
$files = array();
while (false !== ($file = readdir($handle))) {
if(!in_array($file, $ignore)) {
$file = trim(strtolower($file), '/');
$path = rtrim($folder, '/') . '/'. $file;
if($recursive && is_dir($path)) {
self::get_pages_from_folder($path, true, $pages);
}
else {
$files[] = $file;
}
}
}
natsort($files);
if($files) {
foreach($files as $file) {
$clean = ($pos = strrpos($file, '.')) ? substr($file, 0, $pos) : $file;
$path = rtrim($folder, '/') . '/'. $file;
$pages->push(new ArrayData(array(
'Title' => self::clean_page_name($file),
'Filename' => $clean,
'Path' => $path
)));
}
}
}
closedir($handle);
return $pages;
}
}

View File

@ -1,149 +1,198 @@
<?php
/**
* @todo caching?
* @package sapphiredocs
*/
class DocumentationSearch extends DocumentationViewer {
class DocumentationSearch {
private static $enabled = false;
private $results;
static $casting = array(
'Query' => 'Text'
);
private $totalResults;
static $allowed_actions = array('xml', 'search');
/**
* @var array Cached search results
*/
private $searchCache = array();
/**
* @var Int Page Length
*/
private $pageLength = 10;
/**
* Generates the XML tree for {@link Sphinx} XML Pipes
* Folder name for indexes (in the temp folder). You can override it using
* {@link DocumentationSearch::set_index_location($)}
*
* @uses DomDocument
* @var string
*/
function xml() {
DocumentationService::load_automatic_registration();
$dom = new DomDocument('1.0');
$dom->encoding = "utf-8";
$dom->formatOutput = true;
$root = $dom->appendChild($dom->createElementNS('http://sphinxsearch.com', 'sphinx:docset'));
$schema = $dom->createElement('sphinx:schema');
$field = $dom->createElement('sphinx:field');
$attr = $dom->createElement('sphinx:attr');
foreach(array('Title','Content', 'Language', 'Module', 'Path') as $field) {
$node = $dom->createElement('sphinx:field');
$node->setAttribute('name', strtolower($field));
$schema->appendChild($node);
}
$root->appendChild($schema);
// go through each documentation page and add it to index
$pages = $this->getAllDocumentationPages();
if($pages) {
foreach($pages as $doc) {
$node = $dom->createElement('sphinx:document');
$node->setAttribute('id', $doc->ID);
foreach($doc->getArray() as $key => $value) {
$key = strtolower($key);
if($key == 'id') continue;
$tmp = $dom->createElement($key);
$tmp->appendChild($dom->createTextNode($value));
$node->appendChild($tmp);
}
$root->appendChild($node);
}
}
return $dom->saveXML();
}
private static $index_location = 'sapphiredocs';
static $allowed_actions = array(
'buildindex'
);
/**
* Generate an array of every single documentation page installed on the system.
*
* @todo Add version support
*
* @return array
* @return DataObjectSet
*/
private function getAllDocumentationPages() {
static function get_all_documentation_pages() {
DocumentationService::load_automatic_registration();
$modules = DocumentationService::get_registered_modules();
$output = new DataObjectSet();
if($modules) {
foreach($modules as $module) {
foreach($module->getLanguages() as $language) {
try {
$pages = DocumentationParser::get_pages_from_folder($module->getPath(false, $language));
$pages = DocumentationService::get_pages_from_folder($module);
if($pages) {
foreach($pages as $page) {
$output->push(new ArrayData(array(
'Title' => $page->Title,
'Content' => file_get_contents($page->Path),
'Path' => $page->Path,
'Language' => $language,
'ID' => base_convert(substr(md5($page->Path), -8), 16, 10)
)));
$output->push($page);
}
}
}
catch(Exception $e) {}
catch(Exception $e) {
user_error($e, E_USER_WARNING);
}
}
}
}
return $output;
}
/**
* Enable searching documentation
*/
public static function enable() {
if(!class_exists('ZendSearchLuceneSearchable')) {
return user_error('DocumentationSearch requires the ZendSearchLucene library', E_ERROR);
}
self::$enabled = true;
ZendSearchLuceneSearchable::enable(array());
}
/**
* @return bool
*/
public static function enabled() {
return self::$enabled;
}
/**
* @param string
*/
public function set_index($index) {
self::$index_location = $index;
}
/**
* Takes a search from the URL, performs a sphinx search and displays a search results
* template.
*
* @todo Add additional language / version filtering
* @return string
*/
function search() {
$query = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : false;
$results = false;
$keywords = "";
public function get_index_location() {
return TEMP_FOLDER . '/'. trim(self::$index_location, '/');
}
/**
* Perform a search query on the index
*
* Rebuilds the index if it out of date
*/
public function performSearch($query) {
$this->buildindex();
$index = Zend_Search_Lucene::open(self::get_index_location());
if($query) {
$keywords = urldecode($query);
Zend_Search_Lucene::setResultSetLimit(200);
$results = $index->find($query);
$start = isset($_GET['start']) ? (int)$_GET['start'] : 0;
$cachekey = $query.':'.$start;
$this->results = new DataObjectSet();
$this->totalResults = $index->numDocs();
foreach($results as $result) {
$data = $result->getDocument();
if(!isset($this->searchCache[$cachekey])) {
$this->searchCache[$cachekey] = SphinxSearch::search('DocumentationPage', $keywords, array_merge_recursive(array(
'start' => $start,
'pagesize' => $this->pageLength
)));
}
$results = $this->searchCache[$cachekey];
$this->results->push(new ArrayData(array(
'Title' => DBField::create('Varchar', $data->Title),
'Link' => DBField::create('Varchar',$data->Path),
'Language' => DBField::create('Varchar',$data->Language),
'Version' => DBField::create('Varchar',$data->Version)
)));
}
}
/**
* @return DataObjectSet
*/
public function getResults($start) {
return $this->results;
}
/**
* @return int
*/
public function getTotalResults() {
return (int) $this->totalResults;
}
/**
* Builds the document index
*/
public function buildIndex() {
ini_set("memory_limit", -1);
ini_set('max_execution_time', 0);
return array(
'Query' => DBField::create('Text', $keywords),
'Results' => $results
);
// only rebuild the index if we have to. Check for either flush or the time write.lock.file
// was last altered
$lock = self::get_index_location() .'/write.lock.file';
$lockFileFresh = (file_exists($lock) && filemtime($lock) > (time() - (60 * 60 * 24)));
if($lockFileFresh && !isset($_REQUEST['flush'])) return true;
try {
$index = Zend_Search_Lucene::open(self::get_index_location());
$index->removeReference();
}
catch (Zend_Search_Lucene_Exception $e) {
}
try {
$index = Zend_Search_Lucene::create(self::get_index_location());
}
catch(Zend_Search_Lucene_Exception $c) {
user_error($c);
}
// includes registration
$pages = self::get_all_documentation_pages();
if($pages) {
$count = 0;
foreach($pages as $page) {
$count++;
if(!is_dir($page->getPath())) {
var_dump("Indexing ". $page->getPath());
$doc = Zend_Search_Lucene_Document_Html::loadHTML($page->getHtml());
$doc->addField(Zend_Search_Lucene_Field::Text('Title', $page->getTitle()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Version', $page->getVersion()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Language', $page->getLang()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Path', $page->getPath()));
$index->addDocument($doc);
}
else {
var_dump("Not Indexing ". $page->getPath());
}
}
}
$index->commit();
}
public function optimizeIndex() {
$index = Zend_Search_Lucene::open(self::get_index_location());
if($index) $index->optimize();
}
}

View File

@ -25,47 +25,19 @@ class DocumentationService {
'de' => 'German'
);
/**
* Files to ignore from any documentation listing.
*
* @var Array
* @var array
*/
private static $ignored_files = array('.', '..', '.DS_Store', '.svn', '.git', 'assets', 'themes', '_images');
/**
* Set the ignored files list
*
* @param Array
*/
public function set_ignored_files($files) {
self::$ignored_files = $files;
}
/**
* Return the list of files which are ignored
*
* @return Array
*/
public function get_ignored_files() {
return self::$ignored_files;
}
/**
* Case insenstive values to use as extensions on markdown pages.
*
* @var Array
* @var array
*/
public static $valid_markdown_extensions = array('.md', '.txt', '.markdown');
/**
* Return the allowed extensions
*
* @return Array
*/
public static function get_valid_extensions() {
return self::$valid_markdown_extensions;
}
/**
* Registered modules to include in the documentation. Either pre-filled by the
@ -74,7 +46,7 @@ class DocumentationService {
*
* You can remove registered modules using {@link DocumentationService::unregister()}
*
* @var Array
* @var array
*/
private static $registered_modules = array();
@ -83,19 +55,46 @@ class DocumentationService {
* the documentation but for sapphire/cms and overall we need to register major
* versions via {@link DocumentationService::register}
*
* @var Array
* @var array
*/
private static $major_versions = array();
/**
* Return the major versions
*
* @return Array
* @return array
*/
public static function get_major_versions() {
return self::$major_versions;
}
/**
* Return the allowed extensions
*
* @return array
*/
public static function get_valid_extensions() {
return self::$valid_markdown_extensions;
}
/**
* Set the ignored files list
*
* @param array
*/
public function set_ignored_files($files) {
self::$ignored_files = $files;
}
/**
* Return the list of files which are ignored
*
* @return array
*/
public function get_ignored_files() {
return self::$ignored_files;
}
/**
* Check to see if a given language is registered in the system
*
@ -181,6 +180,12 @@ class DocumentationService {
*/
public static function set_automatic_registration($bool = true) {
self::$automatic_registration = $bool;
if(!$bool) {
// remove current registed modules when disabling automatic registration
// needed to avoid caching issues when running all the tests
self::$registered_modules = array();
}
}
/**
@ -227,8 +232,11 @@ class DocumentationService {
* @return DocumentationEntity $module the registered module
*/
public static function is_registered_module($module, $version = false, $lang = false) {
if(isset(self::$registered_modules[$module])) {
$module = self::$registered_modules[$module];
$check = ($module instanceof DocumentationEntity) ? $module->getModuleFolder() : (string) $module;
if(isset(self::$registered_modules[$check])) {
$module = self::$registered_modules[$check];
if($lang && !$module->hasLanguage($lang)) return false;
if($version && !$module->hasVersion($version)) return false;
@ -250,7 +258,7 @@ class DocumentationService {
*/
public static function register($module, $path, $version = '', $title = false, $major = false) {
if(!file_exists($path)) throw new InvalidArgumentException(sprintf('Path "%s" doesn\'t exist', $path));
// add the module to the registered array
if(!isset(self::$registered_modules[$module])) {
// module is completely new
@ -315,7 +323,7 @@ class DocumentationService {
if($modules) {
foreach($modules as $key => $module) {
if(is_dir(BASE_PATH .'/'. $module) && !in_array($module, self::$ignored_files, true)) {
if(is_dir(BASE_PATH .'/'. $module) && !in_array($module, self::get_ignored_files(), true)) {
// check to see if it has docs
$docs = BASE_PATH .'/'. $module .'/docs/';
@ -337,4 +345,213 @@ class DocumentationService {
public static function get_language_title($lang) {
return (isset(self::$language_mapping[$lang])) ? _t("DOCUMENTATIONSERVICE.LANG-$lang", self::$language_mapping[$lang]) : $lang;
}
/**
* Find a documentation page given a path and a file name. It ignores the extensions
* and simply compares the title.
*
* Name may also be a path /install/foo/bar.
*
* @param DocumentationEntity
* @param array exploded url string
*
* @return String|false - File path
*/
static function find_page($module, $path) {
if($module = self::is_registered_module($module)) {
return self::find_page_recursive($module->getPath(), $path);
}
return false;
}
/**
* Recursive function for finding the goal of a path to a documentation
* page
*
* @return string
*/
private static function find_page_recursive($base, $goal) {
$handle = opendir($base);
$name = strtolower(array_shift($goal));
if(!$name || $name == '/') $name = 'index';
if($handle) {
$extensions = DocumentationService::get_valid_extensions();
$firstFile = false;
// ensure we end with a slash
$base = rtrim($base, '/') .'/';
while (false !== ($file = readdir($handle))) {
if(in_array($file, DocumentationService::get_valid_extensions())) continue;
if(!$firstFile && !is_dir($base . $file)) $firstFile = $file;
$formatted = strtolower($file);
// if the name has a . then take the substr
$formatted = ($pos = strrpos($formatted, '.')) ? substr($formatted, 0, $pos) : $formatted;
$name = ($dot = strrpos($name, '.')) ? substr($name, 0, $dot) : $name;
// the folder is the one that we are looking for.
if(strtolower($name) == strtolower($formatted)) {
// if this file is a directory we could be displaying that
// or simply moving towards the goal.
if(is_dir($base . $file)) {
$base = $base . trim($file, '/') .'/';
// if this is a directory check that there is any more states to get
// to in the goal. If none then what we want is the 'index.md' file
if(count($goal) > 0) {
return self::find_page_recursive($base, $goal);
}
else {
// recurse but check for an index.md file next time around
return self::find_page_recursive($base, array('index'));
}
}
else {
// goal state. End of recursion.
// tidy up the URLs with single trailing slashes
$result = $base . ltrim($file, '/');
if(is_dir($result)) $result = (rtrim($result, '/') . '/');
return $result;
}
}
}
}
closedir($handle);
// if goal has not been found and the index.md file does not exist then the next
// option is to pick the first file in the folder
return $base . ltrim($file, '/');
}
/**
* String helper for cleaning a file name to a readable version.
*
* @param String $name to convert
*
* @return String $name output
*/
public static function clean_page_name($name) {
// remove dashs and _
$name = str_replace(array('-', '_'), ' ', $name);
// remove extension
$name = self::trim_extension_off($name);
// convert first letter
return ucfirst(trim($name));
}
/**
* Helper function to strip the extension off
*
* @param string
*
* @return string
*/
public static function trim_extension_off($name) {
$hasExtension = strrpos($name, '.');
if($hasExtension !== false && $hasExtension > 0) {
$name = substr($name, 0, $hasExtension);
}
return $name;
}
/**
* Return the children from a given module sorted by Title using natural ordering.
* It is used for building the tree of the page.
*
* @param string|DocumentationEntity path
* @param bool enable several recursive calls (more than 1 level)
* @throws Exception
* @return DataObjectSet
*/
public static function get_pages_from_folder($module, $recursive = true) {
$output = new DataObjectSet();
$pages = array();
if($module instanceof DocumentationEntity) {
if(self::is_registered_module($module)) {
self::get_pages_from_folder_recursive($module->getPath(), $module, $recursive, $pages);
}
else {
return user_error("$module is not registered", E_USER_WARNING);
}
}
else {
self::get_pages_from_folder_recursive($module, false, $recursive, $pages);
}
if(count($pages) > 0) {
natsort($pages);
foreach($pages as $key => $path) {
// get file name from the path
$file = ($pos = strrpos($path, '/')) ? substr($path, $pos + 1) : $path;
// trim off the extension
$page = new DocumentationPage();
$page->setTitle(self::clean_page_name($file));
$page->setFullPath($path);
$page->Filename = self::trim_extension_off($file);
if($module instanceof DocumentationEntity) {
$page->setEntity($module);
}
$output->push($page);
}
}
return $output;
}
/**
* Recursively search through $folder
*/
private static function get_pages_from_folder_recursive($folder, $module = false, $recusive, &$pages) {
if(!is_dir($folder)) throw new Exception(sprintf('%s is not a folder', $folder));
$handle = opendir($folder);
if($handle) {
$extensions = self::get_valid_extensions();
$ignore = self::get_ignored_files();
$files = array();
while (false !== ($file = readdir($handle))) {
if(!in_array($file, $ignore)) {
$file = trim(strtolower($file), '/');
$path = rtrim($folder, '/') . '/'. $file;
if(is_dir($path)) {
$pages[] = $path;
if($recusive) self::get_pages_from_folder_recursive($path, $module, $recusive, $pages);
}
else if(in_array(substr($file, (strrpos($file, '.'))), $extensions)) {
$pages[] = $path;
}
}
}
}
closedir($handle);
}
}

View File

@ -8,26 +8,18 @@
*
* For more documentation on how to use this class see the documentation in /sapphiredocs/docs folder
*
* To view the documentation in the browser use:
*
* http://yoursite.com/dev/docs/ Which is locked to ADMIN only
*
* @todo - Add ability to have docs on the front end as the main site.
* - Fix Language Selector (enabling it troubles the handleRequest when submitting)
* - SS_HTTPRequest when we ask for 10 params it gives us 10. Could be 10 blank ones.
* It would mean I could save alot of code if it only gave back an array of size X
* up to a maximum of 10...
*
* @package sapphiredocs
*/
class DocumentationViewer extends Controller {
static $allowed_actions = array(
'home',
'LanguageForm',
'doLanguageForm',
'handleRequest',
'DocumentationSearchForm'
'DocumentationSearchForm',
'results'
);
/**
@ -104,7 +96,7 @@ class DocumentationViewer extends Controller {
public function handleRequest(SS_HTTPRequest $request) {
// if we submitted a form, let that pass
if(!$request->isGET()) return parent::handleRequest($request);
$firstParam = ($request->param('Action')) ? $request->param('Action') : $request->shift();
$secondParam = $request->shift();
$thirdParam = $request->shift();
@ -351,10 +343,11 @@ class DocumentationViewer extends Controller {
*/
function getPage() {
$module = $this->getModule();
if(!$module) return false;
$absFilepath = DocumentationParser::find_page($module->getPath(), $this->Remaining);
$absFilepath = DocumentationService::find_page($module, $this->Remaining);
if($absFilepath) {
$relativeFilePath = str_replace($module->getPath(), '', $absFilepath);
@ -365,9 +358,9 @@ class DocumentationViewer extends Controller {
$page->setVersion($this->Version);
return $page;
} else {
return false;
}
return false;
}
/**
@ -379,7 +372,7 @@ class DocumentationViewer extends Controller {
*/
function getModulePages() {
if($module = $this->getModule()) {
$pages = DocumentationParser::get_pages_from_folder($module->getPath());
$pages = DocumentationService::get_pages_from_folder($module, false);
if($pages) {
foreach($pages as $page) {
@ -425,7 +418,7 @@ class DocumentationViewer extends Controller {
$page->LinkingMode = (isset($this->Remaining[$level + 1])) ? 'section' : 'current';
if(is_dir($page->Path)) {
$children = DocumentationParser::get_pages_from_folder($page->Path);
$children = DocumentationService::get_pages_from_folder($page->Path, false);
$segments = array();
for($x = 0; $x <= $level; $x++) {
@ -495,7 +488,7 @@ class DocumentationViewer extends Controller {
if($i > 0) $path[] = $title;
$output->push(new ArrayData(array(
'Title' => DocumentationParser::clean_page_name($title),
'Title' => DocumentationService::clean_page_name($title),
'Link' => $this->Link($path)
)));
}
@ -605,24 +598,44 @@ class DocumentationViewer extends Controller {
* @return Form
*/
function DocumentationSearchForm() {
if(!DocumentationSearch::enabled()) return false;
$query = (isset($_REQUEST['Search'])) ? Convert::raw2xml($_REQUEST['Search']) : "";
$fields = new FieldSet(
new TextField('Search')
new TextField('Search', _t('DocumentationViewer.SEARCH', 'Search'), $query)
);
$actions = new FieldSet(
new FormAction('doDocumentationSearchForm', 'Search')
new FormAction('results', 'Search')
);
return new Form($this, 'DocumentationSearchForm', $fields, $actions);
$form = new Form($this, 'DocumentationSearchForm', $fields, $actions);
$form->disableSecurityToken();
$form->setFormAction('home/DocumentationSearchForm');
return $form;
}
/**
* Past straight to results, display and encode the query
*/
function doDocumentationSearchForm($data, $form) {
function results($data, $form) {
$query = (isset($data['Search'])) ? urlencode($data['Search']) : "";
$start = (isset($_GET['start'])) ? (int) $_GET['start'] : 0;
$this->redirect('DocumentationSearch/search/'. $query);
$search = new DocumentationSearch();
$search->performSearch($query);
$results = $search->getResults($start);
$total = $search->getTotalResults();
echo $this->customise(array(
'Results' => $results,
'Query' => DBField::create('HTMLVarchar', $query),
'Start' => DBField::create('HTMLVarchar', $start),
'TotalResults'
))->renderWith(array('DocumentationViewer_results', 'DocumentationViewer'));
}
}

View File

@ -0,0 +1,16 @@
<div id="documentation-page">
<div id="left-column">
<% if Results %>
<% control Results %>
<h2><a href="$Link">$Title</a></h2>
$Content.Summary
<% end_control %>
<% else %>
<p>No Results</p>
<% end_if %>
</div>
<div id="right-column">
</div>
</div>

View File

@ -204,37 +204,7 @@ class DocumentationParserTest extends SapphireTest {
);
foreach($names as $key => $value) {
$this->assertEquals(DocumentationParser::clean_page_name($value), $should[$key]);
$this->assertEquals(DocumentationService::clean_page_name($value), $should[$key]);
}
}
function testGetPagesFromFolder() {
$pages = DocumentationParser::get_pages_from_folder(BASE_PATH . '/sapphiredocs/tests/docs/en/');
$this->assertContains('index', $pages->column('Filename'), 'Index');
$this->assertContains('subfolder', $pages->column('Filename'), 'Foldername');
$this->assertContains('test', $pages->column('Filename'), 'Filename');
$this->assertNotContains('_images', $pages->column('Filename'), 'Ignored files');
// test the order of pages
$pages = DocumentationParser::get_pages_from_folder(BASE_PATH . '/sapphiredocs/tests/docs/en/sort');
$this->assertEquals(
array('1 basic', '2 intermediate', '3 advanced', '10 some page', '21 another page'),
$pages->column('Title')
);
}
function testGetPagesFromFolderRecursive() {
$pages = DocumentationParser::get_pages_from_folder(BASE_PATH . '/sapphiredocs/tests/docs-recursive/en/', true);
// check to see all the pages are found, we don't care about order
$this->assertEquals($pages->Count(), 6);
$pages = $pages->column('Title');
foreach(array('Index', 'Subfolder testfile', 'Subsubfolder testfile', 'Testfile') as $expected) {
$this->assertContains($expected, $pages);
}
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @package sapphiredocs
* @subpackage tests
*/
class DocumentationSearchTest extends SapphireTest {
function setUp() {
parent::setUp();
if(!DocumentationSearch::enabled()) return;
DocumentationService::set_automatic_registration(false);
DocumentationService::register('docs-search', BASE_PATH . '/sapphiredocs/tests/docs-search/');
}
function testGetAllPages() {
if(!DocumentationSearch::enabled()) return;
DocumentationService::set_automatic_registration(false);
DocumentationService::register('docs-search', BASE_PATH . '/sapphiredocs/tests/docs-search/');
$search = DocumentationSearch::get_all_documentation_pages();
$this->assertEquals(7, $search->Count(), '5 pages. 5 pages in entire folder');
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @package sapphiredocs
* @subpackage tests
*/
class DocumentationServiceTest extends SapphireTest {
function testFindPath() {
DocumentationService::register("DocumentationViewerTests", BASE_PATH . "/sapphiredocs/tests/docs/");
// file
$path = DocumentationService::find_page('DocumentationViewerTests', array('test'));
$this->assertEquals(BASE_PATH . "/sapphiredocs/tests/docs/en/test.md", $path);
// the home page. The path finder should go to the index.md file in the default language
$path = DocumentationService::find_page('DocumentationViewerTests', array(''));
$this->assertEquals(BASE_PATH . "/sapphiredocs/tests/docs/en/index.md", $path);
// second level
$path = DocumentationService::find_page('DocumentationViewerTests', array('subfolder', 'subpage'));
$this->assertEquals(BASE_PATH . "/sapphiredocs/tests/docs/en/subfolder/subpage.md", $path);
$path = DocumentationService::find_page('DocumentationViewerTests', array('subfolder', 'subsubfolder'));
$this->assertEquals(BASE_PATH . "/sapphiredocs/tests/docs/en/subfolder/subsubfolder/", $path);
// third level
$path = DocumentationService::find_page('DocumentationViewerTests', array('subfolder', 'subsubfolder', 'subsubpage'));
$this->assertEquals(BASE_PATH . "/sapphiredocs/tests/docs/en/subfolder/subsubfolder/subsubpage.md", $path);
// with trailing slash
$path = DocumentationService::find_page('DocumentationViewerTests', array('subfolder', 'subsubfolder', 'subsubpage'));
$this->assertEquals(BASE_PATH . "/sapphiredocs/tests/docs/en/subfolder/subsubfolder/subsubpage.md", $path);
}
function testGetPagesFromFolder() {
$pages = DocumentationService::get_pages_from_folder(BASE_PATH . '/sapphiredocs/tests/docs/en/');
$this->assertContains('index', $pages->column('Filename'), 'The tests/docs/en folder should contain a index file');
$this->assertContains('subfolder', $pages->column('Filename'), 'The tests/docs/en folder should contain a subfolder called subfolder');
$this->assertContains('test', $pages->column('Filename'), 'The tests/docs/en folder should contain a test file');
$this->assertNotContains('_images', $pages->column('Filename'), 'It should not include hidden files');
$this->assertNotContains('.svn', $pages->column('Filename'), 'It should not include hidden files');
// test the order of pages
$pages = DocumentationService::get_pages_from_folder(BASE_PATH . '/sapphiredocs/tests/docs/en/sort');
$this->assertEquals(
array('1 basic', '2 intermediate', '3 advanced', '10 some page', '21 another page'),
$pages->column('Title')
);
}
function testGetPagesFromFolderRecursive() {
$pages = DocumentationService::get_pages_from_folder(BASE_PATH . '/sapphiredocs/tests/docs-recursive/en/');
// check to see all the pages are found, we don't care about order
$this->assertEquals($pages->Count(), 9);
$pages = $pages->column('Title');
foreach(array('Index', 'Subfolder testfile', 'Subsubfolder testfile', 'Testfile') as $expected) {
$this->assertContains($expected, $pages);
}
}
}

View File

@ -37,10 +37,6 @@ class DocumentationViewerTests extends FunctionalTest {
DocumentationService::unregister("DocumentationViewerTests");
DocumentationService::set_automatic_registration($this->origEnabled);
DocumentationViewer::set_link_base($this->origLinkBase);
// $this->origModules = Documentation::get_registered_modules();
// foreach($this->origModules as $name => $module) {
// DocumentationService::register($name);
// }
}
function testCurrentRedirection() {
@ -148,12 +144,14 @@ class DocumentationViewerTests extends FunctionalTest {
$this->assertFalse($child1->Children);
$child2 = $pagesArr[2];
$this->assertType('DataObjectSet', $child2->Children);
$this->assertEquals(
array('subpage', 'subsubfolder'),
$child2->Children->column('Filename')
);
$child2Links = $child2->Children->column('Link');
$this->assertStringEndsWith('2.4/en/DocumentationViewerTests/subfolder/subpage/', $child2Links[0]);
$this->assertStringEndsWith('2.4/en/DocumentationViewerTests/subfolder/subsubfolder/', $child2Links[1]);

View File

View File

@ -0,0 +1 @@
Foo

View File

View File

@ -0,0 +1 @@
Query is foo