2023-11-01 20:29:07 -04:00
|
|
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
2023-10-29 20:22:30 -04:00
|
|
|
import * as tfrpc from '/static/tfrpc.js';
|
2023-11-15 20:33:00 -05:00
|
|
|
import * as commonmark from './commonmark.min.js';
|
2023-10-29 20:22:30 -04:00
|
|
|
|
|
|
|
class TfWikiDocElement extends LitElement {
|
|
|
|
static get properties() {
|
|
|
|
return {
|
|
|
|
whoami: {type: String},
|
2023-12-06 12:48:44 -05:00
|
|
|
wiki: {type: Object},
|
2023-10-29 20:22:30 -04:00
|
|
|
value: {type: Object},
|
|
|
|
blob: {type: String},
|
|
|
|
blob_original: {type: String},
|
2023-11-01 18:21:42 -04:00
|
|
|
blob_for_value: {type: String},
|
2023-11-01 20:29:07 -04:00
|
|
|
is_editing: {type: Boolean},
|
2023-10-29 20:22:30 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
2023-11-01 20:29:07 -04:00
|
|
|
markdown(md) {
|
2023-12-03 13:03:42 -05:00
|
|
|
let reader = new commonmark.Parser({safe: true});
|
|
|
|
let writer = new commonmark.HtmlRenderer();
|
|
|
|
let parsed = reader.parse(md || '');
|
2023-11-14 12:38:48 -05:00
|
|
|
let walker = parsed.walker();
|
|
|
|
let event;
|
|
|
|
while ((event = walker.next())) {
|
|
|
|
let node = event.node;
|
|
|
|
if (event.entering) {
|
2024-01-28 18:54:28 -05:00
|
|
|
if (node.destination?.startsWith('&')) {
|
2024-02-22 09:36:45 -05:00
|
|
|
node.destination =
|
|
|
|
'/' +
|
|
|
|
node.destination +
|
|
|
|
'/view?filename=' +
|
|
|
|
node.firstChild?.literal;
|
2024-01-28 18:54:28 -05:00
|
|
|
} else if (node.type === 'link') {
|
2024-02-22 09:36:45 -05:00
|
|
|
if (
|
|
|
|
node.destination.indexOf(':') == -1 &&
|
|
|
|
node.destination.indexOf('/') == -1
|
|
|
|
) {
|
2023-11-14 12:38:48 -05:00
|
|
|
node.destination = `#${this.wiki?.name}/${node.destination}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-01 20:29:07 -04:00
|
|
|
return writer.render(parsed);
|
|
|
|
}
|
|
|
|
|
2023-12-09 14:35:41 -05:00
|
|
|
title(md) {
|
|
|
|
let lines = (md || '').split('\n');
|
|
|
|
for (let line of lines) {
|
|
|
|
let m = line.match(/#+ (.*)/);
|
|
|
|
if (m) {
|
|
|
|
return m[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-09 13:35:42 -05:00
|
|
|
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;
|
|
|
|
}
|
2023-12-09 14:35:41 -05:00
|
|
|
if (!line.startsWith('#')) {
|
|
|
|
result.push(line);
|
|
|
|
}
|
2023-12-09 13:35:42 -05:00
|
|
|
}
|
|
|
|
return result.join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
thumbnail(md) {
|
2024-02-22 09:36:45 -05:00
|
|
|
let m = md
|
|
|
|
? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/)
|
|
|
|
: undefined;
|
2023-12-09 13:35:42 -05:00
|
|
|
return m ? m[1] : undefined;
|
|
|
|
}
|
|
|
|
|
2023-10-29 20:22:30 -04:00
|
|
|
async load_blob() {
|
2023-11-07 20:58:02 -05:00
|
|
|
let blob = await tfrpc.rpc.get_blob(this.value?.blob);
|
|
|
|
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;
|
2023-10-29 20:22:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
on_edit(event) {
|
|
|
|
this.blob = event.srcElement.value;
|
|
|
|
}
|
|
|
|
|
2023-11-01 20:29:07 -04:00
|
|
|
on_discard(event) {
|
|
|
|
this.blob = this.blob_original;
|
|
|
|
this.is_editing = false;
|
|
|
|
}
|
|
|
|
|
2023-11-05 18:25:55 -05:00
|
|
|
async append_message(draft) {
|
2023-11-07 20:58:02 -05:00
|
|
|
let blob = this.blob;
|
2023-12-04 12:50:13 -05:00
|
|
|
if (draft || this.value?.private) {
|
2023-12-06 12:48:44 -05:00
|
|
|
blob = await tfrpc.rpc.encrypt(this.whoami, this.wiki.editors, blob);
|
2023-11-07 20:58:02 -05:00
|
|
|
}
|
|
|
|
let id = await tfrpc.rpc.store_blob(blob);
|
2023-11-05 18:25:55 -05:00
|
|
|
let message = {
|
|
|
|
type: 'wiki-doc',
|
|
|
|
key: this.value.id,
|
|
|
|
parent: this.value.parent,
|
|
|
|
blob: id,
|
2024-02-22 09:36:45 -05:00
|
|
|
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})),
|
2023-12-04 12:50:13 -05:00
|
|
|
private: this.value?.private,
|
2023-11-05 18:25:55 -05:00
|
|
|
};
|
|
|
|
if (draft) {
|
|
|
|
message.recps = this.value.editors;
|
2024-02-22 09:36:45 -05:00
|
|
|
message = await tfrpc.rpc.encrypt(
|
|
|
|
this.whoami,
|
|
|
|
this.value.editors,
|
|
|
|
JSON.stringify(message)
|
|
|
|
);
|
2023-11-05 18:25:55 -05:00
|
|
|
}
|
|
|
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
2023-11-01 20:45:20 -04:00
|
|
|
this.is_editing = false;
|
|
|
|
}
|
|
|
|
|
2023-11-05 18:25:55 -05:00
|
|
|
async on_save_draft() {
|
|
|
|
return this.append_message(true);
|
|
|
|
}
|
|
|
|
|
2023-11-01 20:29:07 -04:00
|
|
|
async on_publish() {
|
2023-11-05 18:25:55 -05:00
|
|
|
return this.append_message(false);
|
2023-10-29 20:22:30 -04:00
|
|
|
}
|
|
|
|
|
2023-12-09 14:13:06 -05:00
|
|
|
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,
|
2023-12-09 14:35:41 -05:00
|
|
|
title: this.title(blob),
|
2023-12-09 14:13:06 -05:00
|
|
|
summary: this.summary(blob),
|
|
|
|
thumbnail: this.thumbnail(blob),
|
|
|
|
blog: id,
|
2024-02-22 09:36:45 -05:00
|
|
|
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})),
|
2023-12-09 14:13:06 -05:00
|
|
|
};
|
|
|
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
|
|
this.is_editing = false;
|
|
|
|
}
|
|
|
|
|
2023-12-03 13:03:42 -05:00
|
|
|
convert_to_format(buffer, type, mime_type) {
|
2024-02-22 09:36:45 -05:00
|
|
|
return new Promise(function (resolve, reject) {
|
2023-12-03 13:03:42 -05:00
|
|
|
let img = new Image();
|
2024-02-22 09:36:45 -05:00
|
|
|
img.onload = function () {
|
2023-12-03 13:03:42 -05:00
|
|
|
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);
|
2024-02-22 09:36:45 -05:00
|
|
|
let result = atob(data_url.split(',')[1])
|
|
|
|
.split('')
|
|
|
|
.map((x) => x.charCodeAt(0));
|
2023-12-03 13:03:42 -05:00
|
|
|
resolve(result);
|
|
|
|
};
|
2024-02-22 09:36:45 -05:00
|
|
|
img.onerror = function (event) {
|
2023-12-03 13:03:42 -05:00
|
|
|
reject(new Error('Failed to load image.'));
|
|
|
|
};
|
2024-02-22 09:36:45 -05:00
|
|
|
let raw = Array.from(new Uint8Array(buffer))
|
|
|
|
.map((b) => String.fromCharCode(b))
|
|
|
|
.join('');
|
2023-12-03 13:03:42 -05:00
|
|
|
let original = `data:${type};base64,${btoa(raw)}`;
|
|
|
|
img.src = original;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-01-23 21:58:53 -05:00
|
|
|
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]}`;
|
|
|
|
}
|
|
|
|
|
2023-12-03 13:03:42 -05:00
|
|
|
async add_file(editor, file) {
|
|
|
|
try {
|
|
|
|
let self = this;
|
|
|
|
let buffer = await file.arrayBuffer();
|
|
|
|
let type = file.type;
|
2024-01-23 21:58:53 -05:00
|
|
|
let insert;
|
2023-12-03 13:03:42 -05:00
|
|
|
if (type.startsWith('image/')) {
|
|
|
|
let best_buffer;
|
|
|
|
let best_type;
|
|
|
|
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
2024-02-22 09:36:45 -05:00
|
|
|
let test_buffer = await self.convert_to_format(
|
|
|
|
buffer,
|
|
|
|
file.type,
|
|
|
|
format
|
|
|
|
);
|
2023-12-03 13:03:42 -05:00
|
|
|
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
|
|
|
best_buffer = test_buffer;
|
|
|
|
best_type = format;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
buffer = best_buffer;
|
|
|
|
type = best_type;
|
2024-01-23 21:58:53 -05:00
|
|
|
let id = await tfrpc.rpc.store_blob(buffer);
|
|
|
|
let name = type.split('/')[0] + ':' + file.name;
|
|
|
|
insert = `\n![${name}](${id})`;
|
2023-12-03 13:03:42 -05:00
|
|
|
} else {
|
|
|
|
buffer = Array.from(new Uint8Array(buffer));
|
2024-01-23 21:58:53 -05:00
|
|
|
let id = await tfrpc.rpc.store_blob(buffer);
|
|
|
|
let name = file.name;
|
|
|
|
insert = `\n[${name}](${id}) (${this.humanSize(buffer.length)})`;
|
2023-12-03 13:03:42 -05:00
|
|
|
}
|
2024-01-23 21:58:53 -05:00
|
|
|
document.execCommand('insertText', false, insert);
|
2023-12-03 13:03:42 -05:00
|
|
|
self.on_edit({srcElement: editor});
|
2024-02-22 09:36:45 -05:00
|
|
|
} catch (e) {
|
2023-12-03 13:03:42 -05:00
|
|
|
alert(e?.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
paste(event) {
|
|
|
|
let self = this;
|
|
|
|
for (let item of event.clipboardData.items) {
|
2024-01-23 21:58:53 -05:00
|
|
|
let file = item.getAsFile();
|
|
|
|
if (file) {
|
2023-12-03 13:03:42 -05:00
|
|
|
self.add_file(event.srcElement, file);
|
2024-01-23 21:58:53 -05:00
|
|
|
event.preventDefault();
|
2023-12-03 13:03:42 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-29 20:22:30 -04:00
|
|
|
render() {
|
2023-11-01 18:21:42 -04:00
|
|
|
let value = JSON.stringify(this.value);
|
|
|
|
if (this.blob_for_value != value) {
|
|
|
|
this.blob_for_value = value;
|
|
|
|
this.blob = undefined;
|
|
|
|
this.blob_original = undefined;
|
2023-10-29 20:22:30 -04:00
|
|
|
this.load_blob();
|
|
|
|
}
|
2023-11-01 20:29:07 -04:00
|
|
|
let self = this;
|
2023-12-09 13:35:42 -05:00
|
|
|
let thumbnail_ref = this.thumbnail(this.blob);
|
2023-10-29 20:22:30 -04:00
|
|
|
return html`
|
2024-01-03 18:24:24 -05:00
|
|
|
<style>
|
2024-02-22 09:36:45 -05:00
|
|
|
a:link {
|
|
|
|
color: #268bd2;
|
|
|
|
}
|
|
|
|
a:visited {
|
|
|
|
color: #6c71c4;
|
|
|
|
}
|
|
|
|
a:hover {
|
|
|
|
color: #859900;
|
|
|
|
}
|
|
|
|
a:active {
|
|
|
|
color: #2aa198;
|
|
|
|
}
|
2024-01-03 18:24:24 -05:00
|
|
|
</style>
|
2023-11-01 20:29:07 -04:00
|
|
|
<div style="display: inline-flex; flex-direction: row">
|
2024-02-22 09:36:45 -05:00
|
|
|
<button
|
|
|
|
?disabled=${!this.whoami || this.is_editing}
|
|
|
|
@click=${() => (self.is_editing = true)}
|
|
|
|
>
|
|
|
|
Edit
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
?disabled=${this.blob == this.blob_original}
|
|
|
|
@click=${this.on_save_draft}
|
|
|
|
>
|
|
|
|
Save Draft
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
?disabled=${this.blob == this.blob_original && !this.value?.draft}
|
|
|
|
@click=${this.on_publish}
|
|
|
|
>
|
|
|
|
Publish
|
|
|
|
</button>
|
|
|
|
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>
|
|
|
|
Discard
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
?disabled=${!this.is_editing}
|
|
|
|
@click=${() =>
|
|
|
|
(self.value = Object.assign({}, self.value, {
|
|
|
|
private: !self.value.private,
|
|
|
|
}))}
|
|
|
|
>
|
|
|
|
${this.value?.private ? 'Make Public' : 'Make Private'}
|
|
|
|
</button>
|
|
|
|
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>
|
|
|
|
Publish Blog
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<div ?hidden=${!this.value?.private} style="color: #800">
|
|
|
|
🔒 document is private
|
2023-11-01 20:29:07 -04:00
|
|
|
</div>
|
2024-02-22 09:36:45 -05:00
|
|
|
<div
|
|
|
|
style="display: flex; flex-direction: row; ${this.value?.private
|
|
|
|
? 'border-top: 4px solid #800'
|
|
|
|
: ''}"
|
|
|
|
>
|
2023-11-01 19:39:34 -04:00
|
|
|
<textarea
|
2023-11-01 20:29:07 -04:00
|
|
|
?hidden=${!this.is_editing}
|
2024-02-22 09:36:45 -05:00
|
|
|
style="flex: 1 1; min-height: 10em; ${this.value?.private
|
|
|
|
? 'border: 4px solid #800'
|
|
|
|
: ''}"
|
2023-12-03 13:03:42 -05:00
|
|
|
@input=${this.on_edit}
|
|
|
|
@paste=${this.paste}
|
2024-02-22 09:36:45 -05:00
|
|
|
.value=${this.blob ?? ''}
|
|
|
|
></textarea>
|
2023-12-09 13:35:42 -05:00
|
|
|
<div style="flex: 1 1">
|
2024-02-22 09:36:45 -05:00
|
|
|
<div
|
|
|
|
?hidden=${!this.is_editing}
|
|
|
|
style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em"
|
|
|
|
>
|
|
|
|
<img
|
|
|
|
?hidden=${!thumbnail_ref}
|
|
|
|
style="max-width: 128px; max-height: 128px; float: right"
|
|
|
|
src="/${thumbnail_ref}/view"
|
|
|
|
/>
|
|
|
|
<h1 ?hidden=${!this.title(this.blob)}>
|
|
|
|
${unsafeHTML(this.markdown(this.title(this.blob)))}
|
|
|
|
</h1>
|
2023-12-09 13:35:42 -05:00
|
|
|
${unsafeHTML(this.markdown(this.summary(this.blob)))}
|
|
|
|
</div>
|
|
|
|
${unsafeHTML(this.markdown(this.blob))}
|
|
|
|
</div>
|
2023-11-01 19:39:34 -04:00
|
|
|
</div>
|
2023-10-29 20:22:30 -04:00
|
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-22 09:36:45 -05:00
|
|
|
customElements.define('tf-wiki-doc', TfWikiDocElement);
|