diff --git a/.gitignore b/.gitignore index 163caf6..0e261a5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ /data/ .venv/ venv/ +.venv*/ *.egg-info/ dist/ .pytest_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f2e61..7f0333d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog — FlowDeck +## v4.8.0 (2026-09-03) — Bloc Tableau simple (mode édition Notion) + +> Insertion via `/table`, cellules éditables au clic, ajout/suppression/modification de lignes et colonnes comme dans Notion. + +### Added +- **Bloc `table` dans l'éditeur de page.** Nouvelles commandes slash `/table` (menu « BASIC ») → tableau 3×3 avec en-tête ; première ligne stylée en en-tête, cellules `contenteditable` éditées au clic, auto-sauvegarde (format de bloc conservé : `{type:'table', rows:[...]}`). +- **Gestion des colonnes** : bouton de poignée (⋮⋮) au-dessus de chaque colonne → menu contextuel Insert left / Insert right / Duplicate / Clear contents / Delete column / Delete table. +- **Gestion des lignes** : bouton « + Row » sous le tableau (clic = ajouter une ligne) + menu contextuel (clic droit sur une cellule de corps) → Insert above / Insert below / Duplicate / Clear row / Delete row / Delete table. +- **Import Markdown (GFM)** : `md2b` détecte les tableaux pipe (`| a | b |` + ligne de séparateurs) et produit un bloc `table` éditable au lieu de paragraphes aplatis (pages `.md`/markdown). +- **Roundtrip Markdown** : `blocksToMarkdown` (JS) et l'export Python reconstruisent un tableau pipe valide depuis les blocs. +- **Export** : les pages contenant un bloc `table` sortent correctement en Markdown / HTML / PDF (géré côté serveur). +- UX : cliquer sous un tableau en fin de page ajoute un paragraphe pour continuer la rédaction. + +### Infra +- `VERSION` → 4.8.0 ; `app/main.py` (log + `version=`) ; cache CSS `app.css?v=4.8.0` ; ROADMAP renuméroté (v4.8.0 Bloc Tableau, Collaboration → v4.9.0, Agent → v4.10.0). + +### Tests +- Suite pytest verte (199) ; vérification syntaxe JS du template via `node --check`. + ## v4.7.3 (2026-09-03) — Fix export PDF/HTML : tableaux + émojis ### Fixed diff --git a/ROADMAP.md b/ROADMAP.md index 7ad313b..4540665 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -301,12 +301,18 @@ Détails livrés : - [x] Dockerfile : libs pango/harfbuzz/gdk-pixbuf + polices ; requirements : + weasyprint==69.0 - [x] 4 nouveaux tests → 199/199 ; PDF README.md vérifié : tableau structuré + émojis colorés -### v4.8.0 — Collaboration +### v4.8.0 — Bloc Tableau simple ✅ (2026-09-03) +- [x] **Bloc `table` éditable dans l'éditeur** — insertion via `/table` (commandes slash, section BASIC), tableau 3×3 avec en-tête, cellules éditables au clic, auto-sauvegarde. +- [x] **Lignes** — bouton `+ Row` sous le tableau (clic = ajouter) ; clic droit sur une cellule de corps → Insérer dessus/dessous, Dupliquer, Effacer, Supprimer la ligne, Supprimer le tableau. +- [x] **Colonnes** — poignée (⋮⋮) au-dessus de chaque colonne → Insérer à gauche/droite, Dupliquer, Effacer le contenu, Supprimer la colonne, Supprimer le tableau. +- [x] **Import Markdown (GFM)** — `md2b` transforme les tableaux pipe en bloc `table` au lieu de les aplatir en paragraphes ; roundtrip Markdown (JS + export) et sortie MD/HTML/PDF correcte d'un bloc `table`. + +### v4.9.0 — Collaboration - [ ] **Inline comments** — commentaires sur sélection de texte - [ ] **@mentions** — notifier un utilisateur → page/commentaire - [ ] **Email notifications** — changements, mentions -### v4.9.0 — FlowDeck Agent (Agent IA natif) +### v4.10.0 — FlowDeck Agent (Agent IA natif) **Objectif :** Un agent IA intégré à FlowDeck, capable de planifier, rechercher et **agir** directement sur les workspaces — collections, pages, propriétés, vues, issues Gitea. Inspiré de Notion Agent (2026). @@ -449,8 +455,8 @@ Base + Kanban Éditeur + Gitea UX Pro MVP Onboard + UI Notion + Tags + Admin + Sharing COMPLETED + GitHub OAuth + Library -v4.0.2 ✅ v4.1.0 ✅ v4.2.0 ✅ v4.3.0 ✅ v4.4.0 ✅ v4.5.0 ✅ v4.6.0 ✅ v4.7.0 ✅ v4.8–4.9 ⬜ v5.x–v6.0 ⬜ -Quality Data Sources Templates + 10 Views Tasks & Sprints & Content Export Collab + Pro + Agent -& Tests & Linked DB Dashboards complets Dependencies My Tasks Blocks MD/PDF/HTML Agent IA (futur) +v4.0.2 ✅ v4.1.0 ✅ v4.2.0 ✅ v4.3.0 ✅ v4.4.0 ✅ v4.5.0 ✅ v4.6.0 ✅ v4.7.0 ✅ v4.8.0 ✅ v4.9–4.10 ⬜ v5.x–v6.0 ⬜ +Quality Data Sources Templates + 10 Views Tasks & Sprints & Content Export Bloc Table Collab + Pro + Agent +& Tests & Linked DB Dashboards complets Dependencies My Tasks Blocks MD/PDF/HTML simple Agent IA (futur) -*Dernière mise à jour: 2026-09-03 — v4.7.3 Export tableaux + émojis ✅* +*Dernière mise à jour: 2026-09-03 — v4.8.0 Bloc Tableau simple (édition Notion) ✅* diff --git a/VERSION b/VERSION index 4178d09..6ca6df1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.7.3 \ No newline at end of file +4.8.0 \ No newline at end of file diff --git a/app/main.py b/app/main.py index 159d7c0..3a28738 100644 --- a/app/main.py +++ b/app/main.py @@ -39,13 +39,13 @@ async def lifespan(_app: FastAPI): (admin_hash,) ) conn.commit() - logger.info("FlowDeck v4.7.3 started on port %d", settings.app_port) + logger.info("FlowDeck v4.8.0 started on port %d", settings.app_port) yield app = FastAPI( title="FlowDeck", - version="4.7.3", + version="4.8.0", docs_url="/docs" if settings.log_level == "DEBUG" else None, redoc_url=None, lifespan=lifespan, diff --git a/app/templates/_page_editor_scripts.html b/app/templates/_page_editor_scripts.html index 81fdee8..47b22ed 100644 --- a/app/templates/_page_editor_scripts.html +++ b/app/templates/_page_editor_scripts.html @@ -9,6 +9,7 @@ {id:'to_do',name:'To-do',icon:'☐'},{id:'toggle',name:'Toggle',icon:'▶'},{id:'quote',name:'Quote',icon:'❝'}, {id:'callout',name:'Callout',icon:'lightbulb'},{id:'table_of_contents',name:'Table of contents',icon:'☰'},{id:'math',name:'Math equation',icon:'∑'}, {id:'columns',name:'Column list',icon:'▮'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'}, + {id:'table',name:'Table',icon:'⊞'}, ]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'image'}]}]; let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);} @@ -19,6 +20,120 @@ const CALLOUT_ICONS=['💡','💡','📌','⚠️','ℹ️','✅','❌','🚀','🔔','📣','⭐','❤️','🔥','💬','👀','🧠','🎯','🔑','📅','💰','💡','📚','🧪','🌍']; + /* ═══════════ Simple Table block (v4.8.0) ═══════════ */ + function _tblSplitLine(s){s=(s||'').trim();if(s.charAt(0)==='|')s=s.slice(1);if(s.charAt(s.length-1)==='|')s=s.slice(0,-1);return s.split('|').map(function(x){return x.trim();});} + function _tblIsSep(s){s=(s||'').trim();if(!s||s.charAt(0)!=='|')return false;var cells=_tblSplitLine(s);if(!cells.length)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});} + function _tblIsData(s){s=(s||'').trim();return /^\|/.test(s)&&s.indexOf('|',1)>=0;} + function _tblNew(rows,cols){var out=[];for(var i=0;iw)index=w;b.rows.forEach(function(r){r.splice(index,0,'');});} + function _tblAddRow(b,index){_tblClone(b);var w=b.rows.length&&b.rows[0]?b.rows[0].length:1;if(index<0)index=0;if(index>b.rows.length)index=b.rows.length;b.rows.splice(index,_tblNew(1,w)[0]);} + function _tblDupCol(b,index){_tblClone(b);b.rows.forEach(function(r){var v=(indexr.length?r.length:index+1;r.splice(at,0,v);});} + function _tblDupRow(b,index){_tblClone(b);if(index<0||index>b.rows.length)return;b.rows.splice(index+1,0,(b.rows[index]||[]).slice());} + function _tblClearCol(b,index){_tblClone(b);b.rows.forEach(function(r){if(r&&index=0&&index=0&&index=0&&index';} + function _tblDefault(){return {rows:_tblNew(3,3)};} + function _tblFocus(bid,r,c){var sel=document.querySelector('.ftable-editor[data-bid="'+bid+'"]');if(!sel)return;var q='[data-c="'+c+'"]';var cell=sel.querySelector(q);if(!cell){var first=sel.querySelector('.ftable-cell[data-r]');if(first){first.focus();ce(first);}return;}cell.focus();if(r===undefined)cs(cell);else ce(cell);} + function renderTableBlock(b,idx){ + var rows=(b&&Array.isArray(b.rows)&&b.rows.length)?b.rows:_tblNew(2,2); + var w=0;rows.forEach(function(r){if(r&&r.length>w)w=r.length;});if(w<1)w=2; + var bid=b.id;var h=''; + h+='
'; + h+='
'; + for(var c=0;c'+_tblGrip()+'
'; + } + h+='
'; + h+=''; + for(var ri=0;ri'+esc(v)+''; + } + h+=''; + } + h+='
'; + h+='
'; + h+='
'; + return h; + } + function _tblBlockById(bid){var e=window.E;if(!e||!e.blocks)return null;for(var i=0;i'+it[1]+''; + }); + d.innerHTML=html; + document.body.appendChild(d); + var r=d.getBoundingClientRect(); + x=Math.min(Math.max(8,x),window.innerWidth-r.width-8); + y=Math.min(Math.max(8,y),window.innerHeight-r.height-8); + d.style.left=x+'px';d.style.top=y+'px'; + window._tblCur={bid:bid,kind:kind,index:index}; + } + function _tblDo(action,cur){ + if(!cur)return; + var bid=cur.bid,index=cur.index,b=_tblBlockById(bid),e=window.E; + if(!b||!e)return; + e.sync(); + if(!(b.rows&&b.rows.length))b.rows=_tblNew(2,2); + var focusR=1,focusC=0; + if(action==='col-insert-left'){_tblAddCol(b,index);focusC=index;} + else if(action==='col-insert-right'){_tblAddCol(b,index+1);focusC=index+1;} + else if(action==='col-dup'){_tblDupCol(b,index);focusC=index+1;} + else if(action==='col-clear'){_tblClearCol(b,index);focusC=index;} + else if(action==='col-del'){_tblDelCol(b,index);focusC=Math.min(index,b.rows[0].length-1);} + else if(action==='row-insert-above'){_tblAddRow(b,index);focusR=index;} + else if(action==='row-insert-below'){_tblAddRow(b,index+1);focusR=index+1;} + else if(action==='row-dup'){_tblDupRow(b,index);focusR=index+1;} + else if(action==='row-clear'){_tblClearRow(b,index);focusR=index;} + else if(action==='row-del'){_tblDelRow(b,index);focusR=Math.min(index,b.rows.length-1);if(focusR<1)focusR=1;} + else if(action==='tbl-del'){_tblMenuClose();var bi=_tblIdxByBid(bid);if(bi>=0)e.removeBlock(bi);return;} + e.render();e.dirty=true;e.autoSave(); + _tblFocus(bid,focusR,focusC); + } + function _tblInitDelegate(){ + if(window._tblDelegate)return;window._tblDelegate=true; + document.addEventListener('click',function(ev){ + if(ev.target.closest&&ev.target.closest('#_tblMenu')){var it=ev.target.closest('.ftable-menu-item');if(it){var act=it.getAttribute('data-tbl-act');_tblDo(act,window._tblCur);}return;} + var g=ev.target.closest?ev.target.closest('[data-tbl]'):null; + if(g){var act=g.getAttribute('data-tbl'),bid=g.getAttribute('data-bid'),col=parseInt(g.getAttribute('data-col')||'0',10); + var gr=g.getBoundingClientRect?g.getBoundingClientRect():null; + if(act==='colmenu'){_tblMenuOpen(gr?gr.left:ev.clientX,(gr?gr.bottom:ev.clientY)+4,bid,'col',col);ev.preventDefault();} + else if(act==='colplus'){var b=_tblBlockById(bid);if(b){window.E.sync();_tblAddCol(b,col+1);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,col+1);}} + else if(act==='rowadd'){var b2=_tblBlockById(bid);if(b2){window.E.sync();var n=b2.rows.length;_tblAddRow(b2,n);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,n,0);}} + return; + } + if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('[data-tbl]')))_tblMenuClose(); + }); + document.addEventListener('contextmenu',function(ev){ + var cell=ev.target.closest?ev.target.closest('.ftable-cell'):null; + if(!cell)return; + var w=cell.closest('.ftable-editor');if(!w)return; + var bid=w.getAttribute('data-bid'),r=parseInt(cell.getAttribute('data-r')||'0',10),c=parseInt(cell.getAttribute('data-c')||'0',10); + ev.preventDefault(); + if(r===0)_tblMenuOpen(ev.clientX,ev.clientY,bid,'col',c); + else _tblMenuOpen(ev.clientX,ev.clientY,bid,'row',r); + }); + document.addEventListener('mousedown',function(ev){if(document.getElementById('_tblMenu')&&!ev.target.closest('#_tblMenu'))_tblMenuClose();}); + } + const SM={_o:false,_i:-1,_s:0,_q:'',_it:[], init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML=''; const inp=document.createElement('input');inp.type='text';inp.placeholder='Filter...'; @@ -54,6 +169,7 @@ get isOpen(){return SM._o;}}; function renderBlock(b,idx){ + if(b.type==='table')return renderTableBlock(b,idx); if(b.type==='divider')return `

`; if(b.type==='image')return `
${esc(b.alt||'')}
`; if(b.type==='table_of_contents')return renderTOC(); @@ -170,7 +286,9 @@ else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}} if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')]; this.loadInlineDBs(); - window.E=this;SM.init();this.render(); + window.E=this; + this.insertTable=function(){this.sync();var nb={id:genId(),type:'table',content:'',rows:_tblNew(3,3)};this.blocks.push(nb);this.dirty=true;this.autoSave();this.render();setTimeout(function(){_tblFocus(nb.id,0,0);},60);}; + SM.init();_tblInitDelegate();this.render(); setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return; ct.addEventListener('keydown',e=>this.onKd(e)); ct.addEventListener('paste',e=>{e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();}); @@ -205,8 +323,9 @@ getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);}, getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};}, focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}}, - onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];const el=b&&this.getEl(b.id);if(el){el.focus();ce(el);}}}}, - sync(){const ct=document.getElementById('_blocksCt');if(!ct)return;for(const b of this.blocks){if(b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='table_of_contents')continue; + onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];if(b&&(b.type==='table'||b.type==='divider'||b.type==='image'||b.type==='embed')){this.addAt(this.blocks.length,'paragraph');}else{const el=b&&this.getEl(b.id);if(el){el.focus();ce(el);}}}}}, + sync(){const ct=document.getElementById('_blocksCt');if(!ct)return;for(const b of this.blocks){if(b.type==='table'){const wrap=ct.querySelector(`[data-bid="${b.id}"].ftable-editor`);if(wrap){const rows=[];wrap.querySelectorAll('tr.ftable-row').forEach(function(tr){const cells=tr.querySelectorAll('th.ftable-cell,td.ftable-cell');if(!cells.length)return;const rowarr=[];cells.forEach(function(cell){rowarr.push(cell.textContent||'');});rows.push(rowarr);});if(rows.length)b.rows=rows;}continue;} + if(b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='table_of_contents')continue; if(b.type==='math'){const te=ct.querySelector(`[data-math-bid="${b.id}"]`);if(te)b.content=te.value||'';continue;} const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el&&!el.classList.contains('toggle-title'))b.content=el.textContent||'';else if(el)b.content=el.textContent||''; if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=che.textContent||'';}); @@ -509,9 +628,10 @@ const idx=SM._i;if(idx<0||idx>=this.blocks.length)return; const block=this.blocks[idx];this.sync();block.content=''; if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');} - else{block.type=id;if(id==='toggle'){block.expanded=true;block.children=[];}if(id==='to_do')block.checked=false;if(id==='columns')block.children=[{id:genId(),type:'paragraph',content:''}];if(id==='code')block.language='Plain Text';} + else{block.type=id;if(id==='toggle'){block.expanded=true;block.children=[];}if(id==='to_do')block.checked=false;if(id==='columns')block.children=[{id:genId(),type:'paragraph',content:''}];if(id==='code')block.language='Plain Text';if(id==='table'){block.rows=_tblNew(3,3);block.content='';}} this.render();this.dirty=true;this.autoSave(); - if(id==='math'){setTimeout(()=>{const ne=document.querySelector(`[data-math-bid="${block.id}"]`);if(ne)ne.focus();},60);} + if(id==='table'){setTimeout(()=>_tblFocus(block.id,1,0),60);} + else if(id==='math'){setTimeout(()=>{const ne=document.querySelector(`[data-math-bid="${block.id}"]`);if(ne)ne.focus();},60);} else if(id==='columns'){setTimeout(()=>{const ne=this.getEl(block.children[0].id);if(ne)ne.focus();},60);} else{setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);} }, @@ -533,13 +653,27 @@ const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'New page'; this.saving=true; const csrf=document.cookie.match(/csrf_token=([^;]+)/); - fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})}) + fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})}) .then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString(); document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';}); if(cb)cb(); }).catch(()=>{this.saving=false;}); }, - md2b(md){const bl=[];let cb=null;for(const line of md.split('\n')){if(cb){if(line.trim()==='```'){bl.push(cb);cb=null;}else cb.content+=(cb.content?'\n':'')+line;continue;}const t=line.trim();if(!t)continue;if(t.startsWith('```')){cb=this.mkB('code','',{language:t.substring(3).trim()||'Plain Text'});continue;}if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));else bl.push(this.mkB('paragraph',t));}if(cb)bl.push(cb);return bl;}, + md2b(md){const bl=[];const lines=String(md).split('\n');let i=0;for(;iw)w=r.length;});if(w<1)w=1;const rows2=rows.map(function(r){const a=r.slice();while(a.length '))bl.push(this.mkB('quote',t.substring(2))); + else bl.push(this.mkB('paragraph',t)); + }return bl;}, }; } document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);}},80);}); @@ -560,6 +694,7 @@ case 'math': return '$$\n' + c + '\n$$'; case 'table_of_contents': return '[TOC]'; case 'columns': return (b.children || []).map(blocksToMarkdown).join('\n\n'); + case 'table': { var rows=b.rows||[]; if(!rows.length)return ''; var w=0; rows.forEach(function(r){if(r&&r.length>w)w=r.length;}); var rl=function(r){var cells=[];for(var k=0;k FlowDeck — {% block title_prefix %}Home{% endblock %} - +