Skip to content

Transformation Style Sheets - CSS like sheets for display logic

Notifications You must be signed in to change notification settings

headinbeds/Transphporm

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Transphporm Style Sheets

Scrutinizer-CI

Gitter

Transphporm is fresh look at templating in PHP. Let's face it, Templating in PHP sucks it involves code like this:

<ul>
<?php foreach ($users as $user) : ?>
	<li><?= $user->name; ?></li>
<?php endforeach; ?>
</ul>

Or some variation of this mess:

<ul>
{% for user in users %}
	<li>{{ user.username|e }}</li>
{% endfor %}
</ul>

or

<ul>
	{users}
		<li>{user.name}</li>
	{/users}
</ul>

Why does this suck? It mixes the logic with the template. There are processing instructions mixed inside the template, in the case of the middle example, it's just barely abstracted PHP code. Whoever is writing the template is also in charge of writing the display logic and understanding the data structures that have been supplied.

Template systems like this still mix logic and markup, the one thing they're trying to avoid.

This is equivalent to <h1 style="font-weight:bold">Title</h1>, 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.
  3. To be a lightweight library (Currently it's less than 500 lines and a total cyclomatic complexity (a count of if statements, functions and loops) of less than 100 for the entire project)

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!)

<ul>
	<li>User name</li>
</ul>

It's pure HTML without any processing instructions. Transphporm then takes the XML and renders it with some data.

But where are the processing instructions? Transphporm follows CSS's lead and all this processing logic is stored externally in "Transformation Style Sheets", a completely separate file that contains entirely reusable processing instructions.

At it's most basic, Transphporm works by suppling a stylesheet and XML as strings.

The stylesheet can supply content to a targetted element. For example, this stylesheet:

h1 {content: "My Title";}

Will set the content of any H1 Tag to "My Title". Given the following code:

$xml = '<h1>Original Title</h1>';

$tss = 'h1 {content: "Replaced Title"; }';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

The output will be:

<h1>Replaced Title</h1>

The arguments for Transphporm\Builder can either be xml and tss strings, or file names to load.

//Load files instead of strings, the base path is the current working directory (getcwd())
$template = new \Transphporm\Builder('template.xml', 'stylesheet.tss');

Data

It's not usually possible to specify the content in a static file like a stylesheet. The tss format also allows referencing external data. This data is supplied using to the template builder's output method and can be referened in the stylesheet using the data() function. This can be though of like the url() function in CSS, in that it references an external resource.

$xml = '<h1>Original Title</h1>';

$data = 'My Title!'

$tss = 'h1 {content: data(); }';


$template = new \Transphporm\Builder($xml, $tss)

echo $template->output($data)->body;

Output:

<h1>My Title!</h1>

Most of the time, you will need to work with much more complex data structures. Transphporm allows for reading data from within data structures using the inbuilt data function:

$data = new stdclass;

$data->title = 'My Title!';
$data->description = 'Description of the page...';


$xml = '
	<h1>Example Title</h1>
	<p>Example Description</p>
	';

$tss = '
	h1 {content: data(title);}
	p {content: data(description);}
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

Which will output:

<h1>My Title!</h1>
<p>Description of the page....</p>

Content

The content property can take multiple values, either a function call such as data or a quoted string as each value and will concatenate any supplied values:

$xml = '<h1>Original Title</h1>';

$data = 'My Title!'

$tss = 'h1 {content: "Title: ", data(); }';


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

Output:

<h1>Title: My Title!</h1>

Loops

Going back to the user list example, consider the following data structure:

$users = [];

$user = new stdclass;
$user->name = 'Tom';
$user->email = 'tom@example.org';

$users[] = $user;


$user = new stdclass;
$user->name = 'Scott';
$user->email = 'scott@example.org';

$users[] = $user;

Using Transphporm, the user list can be generated like this:

$xml = '<ul>
	<li>Name</li>	
</ul>';


