├── .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 | [](https://travis-ci.org/drewm/selecta)
4 | [](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 |
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 ''.self::html($name).'>';
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);
23 | }
24 |
25 | public function testCloseTags()
26 | {
27 | $result = Selecta::close('ul li');
28 | $this->assertEquals(' ', $result);
29 | }
30 |
31 | }
--------------------------------------------------------------------------------