gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit20e09d8920e09d89Merge t18 (tracker#18 delta sync) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs, both sides kept; pageShowOf summary = summaryOfmre20e09d89/plugins/web/compile.hl

50.1 KB

  1. // plugins/web/compile.hl — THE FRAMEWORK COMPILES EACH COMPONENT.
  2. //
  3. // Mission 313 (the creator, 15 Sep 14:30: "clientside interpreter + table against
  4. // component code per component, no tables needed"). The old hl:web ships ONE generic client
  5. // that walks a tree the seed carries: it pairs elements with nodes, asks a table which
  6. // members each element shows, asks `view.value` what each bound spot holds, and does it
  7. // again on every paint. hl:web ships the ANSWERS instead — this file writes each
  8. // component's own code, once, where the old hl:web would have walked.
  9. //
  10. // WHAT IS EMITTED, per component, beside the class `hlJs` compiled:
  11. //
  12. // v(c,m,o,host,i,rows,rk,fill,cr,opt) the component's View, unrolled: one
  13. // statement per element, per handler, per
  14. // bound spot. `cr` false CLAIMS the server's
  15. // DOM (take host.children[i], step on), `cr`
  16. // true CREATES it — the two walks the old hl:web
  17. // spells out separately, which differ only in
  18. // where an element comes from.
  19. // b["<for site>"] one `for` body — the row factory the keyed
  20. // region builds a row with.
  21. // t/e["<if site>"] an `if`'s two branches.
  22. // f["<reference site>"] the fragment this component writes into a
  23. // child's `slot` — built in THIS frame, which
  24. // is why it is this component's code.
  25. // p["<element site>"] the paint statements: `p` writes what one
  26. // member feeds (the old hl:web's paint(&m,s,name)),
  27. // `r` rewrites a row's element where the DOM
  28. // does not already hold it (rowPaint).
  29. //
  30. // The generated code calls a FIXED runtime (client.hl) for everything that is not this
  31. // component: the mount records, the keyed region diff, the binding edges, the
  32. // derivations, the socket, navigation. It never looks anything up in a table — the
  33. // names, the sites and the write sets are written into the statements.
  34. //
  35. // It is TEXT the framework produces, served from memory beside the module (WebFramework
  36. // `module`), never a JavaScript file under plugins/.
  37. import { newline, tab } from './view.hl'
  38. import HlwView from './view.hl'
  39. // ---- the emitted half --------------------------------------------------------------
  40. module(gen, key, nodes, fnTable) {
  41. fns = [] // the sub-walks: row bodies, if branches, fills
  42. done = {}
  43. rowVars = []
  44. tbl = tableOf(gen, key) // what the language says about this file, read once
  45. regions = regionIndex(tbl) // a `for`'s list reads, an `if`'s condition reads
  46. // A SHELL'S VIEW IS A `body` ELEMENT and the document already has one: the walk runs
  47. // against document.body itself, so the root list is that element's children and the
  48. // element only takes the marks a region rebuild on it would need.
  49. let roots = nodes
  50. let bodySite = null
  51. if (nodes.length == 1 && nodes[0].k == 'el' && nodes[0].tag == 'body') {
  52. roots = nodes[0].children
  53. bodySite = nodes[0].site
  54. }
  55. let root = walkTop(key, roots)
  56. if (bodySite != null) {
  57. root = root.replace('let r=rows;', 'let r=rows;await c.markHost(m,host,' + str(bodySite) + ');')
  58. }
  59. let src = ';(function(){var g=(typeof globalThis!=="undefined"?globalThis:window);'
  60. src = src + 'var C=(g.__hlC=g.__hlC||{});'
  61. // the element a claim takes or a create makes — the one shape both walks share; a create
  62. // inside an svg (or an svg itself) is made in the SVG namespace, as the parser makes it
  63. // THE PRELUDE, one per module: the element a claim takes or a create makes, and the
  64. // two reads. A read written out in full — `(r["title"]!=null?r["title"]:inst["title"])`
  65. // — is 45 bytes and a component's View holds dozens of them; named, it is 16. The
  66. // rule is the same rule (a `for` row variable in scope wins over a member of the
  67. // same name, and a field path walks into it), spelled once.
  68. // A claim also moves the host's CURSOR — the last node the walk took there — which is
  69. // where a text leaf and a region's mark are looked for next (client.hl `leafNode`,
  70. // `markAt`).
  71. src = src + 'function A(cr,host,i,tag){if(cr){if(tag==="svg"||(host.namespaceURI==="http://www.w3.org/2000/svg"&&host.localName.toLowerCase()!=="foreignobject")){return document.createElementNS("http://www.w3.org/2000/svg",tag);}return document.createElement(tag);}var e=host.children[i];if(e!=null){host.__hlCur=e;}return e;}'
  72. src = src + 'function S(v){return v==null?"":""+v;}'
  73. src = src + 'function RV(r,q,n){return r[n]!=null?r[n]:q[n];}'
  74. src = src + 'function RF(r,q,n,p){var v=RV(r,q,n);for(var k=0;k<p.length;k++){if(v==null)return null;v=v[p[k]];}return v;}'
  75. src = src + 'var B={},T={},E={},F={},P={};'
  76. // ONE FUNCTION PER LIST, however many sites reach it: a nested walk is written once
  77. // and named — emitting it per site made the reference app's home page 215 KB of the
  78. // same code over and over.
  79. let wi = 0
  80. for (f of fns) {
  81. wi = wi + 1
  82. let name = 'w' + wi
  83. src = src + 'var ' + name + '=' + f.src + ';'
  84. for (site of f.sites) { src = src + f.slot + '[' + str(site) + ']=' + name + ';' }
  85. }
  86. let paints = []
  87. collectPaints(&paints, roots)
  88. for (e of paints) { src = src + 'P[' + str(e.site) + ']={p:' + e.paint + ',r:' + e.row + '};' }
  89. src = src + 'C[' + str(key) + ']={v:' + root + ',b:B,t:T,e:E,f:F,p:P'
  90. // THE ANSWERS A LOOKUP STILL NEEDS, and only those. A local event's write set stands
  91. // at its own listener and a region's names at its own region — written into the
  92. // call. These three are asked for by NAME at run time and cannot be: an inbound
  93. // frame names its event, a routine handed down names the member that holds it, and
  94. // a write set names the derivations that follow it.
  95. src = src + ',ev:' + eventMap() + ',mw:' + memberMap() + ',dv:' + derivList() + ',fn:' + fnMap(fnTable) + '};'
  96. return src + '})();' + newline
  97. }
  98. tbl = null
  99. regions = {}
  100. // WHAT A FUNCTION VALUE WRITES, by the site it was made at (tickets #98, #99): the
  101. // module announces a function's return with its site, and the browser repaints this
  102. // set — the members it writes, closed over the methods it calls and the events it
  103. // emits, the same closure a handler's set gets. A function that writes nothing has
  104. // no entry.
  105. fnMap(fns) {
  106. let bySite = {}
  107. if (fns != null) {
  108. for (f of fns) {
  109. let names = closeWrites(tbl, f.writes, f.reads)
  110. if (names.length > 0) { bySite[f.site] = names }
  111. }
  112. }
  113. return mapOf(bySite)
  114. }
  115. // what a DECLARED handler writes, by event: one entry per event name, unioned
  116. eventMap() {
  117. let byEvent = {}
  118. for (h of tbl.handlers) {
  119. if (!h.nested) {
  120. let have = byEvent[h.event] == null ? [] : byEvent[h.event]
  121. for (w of closeWrites(tbl, h.writes, h.reads)) { if (!have.includes(w)) { have.push(w) } }
  122. byEvent[h.event] = have
  123. }
  124. }
  125. return mapOf(byEvent)
  126. }
  127. // A MEMBER HOLDING A ROUTINE: what calling it writes is what its initializer writes
  128. memberMap() {
  129. let byName = {}
  130. for (m of tbl.members) {
  131. if (m.nodes != null) { byName[m.name] = memberNames(m.nodes.writes) }
  132. }
  133. return mapOf(byName)
  134. }
  135. // EVERY INITIALIZER, IN DECLARATION ORDER, with the members it reads — the whole of
  136. // what a derivation needs; the class carries the computation itself as `__derive__`.
  137. // `srv` (written only when true, same rule as `isStatic` on a read — every byte here
  138. // ships to the browser) marks a SERVER REFERENCE SITE (COMPONENTS.md §9.1/§9.3): an
  139. // initializer that calls an imported class (`import Store from './store.hl'`, then
  140. // `Store.chatsOf(…)` — `hlDenotes`'s own `kind: 'class'`, not a second resolution).
  141. // It is evaluated per request, on the SERVER, and its value ships into the page as
  142. // state; `client.hl`'s `moved()` must never call it locally — routger's `conversations
  143. // = … ? store.chatsOf(session.user.id) : []` reached `hl:time`, which ships no client
  144. // half, the moment a session sync (0b17f65b) made `session` move in the browser and
  145. // `moved()` re-ran every derivation that reads it. A fresh value for a server
  146. // reference site ships in `sync` instead (WebFramework.hl `serverSyncNames`).
  147. derivList() {
  148. let src = '['
  149. let first = true
  150. for (d of tbl.derivations) {
  151. if (!first) { src = src + ',' }
  152. first = false
  153. src = src + '{name:' + str(d.name) + ',site:' + str(d.site) + ',reads:' + list(memberNames(d.reads))
  154. if (d.serverSite) { src = src + ',srv:true' }
  155. src = src + '}'
  156. }
  157. return src + ']'
  158. }
  159. // does this read list (RAW — hlDenotes's own kind per name, before `memberNames`
  160. // narrows it to members) reach an imported class or a braced static of one — the two
  161. // forms COMPONENTS.md §9.1/§9.3 calls "a call on an imported class" (`import Store
  162. // from './store.hl'`, `Store.noteRows()`, OR the form the maintained demos actually
  163. // use, `import { postRow } from './store.hl'`, `postRow(id)`)? See `derivList` above.
  164. serverSite(reads) {
  165. if (reads == null) { return false }
  166. for (r of reads) { if (r.kind == 'class' || r.kind == 'import') { return true } }
  167. return false
  168. }
  169. mapOf(byName) {
  170. let src = '{'
  171. let first = true
  172. for (k of byName.keys()) {
  173. if (!first) { src = src + ',' }
  174. first = false
  175. src = src + str(k) + ':' + list(byName[k])
  176. }
  177. return src + '}'
  178. }
  179. // the write set and the read set of the handler at this site, closed over the methods
  180. // it calls and the handlers it emits to — computed here, written into the listener
  181. handlerSets(site) {
  182. for (h of tbl.handlers) {
  183. if (h.nested && h.site == site) {
  184. return list(closeWrites(tbl, h.writes, h.reads)) + ',' + list(closeReads(tbl, h.reads))
  185. }
  186. }
  187. return '[],[]'
  188. }
  189. regionNamesAt(site) {
  190. let names = regions[site]
  191. return list(names == null ? [] : names)
  192. }
  193. fns = []
  194. // ---- the walk, unrolled ------------------------------------------------------------
  195. // `top` marks the View's own root list: an element there is a COMPOSITION ROOT — a
  196. // reference above may have overwritten some of its events and put classes on it, which
  197. // is what `opt` carries (the old hl:web's rootOverwrites and withRootClasses).
  198. walk(key, nodes) {
  199. return plain(key, nodes)
  200. }
  201. // a list's walk
  202. plain(key, nodes) {
  203. let body = ''
  204. for (n of nodes) { body = body + node(key, n, false, false) }
  205. return 'async function(c,m,o,host,i,rows,rk,fill,cr,opt){let inst=m.instance;let r=rows;' + body + 'return i;}'
  206. }
  207. // the View's own root list, whose elements are composition roots
  208. walkTop(key, nodes) {
  209. let body = ''
  210. for (n of nodes) { body = body + node(key, n, true, false) }
  211. return 'async function(c,m,o,host,i,rows,rk,fill,cr,opt){let inst=m.instance;let r=rows;' + body + 'return i;}'
  212. }
  213. // a sub-walk, registered under its own site so the runtime can reach it by name
  214. // A SUB-WALK IS WRITTEN ONCE, however often the compile reaches its list.
  215. sub(key, nodes, slot, site) {
  216. if (done[slot + ' ' + site] != null) { return null }
  217. done[slot + ' ' + site] = true
  218. fns.push({ slot = slot; sites = [site]; src = walk(key, nodes); })
  219. return null
  220. }
  221. done = {}
  222. node(key, n, top, filterOns) {
  223. if (n.k == 'el') { return elNode(key, n, top) }
  224. if (n.k == 'text') { return 'if(cr){host.appendChild(document.createTextNode(' + str(n.text) + '));}else{await c.leafAt(host,false,i,null,' + str(n.text) + ');}' }
  225. if (n.k == 'on') {
  226. // A REFERENCE ABOVE MAY HAVE OVERWRITTEN THIS EVENT on the composition root:
  227. // the root's own handler for it is dropped and the reference's stands instead
  228. // (the old hl:web's withoutOns, which applies to the root element's direct children).
  229. let guard = filterOns ? 'if(!(opt&&opt.over&&opt.over.indexOf(' + str(n.event) + ')>=0)){' : '{'
  230. return guard + 'await c.onAt(m,host,' + str(n.event) + ',' + str(n.site) + ',rows,' + handlerSets(n.site) + ');}'
  231. }
  232. if (n.k == 'member' && n.name == 'slot') {
  233. // the SHELL's slot is where the route component hangs; a child's slot is the
  234. // fragment its host wrote, and the host's frame builds it
  235. return 'if(m.isShell){i=await c.slotShell(m,host,i,cr);}else if(fill!=null){i=await c.slotFill(m,o,host,i,fill,cr);}'
  236. }
  237. // A TEXT LEAF STANDING IN A LIST — an `if` branch, a `for` body — beside elements or
  238. // alone: its own text node, and a site of the member it reads (or of the row it
  239. // stands in), painted by rewriting that node.
  240. if (n.k == 'member' || n.k == 'field') {
  241. let names = (n.name == 'Style' || rowVars.includes(n.name)) ? [] : [n.name]
  242. return 'await c.textAt(m,host,cr,function(inst,r){return ' + textRead(n) + ';},' + list(names) + ',rows,rk);'
  243. }
  244. if (n.k == 'for') {
  245. // THE ROW VARIABLE IS IN SCOPE INSIDE THE BODY, and a read of it is the ROW's,
  246. // not a member of the instance — the language resolves it that way and the site
  247. // sets must say the same, or a member of the same name would paint the row.
  248. rowVars.push(n.row)
  249. sub(key, n.body, 'B', n.site)
  250. rowVars.pop()
  251. return 'i=await c.forAt(m,o,host,i,' + str(n.site) + ',' + str(n.row) + ',function(inst,r){return ' + rawRead(n.list) + ';},rows,rk,fill,cr,' + regionNamesAt(n.site) + ');'
  252. }
  253. if (n.k == 'if') {
  254. sub(key, n.then, 'T', n.site)
  255. sub(key, n.other, 'E', n.site)
  256. return 'i=await c.ifAt(m,o,host,i,' + str(n.site) + ',function(inst,r){return ' + rawRead(n.cond) + ';},rows,rk,fill,cr,' + regionNamesAt(n.site) + ');'
  257. }
  258. if (n.k == 'component') {
  259. if (n.fill != null && n.fill.length > 0) { sub(key, n.fill, 'F', n.site) }
  260. return 'i=await c.kidAt(m,host,i,' + kidSpec(n) + ',rows,rk,fill,cr,opt);'
  261. }
  262. return ''
  263. }
  264. // ---- an element --------------------------------------------------------------------
  265. elNode(key, n, top) {
  266. let src = '{let el=A(cr,host,i,' + str(n.tag) + ');if(!cr){i=i+1;}'
  267. src = src + 'if(el==null){await c.acqFail(' + str(n.tag) + ',host);}else{'
  268. // the attributes, on the walk that BUILDS the element (a claim takes the server's)
  269. src = src + 'if(cr){' + createAttrs(n) + (top ? 'await c.rootClasses(el,opt);' : '') + 'host.appendChild(el);}'
  270. src = src + 'el.__hlRows=rows;el.__hlSite=' + str(n.site) + ';el.__hlKey=' + str(key) + ';'
  271. // the element's own named entries, for the literal a handler on it fires
  272. src = src + 'el.__hlLit=function(inst,r){return ' + litNamed(n) + ';};'
  273. let names = siteNames(n)
  274. let valueMember = 'null'
  275. for (a of n.attrs) { if (a.member != null && a.name == 'value') { valueMember = str(a.member) } }
  276. let sited = ''
  277. if (names.length > 0 || valueMember != 'null') {
  278. sited = 'await c.siteAt(m,el,' + list(names) + ',' + str(n.site) + ',' + valueMember + ',' + (n.tag == 'select' ? 'true' : 'false') + ',rows);'
  279. }
  280. let kids = ''
  281. // AN ELEMENT THAT HOLDS ONLY TEXT is painted whole (`textContent`, see the paints):
  282. // built, it gets that text as one node; claimed, the server's is taken as it stands
  283. let whole = textOf(n)
  284. if (whole != null) {
  285. kids = 'if(cr){host.appendChild(document.createTextNode(' + whole + '));}'
  286. for (c of n.children) { if (c.k == 'on') { kids = kids + node(key, c, false, top) } }
  287. }
  288. else if (mixed(n)) { kids = mixedKids(key, n, top) }
  289. else { for (c of n.children) { kids = kids + node(key, c, false, top) } }
  290. let inner = '{let host=el;let i=0;' + kids + '}'
  291. // A SELECT IS SITED AFTER ITS OPTIONS: the member decides which option stands, and
  292. // the read-back that `siteAt` does reports nothing before they exist.
  293. if (n.tag == 'select') { src = src + inner + (selectValue(n) == 'null' ? '' : 'if(cr){await c.applySelectAt(m,el,' + selectValue(n) + ');}') + sited }
  294. else { src = src + sited + inner }
  295. return src + '}}'
  296. }
  297. // A TEXT LEAF BESIDE AN ELEMENT OR A REGION (ticket #86 follow-up): `td { d.n if (…) {…} }`
  298. // cannot be painted through `textContent`, which would wipe the branch. Each RUN of
  299. // adjacent leaves is one text node, held on the element (`__hlLeaves`, by the run's
  300. // index) where the walk builds or claims it, and the paints rewrite that node alone.
  301. mixed(n) {
  302. if (textOf(n) != null) { return false }
  303. for (c of n.children) { if ((c.k == 'member' || c.k == 'field') && c.name != 'slot') { return true } }
  304. return false
  305. }
  306. isLeaf(c) { return c.k == 'text' || ((c.k == 'member' || c.k == 'field') && c.name != 'slot') }
  307. // the runs of adjacent leaves, in order; an `on` renders nothing and splits none
  308. runsOf(n) {
  309. let out = []
  310. let run = []
  311. for (c of n.children) {
  312. if (isLeaf(c)) { run.push(c) }
  313. else if (c.k != 'on') {
  314. if (run.length > 0) { out.push(run) }
  315. run = []
  316. }
  317. }
  318. if (run.length > 0) { out.push(run) }
  319. return out
  320. }
  321. runText(run) {
  322. let src = '""'
  323. for (c of run) { src = src + '+(' + (c.k == 'text' ? str(c.text) : textRead(c)) + ')' }
  324. return src
  325. }
  326. runFeeds(run) {
  327. for (c of run) { if (c.k != 'text') { return true } }
  328. return false
  329. }
  330. mixedKids(key, n, top) {
  331. let kids = ''
  332. let run = []
  333. let k = 0
  334. for (c of n.children) {
  335. if (isLeaf(c)) { run.push(c) }
  336. else {
  337. if (c.k != 'on' && run.length > 0) { kids = kids + leafRun(run, k) k = k + 1 run = [] }
  338. kids = kids + node(key, c, false, top)
  339. }
  340. }
  341. if (run.length > 0) { kids = kids + leafRun(run, k) }
  342. return kids
  343. }
  344. // EVERY CALL INTO THE CLIENT IS AWAITED: its methods are async in the browser, and one
  345. // not awaited runs its DOM work after the walk has gone on — a built leaf then lands
  346. // after the elements that follow it (ticket #104).
  347. leafRun(run, k) {
  348. if (!runFeeds(run)) { return 'if(cr){host.appendChild(document.createTextNode(' + runText(run) + '));}else{await c.leafAt(host,false,i,null,' + runText(run) + ');}' }
  349. return 'await c.leafAt(host,cr,i,' + k + ',' + runText(run) + ');'
  350. }
  351. // the attributes an element is BUILT with — a bound one written in, a literal one as it
  352. // stands. `value` is a property once the page is live and an attribute as the control's
  353. // default; a textarea has no value attribute at all and a select's value is an option.
  354. createAttrs(n) {
  355. let src = ''
  356. for (a of n.attrs) {
  357. if (isBool(a)) {
  358. src = src + '{' + boolWrite(a, rawRead(a.member != null ? { k = 'member'; name = a.member; } : a.ref)) + '}'
  359. } else {
  360. let rd = a.member != null ? readOf({ k = 'member'; name = a.member; }) : (a.ref != null ? readOf(a.ref) : str(a.text))
  361. src = src + '{let v=' + rd + ';'
  362. if (a.extra != null) { src = src + 'v=v+" "+' + str(a.extra) + ';' }
  363. if (n.tag == 'select' && a.name == 'value') { src = src + '}' }
  364. else if (a.name == 'value') {
  365. src = src + 'if(el.value!=null){el.value=v;' + (n.tag == 'textarea' ? '' : 'el.setAttribute("value",v);') + '}else{el.setAttribute("value",v);}}'
  366. } else { src = src + 'el.setAttribute(' + str(a.name) + ',v);}' }
  367. }
  368. }
  369. if (boundValue(n)) { src = src + 'el.setAttribute("autocomplete","off");' }
  370. return src
  371. }
  372. // a BOUND boolean attribute — a literal one was decided at build (WebFramework `element`)
  373. isBool(a) {
  374. return (a.member != null || a.ref != null) && v.isBoolAttr(a.name)
  375. }
  376. // A BOUND BOOLEAN ATTRIBUTE, WRITTEN (ticket #33): set when the value is on, REMOVED when
  377. // it is off — the markup's rule (view.boolAttrs) — and the DOM property with it, because
  378. // once the page is live `checked` is the property and the attribute only its default.
  379. boolWrite(a, raw) {
  380. let prop = a.name == 'readonly' ? 'readOnly' : a.name
  381. return 'let on=!!' + raw + ';if(on){el.setAttribute(' + str(a.name) + ',"");}else if(el.removeAttribute){el.removeAttribute(' + str(a.name) + ');}if(' + str(prop) + ' in el&&el[' + str(prop) + ']!==on){el[' + str(prop) + ']=on;}'
  382. }
  383. // view.hl's `boundValue`, to the letter: a FORM CONTROL whose `value` reads a member or
  384. // a field, and not one the author gave an `autocomplete` of its own
  385. boundValue(n) {
  386. if (n.tag != 'input' && n.tag != 'textarea' && n.tag != 'select') { return false }
  387. let bound = false
  388. for (a of n.attrs) {
  389. if (a.name == 'autocomplete') { return false }
  390. if (a.name == 'value' && (a.member != null || a.ref != null)) { bound = true }
  391. }
  392. return bound
  393. }
  394. selectValue(n) {
  395. for (a of n.attrs) {
  396. if (a.name == 'value') {
  397. if (a.member != null) { return readOf({ k = 'member'; name = a.member; }) }
  398. if (a.ref != null) { return readOf(a.ref) }
  399. return str(a.text)
  400. }
  401. }
  402. return 'null'
  403. }
  404. // the element's own entries as the literal sees them — its attributes by name, then the
  405. // `for` row variables in scope (the row rides on the value)
  406. litNamed(n) {
  407. let src = '{'
  408. let first = true
  409. for (a of n.attrs) {
  410. if (!first) { src = src + ',' }
  411. first = false
  412. let v = a.member != null ? rawRead({ k = 'member'; name = a.member; }) : (a.ref != null ? rawRead(a.ref) : str(a.text))
  413. src = src + str(a.name) + ':' + v
  414. }
  415. return src + '}'
  416. }
  417. // the reference's own description, written at its use site: what it binds, what it
  418. // overwrites, the classes the host's `#Child` rule puts on it
  419. kidSpec(n) {
  420. let src = '{key:' + str(n.key) + ',kid:' + str(kidKeyOf(n)) + ',site:' + str(n.site) + ',fill:' + ((n.fill != null && n.fill.length > 0) ? 'true' : 'false')
  421. src = src + ',bindings:['
  422. let first = true
  423. for (b of n.bindings) {
  424. if (!first) { src = src + ',' }
  425. first = false
  426. src = src + '{name:' + str(b.name) + ',member:' + (b.member == null ? 'null' : str(b.member))
  427. src = src + ',ref:' + (b.ref == null ? 'null' : 'function(inst,r){return ' + rawRead(b.ref) + ';}')
  428. src = src + ',refName:' + (b.ref == null ? 'null' : str(b.ref.name))
  429. src = src + ',text:' + (b.text == null ? 'null' : 'true') + ',value:' + (b.text == null ? 'null' : refLiteral(b)) + '}'
  430. }
  431. src = src + '],ons:['
  432. first = true
  433. for (o of n.ons) {
  434. if (!first) { src = src + ',' }
  435. first = false
  436. src = src + '{event:' + str(o.event) + ',site:' + str(o.site) + ',sets:[' + handlerSets(o.site) + ']}'
  437. }
  438. src = src + '],classes:' + list(n.classes == null ? [] : n.classes) + '}'
  439. return src
  440. }
  441. // view.hl's kidKey: the reference's path, joined — the key the server mounted it under
  442. kidKeyOf(n) {
  443. let out = ''
  444. for (p of n.path) { out = out + '/' + p }
  445. return out
  446. }
  447. // the value a literal binding carries — view.refValue, folded at compile time
  448. refLiteral(b) {
  449. if (b.kind == 'folded') { return jsValue(b.value) }
  450. if (b.kind == 'number') { return '' + b.text }
  451. if (b.kind == 'boolean') { return b.text == 'true' ? 'true' : 'false' }
  452. if (b.kind == 'null_literal') { return 'null' }
  453. return str(b.text)
  454. }
  455. jsValue(v) {
  456. if (v == null) { return 'null' }
  457. if (hlTypeName(v) == 'String') { return str(v) }
  458. if (hlTypeName(v) == 'Boolean') { return v ? 'true' : 'false' }
  459. if (hlTypeName(v) == 'Number') { return '' + v }
  460. return JSON.stringify(v)
  461. }
  462. // ---- the site set: what this element SHOWS ------------------------------------------
  463. // The old hl:web asked the module's table which members the element at this site reads. The
  464. // answer is a fact about the View, so it is written in: the members its bound
  465. // attributes and its text leaves name. `Style` is folded into a class name at build and
  466. // the shell's `slot` is swapped by navigation — neither is ever painted, and neither is
  467. // a member read that reaches here (a `for` row variable resolves to its row).
  468. siteNames(n) {
  469. let out = []
  470. for (a of n.attrs) {
  471. if (a.member != null && a.member != 'Style' && !rowVars.includes(a.member) && !out.includes(a.member)) { out.push(a.member) }
  472. // A FIELD OF A MEMBER (`value = f.title`, ticket #93) is a read of the member:
  473. // a write to `f.title`, or a new `f`, moves `f`, and this element repaints
  474. if (a.ref != null && a.ref.name != 'Style' && !rowVars.includes(a.ref.name) && !out.includes(a.ref.name)) { out.push(a.ref.name) }
  475. }
  476. for (c of n.children) {
  477. if ((c.k == 'member' || c.k == 'field') && c.name != 'slot' && c.name != 'Style' && !rowVars.includes(c.name)) {
  478. if (!out.includes(c.name)) { out.push(c.name) }
  479. }
  480. }
  481. return out
  482. }
  483. rowVars = []
  484. // ---- the paints ---------------------------------------------------------------------
  485. collectPaints(&out, nodes) {
  486. for (n of nodes) {
  487. if (n.k == 'el') {
  488. let paint = paintFn(n)
  489. let row = rowFn(n)
  490. if (paint != null || row != 'null') {
  491. out.push({ site = n.site; paint = paint == null ? 'null' : paint; row = row; })
  492. }
  493. collectPaints(&out, n.children)
  494. } else if (n.k == 'for') { collectPaints(&out, n.body) }
  495. else if (n.k == 'if') { collectPaints(&out, n.then) collectPaints(&out, n.other) }
  496. else if (n.k == 'component') { if (n.fill != null) { collectPaints(&out, n.fill) } }
  497. }
  498. return null
  499. }
  500. // `paint(&m, s, name)`, compiled: the bound attributes under the member each reads, and
  501. // the text where the member that moved stands among the leaves.
  502. paintFn(n) {
  503. let body = ''
  504. for (a of n.attrs) {
  505. if (a.member != null) {
  506. body = body + 'if(n===' + str(a.member) + '){' + attrWrite(n, a, readOf({ k = 'member'; name = a.member; })) + '}'
  507. } else if (a.ref != null) {
  508. // a field of a member paints when the MEMBER moves (ticket #93)
  509. body = body + 'if(n===' + str(a.ref.name) + '){' + attrWrite(n, a, readOf(a.ref)) + '}'
  510. }
  511. }
  512. let text = textOf(n)
  513. if (text != null) {
  514. let guard = ''
  515. for (c of n.children) {
  516. if ((c.k == 'member' || c.k == 'field') && c.name != 'slot') {
  517. if (guard != '') { guard = guard + '||' }
  518. guard = guard + 'n===' + str(c.name)
  519. }
  520. }
  521. if (guard != '') { body = body + 'if(' + guard + '){el.textContent=' + text + ';}' }
  522. } else if (mixed(n)) {
  523. let k = 0
  524. for (run of runsOf(n)) {
  525. let guard = ''
  526. for (c of run) {
  527. if (c.k != 'text') {
  528. if (guard != '') { guard = guard + '||' }
  529. guard = guard + 'n===' + str(c.name)
  530. }
  531. }
  532. if (guard != '') { body = body + 'if((' + guard + ')&&el.__hlLeaves&&el.__hlLeaves[' + k + ']){el.__hlLeaves[' + k + '].data=' + runText(run) + ';}' }
  533. k = k + 1
  534. }
  535. }
  536. if (body == '') { return null }
  537. return 'function(el,inst,r,n){' + body + '}'
  538. }
  539. // `rowPaint(&m, el)`, compiled: every bound spot, written only where the DOM does not
  540. // already hold it — which is why a push into 2000 rows costs the 1999 nothing.
  541. rowFn(n) {
  542. let body = ''
  543. for (a of n.attrs) {
  544. if (isBool(a)) {
  545. body = body + '{' + boolWrite(a, rawRead(a.member != null ? { k = 'member'; name = a.member; } : a.ref)) + '}'
  546. } else if (a.member != null || a.ref != null) {
  547. let one = 'let v=' + readOf(a.member != null ? { k = 'member'; name = a.member; } : a.ref) + ';'
  548. if (a.extra != null) { one = one + 'v=v+" "+' + str(a.extra) + ';' }
  549. if (n.tag == 'select' && a.name == 'value') { one = one + 'if(el.value!==v){el.value=v;}' }
  550. else {
  551. one = one + 'if(el.getAttribute(' + str(a.name) + ')!==v){el.setAttribute(' + str(a.name) + ',v);}'
  552. if (a.name == 'value') { one = one + 'if(el.value!=null&&el.value!==v){el.value=v;}' }
  553. }
  554. body = body + '{' + one + '}'
  555. }
  556. }
  557. let text = textOf(n)
  558. let feeds = false
  559. for (c of n.children) { if ((c.k == 'member' || c.k == 'field') && c.name != 'slot') { feeds = true } }
  560. if (text != null && feeds) { body = body + '{let t=' + text + ';if(el.textContent!==t){el.textContent=t;}}' }
  561. if (text == null && feeds) {
  562. let k = 0
  563. for (run of runsOf(n)) {
  564. if (runFeeds(run)) { body = body + '{let x=el.__hlLeaves&&el.__hlLeaves[' + k + '];let t=' + runText(run) + ';if(x&&x.data!==t){x.data=t;}}' }
  565. k = k + 1
  566. }
  567. }
  568. if (body == '') { return 'null' }
  569. return 'function(el,inst,r){' + body + '}'
  570. }
  571. attrWrite(n, a, v) {
  572. if (isBool(a)) { return boolWrite(a, rawRead(a.member != null ? { k = 'member'; name = a.member; } : a.ref)) }
  573. let src = 'let v=' + v + ';'
  574. if (a.extra != null) { src = src + 'v=v+" "+' + str(a.extra) + ';' }
  575. if (n.tag == 'select' && a.name == 'value') { return src + 'el.value=v;' }
  576. src = src + 'el.setAttribute(' + str(a.name) + ',v);'
  577. if (a.name == 'value') { src = src + 'if(el.value!=null){el.value=v;}' }
  578. return src
  579. }
  580. textOf(n) {
  581. let parts = []
  582. let content = 0
  583. let feeds = false
  584. for (c of n.children) {
  585. if (c.k != 'on') {
  586. content = content + 1
  587. if (c.k == 'text') { parts.push(str(c.text)) }
  588. else if ((c.k == 'member' || c.k == 'field') && c.name != 'slot') { parts.push(textRead(c)) feeds = true }
  589. }
  590. }
  591. if (!feeds) { return null }
  592. if (parts.length != content) { return null }
  593. let src = '""'
  594. for (p of parts) { src = src + '+(' + p + ')' }
  595. return src
  596. }
  597. // ---- a read, written in ---------------------------------------------------------------
  598. // `view.value`'s own rule as an expression: a `for` row variable in scope wins over a
  599. // member of the same name, and a field path walks into it. `readOf` answers the STRING
  600. // the DOM takes; `rawRead` the value itself (a binding, a list, a condition).
  601. readOf(node) { return '(""+' + rawRead(node) + ')' }
  602. // A READ SHOWN AS TEXT: null is no text, as the server writes it (view.hl `escape`)
  603. textRead(node) { return 'S(' + rawRead(node) + ')' }
  604. rawRead(node) {
  605. if (node == null) { return 'null' }
  606. if (node.k == 'text') { return node.kind == 'folded' && hlTypeName(node.value) == 'Hybrid' ? JSON.stringify(node.value) : str(node.text) }
  607. if (node.k != 'field') { return 'RV(r,inst,' + str(node.name) + ')' }
  608. return 'RF(r,inst,' + str(node.name) + ',' + list(node.path) + ')'
  609. }
  610. list(names) {
  611. let src = '['
  612. let first = true
  613. for (n of names) {
  614. if (!first) { src = src + ',' }
  615. first = false
  616. src = src + str(n)
  617. }
  618. return src + ']'
  619. }
  620. // a JavaScript string literal — JSON's escaping is a subset of JavaScript's, so a
  621. // source text's quotes, newlines and backslashes survive into the statement unchanged
  622. str(s) {
  623. if (s == null) { return '""' }
  624. return JSON.stringify('' + s)
  625. }
  626. // ---- WHAT THE LANGUAGE SAYS ABOUT THIS FILE, read HERE and written into the code ----
  627. // The old hl:web shipped the language's answers to the browser as data: `hlJs` writes each file's
  628. // members, handlers and methods — every node of every initializer, with the names it
  629. // reads — into the module, and the client reduces that table on every boot. The answers
  630. // are facts about the source, so this file asks for them once, at compile time, and
  631. // writes the RESULT into the statements: a handler's write set stands at the listener
  632. // that needs it, a region's names at the region, the derivations as a list of what to
  633. // re-run. The module carries no table at all (WebFramework `module` drops the one the
  634. // emitter writes).
  635. //
  636. // The reflection is the framework's own analysis generation — the same `gen` the View
  637. // trees come from. Nothing starts a second one: a handle is only valid inside the
  638. // generation that made it.
  639. tableOf(gen, key) {
  640. let members = []
  641. let derivations = []
  642. let handlers = []
  643. let methods = []
  644. for (m of hlMembers(gen, key)) {
  645. let entry = { name = m.name; nodes = null; }
  646. if (m.hasValue) {
  647. let n = syntaxTree(gen, key, m.node)
  648. entry.nodes = n
  649. derivations.push({ name = m.name; site = n.site; reads = n.reads; serverSite = serverSite(n.reads); })
  650. nestedHandlers(&handlers, n)
  651. }
  652. members.push(entry)
  653. }
  654. for (h of hlEvents(gen, key).handles) {
  655. let n = syntaxTree(gen, key, h.node)
  656. handlers.push({ event = h.event; site = n.site; reads = n.reads; writes = n.writes; nested = false; })
  657. }
  658. for (mm of hlMethods(gen, key)) {
  659. if (!mm.inlineHandler) {
  660. let n = syntaxTree(gen, key, mm.node)
  661. methods.push({ name = mm.name; reads = n.reads; writes = n.writes; })
  662. }
  663. }
  664. // A SERVER REFERENCE SITE IS ONE THROUGH A LOCAL HELPER TOO (#121): an initializer
  665. // that calls a method or static of this class which calls the import still reaches
  666. // the server only, so it is never re-derived in the browser either.
  667. let post = []
  668. for (d of derivations) {
  669. post.push({ name = d.name; site = d.site; reads = d.reads; serverSite = d.serverSite || reachesServerSite(members, methods, d.reads); })
  670. }
  671. return { members = members; derivations = post; handlers = handlers; methods = methods; }
  672. }
  673. // does a read list reach an imported class THROUGH this class's own methods and statics,
  674. // transitively — the walk of `closeReads`, over the helpers a derivation calls
  675. reachesServerSite(members, methods, reads) {
  676. let queue = []
  677. if (reads != null) {
  678. for (r of reads) {
  679. if (r.kind == 'method' || (r.kind == 'member' && r.isStatic == true)) { queue.push(r.kind + ' ' + r.name) }
  680. }
  681. }
  682. let seen = {}
  683. while (queue.length > 0) {
  684. let at = queue.shift()
  685. if (seen[at] == null) {
  686. seen[at] = true
  687. let sp = at.indexOf(' ')
  688. let kind = at.slice(0, sp)
  689. let name = at.slice(sp + 1)
  690. let hop = []
  691. if (kind == 'method') { for (mm of methods) { if (mm.name == name) { hop = mm.reads } } }
  692. else { for (mb of members) { if (mb.name == name && mb.nodes != null) { hop = nodeReads(mb.nodes) } } }
  693. if (serverSite(hop)) { return true }
  694. for (r of hop) {
  695. if (r.kind == 'method' || (r.kind == 'member' && r.isStatic == true)) { queue.push(r.kind + ' ' + r.name) }
  696. }
  697. }
  698. }
  699. return false
  700. }
  701. // A NESTED `on` IS A DOM HANDLER, filed under its own site — the id the View node
  702. // carries. Its body is not the literal's syntax, so the node answers for it and the
  703. // walk stops there.
  704. nestedHandlers(&into, n) {
  705. if (n == null) { return null }
  706. if (n.tag == 'on_statement') {
  707. into.push({ event = n.event; site = n.site; reads = n.reads; writes = n.writes; nested = true; })
  708. return null
  709. }
  710. for (c of [n.value, n.list, n.body, n.condition, n.consequent, n.alternate, n.object, n.property, n.callee, n.left, n.right, n.operand]) {
  711. if (c != null) { nestedHandlers(&into, c) }
  712. }
  713. for (l of [n.entries, n.args, n.elements]) {
  714. if (l != null) { for (c of l) { nestedHandlers(&into, c) } }
  715. }
  716. return null
  717. }
  718. // the site with its file's base name: everything after the last '/'. The path part
  719. // is not "up to the first ':'" — a package key has a colon of its own
  720. // ('hl:markdown/Markdown.hl:24:3'), and that reading left a package component's
  721. // sites unshortened, so its regions found no reads and never repainted (#111)
  722. shortSite(site) {
  723. if (site == null) { return null }
  724. let cut = site.lastIndexOf('/')
  725. if (cut < 0) { return site }
  726. return site.slice(cut + 1)
  727. }
  728. // the syntax of one node, recursively — hl:core/AST's `tree`, read off the framework's
  729. // own generation instead of starting one of its own
  730. syntaxTree(gen, key, node) {
  731. let n = hlSyntax(gen, key, node)
  732. if (n == null) { return null }
  733. // A SITE IS NAMED BY THE FILE'S BASE NAME. The reflection answers with the project
  734. // key ('components/sort_list.hl:77:26'); the View tree, the emitter's own
  735. // `__derive__` branches and every site this framework writes use the base name
  736. // ('sort_list.hl:77:26'). One spelling, or a lookup by site finds nothing.
  737. let out = { tag = n.tag; site = shortSite(n.site); reads = n.reads; writes = n.writes; }
  738. if (n.tag == 'object_expression') {
  739. let kids = []
  740. for (h of n.entries) { kids.push(syntaxTree(gen, key, h)) }
  741. out.entries = kids
  742. } else if (n.tag == 'object_property') {
  743. out.key = n.key
  744. out.value = syntaxTree(gen, key, n.value)
  745. } else if (n.tag == 'view_for') {
  746. out.list = syntaxTree(gen, key, n.list)
  747. out.body = syntaxTree(gen, key, n.body)
  748. } else if (n.tag == 'view_if') {
  749. out.condition = syntaxTree(gen, key, n.condition)
  750. out.consequent = syntaxTree(gen, key, n.consequent)
  751. out.alternate = syntaxTree(gen, key, n.alternate)
  752. } else if (n.tag == 'on_statement') {
  753. out.event = n.event
  754. } else if (n.tag == 'member_expression') {
  755. out.object = syntaxTree(gen, key, n.object)
  756. out.property = syntaxTree(gen, key, n.property)
  757. } else if (n.tag == 'call_expression') {
  758. out.callee = syntaxTree(gen, key, n.callee)
  759. let args = []
  760. for (h of n.args) { args.push(syntaxTree(gen, key, h)) }
  761. out.args = args
  762. } else if (n.tag == 'binary_expression') {
  763. out.left = syntaxTree(gen, key, n.left)
  764. out.right = syntaxTree(gen, key, n.right)
  765. } else if (n.tag == 'unary_expression') {
  766. out.operand = syntaxTree(gen, key, n.operand)
  767. } else if (n.tag == 'conditional_expression') {
  768. out.condition = syntaxTree(gen, key, n.condition)
  769. out.consequent = syntaxTree(gen, key, n.consequent)
  770. out.alternate = syntaxTree(gen, key, n.alternate)
  771. } else if (n.tag == 'array_expression') {
  772. let els = []
  773. for (h of n.elements) { els.push(syntaxTree(gen, key, h)) }
  774. out.elements = els
  775. }
  776. return out
  777. }
  778. // ---- the reductions the browser used to make on every boot --------------------------
  779. // A `for`'s names are what its list reads, an `if`'s what its condition reads — filed
  780. // under the region's own site.
  781. regionIndex(t) {
  782. let idx = {}
  783. for (m of t.members) { if (m.nodes != null) { indexRegions(&idx, m.nodes) } }
  784. return idx
  785. }
  786. indexRegions(&idx, n) {
  787. if (n == null) { return null }
  788. if (n.tag == 'view_for') {
  789. idx[n.site] = memberNames(nodeReads(n.list))
  790. indexRegions(&idx, n.body)
  791. return null
  792. }
  793. if (n.tag == 'view_if') {
  794. idx[n.site] = memberNames(nodeReads(n.condition))
  795. indexRegions(&idx, n.consequent)
  796. indexRegions(&idx, n.alternate)
  797. return null
  798. }
  799. if (n.tag == 'on_statement') { return null }
  800. for (c of [n.value, n.list, n.body, n.condition, n.consequent, n.alternate, n.object, n.property, n.callee, n.left, n.right, n.operand]) {
  801. if (c != null) { indexRegions(&idx, c) }
  802. }
  803. for (l of [n.entries, n.args, n.elements]) {
  804. if (l != null) { for (c of l) { indexRegions(&idx, c) } }
  805. }
  806. return null
  807. }
  808. // the member reads of a read list — the language resolved every name, so this is a
  809. // filter and not a second resolution
  810. memberNames(reads) {
  811. let out = []
  812. if (reads == null) { return out }
  813. for (r of reads) {
  814. if (r.kind == 'member' && r.isStatic != true && !out.includes(r.name)) { out.push(r.name) }
  815. }
  816. return out
  817. }
  818. nodeReads(n) {
  819. if (n == null) { return [] }
  820. if (n.reads != null) { return n.reads }
  821. let out = []
  822. for (c of [n.value, n.list, n.body, n.condition, n.consequent, n.alternate, n.object, n.property, n.callee, n.left, n.right, n.operand]) {
  823. if (c != null) { for (r of nodeReads(c)) { out.push(r) } }
  824. }
  825. for (l of [n.entries, n.args, n.elements]) {
  826. if (l != null) { for (c of l) { for (r of nodeReads(c)) { out.push(r) } } }
  827. }
  828. return out
  829. }
  830. // A ROUTINE'S WRITES, PLUS THE WRITES OF EVERYTHING IT REACHES — the method it calls
  831. // and the handler it emits to, transitively. Both are READS the language resolved, so
  832. // this is a walk of its answer and never a guess.
  833. closeWrites(t, writes, reads) {
  834. let out = memberNames(writes)
  835. let queue = []
  836. reached(&queue, reads)
  837. let seen = {}
  838. while (queue.length > 0) {
  839. let at = queue.shift()
  840. if (seen[at] == null) {
  841. seen[at] = true
  842. let sp = at.indexOf(' ')
  843. let kind = at.slice(0, sp)
  844. let name = at.slice(sp + 1)
  845. for (mm of t.methods) {
  846. if (kind == 'method' && mm.name == name) {
  847. for (w of memberNames(mm.writes)) { if (!out.includes(w)) { out.push(w) } }
  848. reached(&queue, mm.reads)
  849. }
  850. }
  851. for (h of t.handlers) {
  852. if (kind == 'handler' && !h.nested && h.event == name) {
  853. for (w of memberNames(h.writes)) { if (!out.includes(w)) { out.push(w) } }
  854. reached(&queue, h.reads)
  855. }
  856. }
  857. for (mb of t.members) {
  858. if (kind == 'member' && mb.name == name && mb.nodes != null) {
  859. for (w of memberNames(mb.nodes.writes)) { if (!out.includes(w)) { out.push(w) } }
  860. reached(&queue, nodeReads(mb.nodes))
  861. }
  862. }
  863. }
  864. }
  865. return out
  866. }
  867. // the mirror: a handler's reads plus the reads of everything it reaches — the
  868. // candidates for a routine a host handed down, whose writes only the host's table names
  869. closeReads(t, reads) {
  870. let out = memberNames(reads)
  871. let queue = []
  872. reached(&queue, reads)
  873. let seen = {}
  874. while (queue.length > 0) {
  875. let at = queue.shift()
  876. if (seen[at] == null) {
  877. seen[at] = true
  878. let sp = at.indexOf(' ')
  879. let kind = at.slice(0, sp)
  880. let name = at.slice(sp + 1)
  881. for (mm of t.methods) {
  882. if (kind == 'method' && mm.name == name) {
  883. for (r of memberNames(mm.reads)) { if (!out.includes(r)) { out.push(r) } }
  884. reached(&queue, mm.reads)
  885. }
  886. }
  887. for (h of t.handlers) {
  888. if (kind == 'handler' && !h.nested && h.event == name) {
  889. for (r of memberNames(h.reads)) { if (!out.includes(r)) { out.push(r) } }
  890. reached(&queue, h.reads)
  891. }
  892. }
  893. for (mb of t.members) {
  894. if (kind == 'member' && mb.name == name && mb.nodes != null) {
  895. for (r of memberNames(nodeReads(mb.nodes))) { if (!out.includes(r)) { out.push(r) } }
  896. reached(&queue, nodeReads(mb.nodes))
  897. }
  898. }
  899. }
  900. }
  901. return out
  902. }
  903. reached(&queue, reads) {
  904. if (reads == null) { return null }
  905. for (r of reads) {
  906. if (r.kind == 'method' || r.kind == 'handler' || (r.kind == 'member' && r.isStatic != true)) { queue.push(r.kind + ' ' + r.name) }
  907. }
  908. return null
  909. }
  910. // ---- THE SERVER RENDER, COMPILED --------------------------------------------------
  911. // The old hl:web rendered a page by WALKING the View tree per request: for every node it asks
  912. // what kind it is, for every element it builds the attribute string, and it decides the
  913. // indentation of every line again, on every request, for every one of the 121 mounts a
  914. // page may hold. Every one of those decisions is a fact about the View.
  915. //
  916. // So a component's render is compiled ONCE into a list of STEPS — constant text, and a
  917. // read with its own closure — and a request is a concatenation. The structure (tags,
  918. // static attributes, the readable form's line breaks, which element is a block) is
  919. // decided at compile time; only the values are asked for per request.
  920. //
  921. // KEYED BY THE INDENT IT IS RENDERED AT, because that is the only thing about a
  922. // component's HTML that its caller decides: the shell renders at one tab, the page at
  923. // the depth the slot stands, a fill compactly (null). An app has a handful of those.
  924. serverSteps(key, nodes, indent, classes, slotted) {
  925. let ck = key + '|' + (indent == null ? '~' : indent) + '|' + joinList(classes) + '|' + (slotted ? '1' : '0')
  926. if (steps[ck] == null) {
  927. steps[ck] = blockSteps(classes == null || classes.length == 0 ? nodes : v.withRootClasses(nodes, classes), indent, slotted)
  928. }
  929. return steps[ck]
  930. }
  931. // A `slot` THE HOST FILLED COUNTS AS CONTENT, and an empty one does not — the one
  932. // thing about a component's shape its caller decides, so it is part of the key.
  933. staticVoid = [ 'area' 'base' 'br' 'col' 'embed' 'hr' 'img' 'input' 'link' 'meta' 'source' 'track' 'wbr' ]
  934. staticVerbatim = [ 'textarea' 'pre' 'script' 'style' ]
  935. blocky(n, slotted) {
  936. if (staticVerbatim.includes(n.tag)) { return false }
  937. if (n.children.length == 0) { return false }
  938. for (c of n.children) {
  939. if (c.k == 'text' || c.k == 'field') { return false }
  940. if (c.k == 'member' && !(c.name == 'slot' && slotted)) { return false }
  941. }
  942. return true
  943. }
  944. steps = {}
  945. v = new HlwView
  946. joinList(l) {
  947. if (l == null) { return '' }
  948. let out = ''
  949. for (c of l) { out = out + ',' + c }
  950. return out
  951. }
  952. // `render(nodes, …, indent)`: the readable form puts each element on its own line at
  953. // `indent`, the compact one writes them end to end.
  954. blockSteps(nodes, indent, slotted) {
  955. let out = []
  956. for (n of nodes) {
  957. if (indent != null) {
  958. if (n.k == 'el') { push(&out, { k = 's'; v = newline + indent; }) nodeSteps(&out, n, indent, slotted) }
  959. else if (n.k == 'component') { nodeSteps(&out, n, indent, slotted) }
  960. else if (n.k == 'for') { forSteps(&out, n, indent, slotted) }
  961. else if (n.k == 'if') { ifSteps(&out, n, indent, slotted) }
  962. else if (n.k == 'member' && n.name == 'slot') { push(&out, { k = 'slotline'; v = newline + indent; }) }
  963. else if (n.k != 'on') { nodeSteps(&out, n, indent, slotted) }
  964. } else {
  965. nodeSteps(&out, n, null, slotted)
  966. }
  967. }
  968. return out
  969. }
  970. forSteps(&out, n, indent, slotted) {
  971. out.push({ k = 'for'; row = n.row; read = readFn(n.list); body = blockSteps(n.body, indent, slotted); })
  972. return null
  973. }
  974. ifSteps(&out, n, indent, slotted) {
  975. out.push({ k = 'if'; read = readFn(n.cond); then = blockSteps(n.then, indent, slotted); other = blockSteps(n.other, indent, slotted); })
  976. return null
  977. }
  978. // one node's steps — `html(node, …)`, decided once
  979. nodeSteps(&out, n, indent, slotted) {
  980. if (n.k == 'text') { push(&out, { k = 's'; v = v.escape(n.text); }) return null }
  981. if (n.k == 'on') { return null }
  982. if (n.k == 'member' && n.name == 'slot') { out.push({ k = 'slot'; }) return null }
  983. if (n.k == 'member' || n.k == 'field') { out.push({ k = 'r'; read = readFn(n); }) return null }
  984. if (n.k == 'for') { forSteps(&out, n, indent, slotted) return null }
  985. if (n.k == 'if') { ifSteps(&out, n, indent, slotted) return null }
  986. if (n.k == 'component') {
  987. out.push({ k = 'kid'; path = n.path; cls = n.cls; key = n.key; classes = n.classes; indent = indent;
  988. fill = (n.fill == null || n.fill.length == 0) ? null : blockSteps(n.fill, null, slotted); })
  989. return null
  990. }
  991. if (n.k == 'el') { elSteps(&out, n, indent, slotted) return null }
  992. push(&out, { k = 's'; v = '<!-- ' + n.tag + ' not rendered yet -->'; })
  993. return null
  994. }
  995. elSteps(&out, n, indent, slotted) {
  996. let attrs = []
  997. let valueRead = null // a textarea's value is its content
  998. let selRead = null // a select's value selects an option
  999. for (a of n.attrs) {
  1000. if (n.tag == 'textarea' && a.name == 'value') { valueRead = attrRead(a) }
  1001. else if (n.tag == 'select' && a.name == 'value') { selRead = attrRead(a) }
  1002. // a bound BOOLEAN attribute is its presence (ticket #33): ` checked=""` or nothing
  1003. else if ((a.member != null || a.ref != null) && v.isBoolAttr(a.name)) { attrs.push({ k = 'b'; name = a.name; read = attrRead(a); }) }
  1004. else if (a.member != null || a.ref != null) {
  1005. push(&attrs, { k = 's'; v = ' ' + a.name + '="'; })
  1006. attrs.push({ k = 'r'; read = attrRead(a); })
  1007. push(&attrs, { k = 's'; v = v.extraOf(a) + '"'; })
  1008. } else {
  1009. push(&attrs, { k = 's'; v = ' ' + a.name + '="' + v.escape(a.text) + '"'; })
  1010. }
  1011. }
  1012. if (v.boundValue(n)) { push(&attrs, { k = 's'; v = ' autocomplete="off"'; }) }
  1013. // the children, in the form this element takes: a block puts them one per line
  1014. let kids = []
  1015. if (indent != null && blocky(n, slotted)) {
  1016. kids = blockSteps(n.children, indent + tab, slotted)
  1017. push(&kids, { k = 's'; v = newline + indent; })
  1018. } else {
  1019. kids = blockSteps(n.children, null, slotted)
  1020. }
  1021. if (valueRead != null) { kids = [{ k = 'r'; read = valueRead; }] }
  1022. // AN OPTION IS THE ONE ELEMENT WHOSE OWN TAG DEPENDS ON THE VALUE ABOVE IT: it
  1023. // carries `selected` when its value is the select's. Everything else is settled.
  1024. if (n.tag == 'option') {
  1025. out.push({ k = 'opt'; attrs = attrs; body = kids; ov = optionRead(n); })
  1026. return null
  1027. }
  1028. push(&out, { k = 's'; v = '<' + n.tag; })
  1029. for (a of attrs) { push(&out, a) }
  1030. if (staticVoid.includes(n.tag)) { push(&out, { k = 's'; v = '>'; }) return null }
  1031. push(&out, { k = 's'; v = '>'; })
  1032. // A SELECT HANDS ITS VALUE TO ITS OPTIONS, however deep they stand: its children
  1033. // run under it, and it is the only element that changes anything for them.
  1034. if (n.tag == 'select') { out.push({ k = 'sel'; read = selRead; body = kids; }) }
  1035. else { for (kk of kids) { push(&out, kk) } }
  1036. push(&out, { k = 's'; v = '</' + n.tag + '>'; })
  1037. return null
  1038. }
  1039. // CONSTANT TEXT MERGES. Two literal steps in a row are one string decided at compile
  1040. // time — which is what makes a request a concatenation of a handful of pieces instead
  1041. // of one per node.
  1042. push(&out, step) {
  1043. if (step.k == 's' && out.length > 0 && out[out.length - 1].k == 's') {
  1044. let last = out[out.length - 1]
  1045. last.v = last.v + step.v
  1046. out[out.length - 1] = last
  1047. return null
  1048. }
  1049. out.push(step)
  1050. return null
  1051. }
  1052. // ---- the reads, each with its own closure ------------------------------------------
  1053. // `view.value`'s rule, split once: a `for` row variable in scope wins over a member of
  1054. // the same name, and a field path walks into it. The closure holds the name — there is
  1055. // no node to look at per request.
  1056. readFn(node) {
  1057. if (node == null) { return (inst, rows) => { return null } }
  1058. if (node.k == 'text') { let t = v.refValue(node) return (inst, rows) => { return t } }
  1059. let name = node.name
  1060. if (node.k != 'field') {
  1061. return (inst, rows) => { return rows[name] != null ? rows[name] : inst[name] }
  1062. }
  1063. let path = node.path
  1064. return (inst, rows) => {
  1065. let val = rows[name] != null ? rows[name] : inst[name]
  1066. for (f of path) {
  1067. if (val == null) { return null }
  1068. val = val[f]
  1069. }
  1070. return val
  1071. }
  1072. }
  1073. attrRead(a) {
  1074. if (a.member != null) { return readFn({ k = 'member'; name = a.member; }) }
  1075. if (a.ref != null) { return readFn(a.ref) }
  1076. let t = a.text
  1077. return (inst, rows) => { return t }
  1078. }
  1079. // an option's own value: its `value` attribute, or null when it has none (the browser
  1080. // then reports its text, and the run compares against what it rendered)
  1081. optionRead(n) {
  1082. for (a of n.attrs) {
  1083. if (a.name == 'value') { return attrRead(a) }
  1084. }
  1085. return null
  1086. }
  1087. // ---- the run: a concatenation ------------------------------------------------------
  1088. // EVERY REGION IS WRITTEN BETWEEN TWO MARKS (tickets #103, #104): an `if` around the
  1089. // branch that stands, a `for` around its rows and each row around its own nodes. The
  1090. // client claims them as they are and builds the same ones, and a region's nodes are
  1091. // exactly what stands between its two marks — nothing it holds has to be remembered
  1092. // node by node, and text beside a region can never run into the region's text.
  1093. markOpen = '<!--[-->'
  1094. markClose = '<!--]-->'
  1095. runSteps(list, &inst, &rows, &ctx) {
  1096. let out = ''
  1097. for (s of list) {
  1098. if (s.k == 's') { out = out + s.v }
  1099. else if (s.k == 'r') { let f = s.read out = out + v.escape(f(&inst, &rows)) }
  1100. else if (s.k == 'b') { let f = s.read if (v.boolOn(f(&inst, &rows))) { out = out + ' ' + s.name + '=""' } }
  1101. else if (s.k == 'slot') { if (ctx.slot != null) { out = out + ctx.slot } }
  1102. else if (s.k == 'slotline') { if (ctx.slot != null) { out = out + s.v + ctx.slot } }
  1103. else if (s.k == 'for') {
  1104. let f = s.read
  1105. let list2 = f(&inst, &rows)
  1106. out = out + markOpen
  1107. if (list2 != null) {
  1108. let ri = 0
  1109. for (entry of list2) {
  1110. let inner = rows + {}
  1111. inner[s.row] = entry
  1112. let rc = v.rowCtx(&ctx, &entry, ri)
  1113. out = out + markOpen + runSteps(s.body, &inst, &inner, &rc) + markClose
  1114. ri = ri + 1
  1115. }
  1116. }
  1117. out = out + markClose
  1118. }
  1119. else if (s.k == 'if') {
  1120. let f = s.read
  1121. if (f(&inst, &rows)) { out = out + markOpen + runSteps(s.then, &inst, &rows, &ctx) + markClose }
  1122. else { out = out + markOpen + runSteps(s.other, &inst, &rows, &ctx) + markClose }
  1123. }
  1124. else if (s.k == 'opt') {
  1125. let head = runSteps(s.attrs, &inst, &rows, &ctx)
  1126. let body = runSteps(s.body, &inst, &rows, &ctx)
  1127. let mark = ''
  1128. if (ctx.sel != null) {
  1129. let ov = body
  1130. if (s.ov != null) { let f = s.ov ov = '' + f(&inst, &rows) }
  1131. if (ov == ctx.sel) { mark = ' selected' }
  1132. }
  1133. out = out + '<option' + head + mark + '>' + body + '</option>'
  1134. }
  1135. else if (s.k == 'sel') {
  1136. let f = s.read
  1137. let inner = ctx + {}
  1138. inner.sel = f == null ? null : '' + f(&inst, &rows)
  1139. out = out + runSteps(s.body, &inst, &rows, &inner)
  1140. }
  1141. else if (s.k == 'kid') {
  1142. let kid = ctx.kids[v.kidKeyIn(s.path, ctx)]
  1143. if (kid == null) { out = out + '<!-- ' + s.cls + ' not mounted -->' }
  1144. else {
  1145. let fillText = s.fill == null ? null : runSteps(s.fill, &inst, &rows, &ctx)
  1146. let kidCtx = { slot = fillText; kids = kid.kids; }
  1147. let kidInst = kid.page
  1148. let empty = {}
  1149. out = out + runSteps(serverSteps(s.key, kid.view, s.indent, s.classes, fillText != null), &kidInst, &empty, &kidCtx)
  1150. }
  1151. }
  1152. }
  1153. return out
  1154. }

Branches

Latest commits

  • 20e09d89Merge t18 (tracker#18 delta sync) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs, both sides kept; pageShowOf summary = summaryOfmre
  • f83571c3tracker#18 (mission 067): daily sync by change lists — TMDB /tv|movie/changes (since the stored day, paged) + TVmaze /updates/shows → only our changed titles (followed: full step, unfollowed: light step — changed seasons, no TVmaze), full walk on first run / gap > 14 days / failed list; show record refreshed (title, tmdbSummary, tagline, status, genres …; renamed titles re-indexed); summary = the creator's own text (page: summary > tmdbSummary > tvmazeSummary), one-time clear of copied summaries (9,647 on the live copy); gate 261, tests/realdata-018*.mjs, README + STATUSmre
  • f2b00674Merge t26 (tracker#26 + #28) into main (mission 062): short ids for every new person (castPersonId, guest route), guest stars stored on the title and created as people only when opened (/person/tmdb/<id>?show=<id> → 302), lean watch/follow clicks (showRow a small object, cast/crew from the slug, watches cached per user, face rows only after a season toggle); gate 311, tests/realdata-062.mjs, README + STATUSmre
  • fa1f9dfatracker#29 (mission 063): unwatched check muted grey outline + check (accent only on hover), watched stays solid — no code regression, the accent outline read as ticked; gate checks real checks visibly (computed style + screenshot pixel) on /my/unwatched, show, movie, /my/movies; gate 266, tests/realdata-063.mjs, README + STATUSmre
  • 10bb3f93tracker#28 (mission 061): full cast (all seasons, main cast by episodes, guest stars) + crew (created by, directed by, written by, screenplay, story, music) — stored by the details completion, the daily sync, the search import (one details request) and a background credits job (resumes, RSS limit); show page collapsed after 20 with client-side Show all; showBySlug via a slug map; gate 249, tests/realdata-028.mjs, README + STATUSmre
  • d8b12d67tracker#27 (mission 060): short ids for movies, series and persons — old 702 kept (data/old-short-ids.json), new random [a-z0-9]{5} unique across both, claimed at creation, background backfill (resumes), shown under poster/photo, /<shortId> → 301; gate 259, tests/realdata-060*, README + STATUSmre
  • 25a50bc4tracker#26 (mission 059): titles from a filmography are completed — on open (skeleton, step-wise face showComplete, no reload) and by the in-app details repair (resumes, TMDB-paced, series in parts); cast from TMDB credits; gate 231, tests/realdata-026.mjs, README + STATUSmre
  • f14db671tracker#22-#25 (mission 058): episode air dates, season check = all episodes watched, movie watched check (+ /my/movies count), /genres/<genre> pages (movies + series, newest first, paginated); gate 238, tests/realdata-058.mjs, README + STATUSmre
  • 1704ec45tracker#17 (mission 057): season caret down/up, skeleton rows while a season loads, sessionless showSeasonEpisodes face (no page re-mount), client-only close; gate 214, tests/realdata-057.mjs, README + STATUSmre
  • f2fe3e36mission 056: README + STATUS (merge, fixes, Hybriel 8590df63, real-data check), tests/realdata-056.mjs, tools/check-public-slugs.hlmre
  • f40c250emission 056: re-vendor hybriel master 8590df63 (#121, #122); an adult title's page is Not found for non-followers; gate: leave the page before stopping the servermre
  • 2b7fdd6cmission 056: signed-out header one row on phones ("Log in", nowrap), backfill skips adult titles' posters, gate checksmre
  • 2c53d5efMerge branch 't16-person' (tracker#16 person pages) into main; filmography shows only public titles (054 adult flag), gate race fix (backfill start line)mre
  • c171227emission 054: hide adult/unknown titles from the public lists and the search; in-app adult-flag backfill (TMDB details + poster per title, resumes), gate + real-data proofmre
  • 139fafd8tracker#16: short bio (4 lines, click = all), real-data check script, README + STATUSmre
  • 93be9476tracker#16: person pages /person/<slug> with the filmography fetched from TMDB on the first visit (step by step), gatemre
  • 47a3cae6STATUS: mission 053 merge commit idsmre
  • dcc5eecaMerge branch 't14-search'mre
  • 03edc783Merge branch 't15-tvmaze'mre
  • 71b46345tracker#15: numbering check by date or title, placeholder titles in other languages, docs + real-data proofmre