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.
Files changed (55) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +206 -0
  3. package/TODO.md +140 -0
  4. package/index.html +188 -0
  5. package/logo.jpg +0 -0
  6. package/logo.svg +96 -0
  7. package/package.json +32 -0
  8. package/packages/galath/package.json +28 -0
  9. package/packages/galath/src/behavior.js +193 -0
  10. package/packages/galath/src/binding.js +247 -0
  11. package/packages/galath/src/boot.js +52 -0
  12. package/packages/galath/src/command.js +117 -0
  13. package/packages/galath/src/component.js +505 -0
  14. package/packages/galath/src/controller.js +181 -0
  15. package/packages/galath/src/core.js +190 -0
  16. package/packages/galath/src/imports.js +132 -0
  17. package/packages/galath/src/index.js +38 -0
  18. package/packages/galath/src/instance-model.js +343 -0
  19. package/packages/galath/src/morph.js +237 -0
  20. package/packages/galath/src/rendering.js +556 -0
  21. package/packages/galath/src/signals.js +215 -0
  22. package/packages/galath/src/templates.js +24 -0
  23. package/packages/galath/src/xml-events.js +53 -0
  24. package/packages/galath-css/css/bootstrap-icons.min.css +5 -0
  25. package/packages/galath-css/css/bootstrap.min.css +6 -0
  26. package/packages/galath-css/css/fonts/bootstrap-icons.json +2077 -0
  27. package/packages/galath-css/css/fonts/bootstrap-icons.woff +0 -0
  28. package/packages/galath-css/css/fonts/bootstrap-icons.woff2 +0 -0
  29. package/packages/galath-css/js/bootstrap.bundle.min.js +7 -0
  30. package/packages/galath-css/package.json +13 -0
  31. package/playground/app.xml +214 -0
  32. package/playground/chapters/01-welcome.xml +94 -0
  33. package/playground/chapters/02-signals.xml +166 -0
  34. package/playground/chapters/03-instance.xml +130 -0
  35. package/playground/chapters/04-bindings.xml +156 -0
  36. package/playground/chapters/05-lists.xml +138 -0
  37. package/playground/chapters/06-commands.xml +144 -0
  38. package/playground/chapters/07-controller.xml +115 -0
  39. package/playground/chapters/08-events.xml +126 -0
  40. package/playground/chapters/09-behaviors.xml +210 -0
  41. package/playground/chapters/10-components.xml +152 -0
  42. package/playground/chapters/11-imports.xml +108 -0
  43. package/playground/chapters/12-expressions.xml +161 -0
  44. package/playground/chapters/13-paths.xml +197 -0
  45. package/playground/components/chapter-shell.xml +29 -0
  46. package/playground/components/highlighter.js +111 -0
  47. package/playground/components/run-snippet.js +120 -0
  48. package/public/basic/bootstrap-icons.min.css +5 -0
  49. package/public/basic/bootstrap.bundle.min.js +7 -0
  50. package/public/basic/bootstrap.min.css +6 -0
  51. package/public/basic/fonts/bootstrap-icons.json +2077 -0
  52. package/public/basic/fonts/bootstrap-icons.woff +0 -0
  53. package/public/basic/fonts/bootstrap-icons.woff2 +0 -0
  54. package/public/basic/theme.css +209 -0
  55. 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';