Working JS Template Engine with a basic functionality

This commit is contained in:
Deon George 2025-06-20 17:05:51 +10:00
parent fac560750e
commit a013276e39
10 changed files with 284 additions and 18 deletions

View File

@ -3,12 +3,16 @@
namespace App\Classes; namespace App\Classes;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class Template class Template
{ {
private string $file; private const LOGKEY = 'T--';
private(set) string $file;
private array $template; private array $template;
private(set) bool $invalid = FALSE; private(set) bool $invalid = FALSE;
private(set) string $reason = ''; private(set) string $reason = '';
@ -48,4 +52,137 @@ class Template
{ {
return array_key_exists($key,$this->template); return array_key_exists($key,$this->template);
} }
/**
* Return the onchange JavaScript for attribute
*
* @param string $attribute
* @return Collection
*/
public function onChange(string $attribute): Collection
{
$attr = collect($this->template['attributes'])
->filter(fn($item,$key)=>! strcasecmp($key,$attribute))
->pop();
if (! $attr)
return collect();
$result = collect();
foreach (Arr::get($attr,'onchange',[]) as $item) {
list($command,$args) = preg_split('/^=([a-zA-Z]+)\((.+)\)$/',$item,-1,PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
switch ($command) {
case 'autoFill':
$result->push($this->autofill($args));
break;
}
}
return $result;
}
/**
* autoFill - javascript to have one attribute fill the value of another
*
* args: is a literal string, with two parts, delimited by a semi-colon ;
* + The first part is the attribute that will be populated
* + The second part may contain many fields like %attr|start-end/flags|additionalcontrolchar%
* to substitute values read from other fields.
* + |start-end is optional, but must be present if the k flag is used
* + /flags is optional
* + |additionalcontrolchar is optional, and specific to a flag being used
*
* + flags may be:
* T:(?) Read display text from selection item (drop-down list), otherwise, read the value of the field
* For fields that aren't selection items, /T shouldn't be used, and the field value will always be read
* k:(?) Tokenize:
* If the "k" flag is not given:
* + A |start-end instruction will perform a sub-string operation upon the value of the attr, passing
* character positions start-end through
* + start can be 0 for first character, or any other integer
* + end can be 0 for last character, or any other integer for a specific position
* If the "k" flag is given:
* + The string read will be split into fields, using : as a delimiter
* + start indicates which field number to pass through
*
* If additionalcontrolchar is given, it will be used as delimiter (e.g. this allows for splitting
* e-mail addresses into domain and domain-local part)
* l: Make the result lower case
* U: Make the result upper case
* A:(?) Remap special characters to their corresponding ASCII value
*
* @note Attributes rendered on the page are lowercase, eg: <attribute id="gidnumber"> for gidNumber
* @note JavaScript generated here depends on js/template.js
* (?) = to test
*/
private function autofill(string $arg): string
{
if (! preg_match('/;/',$arg)) {
Log::alert(sprintf('%s:Invalid argument given to autofill [%s]',self::LOGKEY,$arg));
return '';
}
$result = '';
// $attr has our attribute to update, $string is the format to use when updating it
list($attr,$string) = preg_split('(([^,]+);(.*))',$arg,-1,PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$output = $string;
//$result .= sprintf("\n// %s\n",$arg);
$m = [];
// MATCH : 0 = highlevel match, 1 = attr, 2 = subst, 3 = mod, 4 = delimiter
preg_match_all('/%(\w+)(?:\|([0-9]*-[0-9])+)?(?:\/([klTUA]+))?(?:\|(.)?)?%/U',$string,$m);
foreach ($m[0] as $index => $null) {
$match_attr = strtolower($m[1][$index]);
$match_subst = $m[2][$index];
$match_mod = $m[3][$index];
$match_delim = $m[4][$index];
$substrarray = [];
$result .= sprintf("var %s;\n",$match_attr);
if (str_contains($match_mod,'k')) {
preg_match_all('/([0-9]+)/',trim($match_subst),$substrarray);
$delimiter = ($match_delim === '') ? ' ' : preg_quote($match_delim);
$result .= sprintf(" %s = %s.split('%s')[%s];\n",$match_attr,$match_attr,$delimiter,$substrarray[1][0] ?? '0');
} else {
// Work out the start and end chars needed from this value if we have a range specifier
preg_match_all('/([0-9]*)-([0-9]+)/',$match_subst,$substrarray);
if ((isset($substrarray[1][0]) && $substrarray[1][0]) || (isset($substrarray[2][0]) && $substrarray[2][0])) {
$result .= sprintf("%s = get_attribute('%s',%d,%s);\n",
$match_attr,$match_attr,
$substrarray[1][0] ?? '0',
$substrarray[2][0] ?: sprintf('%s.length',$match_attr));
} else {
$result .= sprintf("%s = get_attribute('%s');\n",$match_attr,$match_attr);
}
}
if (str_contains($match_mod,'l'))
$result .= sprintf("%s = %s.toLowerCase();\n",$match_attr,$match_attr);
if (str_contains($match_mod,'U'))
$result .= sprintf("%s = %s.toUpperCase();\n",$match_attr,$match_attr);
if (str_contains($match_mod,'A'))
$result .= sprintf("%s = toAscii(%s);\n",$match_attr,$match_attr);
// For debugging
//$result .= sprintf("console.log('%s will return:'+%s);\n",$match_attr,$match_attr);
// Reformat out output into JS variables
$output = preg_replace('/'.preg_quote($m[0][$index],'/').'/','\'+'.$match_attr.'+\'',$output);
}
$result .= sprintf("put_attribute('%s','%s');\n",strtolower($attr),$output);
$result .= "\n";
return $result;
}
} }

View File

@ -46,18 +46,12 @@ class HomeController extends Controller
$o = new Entry; $o = new Entry;
$o->setRDNBase($key['dn']); $o->setRDNBase($key['dn']);
foreach (collect(old())->except(['_token','key','step','rdn','rdn_value','userpassword_hash']) as $old => $value) foreach (collect(old())->except(['_token','key','step','rdn','rdn_value','_template','userpassword_hash']) as $old => $value)
$o->{$old} = array_filter($value); $o->{$old} = array_filter($value);
if (count($x=collect(old('objectclass',$request->validated('objectclass')))->dot()->filter())) { if (old('_template',$request->validated('template'))) {
$o->objectclass = Arr::undot($x); $template = $o->template(old('_template',$request->validated('template')));
// Also add in our required attributes
foreach ($o->getAvailableAttributes()->filter(fn($item)=>$item->is_must) as $ao)
$o->{$ao->name} = [Entry::TAG_NOTAG=>''];
} elseif ($request->validated('template')) {
$template = $o->template($request->validated('template'));
$o->objectclass = [Entry::TAG_NOTAG=>$template->objectclasses->toArray()]; $o->objectclass = [Entry::TAG_NOTAG=>$template->objectclasses->toArray()];
foreach ($o->getAvailableAttributes() foreach ($o->getAvailableAttributes()
@ -66,6 +60,13 @@ class HomeController extends Controller
{ {
$o->{$ao->name} = [Entry::TAG_NOTAG=>'']; $o->{$ao->name} = [Entry::TAG_NOTAG=>''];
} }
} elseif (count($x=collect(old('objectclass',$request->validated('objectclass')))->dot()->filter())) {
$o->objectclass = Arr::undot($x);
// Also add in our required attributes
foreach ($o->getAvailableAttributes()->filter(fn($item)=>$item->is_must) as $ao)
$o->{$ao->name} = [Entry::TAG_NOTAG=>''];
} }
$step = $request->step ? $request->step+1 : old('step'); $step = $request->step ? $request->step+1 : old('step');
@ -104,6 +105,7 @@ class HomeController extends Controller
return $view return $view
->with('o',$o) ->with('o',$o)
->with('langtag',Entry::TAG_NOTAG) ->with('langtag',Entry::TAG_NOTAG)
->with('template',NULL)
->with('updated',FALSE); ->with('updated',FALSE);
} }
@ -116,7 +118,7 @@ class HomeController extends Controller
$o = new Entry; $o = new Entry;
$o->setDn($dn); $o->setDn($dn);
foreach ($request->except(['_token','key','step','rdn','rdn_value','userpassword_hash']) as $key => $value) foreach ($request->except(['_token','key','step','rdn','rdn_value','_template','userpassword_hash']) as $key => $value)
$o->{$key} = array_filter($value); $o->{$key} = array_filter($value);
try { try {
@ -368,6 +370,7 @@ class HomeController extends Controller
* @param Request $request * @param Request $request
* @param Collection|null $old * @param Collection|null $old
* @return \Illuminate\View\View * @return \Illuminate\View\View
* @throws InvalidUsage
*/ */
public function frame(Request $request,?Collection $old=NULL): \Illuminate\View\View public function frame(Request $request,?Collection $old=NULL): \Illuminate\View\View
{ {

23
public/js/template.js vendored Normal file
View File

@ -0,0 +1,23 @@
/* JavaScript template engine abstraction layer */
/* Currently implemented for jquery */
// Get a value from an attribute
function get_attribute(attribute,start,end) {
var val = $('#'+attribute).find('input').val();
return ((start !== undefined) && (end !== undefined))
? val.substring(start,end)
: val;
}
// Put a value to an attribute
function put_attribute(attribute,result) {
// Get the value, if the value hasnt changed, then we dont need to do anything
if (get_attribute(attribute) === result)
return;
$('#'+attribute)
.find('input')
.val(result)
.trigger('change');
}

81
public/js/toAscii.js vendored Normal file
View File

@ -0,0 +1,81 @@
//
// Purpose of this file is to remap characters as ASCII characters
//
//
var to_ascii_array = new Array();
to_ascii_array['à'] = 'a';
to_ascii_array['á'] = 'a';
to_ascii_array['â'] = 'a';
to_ascii_array['À'] = 'a';
to_ascii_array['ã'] = 'a';
to_ascii_array['Ã¥'] = 'a';
to_ascii_array['À'] = 'A';
to_ascii_array['Á'] = 'A';
to_ascii_array['Ä'] = 'A';
to_ascii_array['Â'] = 'A';
to_ascii_array['Ã'] = 'A';
to_ascii_array['Å'] = 'A';
to_ascii_array['é'] = 'e';
to_ascii_array['Ú'] = 'e';
to_ascii_array['ë'] = 'e';
to_ascii_array['ê'] = 'e';
to_ascii_array['€'] = 'E';
to_ascii_array['ï'] = 'i';
to_ascii_array['î'] = 'i';
to_ascii_array['ì'] = 'i';
to_ascii_array['í'] = 'i';
to_ascii_array['Ï'] = 'I';
to_ascii_array['Î'] = 'I';
to_ascii_array['Ì'] = 'I';
to_ascii_array['Í'] = 'I';
to_ascii_array['ò'] = 'o';
to_ascii_array['ó'] = 'o';
to_ascii_array['ÃŽ'] = 'o';
to_ascii_array['õ'] = 'o';
to_ascii_array['ö'] = 'o';
to_ascii_array['Þ'] = 'o';
to_ascii_array['Ò'] = 'O';
to_ascii_array['Ó'] = 'O';
to_ascii_array['Ô'] = 'O';
to_ascii_array['Õ'] = 'O';
to_ascii_array['Ö'] = 'O';
to_ascii_array['Ø'] = 'O';
to_ascii_array['ù'] = 'u';
to_ascii_array['ú'] = 'u';
to_ascii_array['Ì'] = 'u';
to_ascii_array['û'] = 'u';
to_ascii_array['Ù'] = 'U';
to_ascii_array['Ú'] = 'U';
to_ascii_array['Ü'] = 'U';
to_ascii_array['Û'] = 'U';
to_ascii_array['Ê'] = 'ae';
to_ascii_array['Æ'] = 'AE';
to_ascii_array['Ü'] = 'y';
to_ascii_array['ÿ'] = 'y';
to_ascii_array['ß'] = 'SS';
to_ascii_array['Ç'] = 'C';
to_ascii_array['ç'] = 'c';
to_ascii_array['Ñ'] = 'N';
to_ascii_array['ñ'] = 'n';
to_ascii_array['¢'] = 'c';
to_ascii_array['©'] = '(C)';
to_ascii_array['®'] = '(R)';
to_ascii_array['«'] = '<<';
to_ascii_array['»'] = '>>';
function toAscii(text) {
//var text = field.value;
var output = '';
for (position=0; position < text.length; position++) {
var tmp = text.substring(position,position+1);
if (to_ascii_array[tmp] !== undefined)
tmp = to_ascii_array[tmp];
output += tmp;
}
return output;
}

View File

@ -17,4 +17,9 @@
@if(file_exists('js/custom.js')) @if(file_exists('js/custom.js'))
<!-- Any Custom JS --> <!-- Any Custom JS -->
<script src="{{ asset('js/custom.js') }}"></script> <script src="{{ asset('js/custom.js') }}"></script>
@endif
@if(file_exists('js/template.js'))
<!-- Template Engine JS -->
<script src="{{ asset('js/template.js') }}"></script>
@endif @endif

View File

@ -7,6 +7,10 @@
<span class="d-flex justify-content-between"> <span class="d-flex justify-content-between">
<span style="width: 20em;"> <span style="width: 20em;">
<strong class="align-middle"><abbr title="{{ $o->description }}">{{ $o->name }}</abbr></strong> <strong class="align-middle"><abbr title="{{ $o->description }}">{{ $o->name }}</abbr></strong>
@if($new && $template?->onChange($o->name)->count())
<sup data-bs-toggle="tooltip" title="@lang('Value calculated by template')"><i class="fas fa-wand-magic-sparkles"></i></sup>
@endif
@if($o->hints->count()) @if($o->hints->count())
<sup> <sup>
[ [

View File

@ -33,4 +33,16 @@
@endif @endif
</div> </div>
</div> </div>
</x-attribute.layout> </x-attribute.layout>
@if($new && ($x=$template?->onChange($o->name))?->count())
@section('page-scripts')
<!-- START: ONCHANGE PROCESSING {{ $o->name }} -->
<script type="text/javascript">
$('#{{ $o->name_lc }}').on('change',function() {
{!! $x->join('') !!}
});
</script>
<!-- END: ONCHANGE PROCESSING {{ $o->name }} -->
@append
@endif

View File

@ -63,10 +63,11 @@
@break @break
@case(2) @case(2)
<x-attribute-type :o="$o->getObject('rdn')" :edit="TRUE" :new="FALSE" :updated="FALSE"/> <input type="hidden" name="_template" value="{{ $template?->file }}">
<x-attribute-type :o="$o->getObject('rdn')" :edit="TRUE" :new="TRUE" :template="$template" :updated="FALSE"/>
@foreach($o->getVisibleAttributes() as $ao) @foreach($o->getVisibleAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :template="$template" :updated="FALSE"/> <x-attribute-type :o="$ao" :edit="TRUE" :new="TRUE" :template="$template" :updated="FALSE"/>
@endforeach @endforeach
@if(! $template) @if(! $template)

View File

@ -101,7 +101,7 @@
<div class="tab-content"> <div class="tab-content">
@php($up=(session()->pull('updated') ?: collect())) @php($up=(session()->pull('updated') ?: collect()))
@foreach($o->getVisibleAttributes() as $ao) @foreach($o->getVisibleAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :updated="$up->contains($ao->name_lc)"/> <x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :template="$template ?? NULL" :updated="$up->contains($ao->name_lc)"/>
@endforeach @endforeach
@include('fragment.dn.add_attr') @include('fragment.dn.add_attr')
@ -124,7 +124,7 @@
<!-- Internal Attributes --> <!-- Internal Attributes -->
<div class="tab-pane mt-3" id="internal" role="tabpanel"> <div class="tab-pane mt-3" id="internal" role="tabpanel">
@foreach($o->getInternalAttributes() as $ao) @foreach($o->getInternalAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="FALSE" :new="FALSE" :updated="FALSE"/> <x-attribute-type :o="$ao" :edit="FALSE" :new="FALSE" :template="$template ?? NULL" :updated="FALSE"/>
@endforeach @endforeach
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@
"givenName": { "givenName": {
"display": "First Name", "display": "First Name",
"onchange": [ "onchange": [
"=autoFill(cn;%givenName% %sn%)", "=autoFill(cn;%givenName% %sn/U%)",
"=autoFill(uid;%givenName|0-1/l%%sn/l%)" "=autoFill(uid;%givenName|0-1/l%%sn/l%)"
], ],
"order": 1 "order": 1
@ -23,7 +23,7 @@
"sn": { "sn": {
"display": "Last Name", "display": "Last Name",
"onchange": [ "onchange": [
"=autoFill(cn;%givenName% %sn%)", "=autoFill(cn;%givenName% %sn/U%)",
"=autoFill(uid;%givenName|0-1/l%%sn/l%)" "=autoFill(uid;%givenName|0-1/l%%sn/l%)"
], ],
"order": 2 "order": 2