WIP create a new eTemplate2 schema definition using Relax NG

This commit is contained in:
ralf 2024-04-17 23:41:18 +02:00
parent c3d74620e8
commit 7f2a3549a5
3 changed files with 16556 additions and 2 deletions

238
doc/dist/etemplate2-rng.php vendored Normal file
View File

@ -0,0 +1,238 @@
<?php
/**
* eTemplate2 XML schema as Relax NG
* - we read the legacy etemplate2.dtd converted by PHPStorm to Relax NG with an XML parser
* - remove the obsolete widgets
* - add new widgets from custom-elements.json file generated by our documentation build
* - output it again as new eTemplate2 Relax NG schema for our xet files
*
* 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://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__."/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');
// overlay can only container template, not all widgets
getByName($grammar, 'overlay')->element->zeroOrMore->ref->attributes()['name'] = 'template';
$classes = [];
foreach($data['modules'] as $module)
{
if (empty($module['exports'])) continue;
foreach($module['exports'] as $export)
{
if ($export['kind'] !== 'custom-element-definition' || preg_match('/_(ro|mobile)$/', $export['name']))
{
$last_export = $export['name'];
continue;
}
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;
}
}
// some widgets use: customElements.defines(<tag>, <class> as any, ...)
if ($export['declaration']['name'] === "anonymous_0")
{
$export['declaration']['name'] = $last_export;
}
/*echo $export['name'].'('.$export['declaration']['name'].'): '.
implode(', ', attributes($classes[$export['declaration']['name']]))."\n";*/
/*
<define name="countdown">
<element name="countdown">
<ref name="attlist.countdown"/>
<empty/>
</element>
</define>
<define name="attlist.countdown" combine="interleave">
<optional>
<attribute name="format" a:defaultValue="s"/>
</optional>
<optional>
<attribute name="onFinish"/>
</optional>
*/
if (in_array($export['name'], ['et2-tab', 'et2-tab-panel']))
{
continue; // we use the legacy tabs and tabpanels
}
// 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');
attributes($classes[$export['declaration']['name']], $attrs);
// widget specific fixes
switch($export['name'])
{
case 'et2-tabbox':
// add legacy children tabs and tabpanels
$element->addChild('ref')->addAttribute('name', 'tabs');
$element->addChild('ref')->addAttribute('name', 'tabpanels');
break;
default:
// dont allow children
$element->addChild('empty');
break;
}
// remove corresponding legacy widget
removeWidget(str_replace('et2-', '', $export['name']));
}
}
$remove = [];
foreach($widgets->choice->children() as $widget)
{
if (preg_match('/^(button|dropdown_button|int|float|menu|select|taglist|tree|passwd|date|time|ajax_select|vfs-(select|path))/', $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');
}
echo $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)
{
$dom=dom_import_simplexml($child);
$dom->parentNode->removeChild($dom);
return true;
}
}
return false;
}
function getByName(SimpleXMLElement $parent, string $name) : ?SimpleXMLElement
{
foreach($parent as $element)
{
if ((string)$element->attributes()['name'] === $name)
{
return $element;
}
}
return null;
}
/**
* 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)
{
static $default_attributes = [
'id' => ['name' => 'id', 'type' => ['text' => 'string']], // commented out with some reasoning in Et2Widget
];
$attributes = $default_attributes+array_filter($class['attributes'] ?? [], static function ($attr)
{
return ($attr['name'] ?? null) && $attr['name'][0] !== '_'; // ignore attributes with empty name or name starting with understore
});
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
$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');
break;
}
}
}

View File

@ -3159,7 +3159,7 @@ tab_height CDATA #IMPLIED
align_tabs (v|h) "h"
>
<!ELEMENT tabs (tab)>
<!ELEMENT tab EMPTY*>
<!ELEMENT tab EMPTY>
<!ATTLIST tab
id CDATA #IMPLIED
label CDATA #IMPLIED

16316
doc/etemplate2/etemplate2.rng Normal file

File diff suppressed because it is too large Load Diff