aiplang 2.7.2 → 2.7.4

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/bin/aiplang.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.7.2'
8
+ const VERSION = '2.7.4'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -615,13 +615,15 @@ function parseBlock(line) {
615
615
  }
616
616
 
617
617
  // ── table ───────────────────────────────────────────────────
618
- if(line.startsWith('table ')) {
618
+ if(line.startsWith('table ') || line.startsWith('table{')) {
619
619
  const idx=line.indexOf('{');if(idx===-1) return null
620
- const binding=line.slice(6,idx).trim()
620
+ const start=line.startsWith('table{')?6:6
621
+ const binding=line.slice(start,idx).trim().replace(/^@/,'@')
621
622
  const content=line.slice(idx+1,line.lastIndexOf('}')).trim()
622
623
  const em=content.match(/edit\s+(PUT|PATCH)\s+(\S+)/), dm=content.match(/delete\s+(?:DELETE\s+)?(\S+)/)
623
624
  const clean=content.replace(/edit\s+(PUT|PATCH)\s+\S+/g,'').replace(/delete\s+(?:DELETE\s+)?\S+/g,'')
624
- return{kind:'table',binding,cols:parseCols(clean),empty:parseEmpty(clean),editPath:em?.[2]||null,editMethod:em?.[1]||'PUT',deletePath:dm?.[1]||null,deleteKey:'id',extraClass,animate}
625
+ const cols=parseCols(clean)
626
+ return{kind:'table',binding,cols:Array.isArray(cols)?cols:[],empty:parseEmpty(clean),editPath:em?.[2]||null,editMethod:em?.[1]||'PUT',deletePath:dm?.[1]||null,deleteKey:'id',extraClass,animate}
625
627
  }
626
628
 
627
629
  // ── form ────────────────────────────────────────────────────
@@ -727,19 +729,27 @@ function applyMods(html, b) {
727
729
 
728
730
  function renderPage(page, allPages) {
729
731
  const needsJS=page.queries.length>0||page.blocks.some(b=>['table','list','form','if','btn','select','faq'].includes(b.kind))
730
- const body=page.blocks.map(b=>applyMods(renderBlock(b,page),b)).join('')
732
+ const body=page.blocks.map(b=>{try{return applyMods(renderBlock(b,page),b)}catch(e){console.error('[aiplang] Block render error:',b.kind,e.message);return ''}}).join('')
731
733
  const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,routes:allPages.map(p=>p.route),state:page.state,queries:page.queries}):''
732
734
  const hydrate=needsJS?`\n<script>window.__AIPLANG_PAGE__=${config};</script>\n<script src="./aiplang-hydrate.js" defer></script>`:''
733
735
  const customVars=page.customTheme?genCustomThemeVars(page.customTheme):''
734
736
  const themeVarCSS=page.themeVars?genThemeVarCSS(page.themeVars):''
737
+ // Extract app name from nav brand if available
738
+ const _navBlock = page.blocks.find(b=>b.kind==='nav')
739
+ const _brand = _navBlock?.brand || ''
740
+ const _title = _brand ? `${esc(_brand)} — ${esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))}` : esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))
735
741
  return `<!DOCTYPE html>
736
742
  <html lang="en">
737
743
  <head>
738
744
  <meta charset="UTF-8">
739
745
  <meta name="viewport" content="width=device-width,initial-scale=1">
740
- <title>${esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))}</title>
746
+ <title>${_title}</title>
741
747
  <link rel="canonical" href="${esc(page.route)}">
742
748
  <meta name="robots" content="index,follow">
749
+ <meta name="description" content="${esc((()=>{const h=page.blocks.find(b=>b.kind==='hero');if(!h)return '';const m=h.rawLine&&h.rawLine.match(/\{([^}]+)\}/);if(!m)return '';const p=m[1].split('>');const s=p[0].split('|')[1];return s?s.trim():''})())}">
750
+ <meta property="og:title" content="${_title}">
751
+ <meta property="og:type" content="website">
752
+ <meta property="og:type" content="website">
743
753
  <style>${css(page.theme)}${customVars}${themeVarCSS}</style>
744
754
  </head>
745
755
  <body>
@@ -800,7 +810,7 @@ function rStats(b) {
800
810
  }
801
811
 
802
812
  function rRow(b) {
803
- const cards=b.items.map(item=>{
813
+ const cards=(b.items||[]).map(item=>{
804
814
  const inner=item.map((f,fi)=>{
805
815
  if(f.isImg) return`<img src="${esc(f.src)}" class="fx-card-img" alt="" loading="lazy">`
806
816
  if(f.isLink) return`<a href="${esc(f.path)}" class="fx-card-link">${esc(f.label)} →</a>`
@@ -825,7 +835,7 @@ function rSect(b) {
825
835
 
826
836
  function rFoot(b) {
827
837
  let inner=''
828
- for(const item of b.items) for(const f of item){
838
+ for(const item of (b.items||[])) for(const f of item){
829
839
  if(f.isLink) inner+=`<a href="${esc(f.path)}" class="fx-footer-link">${esc(f.label)}</a>`
830
840
  else inner+=`<p class="fx-footer-text">${esc(f.text)}</p>`
831
841
  }
@@ -833,13 +843,14 @@ function rFoot(b) {
833
843
  }
834
844
 
835
845
  function rTable(b) {
836
- const ths=b.cols.map(c=>`<th class="fx-th">${esc(c.label)}</th>`).join('')
837
- const keys=JSON.stringify(b.cols.map(c=>c.key))
838
- const cm=JSON.stringify(b.cols.map(c=>({label:c.label,key:c.key})))
846
+ const cols=Array.isArray(b.cols)?b.cols:[]
847
+ const ths=cols.map(c=>`<th class="fx-th">${esc(c.label)}</th>`).join('')
848
+ const keys=JSON.stringify(cols.map(c=>c.key))
849
+ const cm=JSON.stringify(cols.map(c=>({label:c.label,key:c.key})))
839
850
  const ea=b.editPath?` data-fx-edit="${esc(b.editPath)}" data-fx-edit-method="${esc(b.editMethod)}"`:''
840
851
  const da=b.deletePath?` data-fx-delete="${esc(b.deletePath)}"`:''
841
852
  const at=(b.editPath||b.deletePath)?'<th class="fx-th fx-th-actions">Actions</th>':''
842
- const span=b.cols.length+((b.editPath||b.deletePath)?1:0)
853
+ const span=cols.length+((b.editPath||b.deletePath)?1:0)
843
854
  return `<div class="fx-table-wrap"><table class="fx-table" data-fx-table="${esc(b.binding)}" data-fx-cols='${keys}' data-fx-col-map='${cm}'${ea}${da}><thead><tr>${ths}${at}</tr></thead><tbody class="fx-tbody"><tr><td colspan="${span}" class="fx-td-empty">${esc(b.empty)}</td></tr></tbody></table></div>\n`
844
855
  }
845
856
 
@@ -862,12 +873,12 @@ function rBtn(b) {
862
873
  }
863
874
 
864
875
  function rSelectBlock(b) {
865
- const opts=b.options.map(o=>`<option value="${esc(o)}">${esc(o)}</option>`).join('')
876
+ const opts=(b.options||[]).map(o=>`<option value="${esc(o)}">${esc(o)}</option>`).join('')
866
877
  return `<div class="fx-select-wrap"><select class="fx-input fx-select-block" data-fx-model="${esc(b.binding)}">${opts}</select></div>\n`
867
878
  }
868
879
 
869
880
  function rPricing(b) {
870
- const cards=b.plans.map((p,i)=>{
881
+ const cards=(b.plans||[]).map((p,i)=>{
871
882
  let lh='#',ll='Get started'
872
883
  if(p.linkRaw){const m=p.linkRaw.match(/\/([^:]+):(.+)/);if(m){lh='/'+m[1];ll=m[2]}}
873
884
  const f=i===1?' fx-pricing-featured':''
@@ -878,7 +889,7 @@ function rPricing(b) {
878
889
  }
879
890
 
880
891
  function rFaq(b) {
881
- const items=b.items.map(i=>`<div class="fx-faq-item" onclick="this.classList.toggle('open')"><div class="fx-faq-q">${esc(i.q)}<span class="fx-faq-arrow">▸</span></div><div class="fx-faq-a">${esc(i.a)}</div></div>`).join('')
892
+ const items=(b.items||[]).map(i=>`<div class="fx-faq-item" onclick="this.classList.toggle('open')"><div class="fx-faq-q">${esc(i.q)}<span class="fx-faq-arrow">▸</span></div><div class="fx-faq-a">${esc(i.a)}</div></div>`).join('')
882
893
  return `<section class="fx-sect"><div class="fx-faq">${items}</div></section>\n`
883
894
  }
884
895
 
@@ -888,7 +899,7 @@ function rTestimonial(b) {
888
899
  }
889
900
 
890
901
  function rGallery(b) {
891
- const imgs=b.imgs.map(src=>`<div class="fx-gallery-item"><img src="${esc(src)}" alt="" loading="lazy"></div>`).join('')
902
+ const imgs=(b.imgs||[]).map(src=>`<div class="fx-gallery-item"><img src="${esc(src)}" alt="" loading="lazy"></div>`).join('')
892
903
  return `<div class="fx-gallery">${imgs}</div>\n`
893
904
  }
894
905
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.7.2",
3
+ "version": "2.7.4",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
package/server/server.js CHANGED
@@ -1435,7 +1435,7 @@ async function startServer(aipFile, port = 3000) {
1435
1435
 
1436
1436
  // Health
1437
1437
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
1438
- status:'ok', version:'2.7.2',
1438
+ status:'ok', version:'2.7.4',
1439
1439
  models: app.models.map(m=>m.name),
1440
1440
  routes: app.apis.length, pages: app.pages.length,
1441
1441
  admin: app.admin?.prefix || null,