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 + +