mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-07 16:44:20 +01:00
419 lines
13 KiB
PHP
419 lines
13 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* eTemplate2 XML schema
|
||
|
* 1. we read the legacy etemplate2.dtd converted by PHPStorm (Tools > XML Actions > Convert Schema) to Relax NG with an XML parser
|
||
|
* 2. remove the obsolete widgets
|
||
|
* 3. add new widgets from custom-elements.json file generated by our documentation build
|
||
|
* 4. apply overwrites below
|
||
|
* 5. output it again as new eTemplate2 Relax NG schema
|
||
|
* 6. convert it again via PHPStorm (Tools > XML Actions > Convert Schema) to etemplate2.0.dtd referenced in our xet files
|
||
|
* (until we figure out how to use RELAX NG direct)
|
||
|
*
|
||
|
* Open problems:
|
||
|
* - custom-elements.json has no information about element hierarchy: not all elements are allowed be contained by an arbitrary element
|
||
|
* - Relax NG can define attribute types, need to check how with match our internal types to the xet attributes
|
||
|
*
|
||
|
* @link https://en.wikipedia.org/wiki/RELAX_NG RELAX NG (REgular LAnguage for XML Next Generation)
|
||
|
* @link https://relaxng.org/tutorial-20011203.html RELAX NG Tutorial
|
||
|
* @link https://github.com/EGroupware/etemplate/blob/6767672516524444847207d50b21ba59ff7f1540/js/widget_browser.js old widget-browser dtd generation
|
||
|
* @link https://www.jetbrains.com/help/phpstorm/validating-web-content-files.html JetBrains DTD, XML-Schema or RelaxNG support
|
||
|
*/
|
||
|
if (!file_exists($file=__DIR__."/dist/custom-elements.json"))
|
||
|
{
|
||
|
die("Missing '$file file!");
|
||
|
}
|
||
|
if (!($data=json_decode(file_get_contents($file), true)))
|
||
|
{
|
||
|
die("Could not load '$file'!");
|
||
|
}
|
||
|
if (!file_exists(($file = __DIR__."/etemplate2/etemplate2.rng")))
|
||
|
{
|
||
|
die("Missing file '$file', you need to generate it from 'etemplate2.dtd' e.g. with PHPStorm!");
|
||
|
}
|
||
|
$grammar = new SimpleXMLElement(file_get_contents($file));
|
||
|
$widgets = getByName($grammar, 'Widgets');
|
||
|
/**
|
||
|
* Manually overwriting problems / errors in what we automatically generate
|
||
|
* @todo fix in TS sources
|
||
|
*/
|
||
|
$overwrites = [
|
||
|
// RE to remove no longer used legacy widgets not matching "et2-<legacy-name>"
|
||
|
'.remove' => '/^(button|dropdown_button|int|float|menu|select|taglist|tree|passwd|date|time|ajax_select|vfs-(select|path))/',
|
||
|
'*' => [ // all widgets
|
||
|
'.attrs' => [
|
||
|
'id' => 'string', // commented out with some reasoning in Et2Widget
|
||
|
//'data' => null, // ToDo: not sure, but AFAIK this is no attribute, but somehow listed in each widget
|
||
|
'width' => 'string',
|
||
|
'span' => 'string', // actually int|"all"
|
||
|
],
|
||
|
],
|
||
|
'Et2InputWidget' => [
|
||
|
'.attrs' => [
|
||
|
'tabindex' => 'int',
|
||
|
],
|
||
|
],
|
||
|
'Et2Textbox' => [
|
||
|
'.attrs' => [
|
||
|
'placeholder' => 'string',
|
||
|
'maxlength' => 'int',
|
||
|
],
|
||
|
],
|
||
|
'Et2InvokerMixin' => 'Et2TextBox',
|
||
|
'et2-description' => [
|
||
|
'.attrs' => [
|
||
|
'for' => 'string',
|
||
|
],
|
||
|
],
|
||
|
'et2-textarea' => [
|
||
|
'.attrs' => [
|
||
|
'maxlength' => 'int',
|
||
|
'rows' => 'int',
|
||
|
'resizeRatio' => 'number', // is this correct
|
||
|
],
|
||
|
],
|
||
|
'et2-date' => [
|
||
|
'.attrs' => [
|
||
|
'yearRange' => 'string',
|
||
|
'dataFormat' => 'string',
|
||
|
],
|
||
|
],
|
||
|
'et2-hbox' => [
|
||
|
'.children' => 'Widgets',
|
||
|
],
|
||
|
'et2-vbox' => [
|
||
|
'.children' => 'Widgets',
|
||
|
],
|
||
|
'et2-box' => [
|
||
|
'.children' => 'Widgets',
|
||
|
],
|
||
|
'Et2Box' => [ // inherited by et2-(v|h)box too
|
||
|
'.attrs' => [
|
||
|
'overflow' => 'string',
|
||
|
],
|
||
|
],
|
||
|
'et2-tabbox' => [
|
||
|
'.children' => ['tabs','tabpanels'], // add legacy children tabs and tabpanels
|
||
|
'.attrs' => [
|
||
|
'cfDisabled' => 'boolean', // implemented on server-side
|
||
|
'cfTypeFilter' => 'string',
|
||
|
'cfPrivateTab' => 'boolean',
|
||
|
'cfPrepend' => 'string',
|
||
|
'cfExclude' => 'string',
|
||
|
],
|
||
|
],
|
||
|
'et2-tab' => null, // remove/skip, as we currently use legacy tabs and tabpanels
|
||
|
'et2-tab-panel' => null,
|
||
|
'et2-details' => [
|
||
|
'.children' => 'Widgets',
|
||
|
],
|
||
|
'et2-split' => [
|
||
|
'.children' => 'Widgets',
|
||
|
],
|
||
|
'et2-select-*' => 'et2-select', // seems like et2-select-* widgets are NOT parsed
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Fixes on the existing legacy DTD
|
||
|
*/
|
||
|
// make overlay the only allowed start element
|
||
|
removeNode($grammar->start->choice);
|
||
|
$grammar->start->addChild('ref')->addAttribute('name', 'overlay');
|
||
|
// overlay can only container template, not all widgets
|
||
|
getByName($grammar, 'overlay')->element->zeroOrMore->ref->attributes()['name'] = 'template';
|
||
|
getByName($grammar, 'attlist.vfs-upload')->addChild('optional')
|
||
|
->addChild('attribute')->addAttribute('name', 'callback');
|
||
|
// add statustext to tab
|
||
|
getByName($grammar, 'attlist.tab')->addChild('optional')
|
||
|
->addChild('attribute')->addAttribute('name', 'statustext');
|
||
|
|
||
|
// build a hashed version of all classes, members and attributes to e.g. find ancestors
|
||
|
$classes = [];
|
||
|
foreach($data['modules'] as $module)
|
||
|
{
|
||
|
foreach ($module['declarations'] as $declaration)
|
||
|
{
|
||
|
if ($declaration['kind'] === 'class')
|
||
|
{
|
||
|
foreach (['members', 'attributes'] as $collection)
|
||
|
{
|
||
|
foreach ($declaration[$collection] ?? [] as $key => $element)
|
||
|
{
|
||
|
$declaration[$collection][$element['name']] = $element;
|
||
|
unset($declaration[$collection][$key]);
|
||
|
}
|
||
|
}
|
||
|
$classes[$declaration['name']] = $declaration;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// iterate of custom-elements to define in the schema
|
||
|
foreach($data['modules'] as $module)
|
||
|
{
|
||
|
foreach($module['exports'] ?? [] as $export)
|
||
|
{
|
||
|
// some widgets use: customElements.defines(<tag>, <class> as any, ...) --> use previous export
|
||
|
if (!empty($export['declaration']['name']) && $export['declaration']['name'] === "anonymous_0")
|
||
|
{
|
||
|
$export['declaration']['name'] = $last_export;
|
||
|
}
|
||
|
$last_export = $export['name'];
|
||
|
|
||
|
// ignore / skip none-widgets and some of the widgets e.g. r/o versions
|
||
|
if ($export['kind'] !== 'custom-element-definition' ||
|
||
|
preg_match('/_(ro|mobile)$/', $export['name']) ||
|
||
|
array_key_exists($export['name'], $overwrites) && !isset($overwrites[$export['name']]))
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// add the element
|
||
|
$define = $grammar->addChild('define');
|
||
|
$define->addAttribute('name', $export['name']);
|
||
|
$element = $define->addChild('element');
|
||
|
$element->addAttribute('name', $export['name']);
|
||
|
$attrs = $element->addChild('ref');
|
||
|
$attrs->addAttribute('name', 'attlist.'.$export['name']);
|
||
|
// add to widgets
|
||
|
$widgets->choice->addChild('ref')->addAttribute('name', $export['name']);
|
||
|
|
||
|
// add the element-attributes
|
||
|
$attrs = $grammar->addChild('define');
|
||
|
$attrs->addAttribute('name', 'attlist.'.$export['name']);
|
||
|
$attrs->addAttribute('combine', 'interleave');
|
||
|
if (empty($classes[$export['declaration']['name']]['tagName'])) $classes[$export['declaration']['name']]['tagName'] = $export['name'];
|
||
|
attributes($classes[$export['declaration']['name']], $attrs);
|
||
|
|
||
|
// add or disallow children depending on overwrites (not available from the TS sources)
|
||
|
// ToDo: this ignores the use in slots!
|
||
|
if (empty($overwrites[$export['name']]['.children']))
|
||
|
{
|
||
|
// don't allow children
|
||
|
$element->addChild('empty');
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// add allowed children
|
||
|
foreach((array)$overwrites[$export['name']]['.children'] as $child)
|
||
|
{
|
||
|
$element->addChild('ref')->addAttribute('name', $child);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// remove corresponding legacy widget
|
||
|
removeWidget(str_replace('et2-', '', $export['name']));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$remove = [];
|
||
|
foreach($widgets->choice->children() as $widget)
|
||
|
{
|
||
|
if (preg_match($overwrites['.remove'], $name=(string)$widget->attributes()['name']))
|
||
|
{
|
||
|
$remove[] = $name; // removing direct disturbs the foreach!
|
||
|
}
|
||
|
}
|
||
|
foreach($remove as $name)
|
||
|
{
|
||
|
removeWidget($name);
|
||
|
}
|
||
|
|
||
|
$dom = new DOMDocument("1.0");
|
||
|
$dom->preserveWhiteSpace = false;
|
||
|
$dom->formatOutput = true;
|
||
|
$dom->loadXML($grammar->asXML());
|
||
|
if (php_sapi_name() !== "cli")
|
||
|
{
|
||
|
header('Content-Type: application/xml; charset=utf-8');
|
||
|
}
|
||
|
// add <value>1</value> to legacy widget boolean attributes
|
||
|
echo preg_replace('#<choice>
|
||
|
(\s+)<value>true</value>
|
||
|
(\s+)<value>false</value>
|
||
|
(\s+)</choice>#', "<choice>\n\$1<value>false</value>\n\$1<value>true</value>\n\$1<value>1</value>\n\$3</choice>",
|
||
|
// update the header
|
||
|
preg_replace('#<!--.*-->#s', '<!--
|
||
|
==========================================================
|
||
|
EGroupware: eTemplate 2.0 DTD
|
||
|
AUTHOR: Hadi Nategh <hn[AT]egroupware.org>, Ralf Becker <rb[AT]egroupware.org>
|
||
|
COPYRIGHT: 2016-2024 by EGroupware GmbH
|
||
|
LICENSE: https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License Version 2+
|
||
|
PUBLIC ID: "https://www.egroupware.org/etemplate2.0.dtd"
|
||
|
Version: 1.2
|
||
|
==========================================================
|
||
|
|
||
|
==========================================================
|
||
|
An example how to use this DTD from your XML document:
|
||
|
|
||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "https://www.egroupware.org/etemplate2.0.dtd">
|
||
|
<overlay>
|
||
|
...
|
||
|
</overlay>
|
||
|
==========================================================
|
||
|
-->', $dom->saveXML()));
|
||
|
|
||
|
/**
|
||
|
* Remove (legacy-)widget with given name from schema
|
||
|
*
|
||
|
* @param string $name
|
||
|
* @return void
|
||
|
*/
|
||
|
function removeWidget(string $name)
|
||
|
{
|
||
|
global $grammar, $widgets;
|
||
|
if (removeByName($widgets->choice, $name))
|
||
|
{
|
||
|
removeByName($grammar, $name);
|
||
|
removeByName($grammar, 'attlist.'.$name);
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function removeByName(SimpleXMLElement $parent, string $name) : bool
|
||
|
{
|
||
|
foreach($parent->children() as $child)
|
||
|
{
|
||
|
if ((string)$child->attributes()['name'] === $name)
|
||
|
{
|
||
|
removeNode($child);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function removeNode(SimpleXMLElement $node)
|
||
|
{
|
||
|
$dom=dom_import_simplexml($node);
|
||
|
$dom->parentNode->removeChild($dom);
|
||
|
}
|
||
|
|
||
|
function getByName(SimpleXMLElement $parent, string $name) : ?SimpleXMLElement
|
||
|
{
|
||
|
foreach($parent as $element)
|
||
|
{
|
||
|
if ((string)$element->attributes()['name'] === $name)
|
||
|
{
|
||
|
return $element;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Overwrite attributes in given element / class
|
||
|
* @param array& $element
|
||
|
* @param string|null $name overwrites to use e.g. "*", default use $element['name']
|
||
|
* @return void
|
||
|
*/
|
||
|
function overwriteAttributes(array& $element, string $name=null)
|
||
|
{
|
||
|
global $overwrites;
|
||
|
|
||
|
if (!isset($name)) $name = $element['tagName'];
|
||
|
|
||
|
// forward just to another widget, class or mixin
|
||
|
if (isset($overwrites[$name]) && is_string($overwrites[$name]))
|
||
|
{
|
||
|
$name = $overwrites[$name];
|
||
|
}
|
||
|
if (empty($overwrites[$name]['.attrs']))
|
||
|
{
|
||
|
return; // nothing to do
|
||
|
}
|
||
|
foreach($overwrites[$name]['.attrs'] as $attr => $type)
|
||
|
{
|
||
|
if (isset($type))
|
||
|
{
|
||
|
$element['attributes'][$attr] = ['name' => $attr, 'type' => ['text' => $type]];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
unset($element['attributes'][$attr]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate attribute list for an element
|
||
|
*
|
||
|
* @param array $class class defining the element
|
||
|
* @param SimpleXMLElement|null $attrs attribute list element: <define name="attlist.<element>" combine="interleave"/>
|
||
|
* @return string[]|void
|
||
|
*/
|
||
|
function attributes(array $class, ?SimpleXMLElement $attrs=null)
|
||
|
{
|
||
|
overwriteAttributes($class, '*');
|
||
|
overwriteAttributes($class);
|
||
|
// also apply overwrites of own class, direct parent and mixins
|
||
|
foreach(getAncestors($class) as $parent)
|
||
|
{
|
||
|
if ($parent && !empty($parent['name']) && preg_match('/^Et2/', $parent['name'])) // can also be Lit or Sl*
|
||
|
{
|
||
|
overwriteAttributes($class, $parent['name']);
|
||
|
}
|
||
|
}
|
||
|
$attributes = array_filter($class['attributes'] ?? [], static function ($attr)
|
||
|
{
|
||
|
return ($attr['name'] ?? null) && $attr['name'][0] !== '_'; // ignore attributes with empty name or name starting with underscore
|
||
|
});
|
||
|
|
||
|
if (!isset($attrs))
|
||
|
{
|
||
|
return array_map(static function($attr) use ($class)
|
||
|
{
|
||
|
return $attr['name'].'('.($attr['type']['text']??'any').
|
||
|
(isset($attr['fieldName']) && isset($class['members'][$attr['fieldName']]['default']) ?
|
||
|
':'.$class['members'][$attr['fieldName']]['default'] : '').')';
|
||
|
}, $attributes);
|
||
|
}
|
||
|
foreach($attributes as $attr)
|
||
|
{
|
||
|
// todo: are all attributes optional, probably
|
||
|
$optional = $attrs->addChild('optional');
|
||
|
$attribute = $optional->addChild('attribute');
|
||
|
$attribute->addAttribute('name', $attr['name']);
|
||
|
if (isset($attr['fieldName']) && isset($class['members'][$attr['fieldName']]['default']))
|
||
|
{
|
||
|
$default = $class['members'][$attr['fieldName']]['default'];
|
||
|
if (in_array($default[0], ['"', "'"]) && $default[0] === substr($default, -1))
|
||
|
{
|
||
|
$default = substr($default, 1, -1);
|
||
|
}
|
||
|
$attribute->addAttribute('a:defaultValue', $default, 'http://relaxng.org/ns/compatibility/annotations/1.0');
|
||
|
}
|
||
|
switch ($attr['type']['text'] ?? 'any')
|
||
|
{
|
||
|
case 'boolean':
|
||
|
$choice = $attribute->addChild('choice');
|
||
|
$choice->addChild('value', 'false');
|
||
|
$choice->addChild('value', 'true');
|
||
|
$choice->addChild('value', '1'); // often used in our templates
|
||
|
// not understood by DTD :(
|
||
|
//$choice->addChild('text'); // as we allow "@<attr>" or "$cont[name]"
|
||
|
break;
|
||
|
// todo: other types are understood by RELAX NG, but not by DTD
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get ancestors: superclass(es) and mixins of the current class/element
|
||
|
*
|
||
|
* @param array|null $class
|
||
|
* @return array[] of array with name attribute
|
||
|
*/
|
||
|
function getAncestors(?array $class=null)
|
||
|
{
|
||
|
if (!isset($class) || empty($class['name']) || !preg_match('/^Et2/', $class['name']))
|
||
|
{
|
||
|
return [];
|
||
|
}
|
||
|
if (!isset($class['kind']))
|
||
|
{
|
||
|
global $classes;
|
||
|
$class = $classes[$class['name']] ?? null;
|
||
|
}
|
||
|
return $class ? array_filter([$class, ...getAncestors($class['superclass']??null), ...$class['mixins']??[]]) : [];
|
||
|
}
|