ludic/selfhost/frontend/component_class.ludic
Orkuncakilkaya 20d011f8e9 feat(lang): UI components as files - component Name { ... } beside Name.xml and Name.lss
The compiler turns a component declaration into:
- a record of its props and state;
- a constructor, a props setter, a model and a call;
- a class, registered with ludic.ui at start.

Its template and styles are read from beside it and compiled in, with @import inlined, and a
missing template fails the build.

In the runtime:
- each mounted instance keeps its own props and state, and `set` in a template writes the state;
- styles are scoped to the component;
- class, style and id on a component's tag land on its root, styled by the parent's sheet too;
- ui_reload re-reads a component's files from disk and keeps instances.

The package's own module is now ludic_ui, so a program may call a directory of its own ui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:50:12 +03:00

78 lines
3.3 KiB
Text

# component_class.ludic — L11: a component's class: its name, its template and styles as read from
# beside it (and where they are, for a developer's build to read again), and its functions; the
# registration is a global, so every component is known before the program's first frame
function cm_class(c: int, px: pointer, head: Node) -> void {
let name = g_cm_name[c]
let dir = dir_of(g_cm_file[c])
let xml_path = join_path(dir, name + ".xml")
let lss_path = join_path(dir, name + ".lss")
let xml = read_file(xml_path)
if xml == null { cm_err(c, `there is no {name}.xml beside it - a component's template is NAME.xml in its directory`) }
var lss = read_file(lss_path)
var lss_at = lss_path
if lss == null {
lss = ""
lss_at = ""
} else { lss = cm_lss_inline(c, lss, dir, 0) }
var src = `function {px}class() -> UiClass {{\n let k = new UiClass\n k.name = "{name}"\n`
src = src + ` k.xml_path = "{xml_path}"\n k.xml = {px}xml_text()\n k.lss_path = "{lss_at}"\n k.lss = {px}lss_text()\n`
src = src + ` k.make = fn {px}new\n k.props = fn {px}props\n k.model = fn {px}model\n k.call = fn {px}call\n return k\n}}\n`
vw_parse(src, g_cm_file[c], g_cm_line[c], head.vis)
cm_text_fn(c, px + "xml_text", xml, head)
cm_text_fn(c, px + "lss_text", lss, head)
let reg = node(E_CALL)
let callee = node(E_ID)
callee.s = "ui_register"
reg.a = callee
let arg = node(E_CALL)
let make = node(E_ID)
make.s = px + "class"
arg.a = make
arg.kids = new []Node
reg.kids = new []Node
push(reg.kids, arg)
head.a = reg
}
# a function answering a file's text, carried in the program as it was read
function cm_text_fn(c: int, name: pointer, text: pointer, head: Node) -> void {
let at = len(prog)
vw_parse(`function {name}() -> string {{\n return ""\n}}\n`, g_cm_file[c], g_cm_line[c], head.vis)
prog[at].a.kids[0].a.s = text
}
# a stylesheet with each `@import "x.lss";` replaced by that file (beside it, or from the project's
# root when it starts with /), so the program carries its whole look
function cm_lss_inline(c: int, text: pointer, dir: pointer, depth: int) -> pointer {
if depth > 8 { cm_err(c, "its stylesheets @import each other in a circle") }
var out = ""
var i = 0
let n = len(text)
while i < n {
var j = i
while j < n and text[j] != '\n' { j += 1 }
let line: pointer = text[i..j]
let path = cm_import_of(line)
if path == null { out = out + line + "\n" } else {
var at = join_path(dir, path)
if path[0] == '/' { at = path[1..len(path)] }
let inner = read_file(at)
if inner == null { cm_err(c, `its stylesheet imports {path}, which is not there`) }
out = out + cm_lss_inline(c, inner, dir_of(at), depth + 1) + "\n"
}
i = j + 1
}
return out
}
# the path in `@import "x";` (or with single quotes), or null for any other line
function cm_import_of(line: pointer) -> pointer {
var i = 0
while i < len(line) and (line[i] == ' ' or line[i] == '\t') { i += 1 }
if i + 7 > len(line) { return null }
if not (line[i..i + 7] == "@import") { return null }
var a = i + 7
while a < len(line) and line[a] != '"' and line[a] != '\'' { a += 1 }
if a >= len(line) { return null }
let q = line[a]
var b = a + 1
while b < len(line) and line[b] != q { b += 1 }
return line[a + 1..b]
}