Skip to content

Latest commit

 

History

History
1988 lines (1932 loc) · 96.1 KB

File metadata and controls

1988 lines (1932 loc) · 96.1 KB

/*<script src=Modulo.html></script><script type=mdocs>--- version: v0.1.3 copyright: 2026 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.3 [DOCS](https://modulohtml.org/docs/),_,#ᵐ°dᵘ⁄o var Modulo = function Modulo (OPTS = { }) { const Lib = OPTS.globalLibrary || window.Modulo || Modulo; 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', 'queue', 'parser', 'fs' ]; for (const name of globals) { // "modulo[lib] = Modulo.Libs() || {}" const stdLib = Lib[name.charAt(0).toUpperCase() + name.slice(1) + 's']; this[name] = stdLib ? stdLib(this) : { }; }//md:###[ % ] Create App » }//md:###[ % ] Create Library » //md:###[ % ] Create Markdown » //md:About: Modulo (or ᵐ°dᵘ⁄o) is a single file frontend //md:framework. This is an internal menu, with help on common tasks.

Modulo.Parts = function ComponentParts (modulo) {// md: ## Component Parts

class Include { static LoadMode(modulo, def, value) { // md: Add scripts and styles to head. 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'); }//md:html=component,_,<Include> } //md:<script>,_,document.body.innerHTML += '<h1>ᵐ°dᵘ⁄o</h1>',_,<-script> //md:<style>,_,body { background: #B08; },_,</style>,_,</Include>,_,

class 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) } 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:html=component,_,<Props quote name="Unknown"></Props> } // md:<Template>{{ props.name }} says "{{ props.quote }}"</Template>

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)) { selector = ${ def.prefix } ${ selector }; // Prepend prefix } 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:html=component,_,<Template><em class="big">Stylish</em> TEXT</Template> }//md:<Style>,_,.big { font-size: 4rem },_,:host { background: #8da },_,</Style>

class StaticData { prepareCallback() {// md: Use for bundling unchanging data (e.g. API, files, return this.conf.data; // md: config, etc) in with a component. } //md:html=component,_,<Template><pre>{{ staticdata|json:2 }}</pre></Template> } //md:<StaticData -data-type=md -src=Modulo.html></StaticData>

class Script { 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 }}; } // md: Scripts let you embed JavaScript code "inside" your component. 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.exportsmethod || {}); // 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:html=component,_,<Script>,_,function hi(){,_,alert(ref.h1.outerHTML) } //md:},_,<-Script>,_,<Template>,_,<h1 on.click=script.hi script.ref>Click me</h1> //md:</Template>

class State { static factoryCallback(renderObj, def, modulo) { // md: Declare variables. if (def.Store) { // md: If a -store= is specified, State is 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; this.types['select-multiple'] = (v, el) => Array.from(el.selectedOptions).map(opt => opt.value); 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; } // md:html=component,_,<State msg="Lorem" a:=123></State> } // md:<Template>{{ state.msg }}: <input state.bind name=msg></Template>

class Template { static CompileTemplate (modulo, def, value) { // md: Compiles templates const compiled = modulo.util.instance(def, { }).compile(value);//md: to def.Code = return function (CTX, G) { ${ compiled } };;//md: generate } //md: HTML during Component render. constructedCallback() { this.stack = []; 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) }; } 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, code = 'var OUT=[];\n', endCode = '\nreturn OUT.join("");') { const { normalize } = modulo.util; 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); } const unclosed = this.stack.map(({ close }) => close).join(', '); modulo.assert(!unclosed, Unclosed tags: ${ unclosed }); return code + endCode; } render(local) { if (!this.renderFunc) { const mod = this.modulo.registry.modules[this.conf.DefinitionName]; this.renderFunc = mod.call(window, this.modulo); } const ctx = { local, global: this.modulo, file: this.modulo.file }; return this.renderFunc(Object.assign(ctx, 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, 'elseif': (s, tmplt) => tmplt.tags.elif(s, tmplt), '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, conf, sArg ] of modulo.config.syntax[arg]) { s = find ? s.replace(find, conf) : Filters[conf](s, sArg); } return safe(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 editorpanes = (s, arg) => { const ext = s.match(/.([a-z]+)(?:.htm)?$/i)[1]; return [ 'Edit', 'View' ].map(label => ({ label, font: 16, path: s, id: ${ s }${ label }, mode: ext !== 'md' && ext in modulo.config.syntax ? ext : 'txt', height: ${ arg.split("\n").length * 1.5 * 16 }px, tag: 'modulo-File' + label, })); }; 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) => Filtersarg, attributes: s => Object.entries(s) .map(([k,v]) => ${ k }="${ syntax(v + '', 'text') }").join(' '), 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, ''), escaperoute: s => escapere(s).replace(/:([a-z]+)/gi, '(?<$1>[^/]+)'), 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), parse: (s, arg) => Object.assign(new modulo.part.Template(), modulo.parser[arg], { stack: [] }).compile(s, '', ''), 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 > arg1) ? (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) => windowencodeURI${ arg ? 'Component' : ''} .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, editorpanes, syntax }); }

//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: { mode: 'regular', namespace: 'x', rerender: 'event', Contains: 'part', CustomElement: 'window.HTMLElement', // Used to change base class DefinedAs: 'name', BuildLifecycle: 'build', RenderObj: 'component', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src', 'FilterContent', 'Content|LoadDefs' ], DefBuilders: [ 'CustomElement', 'Code' ], DefFinalizers: [ 'MainRequire' ], FilterContent: 'trimfile|trim', 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' ], }, contentfile: { FilterContent: 'trimfile|trim', Name: 'file', htmlBody: '', DefFinalizers: [ 'type|DataType', 'ContentType', 'Load' ], }, contentlist: { DataType: 'CSV', DefFinalizers: [ 'command|Command' ], CommandBuilders: [ 'build|BuildAll' ], build: 'build', command: '', // (default: def.commands = []) }, domloader: { topLevelTags: [ 'modulo', 'contentfile' ], 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' ], }, filesystem: { DefinedAs: 'Store', Load: 'yes please', // TODO fix Dot: '|', }, library: { Contains: 'core', DefinedAs: 'name', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src', 'Content|LoadDefs' ], Override: { library: true }, tagAliases: { 'html-table': 'table', 'js': 'script' }, }, modulo: { build: { mainModules: [ ] }, scriptSelector: "script[src$='mdu.js'],script[src$='Modulo.js']," + "script[src='?'],script[src$='Modulo.html']", version: '0.1.3', timeout: 9000, ChildPrefix: '', Contains: 'core', DefLoaders: [ 'DefTarget', 'DefinedAs', 'Src', 'Content|LoadDefs' ], 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, isolateSelector: null, // later has array of selectors isolateClass: null, prefix: null, 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.library.tagAliases', unsafe: 'filters.escape', modeTokens: [ '{% %}', '{{ }}', '{# #}', '{-{ }-}', '{-% %-}' ], opTokens: '==,>,<,>=,<=,!=,not in,is not,is,in,not,gte,lte,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: { filesystem: <FileSystem -store=BUILD></FileSystem>, artifact: ` <Artifact name="css" -bundle="link,modstyle,style" build=build,buildvanish,buildlib> {% for id in def.ids %}{{ def.data|get:id|safe }}{% endfor %} <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 name=html path-template="{{ file.path|default:'index.html' }}" -remove="head iframe,modulo,script[modulo],template[modulo]" prefix="" build=build,buildvanish> {{ doc.head.innerHTML|safe }} {% if "vanish" not in argv|get:0 %} {% endif %}{{ doc.body.innerHTML|safe }} <Artifact name=edit -collect=? -save-reqs build=edit> <Artifact name=vjs -remove="script" build=buildvanish>

<script Artifact name=new_app path=App.html -collect=? -save-reqs build=newlib,newapp> \n \t\n\t\t

"My App"

\n\t\t\n\t \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> \n\t\n\n\n\t

Lorem

\t

Ipsum

\n <\/script> <script Artifact name=new path=new-lib.html d:='["Lorem","Ipsum"]' build=newlib> \n {% for i,L in def.d %}\n\n{% endfor %}\n\n <\/script> <script Artifact name=new path=new-page.html build=newmd -collect=? -save-reqs> ---\ndate: {{config.date}}\n--- # Title\n### Section\nExample **content**, link: [Edit Me](?argv=edit) <\/script>`, component: ` {{ file.data.body|default:file.data|safe }} <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>

<pre style="font-size:{{props.font}}px"

{{build|get:props.path|default:""|syntax:props.mode|safe}}

<textarea build.bind="{{props.path}}" style="border:none;top:-2px;left:50px; position:absolute;height:{{props.height}};font-size:{{props.font}}px" spellcheck=false ></textarea> <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;font-family: monospace} textarea{resize:none;color:#00000000;caret-color:#000;width:100%;} label{display:block;position:relative;width:100%}</Style> {% with build|get:props.path as d %}<iframe src="{{props.path}}" style="width:100%;height:100%;border:none" srcdoc="{{d}}"></iframe> {% endwith %} <script Template> {% if props.ui is not "md" %}

⟳ {{ global.argv|join:' ' }}  

{% for path, text in state.files %}{{ path }} ({{ text|length }})
{% endfor %}
{% include files_pane %}
{%endif%} {% for p, text in state.files %}{% for pane in p|editorpanes:text %} {% if pane.id in state.pane %}<{{pane.tag}} style="width:100%" path="{{pane.path}}" font="{{pane.font}}" height="{{pane.height}}" mode="{{pane.mode}}" >{% endif %}{% endfor %}{% endfor %}<\/script> {% for p, text in state.files %} {% for pane in p|editorpanes:text %}{{pane.label}}{% endfor %} {% endfor %}function prepareCallback(rO) { const build = modulo.stores.BUILD.data;if(!state.pane) { if (state.mode) {const s=state.mode.split('=');state.mode=s[0];state.demo=s[1]} if (state.demo) { state.value = rO[state.demo + '_demo'].render({ props }) } if (state.value) { const f = '.m.' + (++Modulo.instanceID) + '.swp'; state.pane = [ f + 'Edit', f + 'View' ]; build[f] = state.value } } state.files = {}; for (const f of Object.keys(build)) { if (!f.startsWith('.') || state.value) { state.files[f] = build[f]; if (!state.pane) state.pane = [ f + 'Edit', f + 'View' ]; } } } {{props.value|safe}} <script Template -name=component_demo> \n{{props.value|trim|safe}}\n\n <\/script><Style>modulo-uic > *, .mui--pane {background:#fff; min-width:12vw;display:block;border:1px dotted #111;}</Style> </Style> ` } /*#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, }, f: [ ], // (raw content file) html: [ [ null, 'syntax', 'txt' ], [ /({%[^<>]+?%}|{{[^<>]+?}})/gm, '$1'], [ /(</?)([a-z]+-[A-Za-z]+)/g, '$1$2'], [ /(</?)(script |def |template |)([A-Z][a-z][a-zA-Z])/g, '$1$2$3'], [ /(</?[a-z1-6]+|>)/g, '$1'], ], 'md': [ // 'md' renders (not highlights) Markdown [ null, 'parse', 'markdown' ], [ /(<)-(script)(>)/ig, '$1/$2$3' ], // add / to <-script> ], markdown: [ [ null, 'syntax', 'text' ], [ /^(#+)\s(.+)$/gm, '

$2

' ], [ /![([^\]]+)](([^\)]+))/g, '<img="$2" alt="$1">' ], [ /[([^\]]+)](([^\)]+))/g, '$1' ], [ /([^], mdocs: [ // docs from "md:"-style comments [ /\n(class|function) +([A-Za-z]+)/g, 'md:### $2\n\nmd:- $1\n\n/' ], [ /^((?!.md:).)$/gm, '\n' ], [ /^.?md:\s/gm, '' ], [ /,,/g, '\n' ], [ null, 'syntax', 'md' ], ], text: [ // escape text for HTML [ /&/g, '&' ], [ /</g, '<' ], [ />/g, '>' ], // &<> [/'/g, '''], [ /"/g, '"' ], // "' ], 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, '
' ], [ / /g, '  ' ], ], }; 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: Content Types include: 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') => { // MD - Markdown file with metadata 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) }, MDOCS: s => modulo.contentType.MD(s, 'mdocs'), // md: MDOCS (docs in MD) F: s => s, // md: F (HTML fragment) });

Modulo.Parsers = modulo => ({ markdown: { modeTokens: [ ' \n' ], modes: { '': (text, tmplt) => { // md: Markdown uses "code fence" syntax. tmplt._syntax = tmplt._syntax === 'text' ? 'markdown' : 'text'; return tmplt._syntax === 'text' ? </p><modulo-Editor mode="${ text }" value=" : "></modulo-Editor>; }, // md: Additional "mode" tags can be configured at modulo.parser. text: (s, tmplt) => modulo.templateFilter.syntax(s, tmplt._syntax || 'markdown'), }}});

// md: ## Utility Functions 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', extar) { // Absolutize URLs const abs = (url, origin) => (new window.URL(origin + '/../' + url))[field] const ifURL = (all, pre, url, suf) => /^[a-z]+://./i.test(url) ? all : ${ pre }"${ (!origin ? field : abs)(url, origin) }"${ suf }; return str.replace(/(href=|src=|url()['"]?(.+?)['"]?([>\s)])/gi, ifURL); }/ function urlReplace(str, origin, field = 'href', urlFilter = null) { urlFilter = urlFilter || (s => s); const ifURL = (all, pre, url, suf) => /^[a-z]+://./i.test(url) ? all : ${ pre }"${ urlFilter((new window.URL(origin + '/../' + url))[field]) }"${ suf }; //return str.replace(/(href=|src=|url()['"]?(.+?)['"]?([>\s)])/gi, ifURL); //return str.replace(/(rel=.?stylesheet.?\s*href=|src=|url()['"]?(.+?)['"]?([>\s)])/gi, ifURL); return str.replace(/(src=|url()['"]?(.+?)['"]?([>\s)])/gi, ifURL); } Object.assign(Utilities, { initComponentClass, instance, instanceParts, newNode, makeStore, keyFilter, urlReplace })

/#UNLESS#/ function loadString (text, pName, allowedTypes = null) { text = text.replace(/^([^\n]+?script[^\n]+?[ \n]type=[^\n>]+?>)(.)$/is, '$2'); return loadFromDOM(newNode(text), pName, false, allowedTypes); } function loadFromDOM(elem, parentName = null, quietErrors = false, allowedTypes = null) { const loader = new modulo.engine.DOMLoader(modulo); return loader.loadFromDOM(elem, parentName, quietErrors, allowedTypes); } function repeatProcessors(defs, field, cb) { const { WAIT, WAITALL } = modulo.consts; let changed = true; // Run at least once const defaults = modulo.config.modulo['default' + field] || []; while (changed !== false) { changed = false; // TODO: Make deterministic order e.g. arr for (const def of (defs || Object.values(modulo.definitions))) { const processors = def[field] || defaults; const result = applyNextProcessor(def, processors); if (result === WAIT || result === WAITALL) { changed = result break; } changed = changed || result; } } // TODO: Refactor this area const repeat = () => repeatProcessors(defs, field, cb); if (changed !== WAIT && changed !== WAITALL && Object.keys( modulo.queue ? modulo.queue.queue : {}).length === 0) { if (cb) { cb(); } } else { modulo.queue.enqueue(repeat, changed === WAITALL); } } function applyNextProcessor (def, processorNameArray) { const cls = modulo.part[def.Type] || modulo.core[def.Type] || {}; for (const name of processorNameArray) { modulo.assert(name, ${ def.DefinitionName } - Invalid: ${ processorNameArray }"); const [ attrName, aliasedName ] = name.split('|'); if (attrName in def) { const funcName = aliasedName || attrName; const proc = modulo.processor[funcName.toLowerCase()]; const func = funcName in cls ? cls[funcName].bind(cls) : proc; modulo.assert(func, Invalid processor: "${ funcName }"); const value = def[attrName]; // Pluck value & remove attribute delete def[attrName]; const ret = func(modulo, def, value); return ret ? ret : true; // falsy -> true } } return false; // No processors were applied, return false } function configureStatic (modulo) { // Load default static dir const { staticDir, rootDir, scriptSelector } = modulo.config.modulo; const file = modulo.definitions.file || { }; // file singleton const dir = staticDir || 'static/'; const mdu = window.document.querySelector(scriptSelector); const mduPath = ((mdu || {}).src || ''); const root = rootDir || mduPath.split(dir)[0]; const path = (window.location + '').replace(root, '').split('?')[0]; const rPath = path.split('/').slice(1).map(s => '..').join('/'); const rootPath = rPath ? (rPath + '/') : ''; modulo.file = Object.assign(file, { root, path, rootPath, Load: null }); modulo.file.mduSrc = mduPath.replace(root, '') || 'Modulo.html'; if (modulo.definitions.modulo) { delete file.Load; // ensure does not load } else if (mdu && root !== mdu.src && !path.startsWith(dir)) { modulo.util.loadString(<Modulo -src="${ root + dir }">); // auto } } function hash (str) { // Returns base32 hash let h = 0; // Simple, insecure, "hashCode()" implementation for (let i = 0; i < str.length; i++) { h = Math.imul(31, h) + str.charCodeAt(i) | 0; } //h = ((h << 5 - h) + str.charCodeAt(i)) | 0; const hash8 = ('---------' + (h || 0).toString(32)).slice(-8); return hash8.replace(/-/g, 'x'); // Pad with 'x' } function bundleHead(modulo, elem, bundle = null, doc = null) { doc = doc || window.document; const { newNode, hash } = modulo.util; const url = elem.getAttribute('src') || elem.getAttribute('href'); const id = 'include' + hash(elem.name || url || elem.textContent); bundle = bundle || modulo.bundles[elem.tagName.toLowerCase()]; if (doc.getElementById(id) || bundle.includes(id)) { return; // already included in this bundle! } bundle.push(id); // Keep ordering of insertion in this list const newElem = newNode(elem.innerHTML, elem.tagName); if (elem.tagName === 'SCRIPT' && url && !elem.hasAttribute('async')) { modulo.queue.queue[id] = [ ] // Add a "waitable" queue newElem.onload = () => modulo.queue.receiveData(null, id); } for (const attr of elem.attributes || []) { // Copy all attributes from old elem newElem.setAttributeNode(attr.cloneNode(true)); // ...to new elem } newElem.setAttribute('id', id); newElem.textContent = elem.textContent; // Evaluate code doc.head.append(newElem); // add to document } function getParentDefPath(modulo, def) { const pDef = def.Parent ? modulo.definitions[def.Parent] : null; const url = String(window.location).split('?')[0]; // Remove ? info return pDef ? pDef.Source || getParentDefPath(modulo, pDef) : url; } function setupDevLib(modulo, subFS = null) { // Setup config info, sets up the "FS" stores (or parent's stores + queue) const { config, util, stores, queue, assert } = modulo; config.pathName = window.location.pathname.split('/').pop(); config.date = (new Date()) + ''; // String version of date const { timeout, devLoad } = config.modulo; for (const type of devLoad || [ 'artifact', 'component', 'filesystem' ]) { const str = config._dev[type].replace(/\n[ \n\r]+/gm, '\n'); // norm ws util.loadString(str.replace(/\t/g, ' '), _${ type }, [ type ]); } modulo._loadTimeout = timeout && setTimeout(() => assert( Object.keys(queue.queue).length < 1, timeout, '[TIMEOUT]', ...Object.keys(queue.queue)), timeout); modulo.DEV = true; } function getCommand(modulo) { const cmdName = modulo.argv.length > 0 ? modulo.argv[0] : '_default'; return () => modulo.commandcmdName; } Object.assign(Utilities, { applyNextProcessor, configureStatic, hash, loadString, bundleHead, getParentDefPath, loadFromDOM, setupDevLib, getCommand, repeatProcessors }) /#ENDUNLESS#*/

