Index: inc/admin/class.dc.tab.php
===================================================================
--- inc/admin/class.dc.tab.php	(revision 1092)
+++ inc/admin/class.dc.tab.php	(revision 1092)
@@ -0,0 +1,397 @@
+<?php
+# -- BEGIN LICENSE BLOCK ---------------------------------------
+#
+# This file is part of Dotclear 2.
+#
+# Copyright (c) 2003-2011 Olivier Meunier & Association Dotclear
+# Licensed under the GPL version 2.0 license.
+# See LICENSE file or
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+#
+# -- END LICENSE BLOCK -----------------------------------------
+if (!defined('DC_RC_PATH')) { return; }
+
+/**
+Template tab node
+*/
+class dcTabNode extends Twig_Node
+{
+	public function __construct($name,Twig_NodeInterface $params=null,Twig_NodeInterface $body,$lineno,$tag=null)
+	{
+		parent::__construct(array('body' => $body),array('name' => $name,'params' => $params),$lineno,$tag);
+	}
+	
+	public function compile(Twig_Compiler $compiler)
+	{
+		$n = $this->getAttribute('name');
+		
+		$compiler
+			->addDebugInfo($this)
+			->write("\$context['dc_tabs']->beginTab('".$n."'");
+		
+		/* if tab is created from template, compile args */
+		if ($this->getAttribute('params')) {
+                foreach ($this->getAttribute('params') as $value) {
+ 				$compiler
+					->raw(", ")
+					->subcompile($value);
+                }
+		}
+		
+		$compiler
+			->write(");\n")
+			
+			/* if tab is not defined, body is compiled but not display */
+			->write("if (\$context['dc_tabs']->isTab('".$n."')) {\n")
+			->subcompile($this->getNode('body'))
+			->write("}\n")
+			
+			->write("\$context['dc_tabs']->endTab('".$n."');\n")
+			;
+	}
+}
+
+/**
+Template tab token parser
+*/
+class dcTabTokenParser extends Twig_TokenParser
+{
+	public function parse(Twig_Token $token)
+	{
+		$lineno = $token->getLine();
+		$stream = $this->parser->getStream();
+		
+		$values = null;
+		$name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
+		
+		/* create tab from template */
+		if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
+            $stream->next();
+            $values = $this->parser->getExpressionParser()->parseMultitargetExpression();
+            $stream->next();
+		}
+		
+		$stream->expect(Twig_Token::BLOCK_END_TYPE);
+		$body = $this->parser->subparse(array($this,'decideBlockEnd'),true);
+		$stream->expect(Twig_Token::BLOCK_END_TYPE);
+
+		return new dcTabNode($name, $values, $body, $lineno, $this->getTag());
+	}
+	
+	public function decideBlockEnd(Twig_Token $token)
+	{
+		return $token->test('endtab');
+	}
+	
+	public function getTag()
+	{
+		return 'tab';
+	}
+}
+
+/**
+Template tab extension
+*/
+class dcTabExtension extends Twig_Extension
+{
+	public static $uri = 'tab';
+	
+	protected $tpl;
+	protected $core;
+	
+	protected $tabs = array();
+	protected $selected_tab = ''; // This is tab id, not tab name !
+	
+	public function __construct($core)
+	{
+		$this->core = $core;
+		$this->tpl = 'tab_layout.html.twig';
+		$this->selected_tab = empty($_REQUEST[self::$uri]) ? '' : $_REQUEST[self::$uri];
+	}
+	
+	public function initRuntime(Twig_Environment $environment)
+	{
+		$this->twig = $environment;
+		$this->template = $this->twig->loadTemplate($this->tpl);
+		$this->blocks = $this->template->getBlocks();
+	}
+	
+	public function getName()
+	{
+		return 'dc_tabs';
+	}
+	
+	public function getTokenParsers()
+	{
+		return array(new dcTabTokenParser());
+	}
+	
+	public function getGlobals()
+	{
+		return array('dc_tabs' => $this);
+	}
+	
+	public function getFunctions()
+	{
+		return array(
+			'init_tabs' => new Twig_Function_Method(
+				$this,
+				'initTabs',
+				array('is_safe' => array('html'))
+			),
+			'faketab' => new Twig_Function_Method(
+				$this,
+				'fakeTab',
+				array('is_safe' => array('html'))
+			)
+		);
+	}
+	
+	/**
+	Display page head required by tabs
+	*/
+	public function initTabs($default_tab=null)
+	{
+		if (null === $default_tab) {
+			$default_tab =  $this->selected_tab;
+		}
+		
+		echo $this->template->renderBlock(
+			'page_tab_head',
+			array(
+				'default_tab' => $default_tab,
+				'theme_url' => DC_ADMIN_URL.'index.php?tf='
+			),
+			$this->blocks
+		);
+	}
+	
+	/**
+	Display begin of a tab block
+	
+	This can also create a new tab from template
+	*/
+	public function beginTab()
+	{
+		$case = func_num_args();
+		$args = func_get_args();
+		
+		switch($case) {
+		
+		# No tab name ?!
+		case 0:
+			throw new Exception('Something went wrong while compile tab.');
+			break;
+		# Normal usage
+		case 1:
+			break;
+		# Create a new tab from template
+		case 3:
+			$this->setTab(new dcTab($args[0],$args[1],$args[2]));
+			break;
+		# Wrong argument count
+		default:
+			throw new Exception('Wrong parameters count for tab.');
+			break;
+		
+		};
+		
+		$tab = $this->isTab($args[0]) ? $this->tabs[$args[0]] : false;
+		$this->renderTab('page_tab_begin',$tab);
+	}
+	
+	/**
+	Display end of a tab block
+	*/
+	public function endTab($name)
+	{
+		$tab = $this->isTab($name) ? $this->tabs[$name] : false;
+		$this->renderTab('page_tab_end',$tab);
+	}
+	
+	/**
+	Display a fake tab block
+	*/
+	public function fakeTab()
+	{
+		$case = func_num_args();
+		$args = func_get_args();
+		
+		switch($case) {
+		
+		# No tab name ?!
+		case 0:
+			throw new Exception('Something went wrong while compile tab.');
+			break;
+		# Normal usage
+		case 1:
+			break;
+		# Create a new fake tab from template
+		case 4:
+			$this->setTab(new dcFakeTab($args[0],$args[1],$args[2],$args[3]));
+			break;
+		# Wrong argument count
+		default:
+			throw new Exception('Wrong parameters count for tab.');
+			break;
+		
+		};
+		
+		$tab = $this->isFake($args[0]) ? $this->tabs[$args[0]] : false;
+		$this->renderTab('page_tab_fake',$tab);
+	}
+	
+	/**
+	Display block
+	*/
+	private function renderTab($block,$tab)
+	{
+		if ($tab) {
+			echo $this->template->renderBlock(
+				$block,
+				$tab->getAttributes(),
+				$this->blocks
+			);
+		}
+	}
+	
+	/**
+	Check if a tab is previously set
+	
+	@param string $name A tab name
+	@return boolean
+	*/
+	public function isTab($name)
+	{
+		return array_key_exists($name,$this->tabs);
+	}
+	
+	
+	/**
+	Check if a tab is previously set and it is a fake tab
+	
+	@param string $name A tab name
+	@return boolean
+	*/
+	public function isFake($name)
+	{
+		return array_key_exists($name,$this->tabs) 
+			&& $this->tabs[$name] instanceOf dcFakeTab;
+	}
+	
+	
+	/**
+	Mark a tab as selected
+	
+	@param string $name A tab name
+	*/
+	public function selectTab($name)
+	{
+		if (!$name) {
+			$this->selected_tab = '';
+		}
+		elseif ($this->isTab($name) && !$this->isFake($name)) {
+			$this->selected_tab =  $this->getTab($name)->getId();
+		}
+	}
+	
+	/**
+	Get a page tab
+	
+	@param string $name A tab name
+	@return object dcTab instance
+	*/
+	public function getTab($name)
+	{
+		return $this->isTab($name) ? $this->tabs[$name] : null;
+	}
+	
+	/**
+	Set (prepapre) a page tab
+	
+	@param object $tab dcTab instance
+	@return object self
+	*/
+	public function setTab($tab)
+	{
+		if ($tab instanceOf dcTab || $tab instanceOf dcFakeTab) {
+			$this->tabs[$tab->getName()] = $tab;
+		}
+		return $this;
+	}
+	
+	/**
+	Check if a tab name is well formed
+	
+	Tab name must look like Twig_Token::NAME_TYPE
+	
+	@param string $name A string to check
+	@param boolean $verbose Throw exception or not
+	@return string The string or false
+	*/
+	public static function checkTabName($name,$verbose=true)
+	{
+		if (!preg_match('/[a-zA-Z0-9_]{2,}/',$name)) {
+			if ($verbose) {
+				throw new Exception(__('Wrong tab name'));
+			}
+			return false;
+		}
+		return $name;
+	}
+}
+
+/**
+Tab
+*/
+class dcTab
+{
+	protected $attributes = array();
+	
+	public function __construct($name,$id,$title)
+	{
+		$this->attributes = array(
+			'name' => dcTabExtension::checkTabName($name),
+			'id' => $id,
+			'title' => $title
+		);
+	}
+	
+	public function getAttributes()
+	{
+		return $this->attributes;
+	}
+	
+	public function getName()
+	{
+		return $this->attributes['name'];
+	}
+	
+	public function getId()
+	{
+		return $this->attributes['id'];
+	}
+	
+	public function getTitle()
+	{
+		return $this->attributes['title'];
+	}
+}
+
+/**
+Fake tab
+*/
+class dcFakeTab extends dcTab
+{
+	public function __construct($name,$id,$title,$url)
+	{
+		parent::__construct($name,$id,$title);
+		$this->attributes['url'] = $url;
+	}
+	
+	public function getURL()
+	{
+		return $this->attributes['url'];
+	}
+}
+?>
Index: inc/admin/default-templates/tab_layout.html.twig
===================================================================
--- inc/admin/default-templates/tab_layout.html.twig	(revision 1092)
+++ inc/admin/default-templates/tab_layout.html.twig	(revision 1092)
@@ -0,0 +1,24 @@
+{# Page tabs #}
+
+{# Add page header for tabs with {{ init_tabs() }} in your page header #}
+{% block page_tab_head %}
+	{% import "js_helpers.html.twig" as js %}
+	{{ js.page_tabs(default_tab) }}
+{% endblock %}
+
+{# Begin an existing tab with {% tab a_tab_name %} #}
+{# Create/overwrite one with {% tab a_tab_name(a_tab_id,a_tab_title) %} #}
+{% block page_tab_begin %}
+	<div class="multi-part" title="{{ title }}" id="{{ id }}">
+{% endblock %}
+
+{# End a tab with {% endtab %} #}
+{% block page_tab_end %}
+	</div>
+{% endblock %}
+
+{# Add fake tab with {{ faketab(a_tab_name) }} #}
+{# Create/overwrite one with {{ faketab(a_tab_name,a_tab_id,a_tab_title,a_tab_url) }} #}
+{% block page_tab_fake %}
+	<a class="multi-part" href="{{ url }}" title="{{ title }}">{{ title }}</a>
+{% endblock %}
Index: inc/core/class.dc.core.php
===================================================================
--- inc/core/class.dc.core.php	(revision 1084)
+++ inc/core/class.dc.core.php	(revision 1092)
@@ -155,4 +155,5 @@
 		);
 		$this->tpl->addExtension(new dcFormExtension($this));
+		$this->tpl->addExtension(new dcTabExtension($this));
 	}
 	
Index: inc/prepend.php
===================================================================
--- inc/prepend.php	(revision 1080)
+++ inc/prepend.php	(revision 1092)
@@ -67,4 +67,5 @@
 $__autoload['dcForm']			= dirname(__FILE__).'/admin/class.dc.form.php';
 $__autoload['dcFormExtension']			= dirname(__FILE__).'/admin/class.dc.form.php';
+$__autoload['dcTabExtension']			= dirname(__FILE__).'/admin/class.dc.tab.php';
 
 # Clearbricks extensions
