July 18, 2022 —John Koster
In this article, I thought it would be fun to look behind the scenes at how both the Antlers and Blade formatters work. We will look at examples from both formatters since they essentially employ the same strategy to format source documents.
Antlers and Blade can be challenging languages to format and produce acceptable results. Let us take a look at this unformatted Blade example:
1<!DOCTYPE html> 2<html> 3<head> 4<style>.some-class { background-color: white; color: black } 5@media only 6 screen and (max-width: 600px) { 7 .some-class { background-color: black; color: white 8 } 9}</style>10</head>11<body>12@if($someValue > 10)13 Branch One @elseif($anotherValue < 50)14Branch Two @else Else Branch @endif15<script>16let items = {{ Js::from($array) }};17items.forEach((item) => { console.log(item); })18</script>19</body>20</html>
compared to its formatted version:
1<!DOCTYPE html> 2<html> 3 <head> 4 <style> 5 .some-class { 6 background-color: white; 7 color: black; 8 } 9 @media only screen and (max-width: 600px) {10 .some-class {11 background-color: black;12 color: white;13 }14 }15 </style>16 </head>17 <body>18 @if($someValue > 10)19 Branch One20 @elseif($anotherValue < 50)21 Branch Two22 @else23 Else Branch24 @endif25 <script>26 let items = {{ Js::from($array) }};27 items.forEach((item) => {28 console.log(item);29 });30 </script>31 </body>32</html>
In that example we have the following languages mixed together:
if
and elseif
parametersIf you look closely, you will also notice the presence of the CSS @media
rule, which looks like the start of a Blade directive. This CSS rule will cause further challenges with the parser implementation (written in TypeScript) since it does not have access to the list of Blade directives at runtime. To see some similar complexity when it comes to formatting an Antlers document, we can look at this example template:
1<!DOCTYPE html> 2<html> 3<head> 4<style>.some-class { background-color: {{ backgroundColor ?= 'white' }}; 5 color: {{ fontColor? = 'black' }} } 6</style> 7</head> 8<body> 9{{ if someValue > 10 }}10 Branch one {{ elseif anotherValue < 50 }}11 Branch Two {{ else }} Else Branch {{ /if }}12 13<script>14let value = '{{ text }}';15console.log(value);16</script>17</body>18</html>
and its formatted version:
1<!DOCTYPE html> 2<html> 3 <head> 4 <style> 5 .some-class { 6 background-color: {{ backgroundColor ?= 'white' }}; 7 color: {{ fontColor ? = 'black' }}; 8 } 9 </style>10 </head>11 <body>12 {{ if someValue > 10 }}13 Branch one14 {{ elseif anotherValue < 50 }}15 Branch Two16 {{ else }}17 Else Branch18 {{ /if }}19 <script>20 let value = "{{ text }}";21 console.log(value);22 </script>23 </body>24</html>
Like with the Blade example, we have a lot of mixed languages. However, unlike the Blade example, the content within the if
and elseif
regions are not PHP and have subtly different rules.
In both examples, JavaScript contains Antlers or Blade code that could have special meaning within JavaScript.
When looking at the formatted examples, how do we even begin to get the input formatted that way? One approach would be to try and write a "super parser" and engine that attempts to accommodate all the different languages together.
While not only impractical, this super parser would most definitely be complicated to write and maintain. Not only would it be complex, but it would also most likely fail on an uncountable number of edge cases. If I've learned nothing else over the last year or so working on Antlers a lot, it's that people will find spectacular ways to push languages to their breaking point and still keep going.
Additionally, people like their existing tools and ecosystems and don't want to leave their configurations and plugins. Building something like that from scratch would probably not take off.
Instead of trying to write a super parser, what if we could convert our Blade and Antlers documents into something that could be formatted using existing tools like Prettier or Beautify HTML?
Let's consider this more straightforward Blade example:
1<div>2@directive($parameters)3 4@pair5<p>Some text.</p>6@endpair7</div>
and its formatted version:
1<div>2 @directive($parameters)3 4 @pair5 <p>Some text.</p>6 @endpair7</div>
Ignoring the complexities of templating languages embedded in other languages like CSS/JavaScript for a moment, when we compare the two code samples, the main difference between them is the structural formatting of the document. For this example, there is no change to the PHP inside the directive itself (we will explore some more complicated examples later).
After noticing that the main challenge is the document's structural formatting, we can ask, "What is the equivalent HTML structure that would produce a similar result?". For our simple Blade document, an HTML structure that would produce a similar indentation might be:
1<div>2<span />3 4<div>5 6<p>Some text.</p>7</div>8 9</div>
which becomes:
1<div>2 <span />3 4 <div>5 <p>Some text.</p>6 </div>7</div>
Suppose we could find a way to convert our Antlers and Blade documents into a structured document and use existing tools like Prettier. In that case, we could save ourselves a lot of work and complexity.
Both the Blade and Antlers TypeScript parsers have the concept of the document transformer. This special class takes the parsed results and converts them into a structured document that can be formatted with existing tools. These transformers do all sorts of things, such as creating new elements that won't be part of the final output to help influence the indentation, adjusting the behavior based on whether or not the embedded Antlers or Blade is inside HTML elements, etc.
The document transformer would convert our simple Blade example from the previous section into a document similar to the following:
1<div>2<BZfPeDp3Szv7tlrYr7nlI9GB />3 4<BHuGDB>5 6<p>Some text.</p>7</BHuGDB>8 9</div>
As you can see, the transformed document is very similar to the HTML document that produces the desired structure of the final output.
A more complicated example would be looking at one possible transformation of the example Blade document we started the article with:
1<!DOCTYPE html> 2<html> 3<head> 4<style>.some-class { background-color: white; color: black } 5@media only 6 screen and (max-width: 600px) { 7 .some-class { background-color: black; color: white 8 } 9}</style>10</head>11<body>12<BcsjPfyny5mTpIkFNl9IY3lF83LJhK2itmFB>13<BSjszvqEz452JtiBdXn3bhc9QB>14 Branch One15</BSjszvqEz452JtiBdXn3bhc9QB>16</BcsjPfyny5mTpIkFNl9IY3lF83LJhK2itmFB>17<BlhTIJLSNEWkfWyX41hLTz1K4gtPptUB>18<BqmUvvBUQDluC0VlzfIt3yA2DB>19Branch Two20</BqmUvvBUQDluC0VlzfIt3yA2DB>21</BlhTIJLSNEWkfWyX41hLTz1K4gtPptUB>22<BDV9MB><BAw0q12nu37dYnaBtf0bVd1d2B>23Else Branch24</BAw0q12nu37dYnaBtf0bVd1d2B>25</BDV9MB>26<script>27let items = BamhYGRrUCLF4cdINVtqj0B;28items.forEach((item) => { console.log(item); })29</script>30</body>31</html>
If we were to format this structured document, we would end up with a result similar to the following:
1<!DOCTYPE html> 2<html> 3 <head> 4 <style> 5 .some-class { 6 background-color: white; 7 color: black; 8 } 9 @media only screen and (max-width: 600px) {10 .some-class {11 background-color: black;12 color: white;13 }14 }15 </style>16 </head>17 <body>18 <BcsjPfyny5mTpIkFNl9IY3lF83LJhK2itmFB>19 <BSjszvqEz452JtiBdXn3bhc9QB>Branch One</BSjszvqEz452JtiBdXn3bhc9QB>20 </BcsjPfyny5mTpIkFNl9IY3lF83LJhK2itmFB>21 <BlhTIJLSNEWkfWyX41hLTz1K4gtPptUB>22 <BqmUvvBUQDluC0VlzfIt3yA2DB>Branch Two</BqmUvvBUQDluC0VlzfIt3yA2DB>23 </BlhTIJLSNEWkfWyX41hLTz1K4gtPptUB>24 <BDV9MB>25 <BAw0q12nu37dYnaBtf0bVd1d2B>Else Branch</BAw0q12nu37dYnaBtf0bVd1d2B>26 </BDV9MB>27 <script>28 let items = BamhYGRrUCLF4cdINVtqj0B;29 items.forEach((item) => {30 console.log(item);31 });32 </script>33 </body>34</html>
As you can see from both examples, the results of the formatted structured documents are getting very close to our goal. Now the only problem is removing the fake elements created by the transformer and get our document back to Antlers or Blade.
Each transformer implementation keeps track of which elements it created, and what parts of the original document they correspond to. The transformer also keeps track of what extra elements it created to influence the structure and which ones should be removed from the final document. All of these processes contribute the most complexity to both transformer implementations.
One thing that you may have noticed in the more complex Blade example is that:
1@if($someValue > 10)
became:
1@if($someValue > 10)
Formatting the embedded PHP is possible due to the Blade parser breaking out the directive names and their parameters for us. When we are reassembling the final document, we can take the parameters and run them through a PHP formatter to help produce a nice-looking final result. The Antlers transformer also does similar things, but since no existing formatters existed to format "just Antlers" code, one had to be written (this formatter is much closer to what you'd expect from a formatter implementation).
The total number of edge cases is probably not that high, but the ones that exist are fascinating. For a quick example of one such edge case, let's compare this Antlers template:
1<{{ as or 'a' }} class="some class names here">2<p>Text</p>3</{{ as or 'a' }}>
to its formatted output:
1<{{ as or 'a' }} class="some class names here">2 <p>Text</p>3</{{ as or 'a' }}>
In this Antlers example, we are using some conditional logic to generate an HTML tag name dynamically. Dynamic element names pose a problem to our document transformation method introduced in the previous section since there are no HTML structures that we can combine to produce the final result except for actually providing a tag name.
When these situations arise, the transformer utilizes the results of a specialized internal parser known as the "Fragments Parser." This parser's only job is to provide contextual information about where a piece of Blade or Antlers appears in the original document. The document transformer then uses this information to produce a temporary document similar to the following:
1<A3js9 class="some class names here">2<p>Text</p>3</A3js9>
The new temporary document can be formatted using existing tools to produce the desired output. The transformer also uses similar techniques when it encounters templating languages embedded inside other languages, such as CSS or JavaScript.
I hope you found this quick overview of how the Antlers and Blade formatters work under the hood. I could spend an entire series of articles on each topic briefly covered within this article. If you are interested in those types of articles, just let me know!
∎
The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.