diff --git a/src/consts.js b/src/consts.js index 43d3a17..0f3525e 100644 --- a/src/consts.js +++ b/src/consts.js @@ -1,5 +1,7 @@ -export const VERSION = '0.0.9' +export const DLVERSION = '0.0.9' // We add some extra properties into various objects throughout, better to use symbols and not interfere. this is just a tiny optimization export const [USE_MAPFN, TARGET, PROXY, STEPS, LISTENERS, IF, STATEHOOK] = Array.from(Array(7), Symbol) + +export const cssBoundary = 'dl-boundary' diff --git a/src/core.js b/src/core.js index 9e79c52..0a5d962 100644 --- a/src/core.js +++ b/src/core.js @@ -8,7 +8,10 @@ import { PROXY, STEPS, IF, + cssBoundary, } from './consts' + + // saves a few characters, since document will never change let doc = document @@ -309,11 +312,13 @@ export function h(type, props, ...children) { let elm = type.apply(newthis) elm.$ = newthis newthis.root = elm + /* FEATURE.CSS.START */ + let cl = elm.classList if (newthis.css) { - let cl = elm.classList cl.add(newthis.css) - cl.add('self') } + cl.add(cssBoundary) + /* FEATURE.CSS.END */ elm.setAttribute('data-component', type.name) if (typeof newthis.mount === 'function') newthis.mount() return elm diff --git a/src/css.js b/src/css.js index a61c4f8..5205aac 100644 --- a/src/css.js +++ b/src/css.js @@ -1,31 +1,70 @@ +import { cssBoundary } from "./consts" + const cssmap = {} -export function css(strings, ...values) { - let str = '' - for (let f of strings) { - str += f + (values.shift() || '') - } - let cached = cssmap[str] - if (cached) return cached - const uid = `dl${Array(5) +/* POLYFILL.SCOPE.START */ +let scopeSupported; + +window.addEventListener('load', () => { + const style = document.createElement('style'); + style.textContent = '@scope (.test) { :scope { color: red } }'; + document.head.appendChild(style); + + const testElement = document.createElement('div'); + testElement.className = 'test'; + document.body.appendChild(testElement); + + const computedColor = getComputedStyle(testElement).color; + document.head.removeChild(style); + document.body.removeChild(testElement); + + scopeSupported = computedColor == 'rgb(255, 0, 0)' +}); + +const depth = 50; +// polyfills @scope for firefox and older browsers, using a :not selector recursively increasing in depth +// depth 50 means that after 50 layers of nesting, switching between an unrelated component and the target component, it will eventually stop applying styles (or let them leak into children) +// this is slow. please ask mozilla to implement @scope +function polyfill_scope(target) { + let boundary = `:not(${target}).${cssBoundary}` + let g = (str, i) => `${str} *${i > depth ? "" : `:not(${g(str + " " + ((i % 2 == 0) ? target : boundary), i + 1)})`}` + return `:not(${g(boundary, 0)})` +} +/* POLYFILL.SCOPE.END */ + + +export function genuid() { + return `dl${Array(5) .fill(0) .map(() => { return Math.floor(Math.random() * 36).toString(36) }) .join('')}` +} - cssmap[str] = uid - const styleElement = document.createElement('style') - document.head.appendChild(styleElement) +const csstag = (scoped) => + function css(strings, ...values) { + let str = '' + for (let f of strings) { + str += f + (values.shift() || '') + } + return genCss(str, scoped) + } + +export const css = csstag(false) +export const scope = csstag(true) + + +function parseCombinedCss(str) { let newstr = '' let selfstr = '' // compat layer for older browsers. when css nesting stablizes this can be removed str += '\n' - for (;;) { + for (; ;) { let [first, ...rest] = str.split('\n') if (first.trim().endsWith('{')) break @@ -33,14 +72,52 @@ export function css(strings, ...values) { str = rest.join('\n') if (!str) break } - styleElement.textContent = str - for (const rule of styleElement.sheet.cssRules) { - rule.selectorText = `.${uid} ${rule.selectorText}` - newstr += rule.cssText + '\n' + return [newstr, selfstr, str] +} + +function genCss(str, scoped) { + let cached = cssmap[str] + if (cached) return cached + + const uid = genuid(); + cssmap[str] = uid + + + const styleElement = document.createElement('style') + document.head.appendChild(styleElement) + + if (scoped) { + /* POLYFILL.SCOPE.START */ + if (!scopeSupported) { + [newstr, selfstr, str] = parseCombinedCss(str) + + styleElement.textContent = str + + let scoped = polyfill_scope(`.${uid}`, 50); + for (const rule of styleElement.sheet.cssRules) { + rule.selectorText = `.${uid} ${rule.selectorText}${scoped}` + newstr += rule.cssText + '\n' + } + + styleElement.textContent = `.${uid} {${selfstr}}` + '\n' + newstr + return uid + } + /* POLYFILL.SCOPE.END */ + + styleElement.textContent = `@scope (.${uid}) to (:not(.${uid}).dl-boundary *) { :scope { ${str} } }` + } else { + [newstr, selfstr, str] = parseCombinedCss(str) + + styleElement.textContent = str + + for (const rule of styleElement.sheet.cssRules) { + rule.selectorText = `.${uid} ${rule.selectorText}` + newstr += rule.cssText + '\n' + } + + styleElement.textContent = `.${uid} {${selfstr}}` + '\n' + newstr } - styleElement.textContent = `.${uid} {${selfstr}}` + '\n' + newstr - return uid } diff --git a/src/jsxLiterals.js b/src/jsxLiterals.js index ee022e5..a6706a9 100644 --- a/src/jsxLiterals.js +++ b/src/jsxLiterals.js @@ -1,32 +1,49 @@ +import { assert } from './asserts' import { h } from './core' +import { genuid } from './css' export function html(strings, ...values) { + // normalize the strings array, it would otherwise give us an object + strings = [...strings] let flattened = '' let markers = {} - for (const i in strings) { + for (let i = 0; i < strings.length; i++) { let string = strings[i] let value = values[i] + // since self closing tags don't exist in regular html, look for the pattern enclosing a function, and replace it with `/.exec(strings[i + 1]) + if (/\< *$/.test(string) && match) { + strings[i + 1] = strings[i + 1].substr( + match.index + match[0].length + ) + } + flattened += string if (i < values.length) { let dupe = Object.values(markers).findIndex((v) => v == value) + let marker if (dupe !== -1) { - flattened += Object.keys(markers)[dupe] + marker = Object.keys(markers)[dupe] } else { - let marker = - 'm' + - Array(16) - .fill(0) - .map(() => Math.floor(Math.random() * 16).toString(16)) - .join('') + marker = genuid() markers[marker] = value - flattened += marker + } + + flattened += marker + + // close the self closing tag + if (match) { + flattened += `>` } } } let dom = new DOMParser().parseFromString(flattened, 'text/html') - if (dom.body.children.length !== 1) - throw 'html builder needs exactly one child' + assert( + dom.body.children.length == 1, + 'html builder needs exactly one child' + ) function wraph(elm) { let nodename = elm.nodeName.toLowerCase() @@ -41,7 +58,7 @@ export function html(strings, ...values) { if (!text) break if (!text.includes(marker)) continue let before - ;[before, text] = text.split(marker) + ;[before, text] = text.split(marker) children = [ ...children.slice(0, i), before, diff --git a/src/main.js b/src/main.js index 139c344..d7f476f 100644 --- a/src/main.js +++ b/src/main.js @@ -1,26 +1,27 @@ -import { VERSION } from './consts' -export { VERSION as DLVERSION } +import { DLVERSION } from './consts' + +export { DLVERSION } export * from './core' // $state was named differently in older versions export { $state as stateful } from './core' /* FEATURE.CSS.START */ -export * from './css' +export { css, scope } from './css' /* FEATURE.CSS.END */ /* FEATURE.JSXLITERALS.START */ -export * from './jsxLiterals' +export { html } from './jsxLiterals' /* FEATURE.JSXLITERALS.END */ /* FEATURE.STORES.START */ -export * from './stores' +export { $store } from './stores' /* FEATURE.STORES.END */ /* DEV.START */ import { log } from './asserts' -log('Version: ' + VERSION) +log('Version: ' + DLVERSION) console.warn( 'This is a DEVELOPER build of dreamland.js. It is not suitable for production use.' )