From f2b59154aed0708ce4502a4e6d569b538cf37ac1 Mon Sep 17 00:00:00 2001
From: Mateusz Uzdowski
Date: Tue, 17 Apr 2012 14:56:23 +1200
Subject: [PATCH] FEATURE: initial commit of code copied from sapphire
---
_config.php | 0
code/CodeViewer.php | 351 ++++++++++++++++++++++++++++++++
code/ModelViewer.php | 213 +++++++++++++++++++
code/TestViewer.php | 257 +++++++++++++++++++++++
config.rb | 19 ++
css/CodeViewer.css | 3 +
css/TestViewer.css | 3 +
scss/CodeViewer.scss | 14 ++
scss/TestViewer.scss | 14 ++
templates/CodeViewer.ss | 9 +
templates/ModelViewer.ss | 34 ++++
templates/ModelViewer_dotsrc.ss | 20 ++
templates/TestViewer.ss | 9 +
13 files changed, 946 insertions(+)
create mode 100644 _config.php
create mode 100644 code/CodeViewer.php
create mode 100644 code/ModelViewer.php
create mode 100644 code/TestViewer.php
create mode 100644 config.rb
create mode 100644 css/CodeViewer.css
create mode 100644 css/TestViewer.css
create mode 100755 scss/CodeViewer.scss
create mode 100755 scss/TestViewer.scss
create mode 100644 templates/CodeViewer.ss
create mode 100644 templates/ModelViewer.ss
create mode 100644 templates/ModelViewer_dotsrc.ss
create mode 100644 templates/TestViewer.ss
diff --git a/_config.php b/_config.php
new file mode 100644
index 0000000..e69de29
diff --git a/code/CodeViewer.php b/code/CodeViewer.php
new file mode 100644
index 0000000..a2546da
--- /dev/null
+++ b/code/CodeViewer.php
@@ -0,0 +1,351 @@
+ 'browse',
+ '$Class' => 'viewClass'
+ );
+
+ static $allowed_actions = array(
+ 'index',
+ 'browse',
+ 'viewClass'
+ );
+
+ /**
+ * Define a simple finite state machine.
+ * Top keys are the state names. 'start' is the first state, and 'die' is the error state.
+ * Inner keys are token names/codes. The values are either a string, new state, or an array(new state, handler method).
+ * The handler method will be passed the PHP token as an argument, and is expected to populate a property of the object.
+ */
+ static $fsm = array(
+ 'start' => array(
+ T_CLASS => array('className','createClass'),
+ T_DOC_COMMENT => array('', 'saveClassComment'),
+ ),
+ 'className' => array(
+ T_STRING => array('classSpec', 'setClassName'),
+ ),
+ 'classSpec' => array(
+ '{' => 'classBody',
+ ),
+ 'classBody' => array(
+ T_FUNCTION => array('methodName','createBodyMethod'),
+ '}' => array('start', 'completeClass'),
+ T_DOC_COMMENT => array('', 'saveMethodComment'),
+ ),
+ 'methodName' => array(
+ T_STRING => array('methodSpec', 'setMethodName'),
+ ),
+ 'methodSpec' => array(
+ '{' => 'methodBody',
+ ),
+ 'methodBody' => array(
+ '{' => array('!push','appendMethodContent'),
+ '}' => array(
+ 'hasstack' => array('!pop', 'appendMethodContent'),
+ 'nostack' => array('classBody', 'completeMethod'),
+ ),
+ T_VARIABLE => array('variable', 'potentialMethodCall'),
+ T_COMMENT => array('', 'appendMethodComment'),
+ T_DOC_COMMENT => array('', 'appendMethodComment'),
+ '*' => array('', 'appendMethodContent'),
+ ),
+ 'variable' => array(
+ T_OBJECT_OPERATOR => array('variableArrow', 'potentialMethodCall'),
+ '*' => array('methodBody', 'appendMethodContent'),
+ ),
+ 'variableArrow' => array(
+ T_STRING => array('methodOrProperty', 'potentialMethodCall'),
+ T_WHITESPACE => array('', 'potentialMethodCall'),
+ '*' => array('methodBody', 'appendMethodContent'),
+ ),
+ 'methodOrProperty' => array(
+ '(' => array('methodCall', 'potentialMethodCall'),
+ T_WHITESPACE => array('', 'potentialMethodCall'),
+ '*' => array('methodBody', 'appendMethodContent'),
+ ),
+ 'methodCall' => array(
+ '(' => array('!push/nestedInMethodCall', 'potentialMethodCall'),
+ ')' => array('methodBody', 'completeMethodCall'),
+ '*' => array('', 'potentialMethodCall'),
+ ),
+ 'nestedInMethodCall' => array(
+ '(' => array('!push', 'potentialMethodCall'),
+ ')' => array('!pop', 'potentialMethodCall'),
+ '*' => array('', 'potentialMethodCall'),
+ ),
+ );
+
+ function init() {
+ parent::init();
+
+ if(!Permission::check('ADMIN')) return Security::permissionFailure();
+ TestRunner::use_test_manifest();
+ }
+
+ public function browse() {
+ $classes = ClassInfo::subclassesFor('SapphireTest');
+
+ array_shift($classes);
+ ksort($classes);
+
+ $result ='View any of the following test classes ';
+ $result .='';
+
+ $result .='View any of the following other classes ';
+
+ $classes = array_keys(ClassInfo::allClasses());
+ sort($classes);
+
+ $result .='';
+
+ return $this->customise(array (
+ 'Content' => $result
+ ))->renderWith('CodeViewer');
+ }
+
+ public function viewClass(SS_HTTPRequest $request) {
+ $class = $request->param('Class');
+
+ if(!class_exists($class)) {
+ throw new Exception('CodeViewer->viewClass(): not passed a valid class to view (does the class exist?)');
+ }
+
+ return $this->customise(array (
+ 'Content' => $this->testAnalysis(getClassFile($class))
+ ))->renderWith('CodeViewer');
+ }
+
+ public function Link($action = null) {
+ return Controller::join_links(Director::absoluteBaseURL(), 'dev/viewcode/', $action);
+ }
+
+ protected $classComment, $methodComment;
+
+ function saveClassComment($token) {
+ $this->classComment = $this->parseComment($token);
+ }
+ function saveMethodComment($token) {
+ $this->methodComment = $this->parseComment($token);
+ }
+
+ function createClass($token) {
+ $this->currentClass = array(
+ "description" => $this->classComment['pretty'],
+ "heading" => isset($this->classComment['heading']) ? $this->classComment['heading'] : null,
+ );
+ $ths->classComment = null;
+ }
+ function setClassName($token) {
+ $this->currentClass['name'] = $token[1];
+ if(!$this->currentClass['heading']) $this->currentClass['heading'] = $token[1];
+ }
+ function completeClass($token) {
+ $this->classes[] = $this->currentClass;
+ }
+
+ function createBodyMethod($token) {
+ $this->currentMethod = array();
+ $this->currentMethod['content'] = "";
+ $this->currentMethod['description'] = $this->methodComment['pretty'];
+ $this->currentMethod['heading'] = isset($this->methodComment['heading']) ? $this->methodComment['heading'] : null;
+ $this->methodComment = null;
+
+ }
+ function setMethodName($token) {
+ $this->currentMethod['name'] = $token[1];
+ if(!$this->currentMethod['heading']) $this->currentMethod['heading'] = $token[1];
+ }
+ function appendMethodComment($token) {
+ if(substr($token[1],0,2) == '/*') {
+ $this->closeOffMethodContentPre();
+ $this->currentMethod['content'] .= $this->prettyComment($token) . "";
+ } else {
+ $this->currentMethod['content'] .= $this->renderToken($token);
+ }
+ }
+
+ function prettyComment($token) {
+ $comment = preg_replace('/^\/\*/','',$token[1]);
+ $comment = preg_replace('/\*\/$/','',$comment);
+ $comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
+ $comment = htmlentities($comment, ENT_COMPAT, 'UTF-8');
+ $comment = str_replace("\n\n", "
", $comment);
+ return "
$comment
";
+ }
+
+ function parseComment($token) {
+ $parsed = array();
+
+ $comment = preg_replace('/^\/\*/','',$token[1]);
+ $comment = preg_replace('/\*\/$/','',$comment);
+ $comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
+
+ foreach(array('heading','nav') as $var) {
+ if(preg_match('/@' . $var . '\s+([^\n]+)\n/', $comment, $matches)) {
+ $parsed[$var] = $matches[1];
+ $comment = preg_replace('/@' . $var . '\s+([^\n]+)\n/','', $comment);
+ }
+ }
+
+ $parsed['pretty'] = "" . str_replace("\n\n", "
", htmlentities($comment, ENT_COMPAT, 'UTF-8')). "
";
+ return $parsed;
+ }
+
+ protected $isNewLine = true;
+
+ function appendMethodContent($token) {
+ if($this->potentialMethodCall) {
+ $this->currentMethod['content'] .= $this->potentialMethodCall;
+ $this->potentialMethodCall = "";
+ }
+ //if($this->isNewLine && isset($token[2])) $this->currentMethod['content'] .= $token[2] . ": ";
+ $this->isNewLine = false;
+ $this->currentMethod['content'] .= $this->renderToken($token);
+ }
+ function completeMethod($token) {
+ $this->closeOffMethodContentPre();
+ $this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
+ $this->currentClass['methods'][] = $this->currentMethod;
+ }
+
+ protected $potentialMethodCall = "";
+ function potentialMethodCall($token) {
+ $this->potentialMethodCall .= $this->renderToken($token);
+ }
+ function completeMethodCall($token) {
+ $this->potentialMethodCall .= $this->renderToken($token);
+ if(strpos($this->potentialMethodCall, '->assert') !== false) {
+ $this->currentMethod['content'] .= "" . $this->potentialMethodCall . " ";
+ } else {
+ $this->currentMethod['content'] .= $this->potentialMethodCall;
+ }
+ $this->potentialMethodCall = "";
+ }
+
+ /**
+ * Finish the "pre" block in method content.
+ * Will remove whitespace and empty "pre" blocks
+ */
+ function closeOffMethodContentPre() {
+ $this->currentMethod['content'] = trim($this->currentMethod['content']);
+ if(substr($this->currentMethod['content'],-5) == '') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
+ else $this->currentMethod['content'] .= ' ';
+ }
+
+ /**
+ * Render the given token as HTML
+ */
+ function renderToken($token) {
+ $tokenContent = htmlentities(
+ is_array($token) ? $token[1] : $token,
+ ENT_COMPAT,
+ 'UTF-8'
+ );
+ $tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
+
+ switch($tokenName) {
+ case "T_WHITESPACE":
+ if(strpos($tokenContent, "\n") !== false) $this->isNewLine = true;
+ return $tokenContent;
+ default:
+ return "$tokenContent ";
+ }
+ }
+
+ protected $classes = array();
+ protected $currentMethod, $currentClass;
+
+ function testAnalysis($file) {
+ $content = file_get_contents($file);
+ $tokens = token_get_all($content);
+
+ // Execute a finite-state-machine with a built-in state stack
+ // This FSM+stack gives us enough expressive power for simple PHP parsing
+ $state = "start";
+ $stateStack = array();
+
+ //echo "state $state";
+ foreach($tokens as $token) {
+ // Get token name - some tokens are arrays, some arent'
+ if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
+ //echo " token '$tokenName'";
+
+ // Find the rule for that token in the current state
+ if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
+ else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
+ else $rule = null;
+
+ // Check to see if we have specified multiple rules depending on whether the stack is populated
+ if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
+ if($stateStack) $rule = $rule['hasstack'];
+ else $rule = $rule = $rule['nostack'];
+ }
+
+ if(is_array($rule)) {
+ list($destState, $methodName) = $rule;
+ $this->$methodName($token);
+ } else if($rule) {
+ $destState = $rule;
+ } else {
+ $destState = null;
+ }
+ //echo " ->state $destState";
+
+ if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
+ $action = $parts[1];
+ $argument = isset($parts[2]) ? substr($parts[2],1) : null;
+ $destState = null;
+
+ switch($action) {
+ case "push":
+ $stateStack[] = $state;
+ if($argument) $destState = $argument;
+ break;
+
+ case "pop":
+ if($stateStack) $destState = array_pop($stateStack);
+ else if($argument) $destState = $argument;
+ else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
+ }
+ }
+
+ if($destState) $state = $destState;
+ if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
+ }
+
+ $subclasses = ClassInfo::subclassesFor('SapphireTest');
+ foreach($this->classes as $classDef) {
+ if(true ||in_array($classDef['name'], $subclasses)) {
+ echo "$classDef[heading] ";
+ echo "$classDef[description]
";
+ if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) {
+ if(true || substr($method['name'],0,4) == 'test') {
+ //$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
+ $title = $method['heading'];
+
+ echo "$title ";
+ echo "$method[description]
";
+ echo $method['content'];
+ }
+ }
+ }
+
+ }
+ }
+}
diff --git a/code/ModelViewer.php b/code/ModelViewer.php
new file mode 100644
index 0000000..c385adb
--- /dev/null
+++ b/code/ModelViewer.php
@@ -0,0 +1,213 @@
+ 'handleModule',
+ );
+
+ protected $module = null;
+
+ function handleModule($request) {
+ return new ModelViewer_Module($request->param('Module'));
+ }
+
+ function init() {
+ parent::init();
+
+ $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
+ if(!$canAccess) return Security::permissionFailure($this);
+
+ // check for graphviz dependencies
+ $returnCode = 0;
+ $output = array();
+ exec("which neato", $output, $returnCode);
+ if($returnCode != 0) {
+ user_error(
+ 'You don\'t seem to have the GraphViz library (http://graphviz.org/) and the "neato" command-line utility available',
+ E_USER_ERROR
+ );
+ }
+ }
+
+ /**
+ * Model classes
+ */
+ function Models() {
+ $classes = ClassInfo::subclassesFor('DataObject');
+ array_shift($classes);
+ $output = new ArrayList();
+ foreach($classes as $class) {
+ $output->push(new ModelViewer_Model($class));
+ }
+ return $output;
+ }
+
+ /**
+ * Model classes, grouped by Module
+ */
+ function Modules() {
+ $classes = ClassInfo::subclassesFor('DataObject');
+ array_shift($classes);
+
+ $modules = array();
+ foreach($classes as $class) {
+ $model = new ModelViewer_Model($class);
+ if(!isset($modules[$model->Module])) $modules[$model->Module] = new ArrayList();
+ $modules[$model->Module]->push($model);
+ }
+ ksort($modules);
+ unset($modules['userforms']);
+
+ if($this->module) {
+ $modules = array($this->module => $modules[$this->module]);
+ }
+
+ $output = new ArrayList();
+ foreach($modules as $moduleName => $models) {
+ $output->push(new ArrayData(array(
+ 'Link' => 'dev/viewmodel/' . $moduleName,
+ 'Name' => $moduleName,
+ 'Models' => $models,
+ )));
+ }
+
+ return $output;
+ }
+}
+
+/**
+ * @package framework
+ * @subpackage tools
+ */
+class ModelViewer_Module extends ModelViewer {
+ static $url_handlers = array(
+ 'graph' => 'graph',
+ );
+
+ /**
+ * ModelViewer can be optionally constructed to restrict its output to a specific module
+ */
+ function __construct($module = null) {
+ $this->module = $module;
+
+ parent::__construct();
+ }
+
+ function graph() {
+ SSViewer::set_source_file_comments(false);
+ $dotContent = $this->renderWith("ModelViewer_dotsrc");
+ $CLI_dotContent = escapeshellarg($dotContent);
+
+ $output= `echo $CLI_dotContent | neato -Tpng:gd &> /dev/stdout`;
+ if(substr($output,1,3) == 'PNG') header("Content-type: image/png");
+ else header("Content-type: text/plain");
+ echo $output;
+ }
+}
+
+/**
+ * Represents a single model in the model viewer
+ *
+ * @package framework
+ * @subpackage tools
+ */
+class ModelViewer_Model extends ViewableData {
+ protected $className;
+
+ function __construct($className) {
+ $this->className = $className;
+ parent::__construct();
+ }
+
+ function getModule() {
+ $classes = SS_ClassLoader::instance()->getManifest()->getClasses();
+ $className = strtolower($this->className);
+
+ if(($pos = strpos($className,'_')) !== false) $className = substr($className,0,$pos);
+ if(isset($classes[$className])) {
+ if(preg_match('/^'.str_replace('/','\/',preg_quote(BASE_PATH)).'\/([^\/]+)\//', $classes[$className], $matches)) {
+ return $matches[1];
+ }
+ }
+ }
+
+ function getName() {
+ return $this->className;
+ }
+
+ function getParentModel() {
+ $parentClass = get_parent_class($this->className);
+ if($parentClass != "DataObject") return $parentClass;
+ }
+
+ function Fields() {
+ $output = new ArrayList();
+
+ $output->push(new ModelViewer_Field($this,'ID', 'PrimaryKey'));
+ if(!$this->ParentModel) {
+ $output->push(new ModelViewer_Field($this,'Created', 'Datetime'));
+ $output->push(new ModelViewer_Field($this,'LastEdited', 'Datetime'));
+ }
+
+ $db = singleton($this->className)->uninherited('db',true);
+ if($db) foreach($db as $k => $v) {
+ $output->push(new ModelViewer_Field($this, $k, $v));
+ }
+ return $output;
+ }
+
+ function Relations() {
+ $output = new ArrayList();
+
+ foreach(array('has_one','has_many','many_many') as $relType) {
+ $items = singleton($this->className)->uninherited($relType,true);
+ if($items) foreach($items as $k => $v) {
+ $output->push(new ModelViewer_Relation($this, $k, $v, $relType));
+ }
+ }
+ return $output;
+ }
+}
+
+/**
+ * @package framework
+ * @subpackage tools
+ */
+class ModelViewer_Field extends ViewableData {
+ public $Model, $Name, $Type;
+
+ function __construct($model, $name, $type) {
+ $this->Model = $model;
+ $this->Name = $name;
+ $this->Type = $type;
+
+ parent::__construct();
+ }
+}
+
+/**
+ * @package framework
+ * @subpackage tools
+ */
+class ModelViewer_Relation extends ViewableData {
+ public $Model, $Name, $RelationType, $RelatedClass;
+
+ function __construct($model, $name, $relatedClass, $relationType) {
+ $this->Model = $model;
+ $this->Name = $name;
+ $this->RelatedClass = $relatedClass;
+ $this->RelationType = $relationType;
+
+ parent::__construct();
+ }
+
+}
+
diff --git a/code/TestViewer.php b/code/TestViewer.php
new file mode 100644
index 0000000..7792b0a
--- /dev/null
+++ b/code/TestViewer.php
@@ -0,0 +1,257 @@
+ array(
+ T_CLASS => array('className','createClass'),
+ ),
+ 'className' => array(
+ T_STRING => array('classSpec', 'setClassName'),
+ ),
+ 'classSpec' => array(
+ '{' => 'classBody',
+ ),
+ 'classBody' => array(
+ T_FUNCTION => array('methodName','createBodyMethod'),
+ '}' => array('start', 'completeClass'),
+ ),
+ 'methodName' => array(
+ T_STRING => array('methodSpec', 'setMethodName'),
+ ),
+ 'methodSpec' => array(
+ '{' => 'methodBody',
+ ),
+ 'methodBody' => array(
+ '{' => array('!push','appendMethodContent'),
+ '}' => array(
+ 'hasstack' => array('!pop', 'appendMethodContent'),
+ 'nostack' => array('classBody', 'completeMethod'),
+ ),
+ T_VARIABLE => array('variable', 'potentialMethodCall'),
+ T_COMMENT => array('', 'appendMethodComment'),
+ '*' => array('', 'appendMethodContent'),
+ ),
+ 'variable' => array(
+ T_OBJECT_OPERATOR => array('variableArrow', 'potentialMethodCall'),
+ '*' => array('methodBody', 'appendMethodContent'),
+ ),
+ 'variableArrow' => array(
+ T_STRING => array('methodOrProperty', 'potentialMethodCall'),
+ T_WHITESPACE => array('', 'potentialMethodCall'),
+ '*' => array('methodBody', 'appendMethodContent'),
+ ),
+ 'methodOrProperty' => array(
+ '(' => array('methodCall', 'potentialMethodCall'),
+ T_WHITESPACE => array('', 'potentialMethodCall'),
+ '*' => array('methodBody', 'appendMethodContent'),
+ ),
+ 'methodCall' => array(
+ '(' => array('!push/nestedInMethodCall', 'potentialMethodCall'),
+ ')' => array('methodBody', 'completeMethodCall'),
+ '*' => array('', 'potentialMethodCall'),
+ ),
+ 'nestedInMethodCall' => array(
+ '(' => array('!push', 'potentialMethodCall'),
+ ')' => array('!pop', 'potentialMethodCall'),
+ '*' => array('', 'potentialMethodCall'),
+ ),
+ );
+
+ function init() {
+ parent::init();
+
+ $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
+ if(!$canAccess) return Security::permissionFailure($this);
+ }
+
+ function createClass($token) {
+ $this->currentClass = array();
+ }
+ function setClassName($token) {
+ $this->currentClass['name'] = $token[1];
+ }
+ function completeClass($token) {
+ $this->classes[] = $this->currentClass;
+ }
+
+ function createBodyMethod($token) {
+ $this->currentMethod = array();
+ $this->currentMethod['content'] = "";
+ }
+ function setMethodName($token) {
+ $this->currentMethod['name'] = $token[1];
+ }
+ function appendMethodComment($token) {
+ if(substr($token[1],0,2) == '/*') {
+ $comment = preg_replace('/^\/\*/','',$token[1]);
+ $comment = preg_replace('/\*\/$/','',$comment);
+ $comment = preg_replace('/\n[\t ]*\* */m',"\n",$comment);
+
+ $this->closeOffMethodContentPre();
+ $this->currentMethod['content'] .= "$comment
";
+ } else {
+ $this->currentMethod['content'] .= $this->renderToken($token);
+ }
+
+ }
+ function appendMethodContent($token) {
+ if($this->potentialMethodCall) {
+ $this->currentMethod['content'] .= $this->potentialMethodCall;
+ $this->potentialMethodCall = "";
+ }
+ $this->currentMethod['content'] .= $this->renderToken($token);
+ }
+ function completeMethod($token) {
+ $this->closeOffMethodContentPre();
+ $this->currentMethod['content'] = str_replace("\n\t\t","\n",$this->currentMethod['content']);
+ $this->currentClass['methods'][] = $this->currentMethod;
+ }
+
+ protected $potentialMethodCall = "";
+ function potentialMethodCall($token) {
+ $this->potentialMethodCall .= $this->renderToken($token);
+ }
+ function completeMethodCall($token) {
+ $this->potentialMethodCall .= $this->renderToken($token);
+ if(strpos($this->potentialMethodCall, '-> assert') !== false) {
+ $this->currentMethod['content'] .= "" . $this->potentialMethodCall . " ";
+ } else {
+ $this->currentMethod['content'] .= $this->potentialMethodCall;
+ }
+ $this->potentialMethodCall = "";
+ }
+
+ /**
+ * Finish the "pre" block in method content.
+ * Will remove whitespace and empty "pre" blocks
+ */
+ function closeOffMethodContentPre() {
+ $this->currentMethod['content'] = trim($this->currentMethod['content']);
+ if(substr($this->currentMethod['content'],-5) == '') $this->currentMethod['content'] = substr($this->currentMethod['content'], 0,-5);
+ else $this->currentMethod['content'] .= ' ';
+ }
+
+ /**
+ * Render the given token as HTML
+ */
+ function renderToken($token) {
+ $tokenContent = htmlentities(
+ is_array($token) ? $token[1] : $token,
+ ENT_COMPAT,
+ 'UTF-8'
+ );
+ $tokenName = is_array($token) ? token_name($token[0]) : 'T_PUNCTUATION';
+
+ switch($tokenName) {
+ case "T_WHITESPACE":
+ return $tokenContent;
+ default:
+ return "$tokenContent ";
+ }
+ }
+
+ protected $classes = array();
+ protected $currentMethod, $currentClass;
+
+ function Content() {
+ $className = $this->urlParams['ID'];
+ if($className && ClassInfo::exists($className)) {
+ return $this->testAnalysis(getClassFile($className));
+ } else {
+ $result = "View any of the following test classes ";
+ $classes = ClassInfo::subclassesFor('SapphireTest');
+ ksort($classes);
+ foreach($classes as $className) {
+ if($className == 'SapphireTest') continue;
+ $result .= "$className ";
+ }
+ return $result;
+ }
+ }
+
+ function testAnalysis($file) {
+ $content = file_get_contents($file);
+ $tokens = token_get_all($content);
+
+ // Execute a finite-state-machine with a built-in state stack
+ // This FSM+stack gives us enough expressive power for simple PHP parsing
+ $state = "start";
+ $stateStack = array();
+
+ //echo "state $state";
+ foreach($tokens as $token) {
+ // Get token name - some tokens are arrays, some arent'
+ if(is_array($token)) $tokenName = $token[0]; else $tokenName = $token;
+ //echo " token '$tokenName'";
+
+ // Find the rule for that token in the current state
+ if(isset(self::$fsm[$state][$tokenName])) $rule = self::$fsm[$state][$tokenName];
+ else if(isset(self::$fsm[$state]['*'])) $rule = self::$fsm[$state]['*'];
+ else $rule = null;
+
+ // Check to see if we have specified multiple rules depending on whether the stack is populated
+ if(is_array($rule) && array_keys($rule) == array('hasstack', 'nostack')) {
+ if($stateStack) $rule = $rule['hasstack'];
+ else $rule = $rule = $rule['nostack'];
+ }
+
+ if(is_array($rule)) {
+ list($destState, $methodName) = $rule;
+ $this->$methodName($token);
+ } else if($rule) {
+ $destState = $rule;
+ } else {
+ $destState = null;
+ }
+ //echo " ->state $destState";
+
+ if(preg_match('/!(push|pop)(\/[a-zA-Z0-9]+)?/', $destState, $parts)) {
+ $action = $parts[1];
+ $argument = isset($parts[2]) ? substr($parts[2],1) : null;
+ $destState = null;
+
+ switch($action) {
+ case "push":
+ $stateStack[] = $state;
+ if($argument) $destState = $argument;
+ break;
+
+ case "pop":
+ if($stateStack) $destState = array_pop($stateStack);
+ else if($argument) $destState = $argument;
+ else user_error("State transition '!pop' was attempted with an empty state-stack and no default option specified.", E_USER_ERROR);
+ }
+ }
+
+ if($destState) $state = $destState;
+ if(!isset(self::$fsm[$state])) user_error("Transition to unrecognised state '$state'", E_USER_ERROR);
+ }
+
+ $subclasses = ClassInfo::subclassesFor('SapphireTest');
+ foreach($this->classes as $classDef) {
+ if(in_array($classDef['name'], $subclasses)) {
+ echo "$classDef[name] ";
+ if($classDef['methods']) foreach($classDef['methods'] as $method) {
+ if(substr($method['name'],0,4) == 'test') {
+ //$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
+ $title = $method['name'];
+
+ echo "$title ";
+ echo $method['content'];
+ }
+ }
+ }
+
+ }
+ }
+}
diff --git a/config.rb b/config.rb
new file mode 100644
index 0000000..cf343fa
--- /dev/null
+++ b/config.rb
@@ -0,0 +1,19 @@
+# Require any additional compass plugins here.
+
+project_type = :stand_alone
+# Set this to the root of your project when deployed:
+http_path = "/"
+css_dir = "css"
+sass_dir = "scss"
+images_dir = "images"
+javascripts_dir = "javascript"
+output_style = :compact
+
+# To enable relative paths to assets via compass helper functions. Uncomment:
+relative_assets = true
+
+# disable comments in the output. We want admin comments
+# to be verbose
+line_comments = false
+
+asset_cache_buster :none
diff --git a/css/CodeViewer.css b/css/CodeViewer.css
new file mode 100644
index 0000000..2cd9f34
--- /dev/null
+++ b/css/CodeViewer.css
@@ -0,0 +1,3 @@
+pre { border: 1px #777 solid; background-color: #CCC; color: #333; margin: 0.5em 2em; padding: 1em; line-height: 120%; }
+
+pre strong { background-color: #FFC; color: #000; padding: 2px; }
diff --git a/css/TestViewer.css b/css/TestViewer.css
new file mode 100644
index 0000000..2cd9f34
--- /dev/null
+++ b/css/TestViewer.css
@@ -0,0 +1,3 @@
+pre { border: 1px #777 solid; background-color: #CCC; color: #333; margin: 0.5em 2em; padding: 1em; line-height: 120%; }
+
+pre strong { background-color: #FFC; color: #000; padding: 2px; }
diff --git a/scss/CodeViewer.scss b/scss/CodeViewer.scss
new file mode 100755
index 0000000..621ee43
--- /dev/null
+++ b/scss/CodeViewer.scss
@@ -0,0 +1,14 @@
+pre {
+ border: 1px #777 solid;
+ background-color: #CCC;
+ color: #333;
+ margin: 0.5em 2em;
+ padding: 1em;
+ line-height: 120%;
+}
+
+pre strong {
+ background-color: #FFC;
+ color: #000;
+ padding: 2px;
+}
\ No newline at end of file
diff --git a/scss/TestViewer.scss b/scss/TestViewer.scss
new file mode 100755
index 0000000..621ee43
--- /dev/null
+++ b/scss/TestViewer.scss
@@ -0,0 +1,14 @@
+pre {
+ border: 1px #777 solid;
+ background-color: #CCC;
+ color: #333;
+ margin: 0.5em 2em;
+ padding: 1em;
+ line-height: 120%;
+}
+
+pre strong {
+ background-color: #FFC;
+ color: #000;
+ padding: 2px;
+}
\ No newline at end of file
diff --git a/templates/CodeViewer.ss b/templates/CodeViewer.ss
new file mode 100644
index 0000000..e87b064
--- /dev/null
+++ b/templates/CodeViewer.ss
@@ -0,0 +1,9 @@
+
+
+<% base_tag %>
+
+
+
+ $Content
+
+
diff --git a/templates/ModelViewer.ss b/templates/ModelViewer.ss
new file mode 100644
index 0000000..a155caa
--- /dev/null
+++ b/templates/ModelViewer.ss
@@ -0,0 +1,34 @@
+
+
+ <% base_tag %>
+ Data Model
+
+
+
+ Data Model for your project
+
+ <% control Modules %>
+ Module $Name
+
+
+
+ <% control Models %>
+ $Name <% if ParentModel %> (subclass of $ParentModel)<% end_if %>
+ Fields
+
+ <% control Fields %>
+ $Name - $Type
+ <% end_control %>
+
+
+ Relations
+
+ <% control Relations %>
+ $Name $RelationType $RelatedClass
+ <% end_control %>
+
+ <% end_control %>
+ <% end_control %>
+
+
+
diff --git a/templates/ModelViewer_dotsrc.ss b/templates/ModelViewer_dotsrc.ss
new file mode 100644
index 0000000..a7070a6
--- /dev/null
+++ b/templates/ModelViewer_dotsrc.ss
@@ -0,0 +1,20 @@
+digraph g {
+ orientation=portrait;
+ overlap=false;
+ splines=true;
+
+ edge[fontsize=8,len=1.5];
+ node[fontsize=10,shape=box];
+
+ <% control Modules %>
+ <% control Models %>
+ $Name [shape=record,label="{$Name|<% control Fields %>$Name\\n<% end_control %>}"];
+ <% if ParentModel %>
+ $Name -> $ParentModel [style=dotted];
+ <% end_if %>
+ <% control Relations %>
+ $Model.Name -> $RelatedClass [label="$Name\\n$RelationType"];
+ <% end_control %>
+ <% end_control %>
+ <% end_control %>
+}
diff --git a/templates/TestViewer.ss b/templates/TestViewer.ss
new file mode 100644
index 0000000..e14ea08
--- /dev/null
+++ b/templates/TestViewer.ss
@@ -0,0 +1,9 @@
+
+
+<% base_tag %>
+
+
+
+ $Content
+
+