import {LitElement, html, unsafeHTML} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; import * as commonmark from './commonmark.min.js'; class TfWikiDocElement extends LitElement { static get properties() { return { whoami: {type: String}, wiki: {type: Object}, value: {type: Object}, blob: {type: String}, blob_original: {type: String}, blob_for_value: {type: String}, is_editing: {type: Boolean}, }; } constructor() { super(); } markdown(md) { let reader = new commonmark.Parser({safe: true}); let writer = new commonmark.HtmlRenderer(); let parsed = reader.parse(md || ''); let walker = parsed.walker(); let event; while ((event = walker.next())) { let node = event.node; if (event.entering) { if (node.destination?.startsWith('&')) { node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal; } else if (node.type === 'link') { if ( node.destination.indexOf(':') == -1 && node.destination.indexOf('/') == -1 ) { node.destination = `#${this.wiki?.name}/${node.destination}`; } } } } return writer.render(parsed); } title(md) { let lines = (md || '').split('\n'); for (let line of lines) { let m = line.match(/#+ (.*)/); if (m) { return m[1]; } } } summary(md) { let lines = (md || '').split('\n'); let result = []; let have_content = false; for (let line of lines) { if (have_content && !line.trim().length) { return result.join('\n'); } if (!line.startsWith('#') && line.trim().length) { have_content = true; } if (!line.startsWith('#')) { result.push(line); } } return result.join('\n'); } thumbnail(md) { let m = md ? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/) : undefined; return m ? m[1] : undefined; } async load_blob() { let blob = await tfrpc.rpc.get_blob(this.value?.blob); if (!blob) { console.warn( "no blob found, we're going to assume the document is empty (load_blob())" ); } else if (blob.endsWith('.box')) { let d = await tfrpc.rpc.try_decrypt(this.whoami, blob); if (d) { blob = d; } } this.blob = blob; this.blob_original = blob; } on_edit(event) { this.blob = event.srcElement.value; } on_discard(event) { this.blob = this.blob_original; this.is_editing = false; } async append_message(draft) { let blob = this.blob; if (draft || this.value?.private) { blob = await tfrpc.rpc.encrypt(this.whoami, this.wiki.editors, blob); } let id = await tfrpc.rpc.store_blob(blob); let message = { type: 'wiki-doc', key: this.value.id, parent: this.value.parent, blob: id, mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})), private: this.value?.private, }; if (draft) { message.recps = this.value.editors; message = await tfrpc.rpc.encrypt( this.whoami, this.value.editors, JSON.stringify(message) ); } await tfrpc.rpc.appendMessage(this.whoami, message); this.is_editing = false; } async on_save_draft() { return this.append_message(true); } async on_publish() { return this.append_message(false); } async on_blog_publish() { let blob = this.blob; let id = await tfrpc.rpc.store_blob(blob); let message = { type: 'blog', key: this.value.id, parent: this.value.parent, title: this.title(blob), summary: this.summary(blob), thumbnail: this.thumbnail(blob), blog: id, mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})), }; await tfrpc.rpc.appendMessage(this.whoami, message); this.is_editing = false; } convert_to_format(buffer, type, mime_type) { return new Promise(function (resolve, reject) { let img = new Image(); img.onload = function () { let canvas = document.createElement('canvas'); let width_scale = Math.min(img.width, 1024) / img.width; let height_scale = Math.min(img.height, 1024) / img.height; let scale = Math.min(width_scale, height_scale); canvas.width = img.width * scale; canvas.height = img.height * scale; let context = canvas.getContext('2d'); context.drawImage(img, 0, 0, canvas.width, canvas.height); let data_url = canvas.toDataURL(mime_type); let result = atob(data_url.split(',')[1]) .split('') .map((x) => x.charCodeAt(0)); resolve(result); }; img.onerror = function (event) { reject(new Error('Failed to load image.')); }; let raw = Array.from(new Uint8Array(buffer)) .map((b) => String.fromCharCode(b)) .join(''); let original = `data:${type};base64,${btoa(raw)}`; img.src = original; }); } humanSize(value) { let units = ['B', 'kB', 'MB', 'GB']; let i = 0; while (i < units.length - 1 && value >= 1024) { value /= 1024; i++; } return `${Math.round(value * 10) / 10} ${units[i]}`; } async add_file(editor, file) { try { let self = this; let buffer = await file.arrayBuffer(); let type = file.type; let insert; if (type.startsWith('image/')) { let best_buffer; let best_type; for (let format of ['image/png', 'image/jpeg', 'image/webp']) { let test_buffer = await self.convert_to_format( buffer, file.type, format ); if (!best_buffer || test_buffer.length < best_buffer.length) { best_buffer = test_buffer; best_type = format; } } buffer = best_buffer; type = best_type; let id = await tfrpc.rpc.store_blob(buffer); let name = type.split('/')[0] + ':' + file.name; insert = `\n![${name}](${id})`; } else { buffer = Array.from(new Uint8Array(buffer)); let id = await tfrpc.rpc.store_blob(buffer); let name = file.name; insert = `\n[${name}](${id}) (${this.humanSize(buffer.length)})`; } document.execCommand('insertText', false, insert); self.on_edit({srcElement: editor}); } catch (e) { alert(e?.message); } } paste(event) { let self = this; for (let item of event.clipboardData.items) { let file = item.getAsFile(); if (file) { self.add_file(event.srcElement, file); event.preventDefault(); } } } render() { let value = JSON.stringify(this.value); if (this.blob_for_value != value) { this.blob_for_value = value; this.blob = undefined; this.blob_original = undefined; this.load_blob(); } let self = this; let thumbnail_ref = this.thumbnail(this.blob); return html`
🔒 document is private
Summary

${unsafeHTML(this.markdown(this.title(this.blob)))}

${unsafeHTML(this.markdown(this.summary(this.blob)))}
${unsafeHTML(this.markdown(this.blob))}
`; } } customElements.define('tf-wiki-doc', TfWikiDocElement);