$tss = '
	ul li {repeat: data(users); content: iteration(name);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

repeat tells Transphporm to repeat the selected element for each of the supplied array.

data(users) reads $data['users'] as supplied in PHP.

iteration(name) points at the value for the current iteration and reads the name property. This code outputs:

<ul>
	<li>Tom</li>	
	<li>Scott</li>	
</ul>

Similarly, iteration can read specific values and be used in nested nodes:

$xml = '<ul>
	<li>
		<h3>Name</h3>
		<span>email</span>
	</li>	
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li h3 {content: iteration(name);}
	ul li span {content: iteration(email);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

Which will output:

<ul>
	<li>
		<h3>Tom</h3>
		<span>tom@example.org</span>
	</li>
	<li>
		<h3>Scott</h3>
		<span>scott@example.org</span>
	</li>
</ul>

Hiding Blocks

Lifted straight from css grammar, Transphporm supports display: none which will actually remove the element from the document entirely:

```php
$xml = '<ul>
	<li>
		<h3>Name</h3>
		<span>email</span>
	</li>	
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li h3 {content: iteration(name);}
	ul li span {display: none}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss)

echo $template->output($data)->body;

Output:

<ul>
	<li>
		<h3>Tom</h3>
	</li>
	<li>
		<h3>Scott</h3>
	</li>
</ul>

N.b. this is very useful with the iteration value pseudo element

CSS Selectors

Transphporm supports the following CSS selectors:

.classname

$xml = '
	<main>
		<p>Paragraph one</p>
		<p class="middle">Paragraph two</p>
		<p>Paragraph 3</p>
	</main>
';

$tss = '
.middle {content: "Middle paragraph"; }
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Output:

	<main>
		<p>Paragraph one</p>
		<p class="middle">Middle Paragraph</p>
		<p>Paragraph 3</p>
	</main>

tagname.classname

$xml = '
	<main>
		<p>Paragraph one</p>
		<p class="middle">Paragraph two</p>
		<p>Paragraph 3</p>
		<a class="middle">A link</a>
	</main>
';

$tss = '
p.middle {content: "Middle paragraph"; }
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Output:

	<main>
		<p>Paragraph one</p>
		<p class="middle">Middle Paragraph</p>
		<p>Paragraph 3</p>
		<a class="middle">A link</a>
	</main>

Direct decedent foo > bar

$xml = '
	<ul>
		<li>One</li>
		<li>Two
			<span>Test</span>
		</li>
		<li>Three
			<div>
				<span>Test 2 </span>
			</div>
		</li>
	</ul>

';

$tss = '
li > span {content: "REPLACED";}
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Output:

	<ul>
		<li>One</li>
		<li>Two
			<span>REPLACED</span>
		</li>
		<li>Three
			<div>
				<span>Test 2 </span>
			</div>
		</li>
	</ul>

ID selector #name

$xml = '
	<main>
		<p>Paragraph one</p>
		<p id="middle">Paragraph two</p>
		<p>Paragraph 3</p>
	</main>
';

$tss = '
#middle {content: "Middle paragraph"; }
';

$template = new \Transphporm\Builder($xml, $tss)

echo $template->output()->body;

Output:

	<main>
		<p>Paragraph one</p>
		<p id="middle">Middle Paragraph</p>
		<p>Paragraph 3</p>
	</main>

Attribute selector

Like CSS, you can select elements that have a specific attribute:

$xml = '
	<textarea name="One">
	</textarea>

	<textarea name="Two">

	</textarea>

	<textarea>

	</textarea>
';

$tss = '
textarea[name="Two"] {content: "TEST"; }
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Output:

	<textarea name="One">
	</textarea>

	<textarea name="Two">
	TEST
	</textarea>

	<textarea>

	</textarea>

Or, any element that has a specific attribute:

$xml = '
	<textarea name="One">
	</textarea>

	<textarea name="Two">

	</textarea>

	<textarea>

	</textarea>
';

$tss = '
textarea[name] {content: "TEST"; }
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Output:

	<textarea name="One">
	TEST
	</textarea>

	<textarea name="Two">
	TEST
	</textarea>

	<textarea>

	</textarea>

Combining selectors

Like CSS, all the selectors can be combined into a more complex expression:

table tr.list td[colspan="2"] {}

Will match any td with a colspan of 2 that is in a tr with a class list and inside a table element

Unsupoorted selectors

Currently the CSS selectors ~ and + are not supported.

Pseudo Elements

Transphporm also supports several pseudo elements.

:before and :after which allows appending/prepending content to what is already there rather than overwriting it:

Before

$data = new stdclass;

$data->title = 'My Title!';
$data->description = 'Description of the page...';


$xml = '
	<h1>Example Title</h1>
	';

$tss = '
	h1:before {content: "BEFORE ";}
';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

Output:

<h1>BEFORE Example Title</h1>

After

$data = new stdclass;

$data->title = 'My Title!';
$data->description = 'Description of the page...';


$xml = '
	<h1>Example Title</h1>
	';

$tss = '
	h1:after {content: " AFTER";}
';

$template = new \Transphporm\Builder($xml, $tss)

echo $template->output($data)->body;

Output:

<h1>Example Title AFTER</h1>

:nth-child();

Straight from CSS, Transphporm also supports nth-child(NUM). As well as nth-child(odd) and nth-child(even)

$xml = '
		<ul>
			<li>One</li>
			<li>Two</li>
			<li>Three</li>
			<li>Four</li>
		</ul>
';

$tss = 'ul li:nth-child(2) {content: "REPLACED"}';

$template = new \Transphporm\Builder($template, $tss);

echo $template->output()->body;

Output:

		<ul>
			<li>One</li>
			<li>REPLACED</li>
			<li>Three</li>
			<li>Four</li>
		</ul>

Even

$xml = '
		<ul>
			<li>One</li>
			<li>Two</li>
			<li>Three</li>
			<li>Four</li>
		</ul>
';

$tss = 'ul li:nth-child(even) {content: "REPLACED"}';

$template = new \Transphporm\Builder($template, $tss);
echo $template->output()->body;

Output:

		<ul>
			<li>One</li>
			<li>REPLACED</li>
			<li>Three</li>
			<li>REPLACED</li>
		</ul>

Odd

$xml = '
		<ul>
			<li>One</li>
			<li>Two</li>
			<li>Three</li>
			<li>Four</li>
		</ul>
';

$tss = 'ul li:nth-child(even) {content: "REPLACED"}';

$template = new \Transphporm\Builder($template, $tss);
echo $template->output()->body;

Output:

		<ul>
			<li>REPLACED</li>
			<li>Two</li>
			<li>REPLACED</li>
			<li>Four</li>
		</ul>

Iteration values

Transphporm can also inspect the iterated data for an element. This is particularly useful when you want to hide a specific block based on the content of an iterated value:

The format is:

element:iteration[name=value] {}

Which will select any element who's iteration content's name attribute is equal to value

The following code will hide any user whose type is 'Admin'.

$users = [];

$user = new stdclass;
$user->name = 'Tom';
$user->email = 'tom@example.org';
$user->type = 'Admin';
$users[] = $user;


$user = new stdclass;
$user->name = 'Scott';
$user->email = 'scott@example.org';
$user->type = 'Standard';
$users[] = $user;

$user = new stdclass;
$user->name = 'Jo';
$user->email = 'jo@example.org';
$user->type = 'Standard';
$users[] = $user;



$xml = '
<ul>
	<li>
		<h3>Name</h3>
		<span>email</span>
	</li>	
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li:iteration[type='Admin'] {display: none;}
	ul li h3 {content: iteration(name);}
	ul li span {content: iteration(email);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

Output:

<ul>
	<li>
		<h3>Scott</h3>
		<span>scott@example.org</span>
	</li>	
	<li>
		<h3>Jo</h3>
		<span>jo@example.org</span>
	</li>	
</ul>

Writing to Attributes

Unlike CSS, Transphporm selectors allow direct selection of individual attributes to set their value. This is done using the pseudo element :attr(name) which selects the attribute on the matched elements.

element:attr(id)

Will select the element's ID attribute.

Working example:

$users = [];

$user = new stdclass;
$user->name = 'Tom';
$user->email = 'tom@example.org';
$users[] = $user;


$user = new stdclass;
$user->name = 'Scott';
$user->email = 'scott@example.org';
$users[] = $user;



$xml = '
<ul>
	<li>
		<h3>Name</h3>
		<a href="mailto:email">email</a>
	</li>	
</ul>';


$tss = '
	ul li {repeat: data(users);}
	ul li a {content: iteration(email);}
	ul li a:attr(href) {content: "mailto:", iteration(email);}
';

$data = ['users' => $users];


$template = new \Transphporm\Builder($xml, $tss);

echo $template->output($data)->body;

Notice this uses multiple values for the content property to concatenate the full URL with mailto

Output:

<ul>
	<li>
		<h3>Tom</h3>
		<a href="Tom@example.org">Tom@example.org</span>
	</li>
	<li>
		<h3>Scott</h3>
		<a href="scott@example.org">scott@example.org</span>
	</li>	
</ul>

Reading from attributes

It's also possible to read from attributes using attr(name) inside the content property.

$xml = '
<h1 class="foo">bar</h1>
';

$tss = 'h1 {content: attr(class);}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Output:

<h1 class="foo">foo</h1>

HTTP Headers

Transphporm supports setting HTTP Headers. You must target an element on the page such as HTML and you can use the :header pseudo element to set a HTTP header. For example a redirect can be done like this:

html:header[location] {content: "/redirect-url"; }

Transphporm does not directly write HTTP headers. The return value of the output() function is an array consisting of a body and headers. body is the rendered HTML code and headers contains any HTTP headers which have been set.

$xml = '<html><div>Foo</div></html>';
$tss = 'html:header[location] {content: "/redirect-url"; }';

$template = new \Transphporm\Builder($xml, $tss);

print_r($template->output());

Will print:

Array (
	'body' => '<html><div>foo</div></html>',
	'headers' => Array (
		Array (
			[0] => 'location',
			[1] => '/redirect-url'
		) 



	)
	
)

To actually send the headers to the browser you need to manually call the header command:

foreach ($template->output()->headers as $header) {
	header($header[0] . ': ' . $header[1]);
}

Conditionally applying HTTP headers

In most cases, you will want to conditionally display a header. For example:

  • Redirect on success
  • Send a 404 header when a record could not be found

To do this, you can use conditional data lookups:

class Model {
	public function getProduct() {
		return false;
	}
}



$tss = 'html:data[getProduct='']:header[status] {content: '404'}

$xml = '<html></html>';

$data = new Model;

$template = new \Transphporm\Builder($xml, $tss);

$output = $template->output($data);

print_r($output->headers)

Prints:

Array (
	[0] => 'status',
	[1] => '404'

)

To use this, you should then call the inbuilt php http_response_code function with this status:

foreach ($template->output()->headers as $header) {
	if ($header[0] === 'status') http_response_code($header[1]);
	else header($header[0] . ': ' . $header[1]);
}

Transphporm does not send any headers

Transphporm does not send any output to the browser by default. This is for maximum flexibility, you must still manually send the headers and echo the body.

Formatting data

Transphporm supports formatting of data as it's output. The syntax for formatting is this:

h1 {content: "content of element"; format: [NAME-OF-FORMAT] [OPTIONAL ARGUMENT OF FORMAT];}

String formatting

Transphporm currently supports the following formats for strings:

  • uppercase
  • lowercase
  • titlecase

Examples:

String format: uppercase

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "TeSt"; format: uppercase}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Prints:

<h1>TEST</h1>

String format: lowercase

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "TeSt"; format: lowercase}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Prints:

<h1>test</h1>

String format: titlecase

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "test"; format: titlecase}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Prints:

<h1>Test</h1>

Number formats

Transphporm supports formatting numbers to a number of decimal places using the decimal format. You can specify the number of decimal places:

Number format: decimal

$xml = '
<h1> </h1>
';

$tss = 'h1 {content: "11.234567"; format: decimal 2}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Prints:

<h1>1.23</h1>

Locales

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.

$template = new \Transphporm\Builder($xml, $tss);
$template->setLocale('enGB');

Currently only enGB is supported. Alternatively, you can provide an array which matches the format used in Formatter/Locale/enGB.json.

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:

$xml = '
<div> </div>
';

$tss = 'div {content: "2015-12-22"; format: date}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

This will format the date using the date format specified in the locale. For enGB this is d/m/Y

<div>22/12/2015</div>

Alternatively you can specify a format as the second parameter of the formatter:

$xml = '
<div> </div>
';

$tss = 'div {content: "2015-12-22"; format: date "jS M Y"}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;
<div>22nd Dec 2015</div>

You can also format using time which defaults to H:i in the locale:

$xml = '
<div> </div>
';

$tss = 'div {content: "2015-12-22 14:34"; format: time}';

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;
<div>14:34</div>

Relative times

You can supply the relative formatter to a date, which will display things like:

  • "Tomorrow"
  • "Yesterady"
  • "Two hours ago"
  • "3 weeks ago"
  • "In 3 months"
  • "In 10 years"

The strings are specified in the locale.

Importing other files

Like CSS, transphporm supports @import for importing other tss files:

imported.tss

h1 {content: "From imported tss"}
$xml = '
<h1> </h1>
<div> </div>
';

$tss = "
	@import 'imported.tss';
	div {content: 'From main tss'}
";

$template = new \Transphporm\Builder($xml, $tss);

echo $template->output()->body;

Output:

<h1>From imported tss</h1>
<div>From main tss</div>

About

Transformation Style Sheets - CSS like sheets for display logic

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • PHP 100.0%