import { marked } from 'marked'; import DOMPurify from 'dompurify'; import {safeFilename, validateArtifact, escapeHTML, sandboxHTML} from './core.js'; export const renderMarkdown = text => DOMPurify.sanitize(marked.parse(text||'',{breaks:true}),{ADD_ATTR:['target'],FORBID_TAGS:['style','iframe','form','input','button','img','video','audio','source','object','embed'],FORBID_ATTR:['style']}); function plainInline(s) {return s.replace(/\[([^\]]+)\]\([^)]*\)/g,'$1').replace(/[*`_]/g,'');} function blocks(markdown) { return marked.lexer(markdown).filter(t=>t.type!=='space'); } export async function createArtifact(name,args) { validateArtifact(name,args); const type=name.slice(7),filename=safeFilename(args.title); let blob; if(type==='html') blob=new Blob([args.html],{type:'text/html;charset=utf-8'}); if(type==='document') { const {Document,Packer,Paragraph,TextRun,HeadingLevel,Table,TableRow,TableCell,WidthType,BorderStyle}=await import('docx'); const runs=s=>marked.lexer(s).flatMap(t=>t.tokens||[{type:'text',text:s}]).map(t=>new TextRun({text:t.text||t.raw||'',bold:t.type==='strong',italics:t.type==='em',font:t.type==='codespan'?'Consolas':'Calibri'})); const children=[new Paragraph({text:args.title,heading:HeadingLevel.TITLE,spacing:{after:320}})]; for(const t of blocks(args.markdown)) { if(t.type==='heading')children.push(new Paragraph({children:runs(t.text),heading:HeadingLevel[`HEADING_${Math.min(t.depth,3)}`],spacing:{before:220,after:140}})); else if(t.type==='list')t.items.forEach((item,i)=>children.push(new Paragraph({children:runs(t.ordered?`${i+1}. ${item.text}`:item.text),...(!t.ordered?{bullet:{level:0}}:{}),spacing:{after:120}}))); else if(t.type==='table')children.push(new Table({width:{size:100,type:WidthType.PERCENTAGE},rows:[t.header,...t.rows].map((row,i)=>new TableRow({children:row.map(c=>new TableCell({shading:i===0?{fill:'E8EAFE'}:undefined,children:[new Paragraph({children:[new TextRun({text:plainInline(c.text),bold:i===0})]})]}))}))})); else children.push(new Paragraph({children:t.type==='code'?[new TextRun({text:t.text,font:'Consolas',size:19})]:runs(t.text||t.raw),spacing:{after:160,line:290}})); } const document=new Document({creator:'MiniCPM5 · ProCreations',title:args.title,styles:{default:{document:{run:{font:'Calibri',size:22,color:'182039'}}},paragraphStyles:[{id:'Title',name:'Title',basedOn:'Normal',run:{size:52,bold:true,color:'343FB2'}},{id:'Heading1',name:'Heading 1',basedOn:'Normal',run:{size:32,bold:true,color:'343FB2'}},{id:'Heading2',name:'Heading 2',basedOn:'Normal',run:{size:27,bold:true,color:'343FB2'}}]},sections:[{properties:{page:{margin:{top:1100,bottom:1100,left:1100,right:1100}}},children}]}); blob=await Packer.toBlob(document); } if(type==='spreadsheet') { const mod=await import('exceljs');const ExcelJS=mod.default; const book=new ExcelJS.Workbook();book.creator='MiniCPM5 · ProCreations';book.calcProperties.fullCalcOnLoad=true; const names=new Set(); args.sheets.forEach((s,index)=>{ let n=String(s.name||`Sheet ${index+1}`).replace(/[\\/*?:\[\]]/g,' ').slice(0,27)||`Sheet ${index+1}`; if(names.has(n.toLowerCase()))n+=` ${index+1}`;names.add(n.toLowerCase()); const sheet=book.addWorksheet(n,{views:[{state:'frozen',ySplit:1}],properties:{tabColor:{argb:'FF6268F1'}}}); sheet.columns=s.columns.map((c,i)=>({header:String(c),key:String(i),width:Math.min(48,Math.max(17,String(c).length+4))})); s.rows.forEach(r=>sheet.addRow(r.map(v=>typeof v==='string'&&v.startsWith('=')?{formula:v.slice(1)}:v))); sheet.getRow(1).height=30;sheet.getRow(1).eachCell(c=>{c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF424BC6'}};c.font={bold:true,color:{argb:'FFFFFFFF'}};c.alignment={vertical:'middle'};}); sheet.eachRow((r,idx)=>{if(idx===1)return;r.height=25;r.eachCell(c=>{c.font={name:'Calibri',size:11,color:{argb:'FF243047'}};c.alignment={vertical:'middle',wrapText:true};if(idx%2===0)c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF1F3FC'}};});}); sheet.autoFilter={from:{row:1,column:1},to:{row:Math.max(1,s.rows.length+1),column:s.columns.length}}; }); blob=new Blob([await book.xlsx.writeBuffer()],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}); } if(type==='pdf') { const {jsPDF}=await import('jspdf'); const pdf=new jsPDF({unit:'mm',format:'a4',compress:true}); // Embed a real Unicode font (loaded only when a PDF is requested). const fontName=/[\u2e80-\u9fff]/.test(args.title+args.markdown)?'NotoSansSC-Regular.ttf':'NotoSans-Regular.ttf'; const font=await fetch('/fonts/'+fontName); if(!font.ok)throw new Error('PDF font could not load. Please try again.'); const bytes=new Uint8Array(await font.arrayBuffer());let binary='';for(let i=0;i{ pdf.setFontSize(size);pdf.setTextColor(color); const lines=pdf.splitTextToSize(plainInline(text),166); const height=size*0.47; for(const line of lines){if(y+height>274){pdf.addPage();y=24;}pdf.text(line,22,y);y+=height;}y+=gap; }; write(args.title,25,'#444cc4',10); for(const t of blocks(args.markdown)) { if(t.type==='heading')write(t.text,t.depth===1?18:14,'#444cc4',5); else if(t.type==='list') t.items.forEach((item,i)=>write(`${t.ordered?`${i+1}.`:'•'} ${item.text}`,11,'#25324b',3)); else if(t.type==='table') {write(t.header.map(c=>c.text).join(' | '),10,'#444cc4',3);t.rows.forEach(r=>write(r.map(c=>c.text).join(' | '),10,'#25324b',3));} else write(t.text||t.raw,11); } const pages=pdf.getNumberOfPages();for(let i=1;i<=pages;i++){pdf.setPage(i);pdf.setDrawColor('#e0e4ed');pdf.line(22,282,188,282);pdf.setFontSize(8);pdf.setTextColor('#7c879b');pdf.text('MiniCPM5 · Created on your device',22,288);pdf.text(`${i} / ${pages}`,188,288,{align:'right'});} pdf.setProperties({title:args.title,creator:'MiniCPM5 · ProCreations'});blob=pdf.output('blob'); } const ext={html:'html',document:'docx',spreadsheet:'xlsx',pdf:'pdf'}[type]; return {id:crypto.randomUUID(),type,title:args.title,filename:`${filename}.${ext}`,args,blob,url:URL.createObjectURL(blob),createdAt:Date.now()}; } export function previewMarkup(artifact) { if(artifact.type==='html')return ``; if(artifact.type==='pdf')return ``; if(artifact.type==='document')return `
MINICPM5 / DOCUMENT

${escapeHTML(artifact.title)}

${renderMarkdown(artifact.args.markdown)}
`; return `
${artifact.args.sheets.map((s,i)=>`
${escapeHTML(s.name||`Sheet ${i+1}`)}${s.rows.length} rows
${s.columns.map(c=>``).join('')}${s.rows.slice(0,200).map((r,ri)=>`${s.columns.map((_,ci)=>``).join('')}`).join('')}
${escapeHTML(c)}
${ri+1}${escapeHTML(r[ci]??'')}
${s.rows.length>200?'

Preview shows the first 200 rows. Download contains all rows.

':''}
`).join('')}

Formula cells are shown as formulas here and recalculate in Excel.

`; } export function downloadArtifact(a) {const link=document.createElement('a');link.href=a.url;link.download=a.filename;link.click();}