Skip to content

Commit

Permalink
Adds support for ‘nostrict’ mode
Browse files Browse the repository at this point in the history
  • Loading branch information
stephband committed Jan 10, 2024
1 parent 6b5ee19 commit 96b8b6a
Show file tree
Hide file tree
Showing 16 changed files with 146 additions and 93 deletions.
26 changes: 25 additions & 1 deletion module.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@


import TemplateRenderer, { cache } from './modules/renderer-template.js';

export default function Literal(id) {
// TODO: I don't think this works, it replaces whatever template is in the
// DOM, it it probably isn't in the DOM if it's cached?

const id = typeof id === 'object' ?
id.id || '' :
id ;

if (cache[id]) {
return cache[id].create();
}

const template = typeof id === 'object' ?
id :
document.getElementById(id) ;

return new TemplateRenderer(template) ;
}

// TODO: Legacy, remove
export { TemplateRenderer as Renderer };

export { compiled } from './modules/renderer/compile.js';
export { default as config } from './modules/config.js';
export { default as Data, observe } from './modules/data.js';
export { default as Renderer } from './modules/renderer-template.js';
export { default as scope } from './modules/scope.js';
export { urls } from './modules/urls.js';
20 changes: 19 additions & 1 deletion modules/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@
/*
config
Configure Literal's behaviour.
```js
// An event dispatched when Literal updates input values. Default is `false`.
config.updateEvent = CustomEvent('ping');
// Run templates in sloppy mode inside a `with(data)` statement, making
// `${ data.name }` available as simply `${ name }` in a template.
// Warning: MDN says `with` should be considered deprecated:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with
// I really don't see how they can remove it, though.
useWith: false
```
*/

export default {
// An event dispatched when Literal updates input values. May be false.
updateEvent: false
updateEvent: false,
// Runs templates in sloppy mode inside a `with(data)` statment, making
// `${ data.name }` available as simply `${ name }` in a template.
// Warning: MDN says with should be considered deprecated:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with
// I don't see how anyone can remove it, though.
useWith: false
}
4 changes: 3 additions & 1 deletion modules/data.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@

import { Observer as Data, getTarget } from '../../fn/observer/observer.js';
import observe from '../../fn/observer/observe.js';

Data.getObject = getTarget;
Data.observe = observe;

export default Data;
export { default as observe } from '../../fn/observer/observe.js';
export { observe };
16 changes: 9 additions & 7 deletions modules/renderer-template.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

/**
TemplateRenderer(template, element, parameters)
TemplateRenderer(template, element, parameters, options)
Import the `TemplateRenderer` constructor:
Expand Down Expand Up @@ -40,7 +40,7 @@ import { groupCollapsed, groupEnd } from './log.js';

const assign = Object.assign;
const keys = Object.keys;
const cache = {};
export const cache = {};
const nodes = [];


Expand Down Expand Up @@ -98,19 +98,19 @@ function prepareContent(content) {
}
}

function compileContent(content, message) {
function compileContent(content, message, options) {
if (window.DEBUG) { groupCollapsed('compile', message, 'yellow'); }
prepareContent(content);
const renderers = compileNode([], content, '', message);
const renderers = compileNode([], content, '', message, options);
if (window.DEBUG) { groupEnd(); }
return renderers;
}

function compileTemplate(template, id) {
function compileTemplate(template, id, options) {
const content = template.content
|| create('fragment', template.childNodes, template) ;

const renderers = compileContent(content, '#' + id);
const renderers = compileContent(content, '#' + id, options);

return { id, content, renderers };
}
Expand All @@ -136,7 +136,9 @@ export default function TemplateRenderer(template, element = template.parentElem
const id = identify(template) ;

const { content, renderers } = cache[id]
|| (cache[id] = compileTemplate(template, id));
|| (cache[id] = compileTemplate(template, id, {
sloppy: template.hasAttribute('nostrict')
}));

this.element = element;
this.parameters = parameters;
Expand Down
15 changes: 8 additions & 7 deletions modules/renderer/compile-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ compileAttributes(renderers, element, attribute, path, message)
**/

