UNPKG

create-modulo

Version:

Starter projects for Modulo.html - Ready for all uses - Markdown-SSG / SSR / API-backed SPA

962 lines (946 loc) 98.8 kB
/*<script src=Modulo.html></script><meta charset=utf8><script type=mdocs>--- version: v0.1.0 copyright: 2025 Michael Bethencourt - LGPLv3 - NO WARRANTEE OR IMPLIED UTILITY; ANY MODIFICATIONS OR DERIVATIVES OF THE MODULO FRAMEWORK MUST BE LGPLv3+ LGPL Notice: It is acceptable to link ("bundle") and distribute the Modulo Framework with other code as long as the LICENSE and NOTICE remains intact. --- // */ // md: `[ % ] v0.1.0 [ModuloHTML.org](https://modulohtml.org/)` var Modulo = function Modulo (OPTS = { }) { const Lib = OPTS.globalLibrary || window.Modulo || Modulo; //md:# **ᵐ°dᵘ⁄o** Lib.instanceID = Lib.instanceID || 0; this.id = ++Lib.instanceID; const globals = OPTS.globalProperties || [ 'config', 'util', 'engine', 'processor', 'part', 'core', 'templateMode', 'templateTag', 'templateFilter', 'contentType', 'command', 'build', 'definitions', 'stores', 'fetchQueue' ]; for (const name of globals) { const stdLib = Lib[name.charAt(0).toUpperCase() + name.slice(1) + 's']; this[name] = stdLib ? stdLib(this) : { }; // Exe StdLib Module } } /* md: ###`[ % ]` [Create **App** »](?argv=newapp) md:###`[ % ]` [Create **Library** »](?argv=newlib) md:###`[ % ]` [Create **Markdown** »](?argv=newmd) md:_**Hint:** Click starter template for preview. Click file(s) to save._ md:**About:** Modulo (or ᵐ°dᵘ⁄o) is a [single file](?argv=edit) frontend md:framework, squeezing in numerous tools for modern HTML, CSS, and md:JavaScript. Featuring: Web Components, CSS Scoping, Shadow DOM, md:SSG / SSR, Bundling, Store and State Management, Templating, and more. */ Modulo.Parts = function ComponentParts (modulo) {// md: ## Component Parts /* md: ### Include md:```html=component<Include> md:<script>document.body.innerHTML += '<h1>ᵐ°dᵘ⁄o</h1>'<-script> md:<style>:root { --c1: #B90183; } body { background: var(--c1); }</style> md:</Include>``` _Include_ is for global styles, links, and scripts. */ class Include { static LoadMode(modulo, def, value) { const { bundleHead, newNode, urlReplace, getParentDefPath } = modulo.util; const text = urlReplace(def.Content, getParentDefPath(modulo, def)); for (const elem of newNode(text).children) { // md: Include loops bundleHead(modulo, elem); // md: across it's children adding to head, } // md: and pausing during load. When built, it combines into bundles. } static Server({ part, util }, def, value) { def.Content = (def.Content || '') + new part.Template(def.TagTemplate) .render({ entries: util.keyFilter(def), value }); } intitializedCallback(renderObj) { Include.LoadMode(this.modulo, this.conf, 'lazy'); } } class Props { // md: ### Props static factoryCallback({ elementClass }, def, modulo) { const isLower = key => key[0].toLowerCase() === key[0]; // skip "-prefixed" const keys = Array.from(Object.keys(def)).filter(isLower); elementClass.observedAttributes.push(...keys); // (modify elementClass) } // md:```html=component<Props quote name="Unknown"></Props> // md:<Template>{{ props.name }} says "{{ props.quote }}"</Template>``` initializedCallback() { // md: Props loads attributes from the element this.data = { }; // md: when the component is initialized (mounted): Object.keys(this.attrs).forEach(attrName => this.updateProp(attrName)); return this.data; // md: E.g. `<x-App name="Jo"></x-App>` sets _name_. } updateProp(attrName) { // md: It also rerenders if one of those is changed. this.data[attrName] = this.element.hasAttribute(attrName) ? this.element.getAttribute(attrName) : this.attrs[attrName]; } attrCallback({ attrName }) { if (attrName in this.attrs) { this.updateProp(attrName); this.element.rerender(); } } } //md:### Style //md:```html=component<Template><em class="big">Stylish</em> TEXT</Template> //md:<Style>.big { font-size: 4rem } :host { background: #82d4a4 }</Style>``` class Style { static AutoIsolate(modulo, def, value) { // md: _Style_ "auto-isolates" CSS. const { AutoIsolate } = modulo.part.Style; // (for recursion) const { namespace, mode, Name } = modulo.definitions[def.Parent] || {}; if (value === true) { // md: _Style_ uses `<Component mode=....>` to AutoIsolate(modulo, def, mode); //md:isolate: `mode=regular` will } else if (value === 'regular' && !def.isolateClass) {//md:prefix your def.prefix = def.prefix || `${namespace}-${Name}`; //md:selectors } else if (value === 'vanish') { //md:with the component name, while def.isolateClass = def.isolateClass || def.Parent;//md:setting } // md:`mode=vanish` adds the class to children outside of slots. } domCallback(renderObj) { const { mode } = modulo.definitions[this.conf.Parent] || {}; const { innerDOM, Parent } = renderObj.component; const { isolateClass, isolateSelector, shadowContent } = this.conf; if (isolateClass && isolateSelector && innerDOM) { // Attach classes const selector = isolateSelector.filter(s => s).join(',\n'); for (const elem of innerDOM.querySelectorAll(selector)){ elem.classList.add(isolateClass); } } // md: For `mode=shadow`, it adds a "private" sheet to the shadow DOM if (shadowContent && innerDOM) { // md: root during DOM reconciliation. innerDOM.prepend(this.modulo.util.newNode(shadowContent, 'STYLE')); } } static processSelector (modulo, def, selector) {// md: It also permits const hostPrefix = def.prefix || ('.' + def.isolateClass);//md:use of if (def.isolateClass || def.prefix) {//md:the `:host` "outer" selector. const hostRegExp = new RegExp(/:(host|root)(\([^)]*\))?/, 'g'); selector = selector.replace(hostRegExp, hostClause => { hostClause = hostClause.replace(/:(host|root)/gi, ''); return hostPrefix + (hostClause ? `:is(${ hostClause })` : ''); }); } let selectorOnly = selector.replace(/\s*[\{,]\s*,?$/, '').trim(); if (def.isolateClass && selectorOnly !== hostPrefix) { // Remove extraneous characters (and strip ',' for isolateSelector) let suffix = /{\s*$/.test(selector) ? ' {' : ', '; selectorOnly = selectorOnly.replace(/:(:?[a-z-]+)\s*$/i, (all, pseudo) => { if (pseudo.startsWith(':') || def.corePseudo.includes(pseudo)) { suffix = ':' + pseudo + suffix; // Attach to suffix, on outside return ''; // Strip pseudo from the selectorOnly variable } return all; }); def.isolateSelector.push(selectorOnly); // Add to array for later selector = `.${ def.isolateClass }:is(${ selectorOnly })` + suffix; } if (def.prefix && !selector.startsWith(def.prefix)) { // A prefix was specified, so prepend it if it doesn't have it selector = `${ def.prefix } ${ selector }`; } return selector; } static ProcessCSS (modulo, def, value) { const { bundleHead, newNode, urlReplace, getParentDefPath } = modulo.util; value = value.replace(/\/\*.+?(\*\/)/g, ''); // rm comment, rewrite urls value = urlReplace(value, getParentDefPath(modulo, def), def.urlMode); if (def.isolateClass || def.prefix) { def.isolateSelector = []; // Used to accumulate elements to select value = value.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/gi, selector => { selector = selector.trim(); return /^(from|to|@)/.test(selector) ? selector : this.processSelector(modulo, def, selector); }); } if ((modulo.definitions[def.Parent] || {}).mode === 'shadow') { def.shadowContent = (def.shadowContent || '') + value; } else { // md: During `build`, all non-shadow _Style_ parts get bundled. bundleHead(modulo, newNode(value, 'STYLE'), modulo.bundles.modstyle); } } } //md:### StaticData //md:```html=component<Template><pre>{{ staticdata|json:2 }}</pre></Template> //md:<StaticData -data-type=md -src=Modulo.html></StaticData>``` class StaticData {// md: Use for bundling unchanging data (e.g. API, files, prepareCallback() { // md: config, etc) in with a component. return this.conf.data; } } class Script { // md:### Script // md:```html=component<Script>function hi(){ alert(ref.h1.outerHTML) }<-Script> // md:<Template><h1 on.click=script.hi script.ref>Click me</h1></Template>``` // md: Scripts let you embed JavaScript code "inside" your component. static AutoExport (modulo, def, value) { const nameRE = /(function|class)\s+(\w+)/; // gather exports const matches = def.Content.match(new RegExp(nameRE, 'g')) || []; const isSym = sym => sym && !(sym in modulo.config.syntax.jsReserved); const symbols = matches.map(sym => sym.match(nameRE)[2]); const ifUndef = n => `"${n}":typeof ${n} !=="undefined"?${n}:undefined`; const expStr = symbols.filter(isSym).map(ifUndef).join(','); const { ChildrenNames } = modulo.definitions[def.Parent] || { }; const sibs = (ChildrenNames || []).map(n => modulo.definitions[n].Name); sibs.push('component', 'element', 'parts', 'ref'); // gather locals const locals = sibs.filter(name => def.Content.includes(name)); const setLoc = locals.map(name => `${ name }=o.${ name }`).join(';') def.Content += locals.length ? ('var ' + locals.join(',')) : ''; def.Content += `;return{_setLocal:function(o){${ setLoc }}, ${ expStr }}`; } initializedCallback(renderObj) { // md: Upon Component initialization, it const func = modulo.registry.modules[this.conf.DefinitionName];//md:will this.exports = func.call(window, modulo);// md: run your code, gathering for (const method of Object.keys(this.exports)) { // md: each export. if (method === 'initializedCallback' || !method.endsWith('Callback')) { continue; // md: Named functions (e.g. `function foo`) and } // md: classes get "auto-exported", for attachment to events. this[method] = arg => { const renderObj = this.element.getCurrentRenderObj(); const script = renderObj[this.conf.Name]; this.eventCallback(renderObj); Object.assign(script, this.exports[method](arg) || {}); // Run }; } this.ref = { }; this.eventCallback(renderObj); return Object.assign(this.exports, this.exports.initializedCallback ? this.exports.initializedCallback(renderObj) : { }); // Run init } eventCallback(renderObj) { this.exports._setLocal(Object.assign({ ref: this.ref, element: this.element, parts: this.element.cparts }, renderObj)); } refMount({ el, nameSuffix, value }) { // md: The `script.ref` directive const refVal = value ? modulo.util.get(el, value) : el; // md: assigns this.ref[nameSuffix || el.tagName.toLowerCase()] = refVal; // md: DOM } // md: references. E.g. `<img script.ref>` is called `ref.img` in script. refUnmount({ el, nameSuffix }) { // md: When unmounted, the reference is delete this.ref[nameSuffix || el.tagName.toLowerCase()]; // md: deleted. } } // md:### State // md:```html=component<State msg="Lorem" a:=123></State> // md:<Template>{{ state.msg }}: <input state.bind name=msg></Template>``` class State { // _State_ declares _state variables_ to bind to forms. static factoryCallback(renderObj, def, modulo) { if (def.Store) { // md: If a -store= is specified, it's global. const store = modulo.util.makeStore(modulo, def); if (!(def.Store in modulo.stores)) { // md: The first one modulo.stores[def.Store] = store; // md: encountered } else { // md: with that name will create the "Store". Object.assign(modulo.stores[def.Store].data, store.data); } // md: Subsequent usage will share and react to that one "Store". } // md: Without -store=, it will be be private to each component. } initializedCallback(renderObj) { const store = this.conf.Store ? this.modulo.stores[this.conf.Store] : this.modulo.util.makeStore(this.modulo, Object.assign(this.conf, renderObj[this.conf.Init])); store.subscribers.push(Object.assign(this, store)); this.types = { range: Number, number: Number } this.types.checkbox = (v, el) => el.checked; return store.data; } bindMount({ el, nameSuffix, value, listen }) { const name = value || el.getAttribute('name'); const val = this.modulo.util.get(this.data, name, this.conf.Dot); this.modulo.assert(val !== undefined, `state.bind "${name}" undefined`); const isText = el.tagName === 'TEXTAREA' || el.type === 'text'; const evName = nameSuffix ? nameSuffix : (isText ? 'keyup' : 'change'); // Bind the "listen" event to propagate to all, and trigger initial vals listen = listen ? listen : () => this.propagate(name, el.value, el); el.addEventListener(evName, listen); this.boundElements[name] = this.boundElements[name] || []; this.boundElements[name].push([ el, evName, listen ]); this.propagate(name, val, null, [ el ]); // Trigger element assignment } bindUnmount({ el, nameSuffix, value }) { const name = value || el.getAttribute('name'); const remainingBound = []; for (const row of this.boundElements[name]) { if (row[0] === el) { row[0].removeEventListener(row[1], row[2]); } else { remainingBound.push(row); } } this.boundElements[name] = remainingBound; } stateChangedCallback(name, value, el) { this.modulo.util.set(this.data, name, value, this.conf.Dot); if (!this.conf.Only || this.conf.Only.includes(name)) { this.element.rerender(); } } eventCallback() { this._oldData = Object.assign({}, this.data); } propagate(name, val, originalEl = null, arr = null) { arr = arr ? arr : this.subscribers.concat( (this.boundElements[name] || []).map(row => row[0])); const typeConv = this.types[ originalEl ? originalEl.type : null ]; val = typeConv ? typeConv(val, originalEl) : val; // Apply conversion for (const el of arr) { if (originalEl && el === originalEl) { // skip } else if (el.stateChangedCallback) { el.stateChangedCallback(name, val, originalEl, arr); } else if (el.type === 'checkbox') { el.checked = !!val; } else { // Normal input el.value = val; } } } eventCleanupCallback() { for (const name of Object.keys(this.data)) { this.modulo.assert(!this.conf.AllowNew && name in this._oldData, `State variable "${ name }" is undeclared (no "-allow-new")`); if (this.data[name] !== this._oldData[name]) { this.propagate(name, this.data[name], this); } } this._oldData = null; } } class Template { // md: ### Template // md: Templates run _Modulo Template Language_ to generate HTML. static CompileTemplate (modulo, def, value) { const compiled = modulo.util.instance(def, { }).compile(value); def.Code = `return function (CTX, G) { ${ compiled } };`; } constructedCallback() { // Flatten filters, tags, and modes this.stack = []; // cause err on unclosed const { filters, tags, modes } = this.conf; const { templateFilter, templateTag, templateMode } = this.modulo; Object.assign(this, this.modulo.config.template, this.conf); // md: Templates have numerous built-in _filters_, _tags_, and _modes_. this.filters = Object.assign({ }, templateFilter, filters); this.tags = Object.assign({ }, templateTag, tags); this.modes = Object.assign({ }, templateMode, modes); } initializedCallback() { return { render: this.render.bind(this) }; // Export "render" method } constructor(text, options = null) { // md:In JavaScript, it's available as: if (typeof text === 'string') { // md: `new Template('Hi {{ a }}')` window.modulo.util.instance(options || { }, null, this); // Setup object this.conf.DefinitionName = '_template_template' + this.id; // Unique const code = `return function (CTX, G) { ${ this.compile(text) } };`; this.modulo.processor.code(this.modulo, this.conf, code); } } renderCallback(renderObj) { if (this.conf.Name === 'template' || this.conf.active) { // If primary renderObj.component.innerHTML = this.render(renderObj); // Do render } } parseExpr(text) { // Output JS code that evaluates an equivalent template code expression const filters = text.split('|'); let results = this.parseVal(filters.shift()); // Get left-most val for (const [ fName, arg ] of filters.map(s => s.trim().split(':'))) { const argList = arg ? ',' + this.parseVal(arg) : ''; results = `G.filters["${fName}"](${results}${argList})`; } return results; } parseCondExpr(string) { // Return an Array that splits around ops in an "if"-style statement const regExpText = ` (${this.opTokens.split(',').join('|')}) `; return string.split(RegExp(regExpText)); } toCamel(string) { // Takes kebab-case and converts toCamelCase return string.replace(/-([a-z])/g, g => g[1].toUpperCase()); } parseVal(string) { // Parses str literals, de-escaping as needed, numbers, and context vars const s = string.trim(); if (s.match(/^('.*'|".*")$/)) { // String literal return JSON.stringify(s.substr(1, s.length - 2)); } return s.match(/^\d+$/) ? s : `CTX.${ this.toCamel(s) }` } tokenizeText(text) { // Join all modeTokens with | (OR in regex) const re = '(' + this.modeTokens.map(modulo.templateFilter.escapere) .join('|(').replace(/ +/g, ')(.+?)'); return text.split(RegExp(re)).filter(token => token !== undefined); } compile(text) { const { normalize } = this.modulo.util; let code = 'var OUT=[];\n'; // Variable used to accumulate code let mode = 'text'; // Start in text mode const tokens = this.tokenizeText(text); for (const token of tokens) { if (mode) { // If in a "mode" (text or token), then call mode func const result = this.modes[mode](token, this, this.stack); code += result ? (result + '\n') : ''; } // FSM for mode: ('text' -> null) (null -> token) (* -> 'text') mode = (mode === 'text') ? null : (mode ? 'text' : token); } code += '\nreturn OUT.join("");' const unclosed = this.stack.map(({ close }) => close).join(', '); this.modulo.assert(!unclosed, `Unclosed tags: ${ unclosed }`); return code; } render(local) { if (!this.renderFunc) { const mod = this.modulo.registry.modules[this.conf.DefinitionName]; this.renderFunc = mod.call(window, this.modulo); } return this.renderFunc(Object.assign({ local, global: this.modulo }, local), this); } } // md: --- const cparts = { State, Props, Script, Style, Template, StaticData, Include }; return modulo.util.insObject(cparts); } // /* End of Component Parts */ /*#UNLESS#*/ Modulo.TemplateModes = modulo => ({ // md: ## Template Language '{%': (text, tmplt, stack) => { // md: _Modulo Template Language_ looks for const tTag = text.trim().split(' ')[0]; // md: syntax like `{% ... %}`. const tagFunc = tmplt.tags[tTag]; // md: These are "template tags". if (stack.length && tTag === stack[stack.length - 1].close) { return stack.pop().end; } else if (!tagFunc) { throw new Error(`Unexpected tag "${tTag}": ${text}`); } const result = tagFunc(text.slice(tTag.length + 1), tmplt); if (result.end) { // md: Most expect an end tag: e.g. `{% if %}` has stack.push({ close: `end${ tTag }`, ...result });//md:`{% endif %}`. } // md: However, `{% include %}` and `{% debugger %}` do not. return result.start || result; }, // md: Code like `{{ state.a }}` will insert values in the generated HTML. '{-{': (text, tmplt) => `OUT.push('{{${ text }}}');`, // md: Escape syntax '{-%': (text, tmplt) => `OUT.push('{%${ text }%}');`,//md: is `{-% %-}`. '{#': (text, tmplt) => false, // md: Short comments are `{# like this #}`. '{{': (text, tmplt) => `OUT.push(G.${ tmplt.unsafe }(${ tmplt.parseExpr(text) }));`, text: (text, tmplt) => text && `OUT.push(${JSON.stringify(text)});`, }) // md: Simple example of `{% if %}`: Modulo.TemplateTags = modulo => ({//md:```html=component<Template>{% if state.a %} 'comment':() => ({ start: "/*", end: "*/"}),//md:<p>Y</p>{% else %}<p>N</p> 'debugger': () => 'debugger;', // md:{% endif %}</Template> 'else': () => '} else {', //md:<State a:=true></State>``` 'elif': (s, tmplt) => '} else ' + tmplt.tags['if'](s, tmplt).start, 'empty': (text, {stack}) => { // Empty only runs if loop doesn't run const varName = 'G.FORLOOP_NOT_EMPTY' + stack.length; const oldEndCode = stack.pop().end; // get rid of dangling for const start = `${varName}=true; ${oldEndCode} if (!${varName}) {`; const end = `}${varName} = false;`; return { start, end, close: 'endfor' }; }, // md: `{% for %}` is useful for "plural info", as it repeat its 'for': (text, tmplt) => { // md: contents in a loop: const arrName = 'ARR' + tmplt.stack.length; const [ varExp, arrExp ] = text.split(' in '); let start = `var ${arrName}=${tmplt.parseExpr(arrExp)};`; // TODO: Upgrade to for...of loop (after good testing) start += `for (var KEY in ${arrName}) {`; const [keyVar, valVar] = varExp.split(',').map(s => s.trim()); if (valVar) {//md:```html=component<Template> start += `CTX.${keyVar}=KEY;`;//md:{% for foobar in state.d %} }//md:<h2>{{ foobar }}</h2> start += `CTX.${valVar?valVar:varExp}=${arrName}[KEY];`; //md:{% endfor %} return { start, end: '}'};//md:</Template><State d:='["A", "b"]'></State>``` }, 'if': (text, tmplt) => { // Limit to 3 (L/O/R) const [ lHand, op, rHand ] = tmplt.parseCondExpr(text); const condStructure = !op ? 'X' : tmplt.opAliases[op] || `X ${op} Y`; const condition = condStructure.replace(/([XY])/g, (k, m) => tmplt.parseExpr(m === 'X' ? lHand : rHand)); const start = `if (${condition}) {`; return { start, end: '}' }; }, 'include': (text) => `OUT.push(CTX.${ text.trim() }.render(CTX));`, 'ignoremissing': () => ({ start: 'try{\n', end: '}catch (e){}\n' }), 'with': (text, tmplt) => { const [ varExp, varName ] = text.split(' as '); const code = `CTX.${ varName }=${ tmplt.parseExpr(varExp) };\n`; return { start: 'if(1){' + code, end: '}' }; }, }) /*#ENDUNLESS#*/ Modulo.TemplateFilters = modulo => {//md:Using `|` we can apply _filters_: //md:```html=component<Template><h2>Modulo Filters:</h2><dl> //md:{% for fil in global.template-filter|keys|sorted %}<dt>{{fil}}</dt> //md:<dd>"ab1-cd2"|{{fil}}&rarr; {% ignoremissing %}"{{"ab1-cd2"|apply:fil}}" //md:{% endignoremissing %}</dd>{% endfor %}</dl></Template>``` const { get } = modulo.util; const safe = s => Object.assign(new String(s),{ safe: true }); const escapere = s => s.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&'); const syntax = (s, arg = 'text') => { for (const [ find, sub, sArg ] of modulo.config.syntax[arg]) { s = find ? s.replace(find, sub) : Filters[sub](s, sArg); } return s; }; const tagswap = (s, arg) => { arg = typeof arg === 'string' ? arg.split(/\s+/) : Object.entries(arg); for (const row of arg) { const [ tag, val ] = typeof row === 'string' ? row.split('=') : row; const swap = (a, prefix, suffix) => prefix + val + suffix; s = s.replace(RegExp('(</?)' + tag + '(\\s|>)', 'gi'), swap); } return safe(s); }; const modeRE = /(mode: *| type=)([a-z]+)(>| *;)/; // modeline const Filters = { add: (s, arg) => s + arg, allow: (s, arg) => arg.split(',').includes(s) ? s : '', apply: (s, arg) => Filters[arg](s), camelcase: s => s.replace(/-([a-z])/g, g => g[1].toUpperCase()), capfirst: s => s.charAt(0).toUpperCase() + s.slice(1), combine: (s, arg) => s.concat ? s.concat(arg) : Object.assign({}, s, arg), default: (s, arg) => s || arg, divide: (s, arg) => (s * 1) / (arg * 1), divisibleby: (s, arg) => ((s * 1) % (arg * 1)) === 0, dividedinto: (s, arg) => Math.ceil((s * 1) / (arg * 1)), escapejs: s => JSON.stringify(String(s)).replace(/(^"|"$)/g, ''), escape: (s, arg) => s && s.safe ? s : syntax(s + '', arg || 'text'), first: s => Array.from(s)[0], join: (s, arg) => (s || []).join(typeof arg === "undefined" ? ", " : arg), json: (s, arg) => JSON.stringify(s, null, arg || undefined), guessmode: s => modeRE.test(s.split('\n')[0]) ? modeRE.exec(s)[2] : '', last: s => s[s.length - 1], length: s => s ? (s.length !== undefined ? s.length : Object.keys(s).length) : 0, lines: s => s.split('\n'), lower: s => s.toLowerCase(), multiply: (s, arg) => (s * 1) * (arg * 1), number: (s) => Number(s), pluralize: (s, arg) => (arg.split(',')[(s === 1) * 1]) || '', skipfirst: (s, arg) => Array.from(s).slice(arg || 1), subtract: (s, arg) => s - arg, sorted: (s, arg) => Array.from(s).sort(arg && ((a, b) => a[arg] > b[arg] ? 1 : -1)), trim: (s, arg) => s.replace(new RegExp(`^\\s*${ arg = arg ? escapere(arg).replace(',', '|') : '|' }\\s*$`, 'g'), ''), trimfile: s => s.replace(/^([^\n]+?script[^\n]+?[ \n]type=[^\n>]+?>)/is, ''), truncate: (s, arg) => ((s && s.length > arg*1) ? (s.substr(0, arg-1) + '…') : s), type: s => s === null ? 'null' : (Array.isArray(s) ? 'array' : typeof s), renderas: (rCtx, template) => safe(template.render(rCtx)), reversed: s => Array.from(s).reverse(), upper: s => s.toUpperCase(), urlencode: (s, arg) => window[`encodeURI${ arg ? 'Component' : ''}`](s) .replace(/#/g, '%23'), // Ensure # gets encoded yesno: (s, arg) => `${ arg || 'yes,no' },,`.split(',')[s ? 0 : s === null ? 2 : 1], }; const { values, keys, entries } = Object; return Object.assign(Filters, Modulo.ContentTypes(modulo), { values, keys, entries, tagswap, get, safe, escapere, syntax }); } // md:--- /* md: ## Configuration md: All definitions "extend" a base configuration. See below: md:```html=component<Template>{% for t, c in global.config %} md:<h4>{{ t }}</h4><pre>{{ c|json:2 }}</pre>{% endfor %}</Template>```*/ Modulo.Configs = function DefaultConfiguration() { const CONFIG = { /*#UNLESS#*/ artifact: { tagAliases: { 'js': 'script', 'ht': 'html', 'he': 'head', 'bo': 'body' }, pathTemplate: '{{ tag|default:cmd }}-{{ hash }}.{{ def.name }}', DefLoaders: [ 'DefTarget', 'DefinedAs', 'DataType', 'Src', 'build|Command' ], CommandBuilders: [ 'FilterContent', 'Collect', 'Bundle', 'LoadElems' ], CommandFinalizers: [ 'Remove', 'SaveTo' ], Preprocess: true, // true is "toss code after" DefinedAs: 'name', SaveTo: 'BUILD', // Use "BUILD" filesystem-like store interface FilterContent: 'trimfile|trim|tagswap:config.artifact.tagAliases', }, component: { tagAliases: { 'html-table': 'table', 'html-script': 'script', 'js': 'script' }, mode: 'regular', rerender: 'event', Contains: 'part', CustomElement: 'window.HTMLElement', // Used to change base class DefinedAs: 'name', BuildLifecycle: 'build', RenderObj: 'component', Defer: 'wait', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src', 'Defer', 'FilterContent', 'Content' ], DefBuilders: [ 'CustomElement', 'alias|AliasNamespace', 'Code' ], FilterContent: 'trimfile|trim', DefFinalizers: [ 'MainRequire' ], CommandBuilders: [ 'Prebuild|BuildLifecycle', 'BuildLifecycle' ], Directives: [ 'onMount', 'onUnmount' ], DirectivePrefix: '', // "component.on.click" -> "on.click" }, configuration: { DefTarget: 'config', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src|SrcSync', 'Content|Code', 'DefinitionName|MainRequire' ], }, contentlist: { DataType: 'CSV', DefFinalizers: [ 'command|Command' ], CommandBuilders: [ 'build|BuildAll' ], build: 'build', command: '', // (default: def.commands = []) }, domloader: { topLevelTags: [ 'modulo', 'file' ], genericDefTags: { def: 1, script: 1, template: 1, style: 1 }, }, include: { LoadMode: 'bundle', ServerTemplate: '{% for p, v in entries %}<script src="https://' + '{{ server }}/{{ v }}"></' + 'script>{% endfor %}', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src', 'Server', 'LoadMode' ], }, library: { Contains: 'core', DefinedAs: 'namespace', DefTarget: 'config.component', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src', 'Content' ], }, modulo: { build: { mainModules: [ ] }, defaultContent: '<meta charset=utf8><modulo-Page>', fileSelector: "script[type='mdocs'],template[type='mdocs']," + "style[type='mdocs'],script[type='md'],template[type='md']," + "script[type='f'],template[type='f'],style[type='f']", scriptSelector: "script[src$='mdu.js'],script[src$='Modulo.js']," + "script[src='?'],script[src$='Modulo.html']", version: '0.1.0', timeout: 5000, ChildPrefix: '', Contains: 'core', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src', 'Content' ], defaultDef: { DefTarget: null, DefinedAs: null, DefName: null }, defaultDefLoaders: [ 'DefTarget', 'DefinedAs', 'DataType', 'Src' ], defaultDefBuilders: [ 'FilterContent', 'ContentType', 'Load' ], }, script: { Directives: [ 'refMount', 'refUnmount' ], DefFinalizers: [ 'AutoExport', 'Content|Code' ], AutoExport: '', }, state: { Directives: [ 'bindMount', 'bindUnmount' ], Store: null, }, style: { AutoIsolate: true, // true is "default behavior" (autodetect) isolateSelector: null, // Later has list of selectors isolateClass: null, // By default, it does not use class isolation prefix: null, // Used to specify prefix-based isolation (most common) corePseudo: ['before', 'after', 'first-line', 'last-line' ], DefBuilders: [ 'FilterContent', 'AutoIsolate', 'Content|ProcessCSS' ], }, staticdata: { DataType: '?' }, // (? = use ext) template: { DefFinalizers: [ 'Content|CompileTemplate', 'Code' ], FilterContent: 'trimfile|trim|tagswap:config.component.tagAliases', unsafe: 'filters.escape', modeTokens: [ '{% %}', '{{ }}', '{# #}', '{-{ }-}', '{-% %-}' ], opTokens: '==,>,<,>=,<=,!=,not in,is not,is,in,not,gt,lt', opAliases: { '==': 'X === Y', 'is': 'X === Y', 'is not': 'X !== Y', '!=': 'X !== Y', 'not': '!(Y)', 'gt': 'X > Y', 'gte': 'X >= Y', 'lt': 'X < Y', 'lte': 'X <= Y', 'in': '(Y).includes ? (Y).includes(X) : (X in Y)', 'not in': '!((Y).includes ? (Y).includes(X) : (X in Y))', }, }, _dev: { artifact: ` <Artifact name="css" -bundle="link,modstyle,style" build=build,buildvanish,buildlib> {% for id in def.ids %}{{ def.data|get:id|safe }}{% endfor %} </Artifact> <Artifact name="js" -bundle="script,modscript" -collect="?" build=build,buildlib> {% for id in def.ids %}{% if "collected_" not in id %}{{ def.data|get:id|safe }} {% else %}{{ def.data|get:id|syntax:"trimcode"|safe }}{% endif %}{% endfor %} modulo.definitions = { {% for name, value in definitions %} {% if name|first is not "_" %}{{ name }}: {{ value|json|safe }},{% endif %} {% endfor %} }; {% for name in config.modulo.build.mainModules %}{% if name|first is not "_" %} modulo.registry.modules.{{ name }}.call(window, modulo); {% endif %}{% endfor %} </Artifact> <Artifact name=html path-template="{{ config.path-name|default:'index.html' }}" -remove="head iframe,modulo,script[modulo],template[modulo]" prefix="<!DOCTYPE html>" build=build,buildvanish> <ht><he>{{ doc.head.innerHTML|safe }} <link rel="stylesheet" href="{{ definitions._artifact_css.path }}"></link> {% if "vanish" not in argv|get:0 %} <js defer src="{{ definitions._artifact_js.path }}"></js> {% endif %}</he><bo>{{ doc.body.innerHTML|safe }}</bo></ht> </Artifact> <Artifact name=edit -collect=? -save-reqs build=edit></Artifact> <Artifact name=vjs -remove="script" build=buildvanish></Artifact> <script Artifact name=new_app path=App.html -collect=? -save-reqs build=newlib,newapp> <js src=Modulo.html></js><template type=f>\n<Template> \t<main>\n\t\t<h1>"My App"</h1>\n\t\t<slot></slot>\n\t</main> </Template>\n<Style>\n\tmain,\n\th1,\n\t:host { \t\tpadding: 4%;\n\t\tbackground: #ffffff88;\n\t} \t:host {\n\t\tbackground: linear-gradient(indigo, teal); \t\tdisplay: block;\n\t}\n</Style> <\/script> <script Artifact name=new path=index.html build=newapp> <js Modulo src=Modulo.html>\n\t<Component\n\t\tname=App\n\t\tmode=shadow \t\t-src=App.html\n\t></Component>\n</js>\n<x-App>\n\t<h1>Lorem</h1> \t<p>Ipsum</p>\n</x-App> <\/script> <script Artifact name=new path=new-lib.html d:='["Lorem","Ipsum"]' build=newlib> <js src=Modulo.html></js><template type=mdocs>\n<!--\nmd\:# "New Lib" md\:##[⬇ Get v1.0](?argv=buildlib&argv=NewLib_v1.0)\n--> {% for i,L in def.d %}\n<!--\nmd\:### Use {{i|number|add:1}}: {{L}}\nmd\:{{def.d|join}}: md\:\`\`\`html=embed\nmd\:<js Modulo src=Modulo.html -src="new-lib.html"></js> md\:<nl-App>{{L}}</nl-App>\nmd\:\`\`\`\n-->\n{% endfor %}\n<!-- App -->\n <Component\n\tname=App\n\tnamespace=nl\n\t-src="App.html"\n></Component><\/script> <script Artifact name=new path=new-page.html build=newmd -collect=? -save-reqs> <js src=Modulo.html></js><js type=md>---\ndate: {{config.date}}\n--- # Title\n### Section\nExample **content**, link: [Edit Me](?argv=edit) <\/script>`, component: ` <Component mode=shadow namespace=modulo name=Frame> <Props fs file store=BUILD></Props><State -dot=| -name=build -store=BUILD></State> <State -dot=| -name=cache -store=CACHE></State><Template>{% ignoremissing %} {% with local|get:props.store|get:'fdata'|get:props.file|default:null as value %} <iframe style="{% if props.fs %}height:100%;min-height:80vh{% endif %}; width:100%;border:0;border:1px dotted #111;" srcdoc="{% if value is null %}<h1>404/{{ props|json }}</h1>{% else %} {{ value }}{% endif %}" loading=lazy></iframe>{% endwith %} {% endignoremissing %}</Template> </Component><Component mode=shadow namespace=modulo name=TextEdit> <Props fs store=build mode=html font=17 file readonly></Props> <State -dot=| -name=build -store=BUILD></State> <State -dot=| -name=cache -store=CACHE></State><Template> {% with local|get:props.store|get:'fdata'|get:props.file|default:null as value %} {% with value|lines|length|multiply:props.font|multiply:'1.5' as hg %}<modulo-wrap><pre style=" font-size:{{props.font}}px"><modulo-line></modulo-line></span >{{value|syntax:props.mode|safe}}</pre><textarea style="top:-2px;left:50px; position:absolute;height:{{hg}}px; font-size:{{props.font}}px" {{props.readonly|yesno:"readonly,"}} spellcheck=false {{props.store}}.bind="fdata|{{props.file}}"></textarea></modulo-wrap> {% endwith %}{% endwith %}</Template> <Style>modulo-line:before{counter-increment:line;content:counter(line); position:absolute;left:0;color:#888;padding:0 0 0 3px}pre{padding:0 0 0 53px;} pre,textarea{counter-reset:line;display:block;color:black;background:transparent; white-space:pre;text-align:start;line-height:1.5;overflow-wrap:break-word; margin:0;box-sizing:content-box;border:1px dotted #111; font-family: monospace}modulo-wrap{display:block;position:relative;width:100%} textarea{resize:none;color:#00000000;caret-color:#000;width:100%;} </Style></Component> <Component namespace=modulo name=Editor> <Props mode=html view edit demo value full></Props> <State -dot=| -name=proc -store=PROC></State> <State -dot=| -name=build -store=BUILD></State> <State -dot=| -name=cache -store=CACHE></State> <State fields:='{"edit":"TextEdit","view":"Frame"}' -init=props></State> <Template -name="demo_embed">{{ props.value|safe }}</Template> <script Template -name="demo_component"><js src=Modulo.html></js><template Modulo> <Component name=App>\n{{ props.value|safe }}\n</Component>\n</template> <x-App></x-App><\/script><Template><modulo-grid style="grid-template-columns: auto {{ state.view|yesno:'50%,1fr' }} 53px {{ state.view|yesno:'1fr,auto' }}"><div> {% if props.full %}<h1><a href="?argv={{ global.argv|join:'&argv='|safe }}"> &#x27F3; {{ global.argv|join:' ' }} &nbsp;</a></h1> {% if proc.log|length %} <div>{% for row in proc.log %}<iframe src="{{ global.root-path }}{{ row|get:0 }}" ></iframe>{% endfor %}</div>{% endif %}<pre>{% for row in proc.log %} {{ row|reversed|join:" \t" }}<br />{% endfor %}</pre><pre> {% for path, text in build.fdata %}<a download="{{ path }}" href="data:text/plain;charset=utf-8,{{ text|urlencode:true }}" >{{ path }}</a> ({{ text|length }})<br />{% endfor %}</pre>{% endif %}</div> {% for field, tag in state.fields %}<div>{% if props.full %} <label><select state.bind name="{{ field }}"><option value="">{{ field|upper }} </option>{% with local|get:state.store|get:'fdata' as fs %}{% for p, t in fs %} <option value={{p}}>&#x1F5CE; {{ p }}</option>{% endfor %}{% endwith %} </select></label>{% endif %}{% if state|get:field %}<modulo-{{tag}} fs="{{props.demo|yesno:',y'}}" file={{state|get:field}} readonly="{{props.demo|default:props.full|yesno:',y'}}" mode="{{state.mode|default:'txt'}}" store={{state.store}}></modulo-{{tag}}> {% endif %}</div><div></div>{% endfor %}</modulo-grid> </Template> <def Script>function prepareCallback(rObj) { if (!('store' in state)) { try { window._moduloFS = window._moduloFS || parent._moduloFS } catch { } state.store = 'build'; if (props.value || props.demo) { const edit = (props.demo || 'APP') + (++window.Modulo.instanceID) + '.html'; const tmplt = rObj['demo_' + props.demo]; Object.assign(state, { store: 'cache', view: tmplt ? edit : '', edit }); cache.fdata[edit] = tmplt ? tmplt.render(rObj) : props.value; } } if ((state.edit + '|' + state.view) !== state.last) { element.textContent='';state.last=state.edit + '|' + state.view; } }</def><Style>select{width:100%} modulo-grid{width: 100%; display: grid}@media (max-width:992px){modulo-grid{display:block}} </Style></Component> <Component mode=vanish namespace=modulo name=Page><Template> {% with global.stores.CACHE.data.fdata|values|first as body %}{% if body %} {% with body|guessmode as mode %}{% if mode in global.config.syntax %} {{ body|MD:mode|get:'body'|safe }}{% endif %}{% endwith %}{% endif %} {% endwith %}</Template><Style>p{line-height:1.6;font-size:18px} h2[h]{margin:60px 0 0 0;font-family:sans-serif;font-weight:500;} h2[h='#']{font-size:64px} h2[h='##']{font-size:46px;}h2[h='###']{font-size:30px} h2[h='#'],h2[h='##']{text-align:center}code{background:#88888855} hr{border:0.5vw solid #88888855;margin:5vw 30% 5vw 30%}</Style></Component>` } /*#ENDUNLESS#*/ } CONFIG.syntax = { // Simple RegExp mini langs for |syntax: filter jsReserved: { // Used by Script tags and JS syntax 'break': 1, 'case': 1, 'catch': 1, 'class': 1, 'const': 1, 'continue': 1, 'debugger': 1, 'default': 1, 'delete': 1, 'do': 1, 'else': 1, 'enum': 1, 'export': 1, 'extends': 1, 'finally': 1, 'for': 1, 'function': 1, 'if': 1, 'implements': 1, 'import': 1, 'in': 1, 'instanceof': 1, 'interface': 1, 'new': 1, 'null': 1, 'package': 1, 'private': 1, 'protected': 1, 'public': 1, 'return': 1, 'static': 1, 'super': 1, 'switch': 1, 'throw': 1, 'try': 1, 'typeof': 1, 'var': 1, 'let': 1, 'void': 1, 'while': 1, 'with': 1, 'await': 1, 'async': 1, 'true': 1, 'false': 1, }, html: [ // html syntax highlights some common html / templating [ null, 'syntax', 'txt' ], [ /(\{%[^<>]+?%}|\{\{[^<>]+?\}\})/gm, '<tt style=background:#82d4a444>$1</tt>'], [ /(&lt;\/?)([a-z]+\-[A-Za-z]+)/g, '<tt style=color:#999>$1</tt><tt style=color:indigo>$2</tt>'], [ /(&lt;\/?)(script |def |template |)([A-Z][a-z][a-zA-Z]*)/g, '<tt style=color:#999>$1$2</tt><tt style=color:#B90183>$3</tt>'], [ /(&lt;\/?[a-z1-6]+|&gt;)/g, '<tt style=color:#777>$1</tt>'], ], 'md': [ // md is a (very limited) Markdown implementation [ null, 'syntax', 'text' ], [ /(&lt;)-(script)(&gt;)/ig, '$1/$2$3' ], // fix <-script> [ /```([a-z]*)([a-z=]*)\n?(.+?)\n?```/igs, '<modulo-Editor mode="$1" demo$2 value="$3"></modulo-Editor>' ], [ /^(#+)\s*(.+)$/gm, '<h2 h="$1">$2</h2>' ], [ /!\[([^\]]+)\]\(([^\)]+)\)/g, '<img="$2" alt="$1" />' ], [ /\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2">$1</a>' ], [ /_([^_`]+)_/g, '<em>$1</em>' ], [ /`([^`]+)`/g, '<code>$1</code>' ], [ /\*\*([^\*]+)\*\*/g, '<strong>$1</strong>' ], [ /\*([^\*]+)\*/g, '<em>$1</em>', ], [ /\n+\r?\n---+/g, '</p><hr />' ], [ /(\n|>)\r?\n[\n\r]*/g, '$1<p>' ], ], mdocs: [ // mdocs formats Markdown comments (marked with md\:) [ /^((?!.*md\:).*)$/gm, '\n' ], [ /^.*?md\:\s*/gm, '' ], [ null, 'syntax', 'md' ], // Delete non "md", then do markdown ], text: [ // escape text for HTML [ /&/g, '&amp;' ], [ /</g, '&lt;' ], [ />/g, '&gt;' ], // &<> [/'/g, '&#x27;'], [ /"/g, '&quot;' ], // "' ], trimcode: [ [ /^[\n \t]+/gm, '' ], // rm leading WS, comments, "UNLESS" [ /\/\*\#UNLESS\#[\s\S]+?\#ENDUNLESS\#(\*\/)/gm, '' ], [ /\/\*[^\*\!][\s\S]*?\*\/|\/\/.*$/gm, '' ], ], txt: [ // txt forces WS [ null, 'syntax', 'text' ], [ /\n/g, '<br /><modulo-line></modulo-line>' ], [ / /g, '&nbsp;&nbsp;' ], ], }; CONFIG.syntax.js = Array.from(CONFIG.syntax.html) CONFIG.syntax.js.push([ new RegExp(`(\\b${ Object.keys( CONFIG.syntax.jsReserved).join('\\b|\\b') }\\b)`, 'g'), `<strong style=color:firebrick>$1</strong>` ]); return CONFIG }; Modulo.ContentTypes = modulo => ({ // md: **ContentTypes**: CSV (limited), CSV: s => (s || '').trim().split('\n').map(r => r.trim().split(',')), JS: s => Function('return (' + s + ');')(), // md: JS (expression syntax), JSON: s => JSON.parse(s || '{ }'), // md: JSON (default), MD: (s, arg) => { //**MD** - Parses "Markdown Meta" (e.g. in `---` at top) const headerRE = /^([^\n]*---+\n.+?\n---\n)/s; const obj = { body: s.replace(headerRE, '') }; if (obj.body !== s) { // Meta was specified let key = null; // Used for continuing / multiline keys const lines = s.match(headerRE)[0].split(/[\n\r]/g); for (const line of lines.slice(1, lines.length - 2)) { // omit --- if (key && (new RegExp('^[ \\t]')).test(line)) { // Multiline? obj[key] += '\n' + line; // Add back \n, verbatim (no trim) } else if (line.trim() && (key = line.split(':')[0])) { // Key? obj[key.trim()] = line.substr(key.length + 1).trim(); } } } obj.body = arg ? modulo.templateFilter.syntax(obj.body, arg) : obj.body; return obj; }, TXT: s => s, // md: TXT (plain text), BIN: (s, arg = 'application/octet-stream') => //md: BIN (binary types). `data:${ arg };charset=utf-8,${ window.encodeURIComponent(s) }`, }); /* Utility Functions that setup Modulo */ Modulo.Utils = function UtilityFunctions (modulo) { const Utilities = { escapeRegExp: s => // Escape string for regexp s.replace(/[.*+?^${}()|[\]\\]/g, "\\" + "\x24" + "&"), insObject: obj => Object.assign(obj || {}, Utilities.lowObject(obj)), get: (obj, key, sep='.') => (key in obj) ? // Get key path from object obj[key] : (key + '').split(sep).reduce((o, name) => o[name], obj), lowObject: obj => Object.fromEntries(Object.keys(obj || {}).map( key => [ key.toLowerCase(), obj[key] ])), normalize: s => // Normalize space to ' ' & trim around tags s.replace(/\s+/g, ' ').replace(/(^|>)\s*(<|$)/g, '$1$2').trim(), set: (obj, keyPath, val, sep = null) => // Set key path in object new modulo.engine.ValueResolver(modulo, sep).set(obj, keyPath, val), trimFileLoader: s => // Remove first lines like "...script...file>" s.replace(/^([^\n]+script[^\n]+[ \n]file[^\n>]+>(\*\/\n|---\n|\n))/is, '$2'), }; function instance(def, extra, inst = null) { const registry = (def.Type in modulo.core) ? modulo.core : modulo.part; inst = inst || new registry[def.Type](modulo, def, extra.element || null); const id = ++window.Modulo.instanceID; // Unique number //const conf = Object.assign({}, modulo.config[name.toLowerCase()], def); const conf = Object.assign({}, def); // Just shallow copy "def" const attrs = modulo.util.keyFilter(conf); Object.assign(inst, { id, attrs, conf }, extra, { modulo: modulo }); if (inst.constructedCallback) { inst.constructedCallback(); } return inst; } function instanceParts(def, extra, parts = {}) { // Loop through all children, instancing each class with configuration const allNames = [ def.DefinitionName ].concat(def.ChildrenNames); for (const def of allNames.map(name => modulo.definitions[name])) { parts[def.RenderObj || def.Name] = modulo.util.instance(def, extra); } return parts; } function initComponentClass (modulo, def, cls) { // Run factoryCallback static lifecycle method to create initRenderObj const initRenderObj = { elementClass: cls }; // TODO: "static classCallback" for (const defName of def.ChildrenNames) { const cpartDef = modulo.definitions[defName]; const cpartCls = modulo.part[cpartDef.Type]; modulo.assert(cpartCls, 'Unknown Part:' + cpartDef.Type); if (cpartCls.factoryCallback) { const result = cpartCls.factoryCallback(initRenderObj, cpartDef, modulo); initRenderObj[cpartDef.RenderObj || cpartDef.Name] = result; } } cls.prototype.init = function init () { this.modulo = modulo; this.isMounted = false; this.isModulo = true; this.originalHTML = null; this.originalChildren = []; this.cparts = modulo.util.instanceParts(def, { element: this }); }; cls.prototype.connectedCallback = function connectedCallback () { modulo._connectedQueue.push(this); window.setTimeout(modulo._drainQueue, 0); }; cls.prototype.moduloMount = function moduloMount(force = false) { if ((!this.isMounted && !modulo.paused) || force) { this.cparts.component._lifecycle([ 'initialized', 'mount' ]); } }; cls.prototype.attributeChangedCallback = function (attrName) { if (this.isMounted) { // pass on info as attr callback this.cparts.component._lifecycle([ 'attr' ], { attrName }); } }; cls.prototype.initRenderObj = initRenderObj; cls.prototype.rerender = function (original = null) { if (!this.isMounted) { // Not mounted, do Mount which will also rerender return this.moduloMount(); } this.cparts.component.rerender(original); // Otherwise, normal rerender }; cls.prototype.getCurrentRenderObj = function () { return this.cparts.component.getCurrentRenderObj(); }; modulo.registry.elements[cls.name] = cls; // Copy class to Modulo } function newNode(innerHTML, tag, extra) { const obj = Object.assign({ innerHTML }, extra); return Object.assign(window.document.createElement(tag || 'div'), obj); } function makeStore (modulo, def) { const data = JSON.parse(JSON.stringify(modulo.util.keyFilter(def))); return { data, boundElements: {}, subscribers: [] }; } function keyFilter (obj, func = null) { func = func || (key => /^[a-z]/.test(key)); // Start with lower alpha const keys = func.call ? Object.keys(obj).filter(func) : func; return Object.fromEntries(keys.map(key => [ key, obj[key] ])); } function urlReplace(str, origin, field = 'href') { // Absolutize URL