aiplang 2.11.6 → 2.11.7

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.11.6'
8
+ const VERSION = '2.11.7'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -686,7 +686,7 @@ function generateTypes(app, srcFile) {
686
686
  }
687
687
 
688
688
  lines.push(`// ── aiplang version ──────────────────────────────────────────`)
689
- lines.push(`export const AIPLANG_VERSION = '2.11.6'`)
689
+ lines.push(`export const AIPLANG_VERSION = '2.11.7'`)
690
690
  lines.push(``)
691
691
  return lines.join('\n')
692
692
  }
@@ -1044,6 +1044,11 @@ function parseBlock(line) {
1044
1044
  if(line.startsWith('benchmark{')) { const m=line.match(/^benchmark\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/);return{kind:'benchmark',items:m[1].split('|').map(it=>{const p=it.trim().split(':');return{num:p[0]?.trim(),label:p[1]?.trim(),vs:p[2]?.trim(),pct:parseInt(p[3])||0}}),variant:vm?.[1],animate:am?.[1]}} }
1045
1045
  if(line.startsWith('install{')) { const m=line.match(/^install\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'install',cmds:m[1].split('|').map(c=>c.trim()).filter(Boolean),variant:vm?.[1]}} }
1046
1046
  if(line.startsWith('feature{')) { const b=parseBlock(line.replace(/^feature/,'row3'));if(b){b.variant='feature'};return b }
