Skip to content

Commit

Permalink
feat: add prop shorthand syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
arnoson committed Nov 24, 2023
1 parent cef8534 commit 269799b
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 42 deletions.
44 changes: 31 additions & 13 deletions README.md
Expand Up @@ -10,13 +10,13 @@ A lightweight template compiler that adds some [syntactic sugar](https://en.wiki
Kirby's new [snippets with slots](https://getkirby.com/docs/guide/templates/snippets#passing-slots-to-snippets) allow you to adapt a component-based workflow, similar to Laravel blade templates or javascript frameworks like Vue. However, the plain php syntax can be verbose. So with some template sugar you can write this:

```html
<snippet:card @rounded="<? true ?>" class="bg-yellow" id="my-card">
<snippet:card $rounded="<? true ?>" class="bg-yellow" id="my-card">
<slot:icon>🍬</slot:icon>
<slot:title>
<h2>Kirby Template Sugar</h2>
</slot:title>
<slot>
<snippet:link @url="github.com/arnoson/kirby-template-sugar">
<snippet:link $url="github.com/arnoson/kirby-template-sugar">
<i>Read more ...</i>
</snippet:link>
</slot>
Expand Down Expand Up @@ -98,7 +98,7 @@ Snippets can have slots or be self-closing:

### Props and attributes

Snippets can have `props`, which are passed directly to the snippet, and attributes, which are grouped into an `$attr` variable passed to the snippet along with the props. Props start with `@` (like `@open` and `@items`) and attributes are just specified like regular html attributes (`class`, `aria-label`).
Snippets can have `props`, which are passed directly to the snippet, and attributes, which are grouped into an `$attr` variable passed to the snippet along with the props. Props start with `$` (like `$open` and `$items`) and attributes are just specified like regular html attributes (`class`, `aria-label`).

If you want to pass a php expression to a snippet, e.g.: `items => $site->children()->listed()`, you just have to wrap it in php tags (see the code below):

Expand All @@ -112,8 +112,8 @@ If you want to pass a php expression to a snippet, e.g.: `items => $site->childr

```html
<snippet:menu
@open="<? true ?>"
@items="<? $site->children()->listed() ?>"
$open="<? true ?>"
$items="<? $site->children()->listed() ?>"
class="bg-red"
aria-label="Main Menu"
/>
Expand Down Expand Up @@ -149,8 +149,8 @@ Well... actually the compiled code looks like this. To make debugging easier, th

```html
<snippet:menu
@open="<? true ?>"
@items="<? $site->children() ?>"
$open="<? true ?>"
$items="<? $site->children() ?>"
class="bg-red"
aria-label="Main Menu"
/>
Expand All @@ -161,8 +161,8 @@ Well... actually the compiled code looks like this. To make debugging easier, th

```php
<?php snippet('menu', __snippetData([
'@open' => true,
'@items' => $site->children(),
'$open' => true,
'$items' => $site->children(),
'class' => 'bg-red',
'aria-label' => 'Main Menu'
])); ?>
Expand Down Expand Up @@ -190,6 +190,24 @@ Or even better with @fabianmichael's fantastic [kirby-template-attributes](https
</nav>
```

### Prop shorthand syntax

If your prop names and php variable names are the same you can use the shorthand syntax:

```php
<?php foreach ($projects as $project): ?>
<snippet:project $project />
<?php endforeach ?>
```

Would be the same as:

```php
<?php foreach ($projects as $project): ?>
<snippet:project $project="<? $project ?>" />
<?php endforeach ?>
```

### CSS Variables

You can assign CSS variables with an attribute-like syntax. This works on any tag, not just `<snippet>` and `<layout>`.
Expand Down Expand Up @@ -272,8 +290,8 @@ Or with slots and even props and attributes

```html
<layout:gallery
@showMenu="<? false ?>"
@layout="portrait"
$showMenu="<? false ?>"
$layout="portrait"
>

<slot:img><img /></slot:img>
Expand All @@ -288,8 +306,8 @@ Or with slots and even props and attributes

```php
<?php layout('gallery', __snippetData([
'@showMenu' => false,
'@layout' => 'portrait'
'$showMenu' => false,
'$layout' => 'portrait'
])); ?>

<?php slot('img'); ?><img /><?php endslot(/* img */); ?>
Expand Down
6 changes: 3 additions & 3 deletions packages/npm-package/src/parser/index.ts
Expand Up @@ -31,7 +31,7 @@ let tag = createTag()

const createAttribute = (data: Partial<Attribute> = {}): Attribute => ({
name: '',
value: '',
value: undefined,
isPhp: false,
line: tag?.lineCount ?? 0,
indent: getIndent(html, position),
Expand Down Expand Up @@ -167,7 +167,7 @@ export const parse = (

// Handle value-less attributes directly at the end of a tag,
// like `<div aria-enabled>`.
if (attribute?.name) tag.attributes.push({ ...attribute, value: '' })
if (attribute?.name) tag.attributes.push(attribute)
attribute = undefined

const isCloseTag = tag.name.startsWith('/')
Expand All @@ -187,7 +187,7 @@ export const parse = (
}
} else if (isWhitespace(char)) {
// Handle value-less attributes `<div aria-enabled class="fu">`.
if (attribute?.name) tag.attributes.push({ ...attribute, value: '' })
if (attribute?.name) tag.attributes.push(attribute)
attribute = undefined
} else if (char !== '=' && char !== '/') {
attribute ??= createAttribute()
Expand Down
20 changes: 12 additions & 8 deletions packages/npm-package/src/transformers/snippetOrLayout.ts
@@ -1,9 +1,5 @@
import { Tag, Attribute } from '../types'
import {
joinLines,
resolveCssVarShorthand,
phpTagsToConcatenation,
} from '../utils'
import { Attribute, Tag } from '../types'
import { joinLines, phpTagsToConcatenation } from '../utils'

const match = ({ name }: Tag) =>
name.startsWith('snippet:') || name.startsWith('layout')
Expand Down Expand Up @@ -36,7 +32,8 @@ const transformOpenTag = (tag: Tag): string => {

const attributeLines = sortedAttributes.map((attribute, index) => {
const { name, indent } = attribute
const isCssVar = attribute.name.startsWith('--')
const isCssVar = name.startsWith('--')
const isPhpVar = name.startsWith('$')
const isFirstCssVar = index === firstCssVarIndex
const isLastCssVar = index === lastCssVarIndex
const isOnlyCssVar = isFirstCssVar && isLastCssVar
Expand All @@ -50,7 +47,14 @@ const transformOpenTag = (tag: Tag): string => {
// are already inside PHP): `'attr' => 'id-<?= $id ?>'`. So we need to
// translate them to string concatenation: `'attr' => 'id-' . $id`.
let value = phpTagsToConcatenation(attribute.value, isInsideQuotes)
if (isCssVar) value = resolveCssVarShorthand(value)

// Allow css var shorthands: `<div --var="--fu" />` will be the same as
// `<div --var="var(--fu)" />`.
if (isCssVar) value = value.startsWith('--') ? `var(${value})` : value

// Allow php var shorthands: `<div $fu />` will be the same as
// `<div $fu="<? $fu ?>" >`.
if (isPhpVar) value ??= name

let text = indent
if (isOnlyCssVar) {
Expand Down
9 changes: 5 additions & 4 deletions packages/npm-package/src/transformers/tag.ts
@@ -1,5 +1,5 @@
import { Attribute, Tag } from '../types'
import { joinLines, resolveCssVarShorthand } from '../utils'
import { joinLines } from '../utils'

// We can leave most HTML tags as is. We only have to transform them if they use
// the CSS variable attribute syntax, like `<div --color="red" >`.
Expand Down Expand Up @@ -34,9 +34,10 @@ const transformOpenTag = (tag: Tag) => {
const isLastCssVar = index === lastCssVarIndex
const isOnlyCssVar = isFirstCssVar && isLastCssVar

const value = isCssVar
? resolveCssVarShorthand(attribute.value)
: attribute.value
let value = attribute.value
// Allow css var shorthands: `<div --var="--fu" />` will be the same as
// `<div --var="var(--fu)" />`.
if (isCssVar) value = value.startsWith('--') ? `var(${value})` : value

let text = indent
if (isOnlyCssVar) {
Expand Down
9 changes: 3 additions & 6 deletions packages/npm-package/src/utils.ts
Expand Up @@ -30,9 +30,11 @@ export const joinLines = (lines: { text: string; line: number }[]) => {
}

export const phpTagsToConcatenation = (
value: string,
value: string | undefined,
isInsideQuotes = false,
) => {
if (value === undefined) return

const startsWithPhp = /^<\?(php|=)?/i.test(value)
const endsWithPhp = value.endsWith('?>')

Expand All @@ -52,11 +54,6 @@ export const phpTagsToConcatenation = (
return value.trim()
}

export const resolveCssVarShorthand = (value: string) => {
const valueIsCssVar = value.startsWith('--')
return valueIsCssVar ? `var(${value})` : value
}

export const changeFileExtension = (filename: string, newExtension: string) =>
join(
dirname(filename),
Expand Down
4 changes: 2 additions & 2 deletions packages/npm-package/test/parser.test.ts
Expand Up @@ -20,12 +20,12 @@ aria-disabled></div>`
// prettier-ignore
attributes: [
{ name: 'id', value: 'fu', indent: ' ', line: 1, isPhp: false },
{ name: 'disabled', value: '', indent: ' ', line: 2, isPhp: false },
{ name: 'disabled', value: undefined, indent: ' ', line: 2, isPhp: false },
{ name: 'class', value: '<?= $bar ?>', indent: ' ', line: 3, isPhp: false },
{ name: 'data-1', value: '1', indent: ' ', line: 3, isPhp: false },
{ name: 'data-2', value: '2', indent: '\t', line: 4, isPhp: false },
{ name: 'data-3', value: '3', indent: '\t\t', line: 4, isPhp: false },
{ name: 'aria-disabled', value: '', indent: '', line: 5, isPhp: false },
{ name: 'aria-disabled', value: undefined, indent: '', line: 5, isPhp: false },
],
isSelfClosing: false,
}),
Expand Down
26 changes: 20 additions & 6 deletions packages/npm-package/test/transform.test.ts
Expand Up @@ -16,22 +16,36 @@ describe('transform', () => {

it('handles snippet attributes', () => {
const input = `<snippet:test
@myProp="value"
@myPhpProp="<? [1, 2, 3] ?>"
$myProp="value"
$myPhpProp="<? [1, 2, 3] ?>"
class="red"
id="id-<?= $id ?>-fu"
aria-label="<?php 'text' ?>"
/>`
const output = `<?php snippet('test', __snippetData([
'@myProp' => 'value',
'@myPhpProp' => [1, 2, 3],
'$myProp' => 'value',
'$myPhpProp' => [1, 2, 3],
'class' => 'red',
'id' => 'id-' . $id . '-fu',
'aria-label' => 'text',
])); ?>`
expect(transform(input)).toBe(output)
})

it('handles attribute shorthands', () => {
const input = `<snippet:test
$a="<?= $a ?>"
$b
$c
/>`
const output = `<?php snippet('test', __snippetData([
'$a' => $a,
'$b' => $b,
'$c' => $c,
])); ?>`
expect(transform(input)).toBe(output)
})

it('handles slots', () => {
const input = `<snippet:test>
<slot>Default</slot>
Expand All @@ -47,8 +61,8 @@ describe('transform', () => {
})

it('handles layouts', () => {
let input = `<layout @myProp="<? $prop ?>" />`
let output = `<?php layout('default', __snippetData([ '@myProp' => $prop, ])); ?>`
let input = `<layout $myProp="<? $prop ?>" />`
let output = `<?php layout('default', __snippetData([ '$myProp' => $prop, ])); ?>`
expect(transform(input)).toBe(output)

input = `<layout:name class="no-js" />`
Expand Down

0 comments on commit 269799b

Please sign in to comment.