browser-module-runtime 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-module-runtime",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/html/index.js CHANGED
@@ -1,14 +1,8 @@
1
1
  /** Scoped HTML integration: autonomous rendering, explicit discovery, opt-in observation. */
2
2
  import {registerWebComponent} from '../web-components/index.js';
3
3
  import {getScopeRegistry,requestedScope,containsRoot} from './scopes.js';
4
+ import {parseObjectProps as parseObject} from './props.js';
4
5
 
5
- function parseObject(value,context){
6
- if(value==null||value==='')return {};
7
- let result;
8
- try{result=JSON.parse(value);}catch(e){throw new Error(`Invalid JSON in ${context}: ${e.message}`);}
9
- if(!result||typeof result!=='object'||Array.isArray(result))throw new Error(`${context} must be a JSON object`);
10
- return result;
11
- }
12
6
  function emit(el,type,detail){const win=el.ownerDocument.defaultView;el.dispatchEvent(new win.CustomEvent(type,{detail,bubbles:true,composed:true}));}
13
7
  function state(el,value){el.setAttribute('data-runtime-state',value);}
14
8
  function rootDocument(root){return root?.nodeType===9?root:root?.ownerDocument;}
@@ -0,0 +1,112 @@
1
+ /** Parse declarative object props without eval or executing JavaScript.
2
+ * Accepts JSON and a safe JS-literal subset (unquoted keys, single-quoted
3
+ * strings, trailing commas, nested arrays/objects). No expressions or code.
4
+ */
5
+ export function parseObjectProps(value, context = 'props') {
6
+ if (value == null || value.trim() === '') return {};
7
+ if (value.length > 100_000) throw new SyntaxError(`${context}: props literal is too large`);
8
+ let offset = 0;
9
+ const error = (message) => { throw new SyntaxError(`${context}: ${message} at character ${offset + 1}`); };
10
+ const peek = () => value[offset];
11
+ const space = () => { while (offset < value.length && /\s/.test(peek())) offset++; };
12
+ const forbidKey = (key) => {
13
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') error(`Forbidden object key ${key}`);
14
+ return key;
15
+ };
16
+ function str() {
17
+ const quote = value[offset++];
18
+ let result = '';
19
+ while (offset < value.length) {
20
+ let c = value[offset++];
21
+ if (c === quote) return result;
22
+ if (c === '\n' || c === '\r') error('Unterminated string');
23
+ if (c !== '\\') { result += c; continue; }
24
+ if (offset >= value.length) error('Unterminated string escape');
25
+ c = value[offset++];
26
+ const escapes = {n:'\n', r:'\r', t:'\t', b:'\b', f:'\f', v:'\v', '0':'\0'};
27
+ if (Object.hasOwn(escapes, c)) { result += escapes[c]; continue; }
28
+ if (c === 'u' || c === 'x') {
29
+ const count = c === 'u' ? 4 : 2;
30
+ const hex = value.slice(offset, offset + count);
31
+ if (!new RegExp(`^[a-fA-F0-9]{${count}}$`).test(hex)) error('Invalid hex string escape');
32
+ result += String.fromCharCode(parseInt(hex, 16)); offset += count; continue;
33
+ }
34
+ if ('\\\'"/'.includes(c)) { result += c; continue; }
35
+ error(`Unsupported string escape \\${c}`);
36
+ }
37
+ error('Unterminated string');
38
+ }
39
+ function readValue(depth = 0) {
40
+ if (depth > 40) error('Props nesting is too deep');
41
+ space();
42
+ const c = peek();
43
+ if (c === '{') return object(depth + 1);
44
+ if (c === '[') return array(depth + 1);
45
+ if (c === '"' || c === "'") return str();
46
+ const next = value.slice(offset);
47
+ const number = next.match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
48
+ if (number) {
49
+ offset += number[0].length;
50
+ const n = Number(number[0]);
51
+ if (!Number.isFinite(n)) error('Numbers must be finite');
52
+ return n;
53
+ }
54
+ for (const [word, literal] of [['true',true],['false',false],['null',null]]) {
55
+ if (next.startsWith(word) && !/[\w$]/.test(next[word.length] ?? '')) {
56
+ offset += word.length; return literal;
57
+ }
58
+ }
59
+ error('Expected a literal value (no expressions or functions)');
60
+ }
61
+ function key() {
62
+ space();
63
+ if (peek() === '"' || peek() === "'") return forbidKey(str());
64
+ const token = value.slice(offset).match(/^[A-Za-z_$][A-Za-z0-9_$]*/);
65
+ if (!token) error('Expected an object key');
66
+ offset += token[0].length;
67
+ return forbidKey(token[0]);
68
+ }
69
+ function object(depth) {
70
+ offset++; space();
71
+ const result = {};
72
+ if (peek() === '}') { offset++; return result; }
73
+ while (offset < value.length) {
74
+ const prop = key();space();
75
+ if (value[offset++] !== ':') error('Expected : after an object key');
76
+ const val = readValue(depth);
77
+ if (Object.hasOwn(result, prop)) error(`Duplicate object key ${prop}`);
78
+ result[prop] = val;
79
+ space();
80
+ const delimiter = value[offset++];
81
+ if (delimiter === '}') return result;
82
+ if (delimiter !== ',') error('Expected , or }');
83
+ space();
84
+ if (peek() === '}') { offset++; return result; }
85
+ }
86
+ error('Unclosed object');
87
+ }
88
+ function array(depth) {
89
+ offset++; space();
90
+ const result = [];
91
+ if (peek() === ']') { offset++; return result; }
92
+ while (offset < value.length) {
93
+ result.push(readValue(depth));space();
94
+ const delimiter = value[offset++];
95
+ if (delimiter === ']') return result;
96
+ if (delimiter !== ',') error('Expected , or ]');
97
+ space();
98
+ if (peek() === ']') { offset++; return result; }
99
+ }
100
+ error('Unclosed array');
101
+ }
102
+ space();
103
+ if (peek() !== '{') {
104
+ // Preserve the old object-only error wording for callers relying on it.
105
+ try { const json = JSON.parse(value); if (json === null || typeof json !== 'object' || Array.isArray(json)) throw new Error(`${context} must be a JSON object or safe object literal`); }
106
+ catch (e) { if (e.message.includes('must be a JSON object')) throw e; }
107
+ error('Props must be a JSON object or safe object literal');
108
+ }
109
+ const result = readValue();space();
110
+ if (offset !== value.length) error('Unexpected trailing content');
111
+ return result;
112
+ }