1047
+ if(line.startsWith('marquee{')) { const m=line.match(/^marquee\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'marquee',items:m[1].split('|').map(s=>s.trim()).filter(Boolean),variant:vm?.[1]}} }
1048
+ if(line.startsWith('cta{')) { const m=line.match(/^cta\{([^}]*)\}/);if(m){const pts=m[1].split('|');let t='',s='',links=[];pts.forEach(p=>{const lm=p.match(/^([^>]+)>([^>]+)$/);if(lm)links.push({label:lm[1].trim(),path:lm[2].trim()});else if(!t)t=p.trim();else s=p.trim()});const vm=line.match(/variant:(\S+)/);return{kind:'cta',title:t,sub:s,links,variant:vm?.[1]}} }
1049
+ if(line.startsWith('steps{')) { const m=line.match(/^steps\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'steps',items:m[1].split('|').map(it=>{const p=it.trim().split('>');return{num:p[0]?.trim(),title:p[1]?.trim(),desc:p[2]?.trim()}}),variant:vm?.[1]}} }
1050
+ if(line.startsWith('compare{')) { const m=line.match(/^compare\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'compare',rows:m[1].split('|').map(r=>r.trim().split(':').map(c=>c.trim())),variant:vm?.[1]}} }
1051
+ if(line.startsWith('video{')) { const m=line.match(/^video\{([^}]*)\}/);if(m){const pts=m[1].split('|');return{kind:'video',url:pts[0]?.trim(),poster:pts[1]?.trim()}} }
1047
1052
  if(line.startsWith('faq{')) {
1048
1053
  const body=line.slice(4,line.lastIndexOf('}')).trim()
1049
1054
  const items=body.split('|').map(i=>{const idx=i.indexOf('>');return{q:i.slice(0,idx).trim(),a:i.slice(idx+1).trim()}}).filter(i=>i.q&&i.a)
@@ -1277,6 +1282,11 @@ function renderBlock(b, page) {
1277
1282
  case 'install': return rInstall(b)
1278
1283
  case 'feature': return rRow(b)
1279
1284
  case 'testimonial': return rTestimonial(b)
1285
+ case 'marquee': return rMarquee(b)
1286
+ case 'cta': return rCta(b)
1287
+ case 'steps': return rSteps(b)
1288
+ case 'compare': return rCompare(b)
1289
+ case 'video': return rVideo(b)
1280
1290
  case 'gallery': return rGallery(b)
1281
1291
  case 'raw': return (b.html||'')+'\n'
1282
1292
  case 'html': return `<div class="fx-html">${b.content||''}</div>\n`
@@ -1400,10 +1410,17 @@ function parseFeature(line) {
1400
1410
 
1401
1411
  function rHero(b) {
1402
1412
  let h1='',sub='',img='',ctas=''
1413
+ let heroBadge = ''
1403
1414
  for(const item of (b.items||[])) for(const f of item){
1415
+ if(f.text?.startsWith('badge:')) { heroBadge=`<div class="fx-hero-badge"><span class="fx-hero-badge-dot"></span>${esc(f.text.slice(6).trim())}</div>`; continue }
1404
1416
  if(f.isImg) img=`<img src="${esc(f.src)}" class="fx-hero-img" alt="hero" loading="eager">`
1405
1417
  else if(f.isLink) ctas+=`<a href="${esc(f.path)}" class="fx-cta">${esc(f.label)}</a>`
1406
- else if(!h1) h1=`<h1 class="fx-title">${esc(f.text)}</h1>`
1418
+ else if(!h1) {
1419
+ // *texto* entre asteriscos = gradient text
1420
+ const gt = f.text.match(/^\*(.*?)\*$/)
1421
+ if(gt) h1=`<h1 class="fx-title"><span class="fx-gradient-text">${esc(gt[1])}</span></h1>`
1422
+ else h1=`<h1 class="fx-title">${esc(f.text)}</h1>`
1423
+ }
1407
1424
  else sub+=`<p class="fx-sub">${esc(f.text)}</p>`
1408
1425
  }
1409
1426
  const v = b.variant || (img ? 'split' : 'centered')
@@ -1413,6 +1430,7 @@ function rHero(b) {
1413
1430
  return `<section class="fx-hero fx-hero-landing"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>
1414
1431
  `
1415
1432
  }
1433
+ if (h1) h1 = heroBadge + h1
1416
1434
  if (v === 'minimal') {
1417
1435
  return `<section class="fx-hero fx-hero-minimal"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>\n`
1418
1436
  }
@@ -1468,7 +1486,7 @@ function rRow(b) {
1468
1486
  }).join('')
1469
1487
  const v=b.variant||''
1470
1488
  const wrapStyle=b.style?` style="${b.style.replace(/,/g,';')}"`:''
1471
- return `<div class="fx-grid fx-grid-${b.cols||3}${v?' fx-grid-'+v:''}"${wrapStyle}>${cards}</div>\n`
1489
+ return `<div class="fx-grid fx-grid-${b.cols||3}${v?' fx-grid-'+v:''} fx-animate-stagger"${wrapStyle}>${cards}</div>\n`
1472
1490
  }
1473
1491
 
1474
1492
  function rSect(b) {
@@ -1482,7 +1500,7 @@ function rSect(b) {
1482
1500
  const bgStyle=b.bg?` style="background:${b.bg}"`:(b.style?` style="${b.style.replace(/,/g,';')}"`:'' )
1483
1501
  const v = b.variant||''
1484
1502
  const cls = v ? ` fx-sect-${v}` : ''
1485
- return `<section class="fx-sect${cls}"${bgStyle}>${inner}</section>\n`
1503
+ return `<section class="fx-sect${cls} fx-animate"${bgStyle}>${inner}</section>\n`
1486
1504
  }
1487
1505
 
1488
1506
 
@@ -1525,7 +1543,7 @@ function rBenchmark(b) {
1525
1543
  <div class="fx-bench-bar"><div class="fx-bench-fill" style="width:${isLow?pct+'%':pct+'%'}"></div></div>
1526
1544
  </div>`
1527
1545
  }).join('')
1528
- return `<div class="fx-benchmark">${cards}</div>\n`
1546
+ return `<div class="fx-benchmark fx-animate-stagger">${cards}</div>\n`
1529
1547
  }
1530
1548
 
1531
1549
  // ── rInstall: multi-step code box com botões ──────────────────────
@@ -1552,8 +1570,12 @@ function rStatsUpgraded(b) {
1552
1570
  const lbl = parts[1]?.trim()
1553
1571
  const vs = parts[2]?.trim()
1554
1572
  const bind = isDyn(val) ? ` data-fx-bind="${esc(val)}"` : ''
1573
+ // Números → animados com counter
1574
+ const isNum = !isDyn(val) && /^[\d.,]+[KkMmBb%]?$/.test(val?.replace(/ms|KB|GB|px/,''))
1575
+ const numAttr = isNum && !isDyn(val) ? ` data-to="${val.replace(/[^\d.]/g,'')}" data-dec="${val.includes('.')?val.split('.')[1]?.replace(/[^\d]/g,'').length||0:0}"` : ''
1576
+ const countCls = isNum && !isDyn(val) ? ' fx-count' : ''
1555
1577
  return `<div class="fx-stat">
1556
- <div class="fx-stat-val"${bind}>${esc(val)}</div>
1578
+ <div class="fx-stat-val${countCls}"${bind}${numAttr}>${esc(val)}</div>
1557
1579
  <div class="fx-stat-lbl">${esc(lbl||'')}</div>
1558
1580
  ${vs ? `<div class="fx-stat-vs">${esc(vs)}</div>` : ''}
1559
1581
  </div>`
@@ -1562,6 +1584,88 @@ function rStatsUpgraded(b) {
1562
1584
  }
1563
1585
 
1564
1586
 
1587
+
1588
+ // ── rMarquee: faixa de logos/texto em loop infinito ───────────────
1589
+ function rMarquee(b) {
1590
+ const speed = b.variant === 'fast' ? '15s' : b.variant === 'slow' ? '40s' : '25s'
1591
+ const items = (b.items||[]).map(item =>
1592
+ `<span class="fx-marquee-item">${esc(item)}</span><span class="fx-marquee-sep">·</span>`
1593
+ ).join('')
1594
+ // Duplicar para loop contínuo
1595
+ return `<div class="fx-marquee"><div class="fx-marquee-track" style="animation-duration:${speed}">${items}${items}</div></div>\n`
1596
+ }
1597
+
1598
+ // ── rCta: seção call-to-action com glow ───────────────────────────
1599
+ function rCta(b) {
1600
+ const btns = (b.links||[]).map((l,i) =>
1601
+ `<a href="${esc(l.path)}" class="fx-cta${i===0?'':' fx-cta-outline'}">${esc(l.label)}</a>`
1602
+ ).join('')
1603
+ const v = b.variant || 'default'
1604
+ return `<section class="fx-cta-section fx-cta-${v}">
1605
+ <div class="fx-cta-glow"></div>
1606
+ <div class="fx-cta-inner">
1607
+ ${b.title?`<h2 class="fx-cta-title">${esc(b.title)}</h2>`:''}
1608
+ ${b.sub?`<p class="fx-cta-sub">${esc(b.sub)}</p>`:''}
1609
+ <div class="fx-cta-actions">${btns}</div>
1610
+ </div>
1611
+ </section>\n`
1612
+ }
1613
+
1614
+ // ── rSteps: passos 1-2-3 com linha conectora ──────────────────────
1615
+ function rSteps(b) {
1616
+ const items = (b.items||[]).map((step, i) =>
1617
+ `<div class="fx-step">
1618
+ <div class="fx-step-num">${esc(step.num||String(i+1))}</div>
1619
+ <div class="fx-step-body">
1620
+ <div class="fx-step-title">${esc(step.title||'')}</div>
1621
+ <div class="fx-step-desc">${esc(step.desc||'')}</div>
1622
+ </div>
1623
+ </div>`
1624
+ ).join('')
1625
+ const v = b.variant === 'vertical' ? ' fx-steps-vertical' : ''
1626
+ return `<div class="fx-steps${v} fx-animate-stagger">${items}</div>\n`
1627
+ }
1628
+
1629
+ // ── rCompare: tabela X vs Y ───────────────────────────────────────
1630
+ function rCompare(b) {
1631
+ const rows = b.rows || []
1632
+ if (!rows.length) return ''
1633
+ // Primeira linha = cabeçalho se tiver textos
1634
+ const header = rows[0]
1635
+ const isHeader = header.length > 1 && !header[0].startsWith('✅') && !header[0].startsWith('❌')
1636
+ const headerHtml = isHeader
1637
+ ? `<div class="fx-compare-header">${header.map((h,i) => `<div class="fx-compare-cell${i===0?' fx-compare-feature':i===1?' fx-compare-col-a':' fx-compare-col-b'}">${esc(h)}</div>`).join('')}</div>`
1638
+ : ''
1639
+ const dataRows = isHeader ? rows.slice(1) : rows
1640
+ const bodyHtml = dataRows.map(row => {
1641
+ const feature = row[0] || ''
1642
+ const a = row[1] || ''
1643
+ const b2 = row[2] || ''
1644
+ const checkA = a === '✅' || a === 'sim' || a === 'yes' ? '✅' : a === '❌' || a === 'nao' || a === 'no' ? '❌' : esc(a)
1645
+ const checkB = b2 === '✅' || b2 === 'sim' || b2 === 'yes' ? '✅' : b2 === '❌' || b2 === 'nao' || b2 === 'no' ? '❌' : esc(b2)
1646
+ return `<div class="fx-compare-row">
1647
+ <div class="fx-compare-cell fx-compare-feature">${esc(feature)}</div>
1648
+ <div class="fx-compare-cell fx-compare-col-a">${checkA}</div>
1649
+ <div class="fx-compare-cell fx-compare-col-b">${checkB}</div>
1650
+ </div>`
1651
+ }).join('')
1652
+ return `<div class="fx-compare">${headerHtml}${bodyHtml}</div>\n`
1653
+ }
1654
+
1655
+ // ── rVideo: embed de vídeo ou youtube ─────────────────────────────
1656
+ function rVideo(b) {
1657
+ const url = b.url || ''
1658
+ const poster = b.poster || ''
1659
+ // Detectar YouTube
1660
+ const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/)
1661
+ if (ytMatch) {
1662
+ const id = ytMatch[1]
1663
+ return `<div class="fx-video-wrap"><div class="fx-video-yt"><iframe src="https://www.youtube-nocookie.com/embed/${esc(id)}?rel=0" frameborder="0" allowfullscreen loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe></div></div>\n`
1664
+ }
1665
+ // Vídeo HTML5
1666
+ return `<div class="fx-video-wrap"><video class="fx-video" controls${poster?` poster="${esc(poster)}"`:''} preload="metadata"><source src="${esc(url)}"></video></div>\n`
1667
+ }
1668
+
1565
1669
  // ── autoYear: substitui © YYYY por © <span data-fx-year></span> ──
1566
1670
  function autoYear(text) {
1567
1671
  // Substitui padrões como "© 2024", "© 2025", "© 2026" ou só "©" pelo ano dinâmico
@@ -1757,6 +1861,69 @@ function css(theme) {
1757
1861
  .fx-footer-brand{font-size:1rem;font-weight:800;letter-spacing:-.02em}
1758
1862
  .fx-footer-links{display:flex;gap:1.5rem;flex-wrap:wrap}
1759
1863
  .fx-footer-note{font-size:.72rem;opacity:.3;font-family:monospace}
1864
+ /* ── marquee ── */
1865
+ .fx-marquee{overflow:hidden;padding:1.5rem 0;border-top:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06);-webkit-mask:linear-gradient(90deg,transparent,black 10%,black 90%,transparent);mask:linear-gradient(90deg,transparent,black 10%,black 90%,transparent)}
1866
+ .fx-marquee-track{display:flex;width:max-content;animation:fx-marquee 25s linear infinite}
1867
+ .fx-marquee:hover .fx-marquee-track{animation-play-state:paused}
1868
+ @keyframes fx-marquee{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
1869
+ .fx-marquee-item{font-size:.9375rem;font-weight:600;opacity:.35;white-space:nowrap;padding:0 1.5rem;transition:opacity .2s}
1870
+ .fx-marquee-item:hover{opacity:.7}
1871
+ .fx-marquee-sep{opacity:.15;padding:0 .25rem}
1872
+ /* ── cta section ── */
1873
+ .fx-cta-section{position:relative;padding:6rem 2.5rem;text-align:center;overflow:hidden}
1874
+ .fx-cta-glow{position:absolute;width:600px;height:400px;border-radius:50%;background:radial-gradient(ellipse,rgba(var(--accent-rgb,255,87,34),.12),transparent 70%);left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none}
1875
+ .fx-cta-inner{position:relative;z-index:1;max-width:44rem;margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:1.5rem}
1876
+ .fx-cta-title{font-size:clamp(2rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1.05}
1877
+ .fx-cta-sub{font-size:1.0625rem;line-height:1.75;opacity:.65;max-width:36rem}
1878
+ .fx-cta-actions{display:flex;gap:.875rem;flex-wrap:wrap;justify-content:center}
1879
+ .fx-cta-outline{background:transparent!important;border:1px solid rgba(255,255,255,.2)!important;color:inherit!important}
1880
+ .fx-cta-outline:hover{background:rgba(255,255,255,.05)!important}
1881
+ /* ── steps ── */
1882
+ .fx-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:2rem;padding:2rem 2.5rem 4rem;position:relative}
1883
+ .fx-steps::before{content:"";position:absolute;top:3.5rem;left:calc(2.5rem + 1.5rem);right:calc(2.5rem + 1.5rem);height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.1),transparent);pointer-events:none}
1884
+ .fx-step{display:flex;flex-direction:column;align-items:flex-start;gap:1rem}
1885
+ .fx-step-num{width:48px;height:48px;border-radius:50%;background:var(--accent,#ff5722);color:#fff;font-weight:900;font-size:1.125rem;display:flex;align-items:center;justify-content:center;flex-shrink:0;position:relative;z-index:1}
1886
+ .fx-step-title{font-size:1rem;font-weight:700;margin-bottom:.375rem;letter-spacing:-.02em}
1887
+ .fx-step-desc{font-size:.875rem;line-height:1.65;opacity:.6}
1888
+ .fx-steps-vertical{grid-template-columns:1fr}
1889
+ .fx-steps-vertical::before{display:none}
1890
+ .fx-steps-vertical .fx-step{flex-direction:row}
1891
+ /* ── compare ── */
1892
+ .fx-compare{max-width:640px;margin:0 auto 4rem;padding:0 2.5rem}
1893
+ .fx-compare-header{display:grid;grid-template-columns:1fr 1fr 1fr;padding:.75rem 1rem;background:rgba(255,255,255,.04);border-radius:.75rem .75rem 0 0;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;opacity:.6}
1894
+ .fx-compare-row{display:grid;grid-template-columns:1fr 1fr 1fr;padding:.75rem 1rem;border-bottom:1px solid rgba(255,255,255,.05);transition:background .15s}
1895
+ .fx-compare-row:hover{background:rgba(255,255,255,.02)}
1896
+ .fx-compare-row:last-child{border-bottom:none;border-radius:0 0 .75rem .75rem}
1897
+ .fx-compare-cell{font-size:.875rem}
1898
+ .fx-compare-feature{opacity:.7}
1899
+ .fx-compare-col-a{text-align:center;color:#4ade80}
1900
+ .fx-compare-col-b{text-align:center;opacity:.35}
1901
+ /* ── video ── */
1902
+ .fx-video-wrap{padding:0 2.5rem 3rem}
1903
+ .fx-video-yt{position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:1rem;border:1px solid rgba(255,255,255,.06)}
1904
+ .fx-video-yt iframe{position:absolute;top:0;left:0;width:100%;height:100%}
1905
+ .fx-video{width:100%;border-radius:1rem;border:1px solid rgba(255,255,255,.06)}
1906
+ /* ── gradient text ── */
1907
+ .fx-gradient-text{background:linear-gradient(135deg,var(--accent,#ff5722),#ff8a50,#ffd4c4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
1908
+ /* ── scroll animations (IntersectionObserver) ── */
1909
+ .fx-animate{opacity:0;transform:translateY(20px);transition:opacity .6s cubic-bezier(.4,0,.2,1),transform .6s cubic-bezier(.4,0,.2,1)}
1910
+ .fx-animate.fx-visible{opacity:1;transform:none}
1911
+ .fx-animate-delay-1{transition-delay:.1s}
1912
+ .fx-animate-delay-2{transition-delay:.2s}
1913
+ .fx-animate-delay-3{transition-delay:.3s}
1914
+ .fx-animate-stagger>*{opacity:0;transform:translateY(16px);transition:opacity .5s cubic-bezier(.4,0,.2,1),transform .5s cubic-bezier(.4,0,.2,1)}
1915
+ .fx-animate-stagger.fx-visible>*:nth-child(1){opacity:1;transform:none;transition-delay:.05s}
1916
+ .fx-animate-stagger.fx-visible>*:nth-child(2){opacity:1;transform:none;transition-delay:.15s}
1917
+ .fx-animate-stagger.fx-visible>*:nth-child(3){opacity:1;transform:none;transition-delay:.25s}
1918
+ .fx-animate-stagger.fx-visible>*:nth-child(4){opacity:1;transform:none;transition-delay:.35s}
1919
+ .fx-animate-stagger.fx-visible>*:nth-child(5){opacity:1;transform:none;transition-delay:.45s}
1920
+ .fx-animate-stagger.fx-visible>*:nth-child(6){opacity:1;transform:none;transition-delay:.55s}
1921
+ /* ── number counter ── */
1922
+ .fx-count{display:inline-block}
1923
+ /* ── hero badge ── */
1924
+ .fx-hero-badge{display:inline-flex;align-items:center;gap:.5rem;font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;padding:.3rem 1rem;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.04);margin-bottom:1.25rem}
1925
+ .fx-hero-badge-dot{width:6px;height:6px;border-radius:50%;background:var(--accent,#ff5722);animation:fx-blink 2s ease infinite}
1926
+ @keyframes fx-blink{0%,100%{opacity:1}50%{opacity:.2}}
1760
1927
  .fx-year{font-variant-numeric:tabular-nums}
1761
1928
  .fx-hero-minimal{min-height:50vh!important}
1762
1929
  .fx-hero-minimal .fx-hero-inner{gap:1rem}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.6",
3
+ "version": "2.11.7",
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
@@ -2432,7 +2432,7 @@ async function startServer(aipFile, port = 3000) {
2432
2432
  })
2433
2433
 
2434
2434
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2435
- status:'ok', version:'2.11.6',
2435
+ status:'ok', version:'2.11.7',
2436
2436
  models: app.models.map(m=>m.name),
2437
2437
  routes: app.apis.length, pages: app.pages.length,
2438
2438
  admin: app.admin?.prefix || null,