Skip to content

Commit

Permalink
Merge pull request #41 from EPA-WG/develop
Browse files Browse the repository at this point in the history
0.0.18
  • Loading branch information
sashafirsov authored Mar 21, 2024
2 parents 11195be + ebde5e5 commit 41d40fe
Show file tree
Hide file tree
Showing 19 changed files with 2,332 additions and 74 deletions.
25 changes: 25 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"html.customData": [
"./ide/customData-dce.json",
"./ide/customData-xsl.json"
],
"terminal.integrated.profiles.windows": {

"PowerShell": {
"source": "PowerShell",
"icon": "terminal-powershell"
},
"Command Prompt": {
"path": [
"${env:windir}\\Sysnative\\cmd.exe",
"${env:windir}\\System32\\cmd.exe"
],
"args": [],
"icon": "terminal-cmd"
},
"Git Bash": {
"source": "Git Bash"
}
},
"terminal.integrated.defaultProfile.windows": "Git Bash"
}
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ This project is a POC( Proof of Concept ) targeting to become a base for native
</details>

# use

Use the [bootstrap project](https://github.com/EPA-WG/custom-element-bootstrap) with all pre-configured or
## install
use via CDN
```html
Expand All @@ -56,17 +58,21 @@ npm i -P @epa-wg/custom-element
yarn add @epa-wg/custom-element
```

## [enable IDE support](ide/IDE.md)



## [Live demo 🔗][demo-url]
```html
<custom-element tag="pokemon-tile" hidden>
<h3>{title}</h3> <!-- title is an attribute in instance
mapped into /*/attributes/title -->
<xsl:if test="//smile"> <!-- data-smile DCE instance attribute,
<if test="//smile"> <!-- data-smile DCE instance attribute,
mapped into /*/dataset/smile
used in condition -->
<!-- data-smile DCE instance attribute, used as HTML -->
<div>Smile as: {//smile} </div>
</xsl:if>
</if>
<!-- image would not be visible in sandbox, see live demo -->
<img src="https://unpkg.com/[email protected]/sprites/pokemon/other/dream-world/{pokemon-id}.svg"
alt="{title} image"/>
Expand Down Expand Up @@ -205,12 +211,12 @@ In same way as in DCE itself:
</dce-2>
```
## Attributes
To be served by IDE and to track the attributes changes, they have to be declared via `xsl:param` syntax:
To be served by IDE and to track the attributes changes, they have to be declared via `attribute`:
```html
<custom-element tag="dce-with-attrs" hidden>
<xsl:param name="p1" >default_P1</xsl:param>
<xsl:param name="p2" select="'always_p2'" ></xsl:param>
<xsl:param name="p3" select="//p3 ?? 'def_P3' " ></xsl:param>
<attribute name="p1" >default_P1 </attribute>
<attribute name="p2" select="'always_p2'" ></attribute>
<attribute name="p3" select="//p3 ?? 'def_P3' " ></attribute>
p1: {$p1} <br/> p2: {$p2} <br/> p3: {$p3}
</custom-element>
<dce-with-attrs p1="123" p3="qwe"></dce-with-attrs>
Expand All @@ -229,7 +235,7 @@ i.e. slot `xxx` is matching `<i slot="xxx">...</i>` in payload.
<custom-element tag="with-description" >
<slot name="description">description is not available</slot>
<!-- same as
<xsl:value-of select='/*/payload/*[@slot="description"]'/>
<value-of select='/*/payload/*[@slot="description"]'/>
-->
</custom-element>
<with-description>
Expand All @@ -238,12 +244,12 @@ i.e. slot `xxx` is matching `<i slot="xxx">...</i>` in payload.
```

## loops, variables
Loop implemented via [xsl:for-each](https://developer.mozilla.org/en-US/docs/Web/XSLT/Element/for-each)
Loop implemented via [for-each](https://developer.mozilla.org/en-US/docs/Web/XSLT/Element/for-each)

[Variables in XSLT](https://developer.mozilla.org/en-US/docs/Web/XSLT/Element/variable)

## [XPath](https://developer.mozilla.org/en-US/docs/Web/XSLT/Transforming_XML_with_XSLT/The_Netscape_XSLT_XPath_Reference)
is available in `{}` in attributes, in `xsl:for-each`, `xsl:if`, `xsl:value-of`, and other XSL tags.
is available in `{}` in attributes, in `for-each`, `if`, `value-of`, and other XSL tags.

XPath is a selector language to navigate over custom element instance data, attributes, and payload.

Expand All @@ -262,12 +268,12 @@ in template. In such cases the `xhtml:` prefix in front of troubled tag would so
<local-storage key="basket" slice="basket" live type="json"></local-storage>
<xhtml:table xmlns:xhtml="http://www.w3.org/1999/xhtml" >
<xhtml:tbody>
<xsl:for-each select="//basket/@*">
<for-each select="//basket/@*">
<xhtml:tr>
<xhtml:th> {name()} </xhtml:th>
<xhtml:td> {.} </xhtml:td>
</xhtml:tr>
</xsl:for-each>
</for-each>
</xhtml:tbody>
<xhtml:tfoot>
<xhtml:tr>
Expand Down Expand Up @@ -302,7 +308,7 @@ run transformation under debugger.
* try to add as attribute you could observe and put the value of node name or text to identify the current location in data
within template
```xml
<b title="{name(*)} : {text()}">xml tag name: <xsl:value-of select='name()'/></b>
<b title="{name(*)} : {text()}">xml tag name: <value-of select='name()'/></b>
```

[git-url]: https://github.com/EPA-WG/custom-element
Expand All @@ -317,9 +323,9 @@ within template
[github-image]: https://cdnjs.cloudflare.com/ajax/libs/octicons/8.5.0/svg/mark-github.svg
[npm-image]: https://img.shields.io/npm/v/@epa-wg/custom-element.svg
[npm-url]: https://npmjs.org/package/@epa-wg/custom-element
[coverage-image]: https://unpkg.com/@epa-wg/[email protected].17/coverage/coverage.svg
[coverage-url]: https://unpkg.com/@epa-wg/[email protected].17/coverage/lcov-report/index.html
[storybook-url]: https://unpkg.com/@epa-wg/[email protected].17/storybook-static/index.html?path=/story/welcome--introduction
[coverage-image]: https://unpkg.com/@epa-wg/[email protected].18/coverage/coverage.svg
[coverage-url]: https://unpkg.com/@epa-wg/[email protected].18/coverage/lcov-report/index.html
[storybook-url]: https://unpkg.com/@epa-wg/[email protected].18/storybook-static/index.html?path=/story/welcome--introduction
[sandbox-url]: https://stackblitz.com/github/EPA-WG/custom-element?file=index.html
[webcomponents-url]: https://www.webcomponents.org/element/@epa-wg/custom-element
[webcomponents-img]: https://img.shields.io/badge/webcomponents.org-published-blue.svg
Expand Down
160 changes: 160 additions & 0 deletions bin/xslDtd2Ide.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* node xslDtd2Ide.cjs
* would get xsl 1.0 schema and populate IntelliJ and VS Code IDE custom elements definitions
*
* This is one time use script as XSLT 1.0 schema is not changing.
* DTD parsing here is not generic and cobers only particula XSLT 1.0 schema.
*/
import { readFileSync, writeFileSync } from 'node:fs';


const dtdText = await fetch( 'https://www.w3.org/1999/11/xslt10.dtd' )
.then( ( response ) => response.text() )
.then( ( body ) =>
{
return body;
} );
const matches = dtdText.match( /<([^>]*)>/g );

const chopOff = ( s, begin = 1, end = 1 ) => s.substring( begin, s.length - end );
const trim = s => s?.trim ? s.trim() : s;

let lastComment = ''
const dtdObj = { ENTITY: {}, ELEMENT: {} }
for( const match of matches ){
if( match.startsWith( '<!--' ) ) {
lastComment = match;
continue;
}
const body = chopOff( match, 2 );
const arr = body.split( /\s/ );
const name = arr[ 1 ].trim();
const resolveRef = s => s ? ( s.startsWith ? ( s.startsWith( '%' ) ? dtdObj.ENTITY[ chopOff( s, 1, 0 ).replace( ';',
'' ) ] : s ) : s ) : s;
const attrObj = a =>
{
if( !a || Array.isArray( a ) || !a.trim )
return a;
const as = a.trim();
if( 'CDATA,#PCDATA,NMTOKEN,NMTOKENS,'.includes( as + ',' ) )
return as;
const ar = as.split( ';' )
const aa = ar[ 0 ].split( ' ' );
// if( aa[0].includes('select')){debugger;}
return { name: aa[ 0 ], type: resolveRef( aa[ 1 ] ), defValue: aa[ 1 ], required: (ar[1] || aa[ 2 ])?.trim() }
};
switch( arr[ 0 ] ){
case 'ENTITY':{
let key = arr[ 2 ];
let val = body.substring( body.indexOf( key ) + key.length ).trim();
let ss;
if( val.startsWith( '"' ) || val.startsWith( "'" ) ) {
val = chopOff( val );
if( val.includes( '(#PCDATA' ) ) {
val = val.replace( '(#PCDATA', '' ).replace( ')*', '' ).trim();
ss = [ '#PCDATA', ...val.split( '\n' ).map( s => s.trim() ).map( resolveRef ).flat() ];
} else
ss = val.split( /[\n]/ ).map( s => s.replace( '|', '' ).trim() ).filter( s => s );
} else
ss = val.split( /[|\n]/ );

const v = ss.map( trim ).filter( s => s ).map( resolveRef ).map( attrObj ).flat().filter( s => s );
dtdObj.ENTITY[ key ] = !v.length ? '' : v.length === 1 ? v[ 0 ] : v;
break;
}
case 'ELEMENT':
dtdObj.ELEMENT[ name ] = { values: arr[ 2 ], attributes: [] };
break;
case 'ATTLIST':{
const attrStr = body.split( name )[ 1 ].trim();
const attrs = attrStr.split( '\n' ).map( s => s.trim() );
const elementAttrs = dtdObj.ELEMENT[ name ].attributes;
for( let a of attrs ){
if( a.startsWith( '%' ) ) {
const v = dtdObj.ENTITY[ chopOff( a.split( ';' )[ 0 ], 1, 0 ) ];
if( !v ) {
debugger;
}
Array.isArray( v )
? elementAttrs.push( ...v )
: elementAttrs.push( v );
} else
elementAttrs.push( attrObj( a ) );
}

break;
}
}
}

// replace the tags list in custom-element.js

const tagsCsv = Object.keys( dtdObj.ELEMENT ).map( s => s.replace( 'xsl:', '' ) ).join( ',' );
const jsText = readFileSync( '../custom-element.js', 'utf8' )
const updatedJs = jsText.replace( /^.*export const xslTags = .*$/mg,
`export const xslTags = '${ tagsCsv }'.split(',');` );
writeFileSync( '../custom-element.js', updatedJs );

const vsCode = {
"version": 1.1, tags: Object.keys( dtdObj.ELEMENT ).map( s => (
{ name : s.replace( 'xsl:', '' )
, description : `${ s }`
, attributes : dtdObj.ELEMENT[ s ].attributes.map( a => (
{ name : a.name
, description: `${ JSON.stringify( a ) }`
, type : "string"
, required : a.required === '#REQUIRED'
} ) )
, references : [ { name: "MDN docs"
, url : `https://developer.mozilla.org/en-US/docs/Web/XSLT/Element/${s.replace( 'xsl:', '' )}`
}]
} ) )
};

writeFileSync( '.././ide/customData-xsl.json', JSON.stringify( vsCode, undefined, 4 ) );

const intelliJ = {
"$schema": "http://json.schemastore.org/web-types",
"name": "@epa-wg/custom-element",
"version": "0.0.18",
"js-types-syntax": "typescript",
"description-markup": "markdown",
"contributions": {
"html": {
"elements": [
...Object.keys( dtdObj.ELEMENT ).map( s => (
{ name : s.replace( 'xsl:', '' )
, description : `${ s }`
, attributes : dtdObj.ELEMENT[ s ].attributes.map( a => (
{ name : a.name
, description : `${ JSON.stringify( a ) }`
, type : "string"
, required : a.required === '#REQUIRED'
} ) )
, 'doc-url' : `https://developer.mozilla.org/en-US/docs/Web/XSLT/Element/${s.replace( 'xsl:', '' )}`
} ) ),
{
"name": "for-each",
"description": "The <xsl:for-each> element selects a set of nodes and processes each of them in the same way. It is often used to iterate through a set of nodes or to change the current node. If one or more <xsl:sort> elements appear as the children of this element, sorting occurs before processing. Otherwise, nodes are processed in document order.",
"doc-url": "https://developer.mozilla.org/en-US/docs/Web/XSLT/Element/for-each",
"attributes": [
{
"name": "select",
"description": "Uses an XPath expression to select nodes to be processed.",
"required": true,
"doc-url": "https://developer.mozilla.org/en-US/docs/Web/XSLT/Element/for-each#select",
"value": {
"type": "string"
}
}
]
}
]
}
}
};


writeFileSync( '.././ide/web-types-xsl.json', JSON.stringify( intelliJ, undefined, 4 ) );


2 changes: 1 addition & 1 deletion custom-element.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function log(x: any): void;
* ```html
* <custom-element tag="my-element">
* <template>
* <xsl:param name="p1" >default_P1</xsl:param>
* <attribute name="p1" >default_P1</attribute>
* <style>
* color:green;
* b{ color: blue;}
Expand Down
39 changes: 35 additions & 4 deletions custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ const attr = (el, attr)=> el.getAttribute?.(attr)
, emptyNode = n=> { while(n.firstChild) n.firstChild.remove(); return n; }
, createNS = ( ns, tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElementNS( ns, tag ))
, xslNs = x => ( x?.setAttribute('xmlns:xsl', XSL_NS_URL ), x )
, xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(x) );
, xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(x) )
, cloneAs = (p,tag) =>
{ const px = p.ownerDocument.createElementNS(p.namespaceURI,tag);
for( let a of p.attributes)
px.setAttribute(a.name, a.value);
while( p.firstChild )
px.append(p.firstChild);
return px;
}

function
ASSERT(x)
Expand Down Expand Up @@ -141,6 +149,8 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
, tc = (n =>
{
forEach$(n,'script', s=> s.remove() );
const xslRoot = n.content ?? n.firstElementChild?.content ?? n.body ?? n;
xslTags.forEach( tag => forEach$( xslRoot, tag, el=>toXsl(el,xslRoot) ) );
const e = n.firstElementChild?.content || n.content
, asXmlNode = r => {
const d = xml2dom( '<xhtml/>' )
Expand Down Expand Up @@ -202,14 +212,15 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
if( !fr )
return console.error("transformation error",{ xml:tc.outerHTML, xsl: xmlString( sanitizeXsl ) });
const params = [];
[...fr.querySelectorAll('dce-root>param')].forEach(p=>
{ payload.append(p);
[...fr.querySelectorAll('dce-root>attribute')].forEach(p=>
{ p = cloneAs(p,'xsl:param');
payload.append(p);
let select = attr(p,'select')?.split('??')
if( !select)
{ select = ['//'+attr(p, 'name'), `'${p.textContent}'`];
emptyNode(p);
}
if( select?.length>1){
if( select?.length>1 ){
p.removeAttribute('select');
const c = $( xslDom, 'template[match="ignore"]>choose').cloneNode(true);
c.firstElementChild.setAttribute('test',select[0]);
Expand Down Expand Up @@ -383,6 +394,26 @@ export function assureUID(n,attr)
n.setAttribute(attr, crypto.randomUUID());
return n.getAttribute(attr)
}
export const xslTags = 'stylesheet,transform,import,include,strip-space,preserve-space,output,key,decimal-format,namespace-alias,template,value-of,copy-of,number,apply-templates,apply-imports,for-each,sort,if,choose,when,otherwise,attribute-set,call-template,with-param,variable,param,text,processing-instruction,element,attribute,comment,copy,message,fallback'.split(',');
export const toXsl = (el, defParent) => {
const x = create('xsl:'+el.localName);
for( let a of el.attributes )
x.setAttribute( a.name, a.value );
while(el.firstChild)
x.append(el.firstChild);
if( el.parentElement )
el.parentElement.replaceChild( x, el );
else
{ const p = (el.parentElement || defParent)
, arr = [...p.childNodes];
arr.forEach((n, i) => {
if (n === el)
arr[i] = x;
});
p.replaceChildren(...arr);
}
};

export class
CustomElement extends HTMLElement
{
Expand Down
Loading

0 comments on commit 41d40fe

Please sign in to comment.