Řešení zavináčů – nechť je z Latte základ Nette šablon

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
jakubkulhan
Člen | 55
+
0
-

Nikde jsem nenašel, jestli se hnulo s řešením
zavináčů, takže tohle je
můj návrh řešení.

Základní předpoklad: pokud někdo používá Nette šablony, používá LatteFilter.

Bohužel z pozice filtru (resp. makroprocesoru) toho Latte moc nezmůže.
Jednou možností by bylo z Latte udělat 2-pass
makroprocesor – tzn. že v prvním běhu by si zapamatoval potřebné informace o šabloně
(kde je jaký snippet, jakými podmínkami je obalen) a podle toho by si v druhém běhu
před dané podmínky zavináče „domyslel“.

Ale lepší by podle mě bylo, aby Latte povýšil z pozice filtru na základní stavební
kámen Nette šablon – už ně makroprocesor, nýbrž plnohodnotný jazyk.

Místo toho, aby přechroupával textovou verzi šablony, vytvořil by si její in-memory
reprezentaci, tu zpracoval a z ní až vygeneroval PHP kód. Compile-time šablon by se
asi prodloužil, run-time by byl stejně rychlý v případě zavedení optimalizací by mohl
být ještě rychlejší.

Vytvořil jsem prototyp Latte parseru v phpegu, napsat
plnohodnotný parser, ať v phpegu, nebo ručně, by nemělo být složité. V prototypu
parseru je s HTML tagy pracováno jako s HTML tagy, takže
<li {if $first}class="first"{/fi}> skončí chybou. V plnohodnotném parseru by šlo
i toto řešit. Nicméně bych se přikláněl k něčemu na způsob mého dřívějšího
smart_attrs patche.

Aby z Latte byl opravdu jazyk, krom syntaxe by měl mít také jasně definovanou
sémantiku. U většiny příkazů (podmínky, cykly, odkazy…) je to asi jasné. Zaměřil
bych se na dědičnost.

Příkaz {extends …} musí být v šabloně na prvním místě, na jakémkoli jiném místě
znamená sémantickou chybu. Nechť vše vně bloků
(ať deklarovaných pomocí {block …}, nebo atributu n:block="…") je záporný
prostor a uvnitř bloků kladný prostor (terminologie vypůjčená z
Evoque). Extenduje-li šablona A
šablonu B,
záporný prostor A je nahrazen záporným prostorem B. Resp.
místo TOMu (Template Object Modelu) Ačka použijeme TOM Bčka,
a v něm nahradíme bloky shodnými bloky z Ačka.

Podobně by se měly všechny {include …} uzly nahradit uzly odpovídajícími
vkládanému obsahu.

Například TOM šalony:

{* A.latte *}
some negative space
{block foo}bar{/block}
{block baz}bar{/block}
negative space at the end of file

by byl:

[
    @TextNode {
        text = "some negative space\n"
    },
    @BlockNode {
        name = "foo",
        children = [
            @TextNode {
                text = "bar"
            }
        ]
    },
    @BlockNode {
        name = "baz",
        children = [
            @TextNode {
                text = "bar"
            }
        ]
    }
    @TextNode {
        text = "negative space at the end of file\n"
    },
]

a šablony:

{* B.latte *}
{extends A.latte}

{block foo}wow{/block}

by byl:

[
    @TextNode {
        text = "some negative space\n"
    },
    @BlockNode {
        name = "foo",
        children = [
            @TextNode {
                text = "wow"
            }
        ]
    },
    @BlockNode {
        name = "baz",
        children = [
            @TextNode {
                text = "bar"
            }
        ]
    }
    @TextNode {
        text = "negative space at the end of file\n"
    },
]

Až tedy proběhne fáze starající se v šablonách o dědičnost, dostaneme in-memory
reprezentaci šablony (TOM), která bude obsahovat jak správný záporný prostor, tak
správný kladný prostor a nebude obsahovat žádné includy. S tímhle TOMem se už dají
provádět další transformace a kontroly.

