galath 1.0.2
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/AGENTS.md +1 -0
- package/README.md +206 -0
- package/TODO.md +140 -0
- package/index.html +188 -0
- package/logo.jpg +0 -0
- package/logo.svg +96 -0
- package/package.json +32 -0
- package/packages/galath/package.json +28 -0
- package/packages/galath/src/behavior.js +193 -0
- package/packages/galath/src/binding.js +247 -0
- package/packages/galath/src/boot.js +52 -0
- package/packages/galath/src/command.js +117 -0
- package/packages/galath/src/component.js +505 -0
- package/packages/galath/src/controller.js +181 -0
- package/packages/galath/src/core.js +190 -0
- package/packages/galath/src/imports.js +132 -0
- package/packages/galath/src/index.js +38 -0
- package/packages/galath/src/instance-model.js +343 -0
- package/packages/galath/src/morph.js +237 -0
- package/packages/galath/src/rendering.js +556 -0
- package/packages/galath/src/signals.js +215 -0
- package/packages/galath/src/templates.js +24 -0
- package/packages/galath/src/xml-events.js +53 -0
- package/packages/galath-css/css/bootstrap-icons.min.css +5 -0
- package/packages/galath-css/css/bootstrap.min.css +6 -0
- package/packages/galath-css/css/fonts/bootstrap-icons.json +2077 -0
- package/packages/galath-css/css/fonts/bootstrap-icons.woff +0 -0
- package/packages/galath-css/css/fonts/bootstrap-icons.woff2 +0 -0
- package/packages/galath-css/js/bootstrap.bundle.min.js +7 -0
- package/packages/galath-css/package.json +13 -0
- package/playground/app.xml +214 -0
- package/playground/chapters/01-welcome.xml +94 -0
- package/playground/chapters/02-signals.xml +166 -0
- package/playground/chapters/03-instance.xml +130 -0
- package/playground/chapters/04-bindings.xml +156 -0
- package/playground/chapters/05-lists.xml +138 -0
- package/playground/chapters/06-commands.xml +144 -0
- package/playground/chapters/07-controller.xml +115 -0
- package/playground/chapters/08-events.xml +126 -0
- package/playground/chapters/09-behaviors.xml +210 -0
- package/playground/chapters/10-components.xml +152 -0
- package/playground/chapters/11-imports.xml +108 -0
- package/playground/chapters/12-expressions.xml +161 -0
- package/playground/chapters/13-paths.xml +197 -0
- package/playground/components/chapter-shell.xml +29 -0
- package/playground/components/highlighter.js +111 -0
- package/playground/components/run-snippet.js +120 -0
- package/public/basic/bootstrap-icons.min.css +5 -0
- package/public/basic/bootstrap.bundle.min.js +7 -0
- package/public/basic/bootstrap.min.css +6 -0
- package/public/basic/fonts/bootstrap-icons.json +2077 -0
- package/public/basic/fonts/bootstrap-icons.woff +0 -0
- package/public/basic/fonts/bootstrap-icons.woff2 +0 -0
- package/public/basic/theme.css +209 -0
- package/seed.html +321 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// core.js
|
|
3
|
+
//
|
|
4
|
+
// Defines the language *kernel*: a tiny dependency-injection container that
|
|
5
|
+
// "features" plug into. Each feature attaches its own functions and classes
|
|
6
|
+
// onto the language object. We pick the order at boot time, so a downstream
|
|
7
|
+
// project can swap, omit, or extend features without forking the runtime.
|
|
8
|
+
//
|
|
9
|
+
// This file deliberately stays small. It is the only thing that knows
|
|
10
|
+
// nothing about XML, signals, components, or DOM. Everything else is a
|
|
11
|
+
// feature plugged in via `.use(...)`.
|
|
12
|
+
//
|
|
13
|
+
// Public exports:
|
|
14
|
+
//
|
|
15
|
+
// createLanguage({ source, mount }) - Construct an empty language.
|
|
16
|
+
// coreFeature(language) - Wires up XML parsing + small helpers.
|
|
17
|
+
//
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a brand-new Galath language container.
|
|
22
|
+
*
|
|
23
|
+
* Because each call returns a fresh object (no shared state), you can run
|
|
24
|
+
* multiple Galath apps on the same page if you want.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} options
|
|
27
|
+
* @param {string} options.source - The Galath XML source as a string.
|
|
28
|
+
* @param {Element} options.mount - DOM element to mount the <application> into.
|
|
29
|
+
* @returns {object} the language object.
|
|
30
|
+
*/
|
|
31
|
+
export function createLanguage({ source, mount }) {
|
|
32
|
+
return {
|
|
33
|
+
// Raw inputs - features may rewrite `source` (e.g. when imports inline
|
|
34
|
+
// additional XML) before it is finally parsed.
|
|
35
|
+
source,
|
|
36
|
+
mount,
|
|
37
|
+
// Resolved DOM document and root after `parseSource()` runs.
|
|
38
|
+
document: null,
|
|
39
|
+
root: null,
|
|
40
|
+
// Names of applied features, useful for debugging / introspection.
|
|
41
|
+
features: [],
|
|
42
|
+
// Custom components keyed by their tag name (e.g. "xes-feature-badge").
|
|
43
|
+
components: new Map(),
|
|
44
|
+
// Behaviors keyed by short name (e.g. "copy", "drag", "drop").
|
|
45
|
+
behaviors: new Map(),
|
|
46
|
+
// Tests collected during feature install. Run on `start()` for sanity.
|
|
47
|
+
tests: [],
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Apply a feature plugin.
|
|
51
|
+
*
|
|
52
|
+
* A feature is a function that receives the language object and adds
|
|
53
|
+
* methods/state onto it. We intentionally do not use classes here -
|
|
54
|
+
* mutation by feature keeps the surface area readable for new readers.
|
|
55
|
+
*/
|
|
56
|
+
use(feature) {
|
|
57
|
+
this.features.push(feature.name || 'anonymousFeature');
|
|
58
|
+
feature(this);
|
|
59
|
+
return this;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a self-test. Tests are run during `start()` so a broken
|
|
64
|
+
* feature fails loudly rather than silently corrupting later runs.
|
|
65
|
+
*/
|
|
66
|
+
test(name, fn) {
|
|
67
|
+
this.tests.push({ name, fn });
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute all collected tests. Failed tests print to console.error but
|
|
72
|
+
* do NOT throw - rendering should still proceed so users can see any
|
|
73
|
+
* UI even when a self-test regressed.
|
|
74
|
+
*/
|
|
75
|
+
runTests() {
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (const t of this.tests) {
|
|
78
|
+
try {
|
|
79
|
+
t.fn();
|
|
80
|
+
rows.push({ test: t.name, ok: true });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
rows.push({ test: t.name, ok: false });
|
|
83
|
+
console.error(`[galath test failed] ${t.name}`, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// console.table is a friendly summary in dev tools; no-op in stripped
|
|
87
|
+
// production builds.
|
|
88
|
+
if (typeof console.table === 'function') console.table(rows);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run the language: parse, resolve imports, register components, run
|
|
93
|
+
* tests, mount the application. This is async because import resolution
|
|
94
|
+
* may fetch additional XML files over the network.
|
|
95
|
+
*
|
|
96
|
+
* Order matters:
|
|
97
|
+
* 1. parseSource - the entry document becomes a DOM tree.
|
|
98
|
+
* 2. resolveImports - <import src="..."> nodes are replaced by the
|
|
99
|
+
* children of the fetched documents (recursive,
|
|
100
|
+
* deduped by absolute URL).
|
|
101
|
+
* 3. registerComponents - now all <component> elements are present.
|
|
102
|
+
* 4. runTests / mountApplication - sanity + render.
|
|
103
|
+
*/
|
|
104
|
+
async start() {
|
|
105
|
+
this.parseSource();
|
|
106
|
+
if (typeof this.resolveImports === 'function') await this.resolveImports();
|
|
107
|
+
this.registerComponents();
|
|
108
|
+
this.runTests();
|
|
109
|
+
this.mountApplication();
|
|
110
|
+
return this;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The CORE feature: XML parsing + tiny utilities every other feature needs.
|
|
117
|
+
*
|
|
118
|
+
* Other features can rely on:
|
|
119
|
+
*
|
|
120
|
+
* - `language.parseSource()` populates `language.document` / `language.root`.
|
|
121
|
+
* - `language.childElements(parent, name?)` returns element children.
|
|
122
|
+
* - `language.firstChildElement(parent, name)` is a shortcut.
|
|
123
|
+
* - `language.serialize(node)` emits XML text.
|
|
124
|
+
* - `language.uid()` generates a short, monotonic-ish unique id.
|
|
125
|
+
*/
|
|
126
|
+
export function coreFeature(language) {
|
|
127
|
+
language.parseSource = () => {
|
|
128
|
+
// DOMParser is the platform's XML parser. It returns a Document with a
|
|
129
|
+
// <parsererror> element when input is malformed; we surface the error
|
|
130
|
+
// text so users see exactly what went wrong, with line/column info.
|
|
131
|
+
const parser = new DOMParser();
|
|
132
|
+
const doc = parser.parseFromString(language.source, 'application/xml');
|
|
133
|
+
const err = doc.querySelector('parsererror');
|
|
134
|
+
if (err) throw new Error(err.textContent.trim());
|
|
135
|
+
language.document = doc;
|
|
136
|
+
language.root = doc.documentElement;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Like `parent.children` but optionally filtered by local or qualified tag
|
|
141
|
+
* name. This handles both `<gal:component>` lookups by local name and
|
|
142
|
+
* lifecycle hooks like `<on:mount>` that are addressed by qualified name.
|
|
143
|
+
*/
|
|
144
|
+
language.childElements = (parent, name = null) =>
|
|
145
|
+
[...parent.children].filter(
|
|
146
|
+
el => !name || el.localName === name || el.nodeName === name,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
/** Convenience: first child matching `name`, or null. */
|
|
150
|
+
language.firstChildElement = (parent, name) =>
|
|
151
|
+
language.childElements(parent, name)[0] ?? null;
|
|
152
|
+
|
|
153
|
+
/** Re-serialize a parsed XML node back to a string (for echo/debug views). */
|
|
154
|
+
language.serialize = node => new XMLSerializer().serializeToString(node);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Short, unique, mostly-monotonic id. Used as a default for newly inserted
|
|
158
|
+
* nodes so XPath predicates like `[@id=...]` work without manual ids.
|
|
159
|
+
*/
|
|
160
|
+
language.uid = () =>
|
|
161
|
+
`n${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
162
|
+
|
|
163
|
+
// -- self-tests -------------------------------------------------------------
|
|
164
|
+
language.test('core: source parses as well-formed XML', () => {
|
|
165
|
+
const parser = new DOMParser();
|
|
166
|
+
const doc = parser.parseFromString(language.source, 'application/xml');
|
|
167
|
+
assert(!doc.querySelector('parsererror'), 'source is not well-formed XML');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
language.test('core: childElements matches qualified namespaced tags', () => {
|
|
171
|
+
const parser = new DOMParser();
|
|
172
|
+
const doc = parser.parseFromString(
|
|
173
|
+
'<root xmlns:on="urn:galath:on"><on:mount /></root>',
|
|
174
|
+
'application/xml',
|
|
175
|
+
);
|
|
176
|
+
assert(
|
|
177
|
+
language.childElements(doc.documentElement, 'on:mount').length === 1,
|
|
178
|
+
'qualified namespaced element was not matched',
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Tiny assertion helper. Re-exported so feature tests don't have to ship
|
|
185
|
+
* their own. We don't import a test framework - keeping galath
|
|
186
|
+
* dependency-free is part of the design.
|
|
187
|
+
*/
|
|
188
|
+
export function assert(condition, message = 'assertion failed') {
|
|
189
|
+
if (!condition) throw new Error(message);
|
|
190
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// imports.js
|
|
3
|
+
//
|
|
4
|
+
// `<import src="./foo.xml" />` - the XML modularity primitive.
|
|
5
|
+
//
|
|
6
|
+
// Why this matters:
|
|
7
|
+
// * One huge document is unreadable. We let authors split a Galath app
|
|
8
|
+
// across files the same way ES modules let you split a JS app.
|
|
9
|
+
// * Components live alongside their styles and their data, in their own
|
|
10
|
+
// files, in their own folders.
|
|
11
|
+
// * The playground depends on this: each tutorial chapter is a separate
|
|
12
|
+
// XML file imported into the shell.
|
|
13
|
+
//
|
|
14
|
+
// How it works:
|
|
15
|
+
//
|
|
16
|
+
// 1. Walk every `<import src="...">` in the document, top-down.
|
|
17
|
+
// 2. Fetch the URL relative to either the importing document's URL (for
|
|
18
|
+
// nested imports) or `document.baseURI` (for the entry source).
|
|
19
|
+
// 3. Parse the fetched XML.
|
|
20
|
+
// 4. The fetched document's *root* may be either:
|
|
21
|
+
// - `<galath>` - a root with multiple children, all of which are
|
|
22
|
+
// spliced in at the import point;
|
|
23
|
+
// - or any single element, which is also spliced in.
|
|
24
|
+
// 5. Replace the `<import>` element with those nodes.
|
|
25
|
+
// 6. Recurse into the freshly inlined nodes (their imports are still to
|
|
26
|
+
// be resolved). Cycles are caught by URL: if a URL is already on the
|
|
27
|
+
// resolution stack, skip the re-import.
|
|
28
|
+
//
|
|
29
|
+
// The fetch is gated on a Map keyed by absolute URL. Repeated imports of
|
|
30
|
+
// the same file (e.g. a shared header component) reuse the parsed source.
|
|
31
|
+
//
|
|
32
|
+
// Errors during fetch produce a clear console error AND inject a small
|
|
33
|
+
// `<parseerror>` element so the page surfaces the failure visibly rather
|
|
34
|
+
// than silently dropping content.
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
export function importFeature(language) {
|
|
38
|
+
// url -> Promise<DocumentFragment-like Element[]>. Cached so duplicate
|
|
39
|
+
// imports re-use one network round-trip.
|
|
40
|
+
const cache = new Map();
|
|
41
|
+
|
|
42
|
+
language.resolveImports = async () => {
|
|
43
|
+
if (!language.document) return;
|
|
44
|
+
// Track the resolution stack to break cycles. Keys are absolute URLs.
|
|
45
|
+
const stack = new Set();
|
|
46
|
+
await resolveContainer(language.document.documentElement, document.baseURI, stack);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Recursively resolve `<import>` elements that are direct or transitive
|
|
51
|
+
* children of `container`. We snapshot the list of imports first because
|
|
52
|
+
* we mutate the DOM as we go.
|
|
53
|
+
*
|
|
54
|
+
* `baseUrl` is the URL relative to which `<import src>` is resolved -
|
|
55
|
+
* typically the URL of the document `container` came from.
|
|
56
|
+
*/
|
|
57
|
+
async function resolveContainer(container, baseUrl, stack) {
|
|
58
|
+
// Snapshot of all imports anywhere in the subtree, in document order.
|
|
59
|
+
// We process them sequentially so error handling is straightforward.
|
|
60
|
+
const imports = [...container.querySelectorAll('import[src]')];
|
|
61
|
+
for (const importEl of imports) {
|
|
62
|
+
// Skip imports nested inside CDATA/code samples - they live in text,
|
|
63
|
+
// not in real DOM nodes, so they wouldn't be caught here anyway.
|
|
64
|
+
// Skip imports that are themselves still in the document but were
|
|
65
|
+
// already replaced as part of resolving an outer import.
|
|
66
|
+
if (!importEl.isConnected || !importEl.parentNode) continue;
|
|
67
|
+
const src = importEl.getAttribute('src');
|
|
68
|
+
if (!src) continue;
|
|
69
|
+
const url = new URL(src, baseUrl).toString();
|
|
70
|
+
|
|
71
|
+
// Cycle? Replace with an empty marker so future passes ignore it.
|
|
72
|
+
if (stack.has(url)) {
|
|
73
|
+
console.warn(`[galath import] cycle detected, skipping ${url}`);
|
|
74
|
+
importEl.replaceWith(...[]);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const nodes = await loadImport(url);
|
|
80
|
+
// Recurse into the fetched fragment first - so its own <import>s
|
|
81
|
+
// resolve against its URL, not ours.
|
|
82
|
+
for (const node of nodes) {
|
|
83
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
84
|
+
stack.add(url);
|
|
85
|
+
await resolveContainer(node, url, stack);
|
|
86
|
+
stack.delete(url);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Splice the resolved nodes in place of the <import> element.
|
|
90
|
+
// We import them into the host document so namespaces are preserved.
|
|
91
|
+
const owner = importEl.ownerDocument;
|
|
92
|
+
const adopted = nodes.map(n => owner.importNode(n, true));
|
|
93
|
+
importEl.replaceWith(...adopted);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`[galath import] failed to load ${url}:`, error);
|
|
96
|
+
const errorEl = importEl.ownerDocument.createElement('parseerror');
|
|
97
|
+
errorEl.textContent = `Failed to import ${url}: ${error.message}`;
|
|
98
|
+
importEl.replaceWith(errorEl);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Fetch and parse an XML import. Returns the children of the fetched
|
|
105
|
+
* document's root element (or `[root]` when the root is not a wrapper
|
|
106
|
+
* like `<galath>` / `<fragment>`).
|
|
107
|
+
*
|
|
108
|
+
* Cached by URL.
|
|
109
|
+
*/
|
|
110
|
+
function loadImport(url) {
|
|
111
|
+
if (cache.has(url)) return cache.get(url);
|
|
112
|
+
const promise = (async () => {
|
|
113
|
+
const response = await fetch(url);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
116
|
+
}
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
const doc = new DOMParser().parseFromString(text, 'application/xml');
|
|
119
|
+
const err = doc.querySelector('parsererror');
|
|
120
|
+
if (err) throw new Error(err.textContent.trim());
|
|
121
|
+
const root = doc.documentElement;
|
|
122
|
+
// Wrapper roots - their *children* are what we splice. Otherwise
|
|
123
|
+
// splice the root itself.
|
|
124
|
+
const wrapperNames = new Set(['galath', 'fragment', 'xes']);
|
|
125
|
+
return wrapperNames.has(root.localName)
|
|
126
|
+
? [...root.children]
|
|
127
|
+
: [root];
|
|
128
|
+
})();
|
|
129
|
+
cache.set(url, promise);
|
|
130
|
+
return promise;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// index.js
|
|
3
|
+
//
|
|
4
|
+
// Public surface of the Galath runtime.
|
|
5
|
+
//
|
|
6
|
+
// Two ways to use the library:
|
|
7
|
+
//
|
|
8
|
+
// 1. Compose your own:
|
|
9
|
+
//
|
|
10
|
+
// import { createLanguage, coreFeature, ... } from 'galath';
|
|
11
|
+
// const lang = createLanguage({ source, mount })
|
|
12
|
+
// .use(coreFeature)
|
|
13
|
+
// .use(...other features...)
|
|
14
|
+
// await lang.start();
|
|
15
|
+
//
|
|
16
|
+
// 2. Boot with all features (the 99% case):
|
|
17
|
+
//
|
|
18
|
+
// import { boot } from 'galath';
|
|
19
|
+
// await boot({ source, mount });
|
|
20
|
+
//
|
|
21
|
+
// Re-exporting individual features lets advanced users swap one out (e.g. a
|
|
22
|
+
// custom rendering layer) without forking the package.
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
export { createLanguage, coreFeature, assert } from './core.js';
|
|
26
|
+
export { signalsAndScopesFeature } from './signals.js';
|
|
27
|
+
export { instanceModelFeature } from './instance-model.js';
|
|
28
|
+
export { bindingFeature } from './binding.js';
|
|
29
|
+
export { commandFeature } from './command.js';
|
|
30
|
+
export { controllerFeature } from './controller.js';
|
|
31
|
+
export { templateItemsFeature } from './templates.js';
|
|
32
|
+
export { behaviorFeature } from './behavior.js';
|
|
33
|
+
export { xmlEventsFeature } from './xml-events.js';
|
|
34
|
+
export { importFeature } from './imports.js';
|
|
35
|
+
export { componentFeature } from './component.js';
|
|
36
|
+
export { renderingFeature } from './rendering.js';
|
|
37
|
+
export { morph } from './morph.js';
|
|
38
|
+
export { boot } from './boot.js';
|