├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── Selecta.php └── tests ├── AttributeTest.php ├── BasicTagTest.php ├── ClassTest.php ├── ExtremeTest.php ├── IdTest.php ├── SelectaTest.php └── StackedTagTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | # .scrutinizer.yml 2 | checks: 3 | php: 4 | code_rating: true 5 | duplication: true 6 | 7 | filter: 8 | excluded_paths: 9 | - tests/* 10 | - vendor/* 11 | paths: 12 | - src/* 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | before_script: 9 | - composer self-update 10 | - composer install --prefer-source --no-interaction --dev 11 | script: phpunit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Drew McLellan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selecta: super-light casual templating using CSS selectors 2 | 3 | [![Build Status](https://travis-ci.org/drewm/selecta.svg)](https://travis-ci.org/drewm/selecta) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/drewm/selecta/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/drewm/selecta/?branch=master) 5 | 6 | Use a CSS selector to wrap your content with HTML tags. 7 | 8 | ```php 9 | echo Selecta::wrap('h1.welcome', 'Hello, world'); 10 | ``` 11 | 12 | will output: 13 | 14 | ```html 15 |

Hello, world

16 | ``` 17 | 18 | ## Why? 19 | 20 | Sometimes you need to output a quick bit of HTML at a point where it's really inconvenient to use a full template. Creating strings of HTML in your code is horrible, so this something a bit more humane. 21 | 22 | ## Usage 23 | 24 | Currently supports IDs, classes and attribute selectors. 25 | 26 | ### Class names 27 | 28 | ```php 29 | echo Selecta::wrap('ul.list li', 'So listy'); 30 | ``` 31 | 32 | will output: 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | ### IDs 39 | 40 | ```php 41 | echo Selecta::wrap('div#contact', 'Call me'); 42 | ``` 43 | 44 | will output: 45 | 46 | ```html 47 |
Call me
48 | ``` 49 | 50 | ### Attribute and Pseudo-class selectors 51 | 52 | ```php 53 | echo Selecta::build('input[type=radio][name=color][value=blue]:checked'); 54 | ``` 55 | 56 | will output: 57 | 58 | ```html 59 | 60 | ``` 61 | 62 | Currently supports `:checked` and `:disabled` pseudo-classes. 63 | 64 | ### Mix it up 65 | 66 | All these can be combined and stacked: 67 | 68 | ```php 69 | echo Selecta::build('form#contact div.field input[type=text][required]'); 70 | ``` 71 | 72 | will output (indented for clarity): 73 | 74 | ```html 75 |
76 |
77 | 78 |
79 |
80 | ``` 81 | 82 | ## Methods 83 | 84 | The following methods are available: 85 | 86 | `Selecta::wrap(selector, contents)` will wrap the contents with the tags created by the selector. 87 | 88 | `Selecta::open(selector)` will open the tags created by the selector. 89 | 90 | `Selecta::close(selector)` will close the tags created by the selector. Note that the order of tags is reversed - you can use the same selector string with `open()` and `close()` to get valid tag pairs. 91 | 92 | `Selecta::build(selector, contents, open, close)` will do everything - build the tags, optionally wrap the contents, optionally open and optionally close the tags. 93 | 94 | ### Opening and closing 95 | 96 | Don't have a template to hand but need to output some structural markup? 97 | 98 | ```php 99 | echo Selecta::open('section.sidebar div'); 100 | echo $CMS->display_all_my_weird_sidebar_junk(); 101 | echo Selecta::close('section div'); 102 | ``` 103 | 104 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drewm/selecta", 3 | "description": "", 4 | "license": "MIT", 5 | "keywords": ["selector"], 6 | "authors": [ 7 | { 8 | "name": "Drew McLellan", 9 | "email": "drew.mclellan@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.3" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "4.0.*" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "DrewM\\Selecta\\": "src" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Selecta.php: -------------------------------------------------------------------------------- 1 | 'class', '#'=>'id'); 10 | public static $metas_from = array('.', ':', '#', ' '); 11 | public static $metas_to = array('__DOT__', '__COLON__', '__HASH__', '__SPACE__'); 12 | 13 | public static function build($selector, $contents='', $open_tags=true, $close_tags=true) 14 | { 15 | $selector = self::sanitise_attribute_metas($selector); 16 | $parts = explode(' ', $selector); 17 | 18 | if (count($parts)) { 19 | $parts = array_reverse($parts); 20 | foreach($parts as $part) { 21 | $contents = self::tagify($part, $contents, $open_tags, $close_tags); 22 | } 23 | } 24 | return $contents; 25 | } 26 | 27 | public static function wrap($selector, $contents='') 28 | { 29 | return self::build($selector, $contents, true, true); 30 | } 31 | 32 | public static function open($selector) 33 | { 34 | return self::build($selector, '', true, false); 35 | } 36 | 37 | public static function close($selector) 38 | { 39 | return self::build($selector, '', false, true); 40 | } 41 | 42 | private static function tagify($selector='', $contents='', $open_tags=true, $close_tags=true) 43 | { 44 | $attrs = array(); 45 | 46 | $metas = '\.\#\[\:'; 47 | $pattern = '/(['.$metas.'])([^'.$metas.']*)?/'; 48 | preg_match_all($pattern, $selector, $matches, PREG_SET_ORDER); 49 | 50 | if (count($matches)) { 51 | foreach($matches as $match) { 52 | $attrs = self::build_attributes($match[1], $match[2], $attrs); 53 | } 54 | 55 | // reduce selector to just tag name 56 | $parts = preg_split('/['.$metas.']/', $selector); 57 | $selector = $parts[0]; 58 | } 59 | 60 | return self::build_tag($selector, $attrs, $contents, $open_tags, $close_tags); 61 | } 62 | 63 | private static function build_attributes($meta_char, $value, $attrs) 64 | { 65 | $key = false; 66 | if (isset(self::$meta_map[$meta_char])) { 67 | $key = self::$meta_map[$meta_char]; 68 | }else{ 69 | switch ($meta_char) { 70 | 71 | // Attribute selectors 72 | case '[': 73 | list($key, $value) = self::build_attribute_selector_attribute($value); 74 | break; 75 | 76 | // Pseudo-class selectors 77 | case ':': 78 | list($key, $value) = self::build_pseudo_class_attribute($value); 79 | break; 80 | } 81 | } 82 | 83 | if ($key){ 84 | if (isset($attrs[$key])) { 85 | $attrs[$key] .= ' '.$value; 86 | }else{ 87 | $attrs[$key] = $value; 88 | } 89 | } 90 | 91 | return $attrs; 92 | } 93 | 94 | private static function build_attribute_selector_attribute($value) 95 | { 96 | $value = rtrim($value, ']'); 97 | 98 | if (strpos($value, '=')) { 99 | $parts = explode('=', $value, 2); 100 | $key = $parts[0]; 101 | $value = self::unsanitise_attribute_metas($parts[1]); 102 | }else{ 103 | $key = $value; 104 | $value = false; 105 | } 106 | 107 | return array($key, $value); 108 | } 109 | 110 | private static function build_pseudo_class_attribute($pclass='') 111 | { 112 | if (in_array($pclass, self::$pseudo_classes)) { 113 | return array($pclass, false); 114 | } 115 | 116 | return array(false, false); 117 | } 118 | 119 | private static function build_tag($name, $attributes=array(), $contents='', $open_tag=true, $close_tag=true) 120 | { 121 | $tag = ''; 122 | 123 | if ($open_tag) { 124 | $tag = self::open_tag($name, $attributes); 125 | } 126 | 127 | if (in_array($name, self::$single_tags)) { 128 | return $contents.$tag; 129 | } 130 | 131 | $tag .= $contents; 132 | 133 | if ($close_tag) { 134 | $tag .= self::close_tag($name); 135 | } 136 | 137 | return $tag; 138 | } 139 | 140 | private static function open_tag($name, $attributes=array()) 141 | { 142 | $tag = '<'.self::html($name); 143 | if (count($attributes)) { 144 | // do attributes 145 | $attpairs = array(); 146 | foreach($attributes as $key=>$val) { 147 | if ($val!='') { 148 | $attpairs[] = self::html($key).'="'.self::html($val, true).'"'; 149 | }else{ 150 | $attpairs[] = self::html($key); 151 | } 152 | } 153 | $tag .= ' '.implode(' ', $attpairs); 154 | } 155 | $tag .= '>'; 156 | 157 | return $tag; 158 | } 159 | 160 | private static function close_tag($name) 161 | { 162 | return ''; 163 | } 164 | 165 | private static function html($s, $quotes=false, $double_encode=false) 166 | { 167 | if ($s || (is_string($s) && strlen($s))) { 168 | return htmlspecialchars($s, ($quotes?ENT_QUOTES:ENT_NOQUOTES), 'UTF-8', $double_encode); 169 | } 170 | return ''; 171 | } 172 | 173 | private static function sanitise_attribute_metas($selector) 174 | { 175 | if (strpos($selector, '[')!==false) { 176 | preg_match_all('/\[.*?\]/', $selector, $matches, PREG_SET_ORDER); 177 | if (count($matches)) { 178 | foreach($matches as $match) { 179 | $exact = $match[0]; 180 | $new = str_replace( 181 | self::$metas_from, 182 | self::$metas_to, 183 | $exact); 184 | $selector = str_replace($exact, $new, $selector); 185 | } 186 | } 187 | } 188 | return $selector; 189 | } 190 | 191 | private static function unsanitise_attribute_metas($string) 192 | { 193 | return str_replace(self::$metas_to, self::$metas_from, $string); 194 | } 195 | 196 | } -------------------------------------------------------------------------------- /tests/AttributeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('
', $result); 11 | } 12 | 13 | public function testAttributeSelectorNoValue() 14 | { 15 | $result = Selecta::build('div[foo]'); 16 | $this->assertEquals('
', $result); 17 | } 18 | 19 | public function testDoubleAttributeSelector() 20 | { 21 | $result = Selecta::build('input[type=text][name=foo]'); 22 | $this->assertEquals('', $result); 23 | } 24 | 25 | public function testDoubleEqualsAttributeSelector() 26 | { 27 | $result = Selecta::build('a[href=?page=1]'); 28 | $this->assertEquals('', $result); 29 | } 30 | 31 | public function testPathWithQueryAttributeSelector() 32 | { 33 | $result = Selecta::build('a[href=/foo/bar/?page=1&sort=abc]'); 34 | $this->assertEquals('', $result); 35 | } 36 | 37 | public function testAttributeSelectorWithDot() 38 | { 39 | $result = Selecta::build('a[href=bar.html]'); 40 | $this->assertEquals('', $result); 41 | } 42 | 43 | public function testAttributeSelectorWithFQDN() 44 | { 45 | $result = Selecta::build('a[href=https://secure.gaug.es/dashboard#/foo]'); 46 | $this->assertEquals('', $result); 47 | } 48 | 49 | public function testCheckedSelector() 50 | { 51 | $result = Selecta::build('input[type=checkbox]:checked'); 52 | $this->assertEquals('', $result); 53 | } 54 | 55 | public function testDisabledSelector() 56 | { 57 | $result = Selecta::build('input[type=text]:disabled'); 58 | $this->assertEquals('', $result); 59 | } 60 | 61 | 62 | } -------------------------------------------------------------------------------- /tests/BasicTagTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('
', $result); 11 | } 12 | 13 | public function testSimpleTagWithContent() 14 | { 15 | $result = Selecta::wrap('div', 'Hello'); 16 | $this->assertEquals('
Hello
', $result); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /tests/ClassTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('
', $result); 11 | } 12 | 13 | public function testDoubleClass() 14 | { 15 | $result = Selecta::build('div.foo.bar'); 16 | $this->assertEquals('
', $result); 17 | } 18 | 19 | 20 | } -------------------------------------------------------------------------------- /tests/ExtremeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('', $result); 11 | } 12 | 13 | public function testProblem1() 14 | { 15 | $result = Selecta::wrap('a[href=/content/page/templates/][title=My pages].tab-active', 'hello'); 16 | $this->assertEquals('hello', $result); 17 | } 18 | 19 | 20 | 21 | } -------------------------------------------------------------------------------- /tests/IdTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('
', $result); 11 | } 12 | 13 | public function testIdAndClass() 14 | { 15 | $result = Selecta::build('div#foo.bar'); 16 | $this->assertEquals('
', $result); 17 | } 18 | 19 | 20 | } -------------------------------------------------------------------------------- /tests/SelectaTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(is_object($selecta)); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /tests/StackedTagTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('', $result); 11 | } 12 | 13 | public function testStackedTagsWithContent() 14 | { 15 | $result = Selecta::build('ul li', 'Hello'); 16 | $this->assertEquals('', $result); 17 | } 18 | 19 | public function testOpenTags() 20 | { 21 | $result = Selecta::open('ul li'); 22 | $this->assertEquals('', $result); 29 | } 30 | 31 | } --------------------------------------------------------------------------------