Při generování se může používat technika jako doteď
(if (SnippetHelper::$outputAllowed) {):

class LatteNode
{
    abstract public function isAnySnippetInside();

    abstract public function generatePhp();
}

class SnippetNode
{
    public function isAnySnippetInside()
    {
        return TRUE;
    }

    public function generatePhp() { … }
}

class IfNode
{
    /** @var string */
    private $condition;

    /** @var array of LatteNode */
    private $ifClause;

    /** @var array if LatteNode */
    private $elseClause;

    public function isAnySnippetInside()
    {
        foreach (array_merge($this->ifClause, $this->elseClause) as $node) {
            if ($node->isAnySnippetInside()) {
                return TRUE;
            }
        }

        return FALSE;
    }

    public function generatePhp()
    {
        $php = "";

        if ($this->isAnySnippetInside()) {
            $php .= "}\n";
        }

        $php .= "if ({$this->condition}):\n"

        if ($this->isAnySnippetInside()) {
            $php .= "if (SnippetHelper::\$outputAllowed) {\n";
        }

        foreach ($this->ifClause as $node) {
            $php .= $node->generatePhp();
        }

        if (!empty($this->elseClause)) {
            if ($this->isAnySnippetInside()) {
                $php .= "}\n";
            }

            $php .= "else:\n";

            if ($this->isAnySnippetInside()) {
                $php .= "if (SnippetHelper::\$outputAllowed) {\n";
            }

            foreach ($this->elseClause as $node) {
                $php .= $node->generatePhp();
            }
        }

        if ($this->isAnySnippetInside()) {
            $php .= "}\n";
        }

        $php .= "endif;\n";

        if ($this->isAnySnippetInside()) {
            $php .= "if (SnippetHelper::\$outputAllowed) {\n";
        }
    }
}
Honza Kuchař
Člen | 1662
+
0
-

No což pak o to, Template bez Latte jsem nikdy nepoužil. (na to jsem příliš líný) Ale kdo to naimplementuje?

toka
Člen | 253
+
0
-

David má asi své práce dost, jedině někdo z komunity :-D

jakubkulhan
Člen | 55
+
0
-

Honza Kuchař napsal(a):

No což pak o to, Template bez Latte jsem nikdy nepoužil. (na to jsem příliš líný) Ale kdo to naimplementuje?

Já se na to klidně vrhnu. Z prototypu se k plnohodnotnému parseru dostanu za chvíli (pozor, selfpromo: díky skvělému phpegu :-)). Potřeboval bych vyjádření toho nepovolanějšího, jestli se to vůbec má šanci dostat do frameworku, nebo jestli zavináče začal už řešit jinak (protože na roadmapě 1.0 to je a zavázal se, že beta do 19. září vyjde…).


Mám další nápad k šablonám a jejich spolupráci se zbytkem frameworku. Komponenty vykreslované v šablonách (formuláře např.) by mohly místo $component->render() vrátit Latte strom s LatteNodes, Latte by ho mohlo rovnou zkompilovat do šablony a v dalších voláních by komponenta už jenom vracela data pro strom, který dříve vrátila Latte (resp. zkompilovaný kód). Např. u formulářů je stejně v plánu přepsat ConventionalRenderer do syntaxe LatteFilteru, tak by formuláře místo toho mohly vracet Latte strom.

Honza Kuchař
Člen | 1662
+
0
-

Zní to zajímavě, myslím, že minimálně myšlenka Davida zaujme a pokud jsi ochoten to implementovat, jistě nebude proti. Problém je čas, jako vždy. :)

Dr.Diesel
Člen | 53
+
0
-

jakubkulhan napsal(a):

Mám další nápad k šablonám a jejich spolupráci se zbytkem frameworku. Komponenty vykreslované v šablonách (formuláře např.) by mohly místo $component->render() vrátit Latte strom s LatteNodes, Latte by ho mohlo rovnou zkompilovat do šablony a v dalších voláních by komponenta už jenom vracela data pro strom, který dříve vrátila Latte (resp. zkompilovaný kód). Např. u formulářů je stejně v plánu přepsat ConventionalRenderer do syntaxe LatteFilteru, tak by formuláře místo toho mohly vracet Latte strom.

U tohohle bych jen podotknul, ze si nejsem jisty, zda komponenta bude za vsech okolnosti pouzivat stejnou sablonu / podavat stejny vystup Latte stromu.