const constructors = {
class: TokensRenderer,
value: ValueRenderer,
checked: CheckedRenderer,

async: BooleanRenderer,
autofocus: BooleanRenderer,
autoplay: BooleanRenderer,
Expand All @@ -38,19 +42,16 @@ const constructors = {
reversed: BooleanRenderer,
selected: BooleanRenderer,
default: BooleanRenderer,
checked: CheckedRenderer,
class: TokensRenderer,
value: ValueRenderer,

// Workaround attribute used in cases where ${} cannot be added directly to
// HTML, such as in <tbody> or <tr>
'inner-html': function(path, name, source, message, element) {
'inner-html': function(path, name, source, message, options, element) {
element.removeAttribute(name);
return new TextRenderer(path, 0, decode(source), message, element.childNodes[0]);
return new TextRenderer(path, 0, decode(source), message, options, element.childNodes[0]);
}
};

export default function compileAttribute(renderers, element, attribute, path, message = '') {
export default function compileAttribute(renderers, element, attribute, path, message = '', options) {
const name = attribute.localName;
const source = attribute.value;

Expand All @@ -65,6 +66,6 @@ export default function compileAttribute(renderers, element, attribute, path, me
}

const Constructor = constructors[name] || AttributeRenderer;
renderers.push(new Constructor(path, name, source, message, element));
renderers.push(new Constructor(path, name, source, message, options, element));
return renderers;
}
38 changes: 19 additions & 19 deletions modules/renderer/compile-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ const assign = Object.assign;


/*
compileChildren(renderers, element, path, message)
compileChildren(renderers, element, path, message, options)
*/

function compileChildren(renderers, element, path, message = '') {
function compileChildren(renderers, element, path, message = '', options) {
// Children may mutate during compile, and we only want to compile
// current children
const children = Array.from(element.childNodes);
Expand Down Expand Up @@ -51,7 +51,7 @@ function compileChildren(renderers, element, path, message = '') {
}
}

compileNode(renderers, children[n], path, message);
compileNode(renderers, children[n], path, message, options);
}
}

Expand All @@ -60,24 +60,24 @@ function compileChildren(renderers, element, path, message = '') {


/*
compileAttributes(renderers, node, path, message)
compileAttributes(renderers, node, path, message, options)
*/

function compileAttributes(renderers, element, path, message = '') {
function compileAttributes(renderers, element, path, message = '', options) {
// Attributes may be removed during parsing so copy the list before looping
const attributes = Array.from(element.attributes);
let n = -1, attribute;

while (attribute = attributes[++n]) {
compileAttribute(renderers, element, attribute, path, message);
compileAttribute(renderers, element, attribute, path, message, options);
}

return renderers;
}


/*
compileElement(renderers, node, path, message)
compileElement(renderers, node, path, message, options)
*/

const compileElement = overload((renderers, element) => element.tagName.toLowerCase(), {
Expand All @@ -89,31 +89,31 @@ const compileElement = overload((renderers, element) => element.tagName.toLowerC
// Do not parse the inner DOM of scripts
'script': compileAttributes,

'textarea': (renderers, element, path, message) => {
'textarea': (renderers, element, path, message, options) => {
// A <textarea> does not have children, its textContent becomes its value
compileAttributes(renderers, element, path, message);
compileAttributes(renderers, element, path, message, options);
compileAttribute(renderers, element, {
localName: 'value',
value: element.textContent
}, path, message);
}, path, message, options);
element.textContent = '';
return renderers;
},

'default': (renderers, element, path, message) => {
'default': (renderers, element, path, message, options) => {
// Compiling children first means inner DOM to outer DOM, which allows
// `<select>`, for example, to pick up the correct option value. If we
// decide to change this order we should still make sure value attribute
// is rendered after children for this reason.
compileChildren(renderers, element, path, message);
compileAttributes(renderers, element, path, message);
compileChildren(renderers, element, path, message, options);
compileAttributes(renderers, element, path, message, options);
return renderers;
}
});


/**
compileNode(renderers, node, path, message)
compileNode(renderers, node, path, message, options)
**/

const compileNode = overload((renderers, node) => toType(node), {
Expand All @@ -122,28 +122,28 @@ const compileNode = overload((renderers, node) => toType(node), {
'document': compileChildren,
'fragment': compileChildren,

'element': (renderers, element, path, message = '') => {
compileElement(renderers, element, (path ? path + pathSeparator : '') + indexOf(element), message = '');
'element': (renderers, element, path, message = '', options) => {
compileElement(renderers, element, (path ? path + pathSeparator : '') + indexOf(element), message = '', options);
return renderers;
},

'text': (renderers, node, path, message = '') => {
'text': (renderers, node, path, message = '', options) => {
const string = node.nodeValue;
if (!isLiteralString(string)) {
return renderers;
}

const source = decode(string);
if (window.DEBUG) {
parent = node.parentElement || { tagName: 'template' };
const parent = node.parentElement || { tagName: 'template' };
message = truncate(64, '<'
+ parent.tagName.toLowerCase() + '>'
+ source.trim()
+ '</' + parent.tagName.toLowerCase() + '>')
+ ' (' + message + ')' ;
}

renderers.push(new TextRenderer(path, indexOf(node), source, message, node));
renderers.push(new TextRenderer(path, indexOf(node), source, message, options, node));
return renderers;
},

Expand Down
40 changes: 23 additions & 17 deletions modules/renderer/compile.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@

import compileFn from '../../../fn/modules/compile.js';
import { indent } from './constants.js';
import { log } from '../log.js';

import { indent } from './constants.js';

/**
compile(source, scope, parameters, message)
compile(source, scope, parameters, message, options)
Compiles a literal template string to a function.
(`options.sloppy = true` enables template rendering `with(data)`.)
**/

// Store render functions against their source
export const compiled = {};

// Last param, message, is for logging/throwing message
export default function compile(source, scope, parameters, message = '') {
// Hey hey, we are not in 'strict mode' inside compiled functions so we CAN
// use with(), handy for accessing template variables of `data`. A small
// caveat though: accessing a not-defined property via `with` does not
// appear to be reflected in the `data` proxy access records where the
// property is not defined. This is bad, because that property won't be
// registered for rerendering.
const code = '\n' + indent + 'with(data) { return this.compose`' + source + '`; }\n';
export default function compile(source, scope, parameters, message = '', options = {}) {
// Hey hey, we are not in 'strict mode' inside compiled functions by default
// so we CAN use with(), but let's make it opt-in for the moment at least.
// It's handy for accessing template variables of `data`. A small caveat
// though: accessing a not-defined property via `with` does not appear to be
// reflected in the `data` proxy access records where the property is not
// defined. This is bad, because that property won't be registered for
// rerendering. Also, arrays and other objects have their entire prototype
// chain exposed, so `map()`, `filter()` and stuff can be called on the
// array, which is just weird, and probably bad. If we are going to use
// sloppy mode it would probably be best to devise some way of enforcing the
// base data object to be a prototypeless object of some sort. Just a thought.
const code = '\n' +
indent + (options.sloppy ? 'with(data) {' : '"use strict";' ) + '\n' +
indent + 'return this.compose`' + source + '`;\n' +
(options.sloppy ? indent + '}\n' : '');

// Return cached fn
// Todo: factor in keys from scope and parameters to make this key truly
// unique to all same instances of compiled function
const key = code;
if (compiled[key]) { return compiled[key]; }
if (compiled[code]) { return compiled[code]; }

// The DEBUG logging version, removed in built files
// The DEBUG logging version (removed in built files)
if (window.DEBUG) {
try {
const t0 = window.performance.now();
Expand All @@ -42,7 +48,7 @@ export default function compile(source, scope, parameters, message = '') {
// Log this compile
log('compile', (t1 - t0).toPrecision(3) + 'ms – ' + message, undefined, undefined, 'yellow');

return compiled[key] = fn;
return compiled[code] = fn;
}
catch(e) {
// Append message to error message
Expand Down
4 changes: 2 additions & 2 deletions modules/renderer/renderer-attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const getDescriptor = Object.getOwnPropertyDescriptor;
const getPrototype = Object.getPrototypeOf;

/**
AttributeRenderer(path, name, source, element, message)
AttributeRenderer(path, name, source, message, options, element)
Constructs an object responsible for rendering to a plain text attribute.
**/

Expand Down Expand Up @@ -52,7 +52,7 @@ function setAttribute(node, name, property, writable, value) {
return 1;
}

export default function AttributeRenderer(path, name, source, message, element) {
export default function AttributeRenderer(path, name, source, message, options, element) {
Renderer.apply(this, arguments);
this.property = name in names ?
names[name] :
Expand Down
2 changes: 1 addition & 1 deletion modules/renderer/renderer-boolean.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function setBooleanProperty(node, name, property, writable, value) {
return 1;
}

export default function BooleanRenderer(path, name, source, message, element) {
export default function BooleanRenderer(path, name, source, message, options, element) {
AttributeRenderer.apply(this, arguments);
// Avoid boolean defaulting to true
element.removeAttribute(name);
Expand Down
2 changes: 1 addition & 1 deletion modules/renderer/renderer-checked.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function setChecked(element, value, hasValueAttribute) {
return 1;
}

export default function CheckedRenderer(path, name, source, message, element) {
export default function CheckedRenderer(path, name, source, message, options, element) {
AttributeRenderer.call(this, path, 'checked', source, message, element);
// Flag whether element has a value attribute
this.hasValue = isDefined(element.getAttribute('value'));
Expand Down
Loading

0 comments on commit 96b8b6a

Please sign in to comment.