return Utilities; };

// md: ## Content Processors Modulo.Processors = function DefProcessors (modulo) { /#UNLESS#/ function src (modulo, def, value) { const { getParentDefPath } = modulo.util; // md: Loads content try { def.Source = (new window.URL(value, getParentDefPath(modulo, def))).href; } catch { } modulo.queue.fetch(def.Source || value).then(text => { //def.Content = trimFileLoader(text || '') + (def.Content || ''); def.Content = text || '' + (def.Content || ''); }); }

function srcSync (modulo, def, value) { modulo.processor.src(modulo, def, value); return modulo.consts.WAIT; // md: Like src, except blocking }

function filterContent (modulo, def, value) { if (def.Content && value) { // md: A mini-template of just filters const miniTemplate = {{ def.Content|${ value }|safe }}; //md: to const tmplt = new modulo.part.Template(miniTemplate); //md: apply def.Content = tmplt.render({ def, config: modulo.config }); //md:to } //md: the definition's content before loading it. }

function defTarget (modulo, def, value) { const resolverName = def.DefResolver || 'ValueResolver'; const resolver = new modulo.engineresolverName; const target = value === null ? def : resolver.get(value); // Target object for (const [ key, defValue ] of Object.entries(def)) { // Resolve all values if (key.endsWith(':') || key.includes('.')) { delete def[key]; // Remove & replace unresolved value resolver.set(/^_?[a-z]/.test(key) ? target : def, key, defValue); } } }

function command (modulo, def, value) { def.commands = (value || ' ').split(/,/.test(value) ? ',' : '\n'); for (const cmd of def.commands) { // Register dev commands const commandName = cmd.trim() || 'build'; modulo.command[commandName] = function build (modulo) { for (const [ key, obj ] of Object.entries(modulo.definitions)) { if (obj.commands && !obj.commands.includes(commandName)) { delete obj.CommandBuilders; // stop cmd delete obj.CommandFinalizers; } } modulo._drainQueue(); // wait for mounts modulo.preprocessAndDefine(modulo.cmdCallback, 'Command'); } } }

function loadDefs (modulo, conf, value) { const tags = Object.keys(modulo[conf.Contains]).map(s => s.toLowerCase()); const defs = modulo.util.loadString(value, conf.DefinitionName, tags); if (conf.Override) { defs.forEach(def => Object.assign(def, conf.Override)); } }

function mainRequire (modulo, conf, value) { modulo.config.modulo.build.mainModules.push(value); modulo.registry.modules[value].call(window, modulo); }

function definedAs (modulo, def, value) { def.Name = value ? def[value] : (def.Name || def.Type.toLowerCase()); const parentDef = modulo.definitions[def.Parent]; const parentPrefix = parentDef && ('ChildPrefix' in parentDef) ? parentDef.ChildPrefix : (def.Parent ? def.Parent + '_' : ''); def.DefinitionName = parentPrefix + def.Name; // Search for the next free Name by suffixing numbers while (def.DefinitionName in modulo.definitions) { const match = /([0-9]+)$/.exec(def.Name); const number = match ? match[0] : ''; def.Name = def.Name.replace(number, '') + ((number * 1) + 1); def.DefinitionName = parentPrefix + def.Name; } modulo.definitions[def.DefinitionName] = def; // store definition const parentConf = modulo.definitions[def.Parent]; if (parentConf) { parentConf.ChildrenNames = parentConf.ChildrenNames || []; parentConf.ChildrenNames.push(def.DefinitionName); } }

function dataType (modulo, def, value) { if (value === '?') { // md: '?' means determine based on extension const ext = def.Src && def.Src.match(/.([a-z]+)$/i); value = ext ? ext[1] : 'json'; // md: If extension, use; else use "json" } def.ContentType = [ value.toUpperCase(), def.Hint ]; }

function code (modulo, def, value) { const { newNode, bundleHead } = modulo.util; const name = def.DefinitionName; // Defines global module with name modulo.assert(!(name in modulo.registry.modules), 'Duplicate module name'); const prefix = 'modulo.registry.modules.' + name + ' = function ' + name; const content = prefix + ' (modulo) { ' + value + '}'; if (document && document.head && name[0] !== '_' && !def.Preprocess) { bundleHead(modulo, newNode(content, 'SCRIPT'), modulo.bundles.modscript); } else { // Else: Do not bundle, run in Function Function('window', 'modulo', content)(window, modulo); } }

function contentType (modulo, def, value) { def.data = modulo.contentType[value[0]](def.Content, value[1]); delete def.Content; }

return modulo.util.insObject({ src, srcSync, defTarget, command, code, loadDefs, mainRequire, definedAs, dataType, filterContent, contentType }) /#ENDUNLESS#/ }

Modulo.Engines = function Engines (modulo) { // md: ## Engines

class DOMLoader {/#UNLESS#/ loadFromDOM(elem, Parent = null, quietErrors = false, allowedTypes = null) { const { defaultDef } = modulo.config.modulo; const toCamel = s => s.replace(/-([a-z])/g, g => g[1].toUpperCase()); const tagsLower = allowedTypes || modulo.config.domloader.topLevelTags; const array = []; for (const node of elem.children || []) { const Type = this.getDefType(node, tagsLower, quietErrors); if (node._moduloLoadedBy || Type === null) { continue; // Already loaded, or an ignorable or silenced error } // Valid definition, now create the "def" object node._moduloLoadedBy = modulo.id; // Mark as having loaded this const Content = node.tagName === 'SCRIPT' ? node.textContent : node.innerHTML; const def = Object.assign({ Type, Parent, Content }, defaultDef); array.push(Object.assign(def, modulo.config[Type])); for (let name of node.getAttributeNames()) { // Loop through attrs const value = node.getAttribute(name); if (Type === name && !value) { // e.g. continue; // This is the "Type" attribute itself, skip } def[toCamel(name)] = value; // "-kebab-case" to "CamelCase" } } modulo.util.repeatProcessors(array, 'DefLoaders'); return array; } loadContentFile(node) { // Handle loading DOM Node of contentfile const [ cmdName, src ] = modulo.argv; // is this a _load? if (!window.parent || parent === window || cmdName !== '_load') { node.remove(); // Regular contentfile return node.hasAttribute('f-s') ? 'filesystem' : 'contentfile'; } // else: is "_load" command let text = (document.head && document.head.innerHTML) || ''; text += (document.body && document.body.innerHTML) || ''; text = text.replace(RegExp(</${ node.tagName }>\\s*$, 'i'), ''); node.remove(); parent.postMessage(JSON.stringify({ _FL: [ text, src ] }), ''); modulo.util.repeatProcessors = () => {} // stop all loading return null; } getDefType(node, tagsLower, quiet = false) { const { tagName, nodeType, textContent } = node; if (nodeType !== 1) { // Text nodes, comment nodes, etc if (nodeType === 3 && textContent && textContent.trim() && !quiet) { console.error(Unexpected text in definition: ${textContent}); } return null; } let defType = tagName.toLowerCase(); if (defType in modulo.config.domloader.genericDefTags) { for (const attrName of node.getAttributeNames()) { const attr = attrName.toLowerCase(); const val = node.getAttribute(attr); if (!val && tagsLower.includes(attr)) { defType = attr; // Has an empty string value, is a def } else if (attr === 'type' && val.toUpperCase() in modulo.contentType) { defType = this.loadContentFile(node); // script type=md syntax } break; // Only look at first attribute } } if (!(tagsLower.includes(defType))) { if (!quiet) { // Invalid def / cPart: This type is not allowed here console.error("${ defType }" is not one of: ${ tagsLower }); } return null // not a definition } return defType; // Valid }/#ENDUNLESS#*/ }

class ValueResolver { constructor(contextObj = null, sep = null) { this.ctxObj = contextObj; this.sep = sep || '.'; this.isJSON = /^(true$|false$|null$|[^a-zA-Z])/; // "If not variable" } get(key, ctxObj = null) { const { get } = window.modulo.util; // For drilling down "." const obj = ctxObj || this.ctxObj; // Use given one or in general return this.isJSON.test(key) ? JSON.parse(key) : get(obj, key, this.sep); } set(obj, keyPath, val, autoBind = false) { const index = keyPath.lastIndexOf(this.sep) + 1; // Index at 1 (0 if missing) const key = keyPath.slice(index).replace(/:$/, ''); // Between "." & ":" const prefix = keyPath.slice(0, index - 1); // Get before first "." const target = index ? this.get(prefix, obj) : obj; // Drill down prefix if (keyPath.endsWith(':')) { // If it's a dataProp style attribute const parentKey = val.substr(0, val.lastIndexOf(this.sep)); val = this.get(val); // Resolve "val" from context, or JSON literal /if (autoBind && !this.isJSON.test(val) && parentKey.includes(this.sep)) { val = val.bind(this.get(parentKey)); }/ } target[key] = val; // Assign the value to it's parent object } }

class FrameQueue { constructor() { this.queue = {} this.data = {} this.frames = {} this.protos = { 'file:': 1, 'about:': 1 } if (location.protocol in this.protos) { // check for "fs stack" //try { this.fs = (window._moduloFS || parent._moduloFS).fs } catch { } try { this.fs = window.parent._moduloFS.fs } catch { } const load = ({ data }) => this.receiveData(...JSON.parse(data)._FL); window.addEventListener('message', load, false); } } makeSrc(command, url) { const { urlencode } = modulo.templateFilter; const param = ?argv=${ command }&argv=${ urlencode(url, true) }; return url.includes('?') ? url.replace('?', param + '&') : url + param; } call(url, elem='body', style='width:49%;float:right') { // Thread-like API const iframe = { style, elem, src: this.makeSrc('_thread', url) }; return this.fetch(url, { iframe }); } fetch(url, opts = {}) { // "thennable" that resembling window.fetch url = url === '?' ? modulo.config.pathName : url; // resolve '?' url = url.endsWith('/') ? ${ url }index.html : url; // auto index.html return { then: cb => this.request(url, cb, console.error, opts) }; } request(src, resolve, reject, opts = {}) { // Do fetch & do enqueue const { urlencode } = modulo.templateFilter; if (src in this.data) { // Cache resolve(this.data[src], src); // (sync route) } else if (this.fs && src in Object.assign({ }, ...this.fs)) { resolve(Object.assign({ }, ...this.fs)[src], src); // child route } else if (!(src in this.queue)) { // No cache, no queue this.queue[src] = [ resolve ]; // First time, create the queue Array if (location.protocol in this.protos || opts.iframe) { // Transit? const conf = opts.iframe || { style: 'display:none', src: this.makeSrc('_load', src) } this.frames[src] = modulo.util.newNode('', 'IFRAME', conf); document[opts.iframe ? 'body' : 'head'].prepend(this.frames[src]); } else { // HTTP(s) window.fetch(src, Object.assign({ cache: 'no-store' }, opts.fetch)) .then(response => response.text()) .then(text => this.receiveData(text, src)) .catch(reject); } } else { // Already requested, only enqueue function this.queue[src].push(resolve); } } receiveData(text, src) { if (src in this.frames) { if (this.frames[src].style.display === 'none') { this.frames[src].remove(); } delete this.frames[src]; } this.data[src] = text; const resolveCallbacks = this.queue[src]; // "Consume" entire queue delete this.queue[src]; modulo.assert(resolveCallbacks, Received extra / late data: ${ src }); for (const dataCallback of resolveCallbacks) { dataCallback(text, src); } } enqueue(callback, waitForAll = false) { // Wait for current queue (or all) const allQueues = Array.from(Object.values(this.queue)); // Copy array const { length } = allQueues; if (length === 0) { return callback(); // Synchronous route } else if (waitForAll) { // Doing a wait -- setup re-enqueue loop return this.enqueue(() => Object.keys(this.queue).length === 0 ? callback() : this.enqueue(callback, true)); } let count = 0; // Using count we only do callback() when ALL returned const check = () => ((++count >= length) ? callback() : 0); allQueues.forEach(queue => queue.push(check)); // Add to every queue } }

class DOMCursor { constructor(parentNode, parentRival, slots) { this.slots = slots || {}; // Slottables keyed by name (default is '') this.instanceStack = []; // Used for implementing DFS non-recursively this._rivalQuerySelector = parentRival.querySelector.bind(parentRival); this._querySelector = parentNode.querySelector.bind(parentNode); this.initialize(parentNode, parentRival); } initialize(parentNode, parentRival) { this.parentNode = parentNode; this.nextChild = parentNode.firstChild; this.nextRival = parentRival.firstChild; this.activeExcess = null; this.activeSlot = null; if (parentRival.tagName === 'SLOT') { // Parent will "consume" a slot const slotName = parentRival.getAttribute('name') || ''; this.activeSlot = this.slots[slotName] || null; // Mark active if (this.activeSlot) { // Children were specified for this slot! delete this.slots[slotName]; // (prevent "dupe slot" bug) this._setNextRival(null); // Move the cursor to the first elem } } } saveToStack() { // Creates an object copied with all cursor state this.instanceStack.push(Object.assign({ }, this)); // Copy to empty obj } loadFromStack() { // Remaining stack to "walk back" (non-recursive DFS) const stack = this.instanceStack; return stack.length > 0 && Object.assign(this, stack.pop()); } loadFromSlots() { // There are "excess" slots (copied, but deeply nested) const name = Object.keys(this.slots).pop(); // Get next ("pop" from obj) if (name === '' || name) { // Is name valid? (String of 0 or more) const sel = name ? slot[name="${ name }"] : 'slot:not([name])'; const rivalSlot = this._rivalQuerySelector(sel); if (!rivalSlot) { // No slot (e.g., conditionally rendered, or typo) delete this.slots[name]; // (Ensure "consumed", if not init'ed) return this.loadFromSlots(); // If no elem, try popping again } this.initialize(this._querySelector(sel) || rivalSlot, rivalSlot); return true; // Indicate success: Child and rival slots are ready } } hasNext() { if (this.nextChild || this.nextRival) { return true; // Is pointing at another node } else if (this.loadFromStack() || this.loadFromSlots()) { // Walk back return this.hasNext(); // Possibly loaded nodes nextChild, nextRival } return false; // Every load attempt is "false" (empty), end iteration } _setNextRival(rival) { // Traverse this.nextRival based on DOM or SLOT if (this.activeSlot !== null) { // Use activeSlot array for next instead if (this.activeSlot.length > 0) { this.nextRival = this.activeSlot.shift(); // Pop off next one this.nextRival._moduloIgnoreOnce = true; // Ensure no descend } else { this.nextRival = null; } } else { this.nextRival = rival ? rival.nextSibling : null; // Normal DOM traversal } } next() { let child = this.nextChild; let rival = this.nextRival; if (!rival && this.activeExcess && this.activeExcess.length > 0) { return this.activeExcess.shift(); // Return the first pair } this.nextChild = child ? child.nextSibling : null; this._setNextRival(rival); // Traverse initially return [ child, rival ]; } }

class DOMReconciler { constructor() { this.directives = {}; this.patches = []; this.patch = this.pushPatch; } applyPatches(patches) { for (const patch of patches) { this.applyPatch(patch[0], patch[1], patch[2], patch[3]); } } registerDirectives(thisObj, def) { const prefix = 'DirectivePrefix' in def ? def.DirectivePrefix : (def.RenderObj || def.Name) + '.'; for (const method of def.Directives || []) { this.directives[prefix + method] = thisObj; } } reconcileChildren(childParent, rivalParent, slots) { const cursor = new modulo.engine.DOMCursor(childParent, rivalParent, slots); while (cursor.hasNext()) { // "rival" is node we wish "child" to match const [ child, rival ] = cursor.next(); const needReplace = child && rival && ( // If both exist... child.nodeType !== rival.nodeType || // And type is inequal child.nodeName !== rival.nodeName); // OR the tagName differs if ((child && !rival) || needReplace) { // we have more rival, delete child this.patchAndDescendants(child, 'Unmount'); this.patch(cursor.parentNode, 'removeChild', child); } if (needReplace) { // do swap with insertBefore this.patch(cursor.parentNode, 'insertBefore', rival, child.nextSibling); this.patchAndDescendants(rival, 'Mount'); } if (!child && rival) { // we have less than rival, take rival this.patch(cursor.parentNode, 'appendChild', rival); this.patchAndDescendants(rival, 'Mount'); } if (child && rival && !needReplace) { // Both exist and same type if (child.nodeType !== 1) { // text or comment node if (child.nodeValue !== rival.nodeValue) { // update this.patch(child, 'node-value', rival.nodeValue); } } else if (!child.isEqualNode(rival)) { // sync if not equal this.reconcileAttributes(child, rival); if (rival.hasAttribute('modulo-ignore')) { // Don't descend // console.log('Skipping ignored node'); } else if (child.isModulo) { // is a Modulo component this.patch(child, 'rerender', rival); // TODO rm! } else { //} else if (!this.shouldNotDescend) { cursor.saveToStack(); cursor.initialize(child, rival); } } } } } pushPatch(node, method, arg, arg2 = null) { this.patches.push([ node, method, arg, arg2 ]); } applyPatch(node, method, arg, arg2) { // take that, rule of 3! if (method === 'node-value') { node.nodeValue = arg; } else if (method === 'insertBefore') { node.insertBefore(arg, arg2); // Needs 2 arguments } else { node[method].call(node, arg); // invoke method } } patchDirective(el, rawName, suffix, copyFromEl = null) { const split = rawName.split(/./g); if (split.length < 2) { //if (!(rawName in this.directiveLiterals)) { return; // Fast route: not a directive } const value = (copyFromEl || el).getAttribute(rawName); // Get value let dName = split.shift() // Start with left of '.' while (split.length > 0 && !((dName + suffix) in this.directives)) { dName += '.' + split.shift() // Build potential directive prefix } const nameSuffix = split.join('.'); // e.g. "on.click" -> "click" const fullName = dName + suffix; // e.g. "state.bind" -> "state.bindMount" const patchName = (fullName.split('.')[1] || fullName); const directive = { el, value, nameSuffix, rawName }; // Obj to pass modulo.assert(this.directives[fullName], Bad directive: ${ fullName }) this.patch(this.directives[fullName], patchName, directive); } reconcileAttributes(node, rival) { const myAttrs = new Set(node ? node.getAttributeNames() : []); const rivalAttributes = new Set(rival.getAttributeNames()); // Check for new and changed attributes for (const rawName of rivalAttributes) { const attr = rival.getAttributeNode(rawName); if (myAttrs.has(rawName) && node.getAttribute(rawName) === attr.value) { continue; // Already matches, on to next } if (myAttrs.has(rawName)) { // If exists, trigger Unmount first this.patchDirective(node, rawName, 'Unmount'); } // Set attribute node, and then Mount based on rival value this.patch(node, 'setAttributeNode', attr.cloneNode(true)); this.patchDirective(node, rawName, 'Mount', rival); } // Check for old attributes that were removed (ignoring modulo- prefixed ones) for (const rawName of myAttrs) { if (!rivalAttributes.has(rawName) && !rawName.startsWith('modulo-')) { this.patchDirective(node, rawName, 'Unmount'); this.patch(node, 'removeAttribute', rawName); } } } patchAndDescendants(node, actionSuffix) { if (node.isModulo || node.nodeType !== 1 || node._moduloIgnoreOnce) { delete node._moduloIgnoreOnce; return; // (not element) } for (const rawName of node.getAttributeNames()) { this.patchDirective(node, rawName, actionSuffix); } for (const child of node.children) { // recurse this.patchAndDescendants(child, actionSuffix) } } }

return { FrameQueue, DOMLoader, ValueResolver, DOMReconciler, DOMCursor } }

Modulo.Queues = function Queues (modulo) { Object.assign(modulo, { // (upgrade modulo with queues + build info) _connectedQueue: [], _drainQueue: () => { while (modulo._connectedQueue.length > 0) { modulo._connectedQueue.shift().moduloMount(); } }, cmdCallback: (cmdStatus = 0, edit = null, html = null) => { modulo.cmdStatus = cmdStatus; if (edit || edit === null) { // TODO rm this interface window.document.body.innerHTML = html || <modulo-Editor ui=full>; } }, preprocessAndDefine(cb, prefix = 'Def') { cb = cb || (() => {}); modulo.queue.enqueue(() => { modulo.util.repeatProcessors(null, prefix + 'Builders', () => { modulo.util.repeatProcessors(null, prefix + 'Finalizers', cb) }); }, true); // The "true" causes it to wait for all }, assert: (value, ...info) => { if (!value) { console.error('%cᵐ°dᵘ⁄o', 'background:red', modulo.id, ...info); throw new Error(Assert : "${ Array.from(info).join(' ') }"); } }, bundles: { script: [], style: [], link: [], meta: [], modscript: [], modstyle: [] }, registry: { bundle: { }, elements: { }, modules: { } }, consts: { WAIT: 900, WAITALL: 901 }, }); modulo.argv = new window.URLSearchParams(window.location.search).getAll('argv'); Object.assign(modulo.registry, { utils: modulo.util, cparts: modulo.part, coreDefs: modulo.core, processors: modulo.processor }) // TODO Legacy alias return new modulo.engine.FrameQueue(); }

Modulo.Cores = function CoreDefinitions (modulo) { //md: ## Core Definitions

class Component { static CustomElement (modulo, def, value) { //md: Register a component def.name = def.name || def.DefName || def.Name; def.TagName = ${ def.namespace }-${ def.name }.toLowerCase(); if (def.library) { const libraryName = ${ def.Parent }-${ def.name }.toLowerCase(); modulo.config.library.tagAliases[def.TagName] = libraryName; def.TagName = libraryName; } def.MainRequire = def.DefinitionName; const className = def.TagName.replace('-', '_'); def.Code = const def = modulo.definitions['${ def.DefinitionName }']; class ${ className } extends window.HTMLElement { constructor(){ super(); this.init(); } static observedAttributes = []; } modulo.util.initComponentClass(modulo, def, ${ className }); window.customElements.define("${ def.TagName }", ${ className }); return ${ className };.replace(/\n\s+/g, '\n'); } static BuildLifecycle (modulo, def, value) { for (const elem of document.querySelectorAll(def.TagName)) { elem.cparts.component._lifecycle([ value ]); // Run the lifecycle } return true; } rerender(original = null) { if (original) { if (this.element.originalHTML === null) { this.element.originalHTML = original.innerHTML; } this.element.originalChildren = Array.from( original.hasChildNodes() ? original.childNodes : []); } this._lifecycle([ 'prepare', 'render', 'dom', 'reconcile', 'update' ]); } getCurrentRenderObj() { return (this.element.eventRenderObj || this.element.renderObj || this.element.initRenderObj); } _lifecycle(lifecycleNames, rObj={ }) { const renderObj = Object.assign({}, rObj, this.getCurrentRenderObj()); this.element.renderObj = renderObj; this.runLifecycle(this.element.cparts, renderObj, lifecycleNames); //this.element.renderObj = null; // ?rendering is over, set to null } runLifecycle(parts, renderObj, lifecycleNames) { for (const lifecycleName of lifecycleNames) { const methodName = lifecycleName + 'Callback'; for (const [ name, obj ] of Object.entries(parts)) { if (!(methodName in obj)) { continue; } const result = obj[methodName].call(obj, renderObj); if (result !== undefined) { renderObj[obj.conf.RenderObj || obj.conf.Name] = result; } } } } buildCallback() { this.element.setAttribute('modulo-mount-html', this.element.originalHTML) for (const elem of this.element.querySelectorAll('*')) { for (const name of elem.getAttributeNames()) { if (!(new RegExp('^[a-z0-9-]+$', 'i').exec(name))) { elem.removeAttribute(name); // Not alnum or dash } } } } initializedCallback() { this.modulo.paused = true; const { newNode } = this.modulo.util; const html = this.element.getAttribute('modulo-mount-html'); // Hydrate? this._mountRival = html === null ? this.element : newNode(html); this.element.originalHTML = html === null ? this.element.innerHTML : html; this.resolver = new this.modulo.engine.ValueResolver(this.modulo); this.reconciler = new this.modulo.engine.DOMReconciler(this.modulo); for (const part of Object.values(this.element.cparts)) { // Setup parts this.reconciler.registerDirectives(part, part.conf); } } mountCallback() { // First "mount", trigger render & hydration this.rerender(this._mountRival); // render + mount childNodes delete this._mountRival; // Clear the temporary reference this.element.isMounted = true; // Mark as mounted } prepareCallback() { return { // Create the initial Component renderObj obj originalHTML: this.element.originalHTML, // HTML received at mount id: this.id, // Universally unique ID number innerHTML: null, // String to copy (default: null is "no-op") innerDOM: null, // Node to copy (default: null sets innerHTML) patches: null, // Patch array (default: reconcile vs innerDOM) slots: { }, // Populate with slots to be filled when reconciling }; } domCallback({ component }) { let { slots, root, innerHTML, innerDOM } = component; if (this.attrs.mode === 'regular' || this.attrs.mode === 'vanish') { root = this.element; // default, use element as root } else if (this.attrs.mode === 'shadow') { if (!this.element.shadowRoot) { this.element.attachShadow({ mode: 'open' }); } root = this.element.shadowRoot; // render into attached shadow } else if (!root) { this.modulo.assert(this.attrs.mode === 'custom-root', 'Bad mode') } if (innerHTML !== null && !innerDOM) { // Use component.innerHTML as DOM innerDOM = this.modulo.util.newNode(innerHTML); } if (innerDOM && this.attrs.mode !== 'shadow') { for (const elem of this.element.originalChildren) { const name = (elem.getAttribute && elem.getAttribute('slot')) || ''; elem.remove(); // Remove from DOM so it can't self-match if (!(name in slots)) { slots[name] = [ elem ]; // Sorting into new slot arrays } else { slots[name].push(elem); // Or pushing into existing } } } return { root, innerHTML, innerDOM, slots }; } reconcileCallback({ component }) { let { innerHTML, innerDOM, patches, root, slots } = component; if (innerDOM) { this.reconciler.patches = []; // Reset reconciler patches this.reconciler.reconcileChildren(root, innerDOM, slots); patches = this.reconciler.patches; } return { patches, innerHTML }; // TODO remove innerHTML from here } updateCallback({ component }) { this.modulo.paused = false; // Re-enable children mounting if (component.patches) { this.reconciler.applyPatches(component.patches); } if (this.attrs.mode === 'vanish') { this.element.replaceWith(...this.element.childNodes); } } handleEvent(func, payload, ev) { this._lifecycle([ 'event' ]); func(typeof payload === "undefined" ? ev : payload); this._lifecycle([ 'eventCleanup' ]); if (this.attrs.rerender !== 'manual') { ev.preventDefault(); // Prevent navigation from stopping rerender etc this.element.rerender(); // Rerender after event } } onMount({ el, value, nameSuffix, rawName, listen }) { // on.click=script.show this.modulo.assert(this.resolve(value), Not found: ${ rawName }=${ value }); const getOr = (key, key2) => key2 && el.hasAttribute(key2) ? getOr(key2) : this.resolve(el.getAttribute(key)); listen = listen ? listen : (ev) => { // Define a event func to run handleEvent const payload = getOr(nameSuffix + '.payload:', 'payload:') || el.getAttribute('payload'); this.handleEvent(this.resolve(value, null, true), payload, ev); } el.moduloEvents = el.moduloEvents || {}; // Attach if not already el.moduloEvents[nameSuffix] = listen; el.addEventListener(nameSuffix, listen); } onUnmount({ el, nameSuffix }) { el.removeEventListener(nameSuffix, el.moduloEvents[nameSuffix]); delete el.moduloEvents[nameSuffix]; } resolve(key, defaultVal, autoBind = false) { const { ValueResolver } = this.modulo.engine; const resolver = new ValueResolver(this.getCurrentRenderObj()); let val = resolver.get(key, defaultVal); if (autoBind && typeof val === 'function' && key.includes(resolver.sep)) { const parentKey = key.substr(0, key.lastIndexOf(resolver.sep)); val = val.bind(this.resolve(parentKey)); // Parent is sub-obj, bind } return val } } const core = { Component };

/#UNLESS#/ class Artifact { static Remove (modulo, def, value) { // Delete given excess elements for (const elem of window.document.querySelectorAll(value)) { elem.remove(); } } // md: Registers build and scaffolding commands static Collect (modulo, def, value) { // Gathers any extra elements value = value === '?' ? modulo.config.modulo.scriptSelector : value; def.LoadElems = def.LoadElems || []; // initialize for next processor for (const elem of window.document.querySelectorAll(value)) { elem.id = 'collected_' + (def.LoadElems.length + 1000).toString(); def.LoadElems.push(elem); } } static Bundle (modulo, def, value) { // Runs first to queue up ctx def.LoadElems = def.LoadElems || []; // initialize for next processor for (const bundleName of value.split(',')) { for (const id of modulo.bundles[bundleName]) { def.LoadElems.push(window.document.getElementById(id)); } } } static LoadElems (modulo, def, value) { // Actually enqueues content def.data = def.data || []; // initialize for template def.ids = def.ids || []; // initialize for template if ('SaveReqs' in def) { value.push(modulo.util.newNode('', 'SCRIPT', { src: '?' })); } for (const elem of value) { let url = elem.getAttribute('src') || elem.getAttribute('href') || null; if (url) { // Retrieve from URL modulo.queue.fetch(url).then(text => { def.data[elem.id] = text; // Attach back to element if ('SaveReqs' in def) { url = url.replace(/^?$/, modulo.config.pathName) modulo.fs[def.SaveReqs || 'BUILD'].propagate(url, text); } }); } else { // Retrieve text content def.data[elem.id] = elem.textContent; } def.ids.push(elem.id); // List in order elem.remove(); // Remove from DOM so it doesn't get doubled } } static SaveTo (modulo, def, value, doc = null) { // Build processor const [ cmd, tag ] = modulo.argv const ctx = Object.assign({ def, cmd, tag, doc: doc || window.document }, modulo); const render = s => new modulo.part.Template(s).render(ctx); const text = (def.prefix || '') + render(def.Content); // Execute template if (text) { // Never save an empty string (e.g. ' ' or '\n' is ok) ctx.hash = modulo.util.hash(text); // Compute hash for path def.path = def.path || render(def.pathTemplate); // Render path template modulo.fs[value].propagate(def.path, text); // Save to given FS } } }

class Configuration { }

class ContentList { static Load (modulo, def, value) { // md: Specify -load=md to gather const { file, templateFilter, contentType, queue } = modulo; value = value.toUpperCase() || 'TXT'; // md: file list as markdown. const re = s => file.path.match(RegExp(templateFilterdef.router)); const c = { }; // Gathers files into file cache "c" for (const row of def.data) { const url = file[def.pathOrigin || 'rootPath'] + row[0]; queue.fetch(url).then(data => { const body = contentType[value](data, def.LoadHint); c[row[0]] = typeof body !== 'object' ? { body } : body; c[row[0]].Source = row[0]; if (def.router && re(row[1])) { Object.assign(file, c[row[0]], { route: re(row[1]).groups }); } if (Object.keys(c).length >= def.data.length) { def.files = def.data.map(row => c[row[0]]); } }) } return modulo.consts.WAIT; } static BuildAll (modulo, def, value) { // md: Use command=buildall to for (const row of def.data) { // md: loop through and build each file. modulo.queue.call(${ row[0] }?argv=${ value }, '').then(() => {}); } } }

class ContentFile { static Load (modulo, def, value) { // md: Handles display of content files. document.body.innerHTML += modulo.config.contentfile.htmlBody; } }

class FileSystem extends modulo.part.State { static Load (modulo, def, value) { // md: Configures build filesystems. const { queue, stores, util, fs } = modulo; fs[def.Store] = util.instance(def, { element: { rerender: () => {} } }); try { stores[def.Store] = parent._moduloFS[def.Store] } catch (e) { } fs[def.Store].constructor.factoryCallback({ }, def, modulo); fs[def.Store].initializedCallback({ }); if (modulo.argv.length) { // Expose to child if argv is happening window._moduloFS = window._moduloFS || { fs: [ queue.data ] } window._moduloFS[def.Store] = stores[def.Store]; window._moduloFS.fs.unshift(stores[def.Store].data); } } }

const Include = modulo.part.Include; class Library { } class Modulo { }

Object.assign(core, { Artifact, Configuration, ContentList, ContentFile, FileSystem, Library, Include })

/#ENDUNLESS#/ return modulo.util.insObject(core); }

var modulo = new Modulo(); // Global Instance

/#UNLESS#/ if (typeof window === "undefined") { var window = { } } // non-browsers Object.assign(window, { modulo, Modulo }) // Export

window.modulo.command._load = () => { console.error('%cᵐ°dᵘ⁄o FAIL; NO TYPE', 'background:orange', modulo.argv[1]) parent.postMessage(JSON.stringify({ _FL: [ null, modulo.argv[1] ] }), ''); } window.modulo.command._thread = ({ cmdCallback }) => { const [ cmd, src ] = modulo.argv.splice(0, 2); // rm modulo.cmdCallback = (...args) => parent.postMessage(JSON.stringify({ _FL: [ args, src ] }), ''); modulo.util.getCommand(modulo)(); } window.modulo.command._default = function default (modulo) { // [modu/o] menu if (window.parent !== window) { return modulo.cmdCallback(0, false); // default behavior, no editor or menu } const font = 'font-size: 28px; padding:0 8px 0 8px; border:2px solid #000;'; const names = Object.keys(modulo.command).filter(s => !s.startsWith('')); const gets = names.map(s => get ${ s }(){location.href+="?argv=${ s }"}); const aStr = JSON.stringify([ '%cᵐ°dᵘ⁄o', font, names.join(', ') ]); const suffix = "[MAIN THREAD]",new (class {${ gets.join('\n') }}); Function(console.log(...${ aStr },${ suffix }))(); modulo.cmdCallback(0, false); // default behavior, do not show Editor } if (typeof window.document !== 'undefined' && !window.PAUSE_MODULO) { // Browser modulo.util.loadFromDOM(window.document.head, null, true); // Blocking head modulo.util.setupDevLib(modulo); // Loads default devlib window.document.addEventListener('DOMContentLoaded', () => { modulo.util.loadFromDOM(window.document.head, null, true); // Defer head modulo.util.loadFromDOM(window.document.body, null, true); // Defer body modulo.util.configureStatic(modulo); // Run any default loads modulo.preprocessAndDefine(modulo.util.getCommand(modulo)); }); } else if (typeof module !== 'undefined') { // Node.js module.exports = { Modulo, window }; } /#ENDUNLESS#/