Editoval Dr.Diesel (23. 8. 2010 11:28)

jakubkulhan
Člen | 55
+
0
-

Honza Kuchař napsal(a):

Zní to zajímavě, myslím, že minimálně myšlenka Davida zaujme a pokud jsi ochoten to implementovat, jistě nebude proti. Problém je čas, jako vždy. :)

Čas neřešme, ten teď mám a věnuji ho tomu. Jen nechci, aby to dopadlo jako u patche pro atributy nebo FileJournalu.

Dr.Diesel napsal(a):

U tohohle bych jen podotknul, ze si nejsem jisty, zda komponenta bude za vsech okolnosti pouzivat stejnou sablonu / podavat stejny vystup Latte stromu.

To si už komponenta musí rozhodnout sama, jestli se na stránce bude vykreslovat podle podle doby dne, či podle počasí. Latte by měl zachovávat kompatibilitu s LatteFilterem, takže by se ve výchozím stavu volalo $component->render(). Avšak pokud by komponenta implementovala interface např. ILatteComponent, jenž by obsahoval dvě metody – getNode() a getData() --, v compile-time by Latte nahradil widget uzel stromem získaným voláním getNode() a v run-time by akorát stromu dodal data z metody getData().

David Grudl
Nette Core | 8218
+
0
-

Tento týden jsem strávil pročítáním všech vláken na fóru a konečně se dostal k písmenu „R“ :-)

Jakube, především díky za RFC s návrhem rešení konkrétního problému, moc si toho vážím a doufám, že tím odstartuješ vlnu. Slibuju, že se těmto věcem budu primárně věnovat a zřídil jsem na to vlastní kategorii na fóru.

Než se dostaneme k technické stránce jeden dotaz: přijdeš zítra na Poslední sobotu? Pokud ano, bylo by asi lepší vše probrat osobně, než tu vypisovat elaboráty.

jakubkulhan
Člen | 55
+
0
-

David Grudl napsal(a):

Než se dostaneme k technické stránce jeden dotaz: přijdeš zítra na Poslední sobotu? Pokud ano, bylo by asi lepší vše probrat osobně, než tu vypisovat elaboráty.

Přijdu.

Nilp
Člen | 65
+
0
-

Podobně tohle řeší Twig šablony od F. Potenciera, bylo by myslím možné se inspirovat.

Lopata
Člen | 139
+
0
-

Jak dopadla diskuse na Poslední sobotě?

jakubkulhan
Člen | 55
+
0
-

Lopata napsal(a):

Jak dopadla diskuse na Poslední sobotě?

Jestli jsem to pochopil správně, zavináčová magie je potřeba pouze v případě, že se používají staré snippety {snippet nazevSnippetu}, nové snippety (s dvojtečkou) {snippet:nazevSnippetu} jsou implementovány jinak (staré se odchytávají přímo z vykreslované stránky /zavináče jsou tam k tomu, aby se zbytečně nevykonával kód, který není potřeba vykonávat/ a nové jsou samostatné funkce /šablona se tedy vůbec nevykonává, vykonávají se pouze funkce reprezentující jednotlivé snippety; problém je s tím, že tyhle snippety jsou bez scope, takže všechny proměnné v nich použité musí pocházet přímo z inicializace šablony/).

Navíc kompilovaný parser, který vypadne třebas z phpegu, je velký. Pro syntaxi typu Latte bude přesahovat 50KiB. A aby tvořil parser šablon osminu velikosti frameworku (předpokládám, že velikost ostatního kódu kolem šablon by zůstala stejná /leč pravděpodobnější je, že kvůli práci se stromem, by velikost ostatku šablon vzrostla/)?

Takže v 1.0 bych počítal s rozloučením se se zavináči a starými snippety spíše než s přepisem LatteFilteru.

David Grudl
Nette Core | 8218
+
0
-

Abych to doplnil, snippety implementovány jsou, ale chování není dosud dotaženo. Jestli by bylo i tak užitečné přepsat LatteFilter do Jakubova phpegu je sporné, mimojiné proto, že je to odladěná věc a jsou tu jiné věci, které by bylo fajn udělat. Pokud se do toho ale bude chtít pustit, má samozřejmě ruce volné.