diff --git a/README.md b/README.md index aeeae74..d0bbb56 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/TomBZombie/CDS?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Transphporm is fresh look at templating in PHP. Let's face it, [Templating in PHP sucks](http://www.workingsoftware.com.au/page/Your_templating_engine_sucks_and_everything_you_have_ever_written_is_spaghetti_code_yes_you) because it involves code like this: +Transphporm is a fresh approach to templating in PHP. Let's face it, [templating in PHP sucks](http://www.workingsoftware.com.au/page/Your_templating_engine_sucks_and_everything_you_have_ever_written_is_spaghetti_code_yes_you) because it involves code like this: ```php @@ -45,17 +45,17 @@ Why does this suck? It mixes the logic with the template. There are processing i Template systems like this still mix logic and markup, the one thing they're trying to avoid. -This is equivalent to `

Title

`, as it mixes two very different concerns. +This is equivalent to `

Title

`, as it mixes two very different concerns. ## Transphporm is different ### Project Goals -1. To completely separate the markup from the processing logic. (No if statements or loops in the template!) -2. To follow CSS concepts and grammar as closely as possible. This makes it incredibly easy to learn for anyone who already understands CSS. +1. To completely separate the markup from the processing logic. (No `if` statements or loops in the template!) +2. To follow CSS concepts and grammar as closely as possible. (This makes it incredibly easy to learn for anyone who already understands CSS.) -With Transphporm, the designer just supplies some raw XML that contains some dummy data. (Designers much prefer lorem ipsum to seeing `{{description}}` in their designs!) +With Transphporm, the designer just supplies some raw HTML or XML that contains some dummy data. (Designers much prefer lorem ipsum to seeing `{{description}}` in their designs!) ```php '; @@ -502,11 +502,11 @@ Output:
  • Scott

    scott@example.org -
  • +
  • Jo

    jo@example.org -
  • + ``` @@ -545,7 +545,7 @@ $xml = '
  • Name

    email -
  • + '; @@ -575,12 +575,12 @@ Output: ``` @@ -644,12 +644,8 @@ Array ( Array ( [0] => 'location', [1] => '/redirect-url' - ) - - - + ) ) - ) ``` @@ -706,7 +702,6 @@ Prints: Array ( [0] => 'status', [1] => '404' - ) ``` @@ -740,7 +735,7 @@ h1 {content: "content of element"; format: [NAME-OF-FORMAT] [OPTIONAL ARGUMENT O ### String formatting -Transphporm currently supports the following formats for strings: +Transphporm currently supports the following formats for strings: - uppercase - lowercase @@ -849,10 +844,10 @@ Prints: ``` -## Locales +## Locales -For date, time and currency formatting, Transphporm supports Locales. Currently only enGB is supplied but you can write your own. +For date, time and currency formatting, Transphporm supports locales. Currently only enGB is supplied but you can write your own. To set a locale, use the `builder::setLocale` method. This takes either a locale name, for a locale inside `Formatter/Locale/{name}.json` e.g. @@ -868,7 +863,7 @@ Currently only enGB is supported. Alternatively, you can provide an array which ### Date formats -Transphporm supports formatting dates. Either you can reference a \DateTime object or a string. Strings will be attempted to be converted to dates automatically: +Transphporm supports formatting dates. Either you can reference a `\DateTime` object or a string. Strings will be converted to dates automatically, if possible: ```php $xml = ' @@ -935,7 +930,7 @@ echo $template->output()->body; You can supply the `relative` formatter to a date, which will display things like: - "Tomorrow" -- "Yesterady" +- "Yesterday" - "Two hours ago" - "3 weeks ago" - "In 3 months" @@ -946,10 +941,10 @@ The strings are specified in the locale. ## Importing other files -Like CSS, transphporm supports `@import` for importing other TSS files: +Like CSS, Transphporm supports `@import` for importing other TSS files: -`imported.tss` +`imported.tss` ```css h1 {content: "From imported tss"} @@ -984,7 +979,7 @@ Output: Transphporm has two types of caching, both of which need to be enabled: -1. Caching TSS and XML files. This prevents them being parsed each time the template is rendered. It is worthwhile enabling this even if you do not intend on using `update-frequency` (see below). +1. Caching TSS and XML files. This prevents them from being parsed each time the template is rendered. It is worthwhile to enable this even if you do not intend on using `update-frequency` (see below). 2. `update-frequency` This is a property which allows you to update an element at a specified interval. @@ -999,7 +994,7 @@ $template->setCache($cache); echo $template->output($data)->body; ``` -Doing this will automatically enable file-caching. Once a cache has been assigned, TSS files will only be parsed whenever they are updated. This saves parsing the TSS file each time your page loads and is worthwile even if you are not using `update-frequency`. +Doing this will automatically enable file-caching. Once a cache has been assigned, TSS files will only be parsed whenever they are updated. This saves parsing the TSS file each time your page loads and is worthwhile even if you are not using `update-frequency`. ### update-frequency @@ -1007,35 +1002,31 @@ Doing this will automatically enable file-caching. Once a cache has been assigne `update-frequency` is a TSS directive that describes how frequently a given TSS rule should run. Behind the scenes, Transphporm will save the final output each time a template is rendered and make changes to it based on `update-frequency`. For example: ```tss - ul li {repeat: data(users); update-frequency: 10m} - ``` This will only run the TSS rule every 10 minutes. The way this works behind the scenes is: -- The rendred template is stored in the cache. -- Next time the page loads the the previously rendered template is loaded. -- If the timer has expired, the repeat/content/etc directives are run again on the cached version of the template and the template is updated. +- The rendered template is stored in the cache. +- Next time the page loads, the previously rendered template is loaded. +- If the timer has expired, the repeat/content/etc. directives are run again on the cached version of the template and the template is updated. -This allows different parts of the page to be updated at different speeds. +This allows different parts of the page to be updated at different frequencies. ## Caching in MVC -If you are using MVC ([And not PAC, which most frameworks do](http://r.je/views-are-not-templates.html)) and you are passing your model into your view, if your model is passed in as the `data` argument and has a `getUsers` function, Transphporm can call this and only execute the query when the template is updated. +If you are using real MVC ([not PAC, which most frameworks actually use](http://r.je/views-are-not-templates.html)) and you are passing your model into your view, if your model is passed in as the `data` argument and has a `getUsers` function, Transphporm can call this and only execute the query when the template is updated. ```tss - ul li {repeat: data(getUsers); update-frequency: 10m} ``` - -Most frameowrks do not pass models into views, however for those that do this allows a two level cache. The query is only run when the view is updated based on the view's timeout. +Most frameworks do not pass models into views, however for those that do this allows a two-level cache. The query is only run when the view is updated based on the view's timeout. # Building a whole page -Transphporm uses a top-down approach to construct pages. Most frameworks require writing a layout template and then pulling content into it. It becomes very difficult to make changes to the layout on a per-page basis. (At minimum you need to add some code to the layout HTML). Transphporm uses a top-down approach rather than the popular bottom-up approach where the child template is inserted into the layout at a specific point. +Transphporm uses a top-down approach to construct pages. Most frameworks require writing a layout template and then pulling content into it. This makes it very difficult to make changes to the layout on a per-page basis. (At minimum you'd need to add some code to the layout HTML). Transphporm uses a top-down approach rather than the popular bottom-up approach where the child template is inserted into the layout at a specific point. You still have two files, one for the layout and one for the content, but the TSS is applied to the *layout* which means the TSS can change anything in the layout you want (adding script tags, adding CSS, changing the page title and meta tags, etc). @@ -1126,7 +1117,7 @@ echo $template->output()->body; ``` -There's a little repetition here which can be solved in two ways. +There's a little repetition here which can be solved in two ways. ### 1) Put the layout rules in their own file, e.g. base.tss: diff --git a/composer.json b/composer.json index 70a8758..7090ae4 100644 --- a/composer.json +++ b/composer.json @@ -11,12 +11,12 @@ } ], "require": { - "php": ">=5.6.0" + "php": ">=7.0.0" }, "require-dev": { "phpunit/phpunit": "^5.7.20" }, "autoload": { - "classmap": ["src/"] + "psr-4": {"Transphporm\\": "src/"} } } diff --git a/src/Builder.php b/src/Builder.php index a38ebb6..6318652 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -14,6 +14,7 @@ class Builder { private $modules = []; private $config; private $filePath; + private $cacheKey; private $defaultModules = [ '\\Transphporm\\Module\\Basics', '\\Transphporm\\Module\\Pseudo', @@ -39,44 +40,55 @@ public function setTime($time) { public function loadModule(Module $module) { $this->modules[get_class($module)] = $module; } - + public function setLocale($locale) { - $format = new \Transphporm\Module\Format($locale); - $this->modules[get_class($format)] = $format; - } + $format = new \Transphporm\Module\Format($locale); + $this->modules[get_class($format)] = $format; + } public function addPath($dir) { $this->filePath->addPath($dir); } + private function getSheetLoader() { + $tssRules = is_file($this->tss) ? new SheetLoader\TSSFile($this->tss, $this->filePath, $this->cache, $this->time) : new SheetLoader\TSSString($this->tss, $this->filePath); + return new SheetLoader\SheetLoader($this->cache, $this->filePath, $tssRules, $this->time); + } + public function output($data = null, $document = false) { $headers = []; + $tssCache = $this->getSheetLoader(); + $this->cacheKey = $tssCache->getCacheKey($data); + $result = $this->loadTemplate(); + //If an update is required, run any rules that need to be run. Otherwise, return the result from cache + //without creating any further objects, loading a DomDocument, etc + if (empty($result['renderTime']) || $tssCache->updateRequired($data) === true) { + $template = $this->createAndProcessTemplate($data, $result['cache'], $headers); + $tssCache->processRules($template, $this->config); + + $result = ['cache' => $template->output($document), + 'renderTime' => time(), + 'headers' => array_merge($result['headers'], $headers), + 'body' => $this->doPostProcessing($template)->output($document) + ]; + $this->cache->write($tssCache->getCacheKey($data) . $this->template, $result); + } + unset($result['cache'], $result['renderTime']); + return (object) $result; + } + + private function createAndProcessTemplate($data, $body, &$headers) { $elementData = new \Transphporm\Hook\ElementData(new \SplObjectStorage(), $data); $functionSet = new FunctionSet($elementData); - - $cachedOutput = $this->loadTemplate(); //To be a valid XML document it must have a root element, automatically wrap it in ' ); + $template = new Template($this->isValidDoc($body) ? str_ireplace('' . $body . '' ); + $valueParser = new Parser\Value($functionSet); $this->config = new Config($functionSet, $valueParser, $elementData, new Hook\Formatter(), new Parser\CssToXpath($functionSet, $template->getPrefix(), md5($this->tss)), $this->filePath, $headers); foreach ($this->modules as $module) $module->load($this->config); - - $this->processRules($template, $this->config); - - $result = ['body' => $template->output($document), 'headers' => array_merge($cachedOutput['headers'], $headers)]; - $this->cache->write($this->template, $result); - $result['body'] = $this->doPostProcessing($template)->output($document); - return (object) $result; - } - - private function processRules($template, $config) { - $rules = $this->getRules($template, $config); - - foreach ($rules as $rule) { - if ($rule->shouldRun($this->time)) $this->executeTssRule($rule, $template, $config); - } + return $template; } //Add a postprocessing hook. This cleans up anything transphporm has added to the markup which needs to be removed @@ -85,35 +97,19 @@ private function doPostProcessing($template) { return $template; } - //Process a TSS rule e.g. `ul li {content: "foo"; format: bar} - private function executeTssRule($rule, $template, $config) { - $rule->touch(); - - $pseudoMatcher = $config->createPseudoMatcher($rule->pseudo); - $hook = new Hook\PropertyHook($rule->properties, $config->getLine(), $rule->file, $rule->line, $pseudoMatcher, $config->getValueParser(), $config->getFunctionSet(), $config->getFilePath()); - $config->loadProperties($hook); - $template->addHook($rule->query, $hook); - } //Load a template, firstly check if it's a file or a valid string private function loadTemplate() { - $result = ['body' => $this->template, 'headers' => []]; - if (file_exists($this->template)) $result = $this->loadTemplateFromFile($this->template); + $result = ['cache' => $this->template, 'headers' => []]; + if (strpos($this->template, "\n") === false && is_file($this->template)) $result = $this->loadTemplateFromFile($this->template); return $result; } private function loadTemplateFromFile($file) { - $xml = $this->cache->load($this->template, filemtime($this->template)); - return $xml ? $xml : ['body' => file_get_contents($this->template) ?: "", 'headers' => []]; + $xml = $this->cache->load($this->cacheKey . $this->template, filemtime($this->template)); + return $xml ? $xml : ['cache' => file_get_contents($this->template) ?: "", 'headers' => []]; } - //Load the TSS rules either from a file or as a string - //N.b. only files can be cached - private function getRules($template, $config) { - $cache = new TSSCache($this->cache, $template->getPrefix()); - return (new Parser\Sheet($this->tss, $config->getCssToXpath(), $config->getValueParser(), $cache, $config->getFilePath()))->parse(); - } - public function setCache(\ArrayAccess $cache) { $this->cache = new Cache($cache); } @@ -123,7 +119,7 @@ private function isValidDoc($xml) { } public function __destruct() { - //Required hack as DomXPath can only register static functions clear, the statically stored instance to avoid memory leaks + //Required hack as DomXPath can only register static functions clear the statically stored instance to avoid memory leaks if (isset($this->config)) $this->config->getCssToXpath()->cleanup(); } } diff --git a/src/Cache.php b/src/Cache.php index 7d3fc2d..cb41388 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -13,7 +13,7 @@ public function __construct(\ArrayAccess $cache) { } public function write($key, $content) { - $this->cache[md5($key)] = ['content' => $content, 'timestamp' => time()]; + $this->cache[md5($key)] = ['content' => $content, 'timestamp' => time()]; return $content; } diff --git a/src/Formatter/Locale/deDE.json b/src/Formatter/Locale/deDE.json index cf7764f..ce48e91 100644 --- a/src/Formatter/Locale/deDE.json +++ b/src/Formatter/Locale/deDE.json @@ -1,4 +1,4 @@ -{ +{ "thousands_separator": ".", "decimal_separator": ",", "currency": "€", diff --git a/src/FunctionSet.php b/src/FunctionSet.php index ab1a23d..be4276e 100644 --- a/src/FunctionSet.php +++ b/src/FunctionSet.php @@ -19,13 +19,14 @@ public function __call($name, $args) { if (isset($this->functions[$name])) { return $this->functions[$name]->run($this->getArgs0($name, $args), $this->element); } - + return false; } private function getArgs0($name, $args) { if (isset($this->functions[$name]) && !($this->functions[$name] instanceof TSSFunction\Data)) { $tokens = $args[0]; + if ($tokens->count() == 0) return []; $parser = new \Transphporm\Parser\Value($this); return $parser->parseTokens($tokens, $this->elementData->getData($this->element)); } diff --git a/src/Parser/Sheet.php b/src/Parser/Sheet.php index cc53eae..152bf77 100644 --- a/src/Parser/Sheet.php +++ b/src/Parser/Sheet.php @@ -7,37 +7,27 @@ namespace Transphporm\Parser; /** Parses a .tss file into individual rules, each rule has a query e,g, `ul li` and a set of rules e.g. `display: none; bind: iteration(id);` */ class Sheet { - private $cache; private $tss; - private $rules; - private $file; - private $valueParser; private $xPath; - private $filePath; - private $import = []; + private $valueParser; + private $sheetLoader; + private $file; + private $rules; - public function __construct($tss, CssToXpath $xPath, Value $valueParser, \Transphporm\TSSCache $cache, \Transphporm\FilePath $filePath) { - $this->cache = $cache; + public function __construct($tss, CssToXpath $xPath, Value $valueParser, \Transphporm\FilePath $filePath, \Transphporm\SheetLoader\SheetLoader $sheetLoader, $file = null) { $this->xPath = $xPath; $this->valueParser = $valueParser; $this->filePath = $filePath; - if (is_file($tss)) { - $this->file = $tss; - $this->rules = $this->cache->load($tss); - $this->filePath->addPath(dirname(realpath($tss))); - if (empty($this->rules)) $tss = file_get_contents($tss); - else return; - } + $this->sheetLoader = $sheetLoader; + $this->file = $file; $this->tss = (new Tokenizer($tss))->getTokens(); } public function parse($indexStart = 0) { if (!empty($this->rules)) return $this->rules['rules']; $rules = $this->parseTokens($indexStart); - usort($rules, [$this, 'sortRules']); $this->checkError($rules); - //var_dump($rules); - return $this->cache->write($this->file, $rules, $this->import); + return $rules; } private function parseTokens($indexStart) { @@ -46,7 +36,7 @@ private function parseTokens($indexStart) { if ($processing = $this->processingInstructions($token, count($this->rules)+$indexStart)) { $this->rules = array_merge($this->rules, $processing); } - else if ($token['type'] !== Tokenizer::NEW_LINE) $this->addRules($token, $indexStart++); + else if (!in_array($token['type'], [Tokenizer::NEW_LINE, Tokenizer::AT_SIGN])) $this->addRules($token, $indexStart++); } return $this->rules; @@ -57,7 +47,6 @@ private function addRules($token, $indexStart) { $this->tss->skip(count($selector)); if (count($selector) === 0) return; - $newRules = $this->cssToRules($selector, count($this->rules)+$indexStart, $this->getProperties($this->tss->current()['value']), $token['line']); $this->rules = $this->writeRule($this->rules, $newRules); } @@ -79,7 +68,6 @@ private function CssToRules($selector, $index, $properties, $line) { private function writeRule($rules, $newRules) { foreach ($newRules as $selector => $newRule) { - if (isset($rules[$selector])) { $newRule->properties = array_merge($rules[$selector]->properties, $newRule->properties); $newRule->index = $rules[$selector]->index; @@ -94,34 +82,24 @@ private function processingInstructions($token, $indexStart) { if ($token['type'] !== Tokenizer::AT_SIGN) return false; $tokens = $this->tss->from(Tokenizer::AT_SIGN, false)->to(Tokenizer::SEMI_COLON, false); $funcName = $tokens->from(Tokenizer::NAME, true)->read(); - $args = $this->valueParser->parseTokens($tokens->from(Tokenizer::NAME)); - $rules = $this->$funcName($args, $indexStart); - + $funcToks = $tokens->from(Tokenizer::NAME); + $args = $this->valueParser->parseTokens($funcToks); + $rules = $this->$funcName($args, $indexStart, $funcToks); $this->tss->skip(count($tokens)+2); return $rules; } - private function import($args, $indexStart) { + private function import($args, $indexStart, $tokens) { $fileName = $this->filePath->getFilePath($args[0]); - $this->import[] = $fileName; - $sheet = new Sheet($fileName, $this->xPath, $this->valueParser, $this->cache, $this->filePath); - return $sheet->parse($indexStart); - } - - private function sortRules($a, $b) { - //If they have the same depth, compare on index - if ($a->query === $b->query) return $this->sortPseudo($a, $b); + $this->sheetLoader->addImport($fileName); - if ($a->depth === $b->depth) $property = 'index'; - else $property = 'depth'; - - return ($a->$property < $b->$property) ? -1 : 1; + $tssFile = new \Transphporm\SheetLoader\TSSString(file_get_contents($fileName), $this->filePath); + return $tssFile->getRules($this->xPath, $this->valueParser, $this->sheetLoader, $indexStart); } - - private function sortPseudo($a, $b) { - return count($a->pseudo) < count($b->pseudo) ? -1 :1; + private function cacheKey($args, $indexStart, $tokens) { + $this->sheetLoader->setCacheKey($tokens); } private function getProperties($tokens) { diff --git a/src/Parser/Tokens.php b/src/Parser/Tokens.php index e9be4a4..cc1100f 100644 --- a/src/Parser/Tokens.php +++ b/src/Parser/Tokens.php @@ -39,7 +39,7 @@ public function rewind() { } public function add($token) { - if ($token instanceof Tokens) $this->tokens = array_merge($token->tokens); + if ($token instanceof Tokens) $this->tokens = array_merge($this->tokens, $token->tokens); else $this->tokens[] = $token; } diff --git a/src/Parser/ValueResult.php b/src/Parser/ValueResult.php index 0564c85..186abb4 100644 --- a/src/Parser/ValueResult.php +++ b/src/Parser/ValueResult.php @@ -108,7 +108,6 @@ private function write($index, $value, $allowNull = false) { //Postprocessing - replace values with null where allowed, or override a value at position public function postProcess(ValueData $data, $val, $overrideVal, $allowNull) { - if ($this->getMode() !== Tokenizer::ARG) return; foreach ($this->getResult() as $i => $value) { if (is_scalar($value)) { $val = ($overrideVal == $val) ? $data->read($value) : $overrideVal; diff --git a/src/Property/Content.php b/src/Property/Content.php index 00dcd45..11f7ff2 100644 --- a/src/Property/Content.php +++ b/src/Property/Content.php @@ -22,7 +22,10 @@ public function run(array $values, \DomElement $element, array $rules, \Transphp //Remove the current contents $this->removeAllChildren($element); //Now make a text node - if ($this->getContentMode($rules) === 'replace') $this->replaceContent($element, $values); + if ($this->getContentMode($rules) === 'replace') { + $contentReplace = new ContentReplace($this); + $contentReplace->replaceContent($element, $values); + } else $this->appendContent($element, $values); } } @@ -42,7 +45,7 @@ private function getContentMode($rules) { public function addContentPseudo($name, ContentPseudo $contentPseudo) { $this->contentPseudo[$name] = $contentPseudo; } - + private function processPseudo($value, $element, $pseudoMatcher) { foreach ($this->contentPseudo as $pseudoName => $pseudoFunction) { if ($pseudoMatcher->hasFunction($pseudoName)) { @@ -67,8 +70,6 @@ public function getNode($node, $document) { private function convertNode($node, $document) { if ($node instanceof \DomElement || $node instanceof \DOMComment) { $new = $document->importNode($node, true); - //Removing this might cause problems with caching... - //$new->setAttribute('transphporm', 'added'); } else { if ($node instanceof \DomText) $node = $node->nodeValue; @@ -80,13 +81,6 @@ private function convertNode($node, $document) { return $new; } - private function replaceContent($element, $content) { - //If this rule was cached, the elements that were added last time need to be removed prior to running the rule again. - foreach ($this->getNode($content, $element->ownerDocument) as $node) { - $element->parentNode->insertBefore($node, $element); - } - $element->setAttribute('transphporm', 'remove'); - } private function appendContent($element, $content) { foreach ($this->getNode($content, $element->ownerDocument) as $node) { @@ -97,4 +91,4 @@ private function appendContent($element, $content) { private function removeAllChildren($element) { while ($element->hasChildNodes()) $element->removeChild($element->firstChild); } -} +} \ No newline at end of file diff --git a/src/Property/ContentReplace.php b/src/Property/ContentReplace.php new file mode 100644 index 0000000..78fd95e --- /dev/null +++ b/src/Property/ContentReplace.php @@ -0,0 +1,54 @@ + | https://r.je/ * + * @license http://www.opensource.org/licenses/bsd-license.php BSD License * + * @version 1.2 */ +namespace Transphporm\Property; +class ContentReplace { + private $content; + + public function __construct(Content $content) { + $this->content = $content; + } + + public function replaceContent($element, $content) { + if ($element->getAttribute('transphporm') == 'added') return; + //If this rule was cached, the elements that were added last time need to be removed prior to running the rule again. + if ($element->getAttribute('transphporm')) { + $this->replaceCachedContent($element); + } + + $this->insertNodes($element, $content); + + //Remove the original element from the final output + $element->setAttribute('transphporm', 'remove'); + } + + private function insertNodes($element, $content) { + foreach ($this->content->getNode($content, $element->ownerDocument) as $node) { + if ($node instanceof \DomElement && !$node->getAttribute('transphporm')) $node->setAttribute('transphporm', 'added'); + $element->parentNode->insertBefore($node, $element); + } + } + + private function replaceCachedContent($element) { + $el = $element; + while ($el = $el->previousSibling) { + if ($el->nodeType == 1 && $el->getAttribute('transphporm') != 'remove') { + $el->parentNode->removeChild($el); + } + } + $this->fixPreserveWhitespaceRemoveChild($element); + } + + // $doc->preserveWhiteSpace = false should fix this but it doesn't + // Remove extra whitespace created by removeChild to avoid the cache growing 1 byte every time it's reloaded + // This may need to be moved in future, anywhere elements are being removed and files are cached may need to apply this fix + // Also remove any comments to avoid the comment being re-added every time the cache is reloaded + private function fixPreserveWhitespaceRemoveChild($element) { + if ($element->previousSibling instanceof \DomComment || ($element->previousSibling instanceof \DomText && $element->previousSibling->isElementContentWhiteSpace())) { + $element->parentNode->removeChild($element->previousSibling); + } + } +} \ No newline at end of file diff --git a/src/Rule.php b/src/Rule.php index b34f853..76d4194 100644 --- a/src/Rule.php +++ b/src/Rule.php @@ -45,10 +45,8 @@ public function touch() { private function timeFrequency($frequency, $time = null) { if ($time === null) $time = time(); - $num = (int) $frequency; - $unit = strtoupper(trim(str_replace($num, '', $frequency))); - $offset = $num * constant(self::class . '::' . $unit); + $offset = $this->getUpdateFrequency(); if ($time > $this->lastRun + $offset) return true; else return false; @@ -63,4 +61,20 @@ public function shouldRun($time = null) { } else return true; } + + public function getUpdateFrequency() { + $frequency = isset($this->properties['update-frequency']) ? $this->properties['update-frequency']->read() : false; + + if (empty($frequency)) return 0; + else return $this->calcUpdateFrequency($frequency); + } + + private function calcUpdateFrequency($frequency) { + $num = (int) $frequency; + $unit = strtoupper(trim(str_replace($num, '', $frequency))); + if ($frequency == 'always') return 0; + else if ($frequency == 'never') return self::D*3650; //Not quite never, in 10 years will cause issues on 32 bit PHP builds re 2038 problem + + return $num * constant(self::class . '::' . $unit); + } } diff --git a/src/SheetLoader/SheetLoader.php b/src/SheetLoader/SheetLoader.php new file mode 100644 index 0000000..07c7a9a --- /dev/null +++ b/src/SheetLoader/SheetLoader.php @@ -0,0 +1,98 @@ + | https://r.je/ * + * @license http://www.opensource.org/licenses/bsd-license.php BSD License * + * @version 1.2 */ +namespace Transphporm\SheetLoader; +//Separates out TSS file loading/caching from parsing +class SheetLoader { + private $tss; + private $filePath; + private $time; + private $import = []; + + public function __construct(\Transphporm\Cache $cache, \Transphporm\FilePath $filePath, TSSRules $tss, $time) { + $this->cache = $cache; + $this->filePath = $filePath; + $this->tss = $tss; + $this->time = $time ?? time(); + } + + //Allows controlling whether any updates are required to the template + //e.g. return false + // 1. If all update-frequencies haven't expired + // 2. If the data hasn't changed since the last run + //If this function returns false, the rendered template is sent straight from the cache skipping 99% of transphporm's code + public function updateRequired($data) { + return $this->tss->updateRequired($data); + } + + public function addImport($import) { + $this->filePath->addPath(dirname(realpath($this->filePath->getFilePath($import)))); + $this->import[] = $import; + } + + public function setCacheKey($tokens) { + $newTokens = []; + foreach ($tokens as $token) { + if ($token['type'] == \Transphporm\Parser\Tokenizer::NAME && $token['value'] == 'data') { + $tokens->next(); + $newTokens = array_merge($newTokens, iterator_to_array($tokens->current()['value'])); + } + else $newTokens[] = $token; + } + + $this->tss->setCacheKey(new \Transphporm\Parser\Tokens($newTokens)); + } + + public function getCacheKey($data) { + return $this->tss->getCacheKey($data); + } + + + public function processRules($template, \Transphporm\Config $config) { + $rules = $this->getRules($config->getCssToXpath(), $config->getValueParser()); + + + usort($rules, [$this, 'sortRules']); + + foreach ($rules as $rule) { + if ($rule->shouldRun($this->time)) $this->executeTssRule($rule, $template, $config); + } + + //if (is_file($this->tss)) $this->write($this->tss, $rules, $this->import); + $this->tss->write($rules, $this->import); + } + + //Load the TSS + public function getRules($cssToXpath, $valueParser, $indexStart = 0) { + return $this->tss->getRules($cssToXpath, $valueParser, $this, $indexStart); + } + + //Process a TSS rule e.g. `ul li {content: "foo"; format: bar} + private function executeTssRule($rule, $template, $config) { + $rule->touch(); + + $pseudoMatcher = $config->createPseudoMatcher($rule->pseudo); + $hook = new \Transphporm\Hook\PropertyHook($rule->properties, $config->getLine(), $rule->file, $rule->line, $pseudoMatcher, $config->getValueParser(), $config->getFunctionSet(), $config->getFilePath()); + $config->loadProperties($hook); + $template->addHook($rule->query, $hook); + } + + + private function sortRules($a, $b) { + //If they have the same depth, compare on index + if ($a->query === $b->query) return $this->sortPseudo($a, $b); + + if ($a->depth === $b->depth) $property = 'index'; + else $property = 'depth'; + + return ($a->$property < $b->$property) ? -1 : 1; + } + + + private function sortPseudo($a, $b) { + return count($a->pseudo) > count($b->pseudo) ? 1 : -1; + } +} diff --git a/src/SheetLoader/TSSFile.php b/src/SheetLoader/TSSFile.php new file mode 100644 index 0000000..804c255 --- /dev/null +++ b/src/SheetLoader/TSSFile.php @@ -0,0 +1,96 @@ + | https://r.je/ * + * @license http://www.opensource.org/licenses/bsd-license.php BSD License * + * @version 1.2 */ +namespace Transphporm\SheetLoader; +class TSSFile implements TSSRules { + private $fileName; + private $cacheName; + private $cacheKey; + private $cache; + private $time; + + public function __construct($fileName, \Transphporm\FilePath $filePath, $cache, $time) { + $this->fileName = $fileName; + $this->filePath = $filePath; + $this->cache = $cache; + $this->time = $time ?? time(); + $this->cacheName = $this->fileName; + } + + private function getRulesFromCache($file) { + //Try to load the cached rules, if not set in the cache (or expired) parse the supplied sheet + $rules = $this->cache->load($this->cacheName, filemtime($file)); + + $this->cacheKey = $this->cacheKey ?? $rules['cacheKey'] ?? null; + + if ($rules) { + foreach ($rules['import'] as $file) { + //Check that the import file hasn't been changed since the cache was written + if (filemtime($file) > $rules['ctime']) return false; + } + } + + return $rules; + } + + public function setCacheKey($tokens) { + $this->cacheKey = $tokens; + } + + public function updateRequired($data) { + $this->cacheName = $this->getCacheKey($data) . $this->fileName; + + $rules = $this->getRulesFromCache($this->fileName, $data); + //Nothing was cached or the TSS file has changed, update is required + if (empty($rules)) return true; + + //Find the sheet's minimum update-frequency, if it hasn't passed then no updates are required + if ($rules['ctime']+$rules['minFreq'] <= $this->time) return true; + + return false; + } + + public function getCacheKey($data) { + $this->getRulesFromCache($this->fileName); + if ($this->cacheKey) { + $parser = new \Transphporm\Parser\Value($data); + $cacheKey = $parser->parseTokens($this->cacheKey)[0]; + $this->cacheName = $cacheKey . $this->fileName; + return $cacheKey; + } + else return ''; + } + + public function getRules($cssToXpath, $valueParser, $sheetLoader, $indexStart) { + $rules = $this->getRulesFromCache($this->fileName)['rules']; + $this->filePath->addPath(dirname(realpath($this->fileName))); + if (empty($rules)) $tss = file_get_contents($this->fileName); + else return $rules; + + return $tss == null ? [] : (new \Transphporm\Parser\Sheet($tss, $cssToXpath, $valueParser, $this->filePath, $sheetLoader))->parse($indexStart); + } + + //write the sheet to cache + public function write($rules, $imports = []) { + $existing = $this->cache->load($this->fileName, filemtime($this->fileName)); + if (isset($existing['import']) && empty($imports)) $imports = $existing['import']; + $this->cache->write($this->cacheName, ['rules' => $rules, 'import' => $imports, 'minFreq' => $this->getMinUpdateFreq($rules), 'ctime' => $this->time, 'cacheKey' => $this->cacheKey]); + + return $rules; + } + + //Gets the minimum update-frequency for a sheet's rules + private function getMinUpdateFreq($rules) { + $min = \PHP_INT_MAX; + + foreach ($rules as $rule) { + $ruleFreq = $rule->getUpdateFrequency(); + if ($ruleFreq < $min) $min = $ruleFreq; + } + + return $min; + } +} \ No newline at end of file diff --git a/src/SheetLoader/TSSRules.php b/src/SheetLoader/TSSRules.php new file mode 100644 index 0000000..e20a2c2 --- /dev/null +++ b/src/SheetLoader/TSSRules.php @@ -0,0 +1,9 @@ + | https://r.je/ * + * @license http://www.opensource.org/licenses/bsd-license.php BSD License * + * @version 1.2 */ +namespace Transphporm\SheetLoader; +//Strings do not support caching because we cannot know when the containing PHP script has been modified +class TSSString implements TSSRules { + private $str; + private $filePath; + + public function __construct($str, $filePath) { + $this->str = $str; + $this->filePath = $filePath; + } + + public function updateRequired($data) { + return true; + } + + public function getCacheKey($data) { + return ''; + } + + public function getRules($cssToXpath, $valueParser, $sheetLoader, $indexStart) { + return (new \Transphporm\Parser\Sheet($this->str, $cssToXpath, $valueParser, $this->filePath, $sheetLoader))->parse($indexStart); + } + + public function write($rules, $imports = []) { + return; + } + + public function setCacheKey($tokens) { + return; + } +} \ No newline at end of file diff --git a/src/TSSCache.php b/src/TSSCache.php deleted file mode 100644 index f20c969..0000000 --- a/src/TSSCache.php +++ /dev/null @@ -1,44 +0,0 @@ - | https://r.je/ * - * @license http://www.opensource.org/licenses/bsd-license.php BSD License * - * @version 1.2 */ -namespace Transphporm; -class TSSCache { - private $cache; - private $prefix; - - public function __construct(Cache $cache, $prefix) { - $this->cache = $cache; - $this->prefix = $prefix; - } - - private function getRulesFromCache($file) { - //The cache for the key: the filename and template prefix - //Each template may have a different prefix which changes the parsed TSS, - //Because of this the cache needs to be generated for each template prefix. - $key = $this->getCacheKey($file); - //Try to load the cached rules, if not set in the cache (or expired) parse the supplied sheet - $rules = $this->cache->load($key, filemtime($file)); - if ($rules) { - foreach ($rules['import'] as $file) { - if (!$this->cache->load($this->getCacheKey($file), filemtime($file))) return false; - } - } - return $rules; - } - - private function getCacheKey($file) { - return $file . $this->prefix . dirname(realpath($file)) . DIRECTORY_SEPARATOR; - } - - public function load($tss) { - return $this->getRulesFromCache($tss); - } - - public function write($file, $rules, $imports = []) { - if (is_file($file)) $this->cache->write($this->getCacheKey($file), ['rules' => $rules, 'import' => $imports]); - return $rules; - } -} diff --git a/src/Template.php b/src/Template.php index f7bcbd6..371a438 100644 --- a/src/Template.php +++ b/src/Template.php @@ -16,7 +16,8 @@ class Template { /** Takes an XML string and loads it into a DomDocument object */ public function __construct($doc) { $this->document = new \DomDocument; - + //This should remove whitespace left behind after ->removeChild but it doesn't + $this->document->preserveWhiteSpace = false; $this->loadDocument($doc); $this->xpath = new \DomXPath($this->document); @@ -46,7 +47,7 @@ private function loadDocument($doc) { //XML was loaded, save as XML. $this->save = function($content = null) { return $this->document->saveXml($content, LIBXML_NOEMPTYTAG); - }; + }; } diff --git a/tests/CacheTest.php b/tests/CacheTest.php index 890a20d..246f3a8 100644 --- a/tests/CacheTest.php +++ b/tests/CacheTest.php @@ -40,6 +40,7 @@ private function buildTemplate($frequency, $cache, $time = null) { list($xml, $css) = $this->createFiles($frequency); $template = new Builder($xml, $css); + if ($time) $template->setTime($time); $template->setCache($cache); @@ -65,7 +66,6 @@ public function testCacheBasic() { public function testCacheMinutes() { - $cache = new \ArrayObject; $random = new RandomGenerator; @@ -84,7 +84,6 @@ public function testCacheMinutes() { $date = new \DateTime(); $date->modify('+11 minutes'); - $o3 = $this->buildTemplate('10m', $cache, $date->format('U'))->output($random, false)->body; //The random nummber should now be refreshed and the contents changed @@ -245,6 +244,34 @@ public function testCacheWithAttribute() { $this->assertEquals($expectedOutput, $this->stripTabs($template->output(['hide' => 2])->body)); } + + public function testContentModeReplaceCache() { + $xml = $this->makeXml('
    +

    To be replaced

    +
    '); + + $tss = $this->makeTss(' + h1 {content: data(replacement); content-mode: replace;} + '); + + + $cache = new \ArrayObject(); + $template = new \Transphporm\Builder($xml, $tss); + $template->setCache($cache); + + $output1 = $template->output(['replacement' => 'r1'])->body; + $this->assertEquals('
    r1
    ', $this->stripTabs($output1)); + + + $template = new \Transphporm\Builder($xml, $tss); + $template->setCache($cache); + + $output2 = $template->output(['replacement' => 'r2'])->body; + $this->assertEquals('
    r2
    ', $this->stripTabs($output2)); + + + } + } class RandomGenerator { diff --git a/tests/TransphpormTest.php b/tests/TransphpormTest.php index 67c39cf..18db751 100644 --- a/tests/TransphpormTest.php +++ b/tests/TransphpormTest.php @@ -1784,18 +1784,17 @@ public function testImportHTMLIntoXML() { public function testSetLocale() { $xml = '
    '; - $tss = 'div {content: "now"; format: date}'; + $tss = 'div {content: "2018-04-03"; format: date}'; $template1 = new \Transphporm\Builder($xml, $tss); - - $this->assertEquals('
    ' . date('d/m/Y') . '
    ', $template1->output()->body); + $this->assertEquals('
    03/04/2018
    ', $template1->output()->body); $template2 = new \Transphporm\Builder($xml, $tss); $template2->setLocale('enUS'); - $this->assertEquals('
    ' . date('m/d/Y') . '
    ', $template2->output()->body); + $this->assertEquals('
    04/03/2018
    ', $template2->output()->body); } public function testDebugOutput() { diff --git a/tests/temp.tss b/tests/temp.tss index 79a802f..b308273 100644 --- a/tests/temp.tss +++ b/tests/temp.tss @@ -1,3 +1,3 @@ - span {display: block; update-frequency: always } - span[data-hide=data(hide)] { display: none; update-frequency: always } \ No newline at end of file + h1 {content: data(replacement); content-mode: replace;} + \ No newline at end of file diff --git a/tests/temp.xml b/tests/temp.xml index 791c39c..ee1cb0b 100644 --- a/tests/temp.xml +++ b/tests/temp.xml @@ -1,4 +1,3 @@
    - Test1 - Test2 +

    To be replaced

    \ No newline at end of file