From 2467464433e77cf626e7f2d41cb9b6c9726622b2 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 1 Nov 2022 22:49:37 +0100 Subject: [PATCH] add possibility to define a share index, closes #3265 --- .../attribute_widgets/attribute_detail.js | 1 + src/services/builtin_attributes.js | 1 + src/share/content_renderer.js | 173 ++++++++++-------- src/share/routes.js | 2 +- src/share/shaca/entities/attribute.js | 13 +- src/share/shaca/entities/branch.js | 1 + src/share/shaca/entities/note.js | 29 ++- src/share/shaca/shaca.js | 14 +- 8 files changed, 147 insertions(+), 87 deletions(-) diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js index fb15b92ef..027fa5429 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js @@ -223,6 +223,7 @@ const ATTR_HELP = { "shareRaw": "note will be served in its raw format, without HTML wrapper", "shareDisallowRobotIndexing": `will forbid robot indexing of this note via X-Robots-Tag: noindex header`, "shareCredentials": "require credentials to access this shared note. Value is expected to be in format 'username:password'. Don't forget to make this inheritable to apply to child-notes/images.", + "shareIndex": "note with this this label will list all roots of shared notes", "displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.", "hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.", "titleTemplate": `default title of notes created as children of this note. The value is evaluated as JavaScript string diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js index c25e10d05..d538a169f 100644 --- a/src/services/builtin_attributes.js +++ b/src/services/builtin_attributes.js @@ -54,6 +54,7 @@ module.exports = [ { type: 'label', name: 'shareRaw' }, { type: 'label', name: 'shareDisallowRobotIndexing' }, { type: 'label', name: 'shareCredentials' }, + { type: 'label', name: 'shareIndex' }, { type: 'label', name: 'displayRelations' }, { type: 'label', name: 'hideRelations' }, { type: 'label', name: 'titleTemplate', isDangerous: true }, diff --git a/src/share/content_renderer.js b/src/share/content_renderer.js index a795ee084..bd2a5734e 100644 --- a/src/share/content_renderer.js +++ b/src/share/content_renderer.js @@ -1,6 +1,7 @@ const {JSDOM} = require("jsdom"); const shaca = require("./shaca/shaca"); const assetPath = require("../services/asset_path"); +const shareRoot = require('./share_root'); function getContent(note) { if (note.isProtected) { @@ -11,40 +12,74 @@ function getContent(note) { }; } - let content = note.getContent(); - let header = ''; - let isEmpty = false; + const result = { + content: note.getContent(), + header: '', + isEmpty: false + }; if (note.type === 'text') { - const document = new JSDOM(content || "").window.document; + renderText(result, note); + } else if (note.type === 'code') { + renderCode(result); + } else if (note.type === 'mermaid') { + renderMermaid(result); + } else if (note.type === 'image') { + renderImage(result, note); + } else if (note.type === 'file') { + renderFile(note, result); + } else if (note.type === 'book') { + result.isEmpty = true; + } else if (note.type === 'canvas') { + renderCanvas(result, note); + } else { + result.content = '

This note type cannot be displayed.

'; + } - isEmpty = document.body.textContent.trim().length === 0 - && document.querySelectorAll("img").length === 0; + return result; +} - if (!isEmpty) { - for (const linkEl of document.querySelectorAll("a")) { - const href = linkEl.getAttribute("href"); +function renderIndex(result) { + result.content += ''; +} + +function renderText(result, note) { + const document = new JSDOM(result.content || "").window.document; + + result.isEmpty = document.body.textContent.trim().length === 0 + && document.querySelectorAll("img").length === 0; + + if (!result.isEmpty) { + for (const linkEl of document.querySelectorAll("a")) { + const href = linkEl.getAttribute("href"); + + if (href?.startsWith("#")) { + const notePathSegments = href.split("/"); + + const noteId = notePathSegments[notePathSegments.length - 1]; + const linkedNote = shaca.getNote(noteId); + + if (linkedNote) { + linkEl.setAttribute("href", linkedNote.shareId); + linkEl.classList.add("type-" + linkedNote.type); + } else { + linkEl.removeAttribute("href"); } } + } - content = document.body.innerHTML; + result.content = document.body.innerHTML; - if (content.includes(``)) { - header += ` + if (result.content.includes(``)) { + result.header += ` @@ -54,54 +89,58 @@ document.addEventListener("DOMContentLoaded", function() { renderMathInElement(document.getElementById('content')); }); `; - } + } + + if (note.hasLabel("shareIndex")) { + renderIndex(result); } } - else if (note.type === 'code') { - if (!content?.trim()) { - isEmpty = true; - } - else { - const document = new JSDOM().window.document; +} - const preEl = document.createElement('pre'); - preEl.appendChild(document.createTextNode(content)); +function renderCode(result) { + if (!result.content?.trim()) { + result.isEmpty = true; + } else { + const document = new JSDOM().window.document; - content = preEl.outerHTML; - } + const preEl = document.createElement('pre'); + preEl.appendChild(document.createTextNode(result.content)); + + result.content = preEl.outerHTML; } - else if (note.type === 'mermaid') { - content = ` -
${content}
+} + +function renderMermaid(result) { + result.content = ` +
${result.content}

Chart source -
${content}
+
${result.content}
` - header += ``; + result.header += ``; +} + +function renderImage(result, note) { + result.content = ``; +} + +function renderFile(note, result) { + if (note.mime === 'application/pdf') { + result.content = `` + } else { + result.content = ``; } - else if (note.type === 'image') { - content = ``; - } - else if (note.type === 'file') { - if (note.mime === 'application/pdf') { - content = `` - } - else { - content = ``; - } - } - else if (note.type === 'book') { - isEmpty = true; - } - else if (note.type === 'canvas') { - header += ``; - header += ``; - header += ``; - header += ``; - header += ``; - content = `
+ result.content = `

- Get Image Link + Get Image Link
`; - } - else { - content = '

This note type cannot be displayed.

'; - } - - return { - header, - content, - isEmpty - }; } module.exports = { diff --git a/src/share/routes.js b/src/share/routes.js index 11917afe1..34f0a8f5c 100644 --- a/src/share/routes.js +++ b/src/share/routes.js @@ -88,7 +88,7 @@ function register(router) { addNoIndexHeader(note, res); - if (note.hasLabel('shareRaw') || ['image', 'file'].includes(note.type)) { + if (note.hasLabel('shareRaw')) { res.setHeader('Content-Type', note.mime) .send(note.getContent()); diff --git a/src/share/shaca/entities/attribute.js b/src/share/shaca/entities/attribute.js index 1696312ab..365d1c434 100644 --- a/src/share/shaca/entities/attribute.js +++ b/src/share/shaca/entities/attribute.js @@ -49,39 +49,40 @@ class Attribute extends AbstractEntity { } } + /** @returns {boolean} */ get isAffectingSubtree() { return this.isInheritable || (this.type === 'relation' && this.name === 'template'); } + /** @returns {string} */ get targetNoteId() { // alias return this.type === 'relation' ? this.value : undefined; } + /** @returns {boolean} */ isAutoLink() { return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); } + /** @returns {Note|null} */ get note() { return this.shaca.notes[this.noteId]; } + /** @returns {Note|null} */ get targetNote() { if (this.type === 'relation') { return this.shaca.notes[this.value]; } } - /** - * @returns {Note|null} - */ + /** @returns {Note|null} */ getNote() { return this.shaca.getNote(this.noteId); } - /** - * @returns {Note|null} - */ + /** @returns {Note|null} */ getTargetNote() { if (this.type !== 'relation') { throw new Error(`Attribute ${this.attributeId} is not relation`); diff --git a/src/share/shaca/entities/branch.js b/src/share/shaca/entities/branch.js index a1b55538e..869a8ecd9 100644 --- a/src/share/shaca/entities/branch.js +++ b/src/share/shaca/entities/branch.js @@ -43,6 +43,7 @@ class Branch extends AbstractEntity { return this.shaca.notes[this.noteId]; } + /** @return {Note} */ getNote() { return this.childNote; } diff --git a/src/share/shaca/entities/note.js b/src/share/shaca/entities/note.js index 27f54fc44..ce5a5aa44 100644 --- a/src/share/shaca/entities/note.js +++ b/src/share/shaca/entities/note.js @@ -3,6 +3,7 @@ const sql = require('../../sql'); const utils = require('../../../services/utils'); const AbstractEntity = require('./abstract_entity'); +const escape = require('escape-html'); const LABEL = 'label'; const RELATION = 'relation'; @@ -47,22 +48,32 @@ class Note extends AbstractEntity { this.shaca.notes[this.noteId] = this; } + /** @returns {Branch[]} */ getParentBranches() { return this.parentBranches; } + /** @returns {Branch[]} */ getBranches() { return this.parentBranches; } + /** @returns {Branch[]} */ + getChildBranches() { + return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId)); + } + + /** @returns {Note[]} */ getParentNotes() { return this.parents; } + /** @returns {Note[]} */ getChildNotes() { return this.children; } + /** @returns {Note[]} */ getVisibleChildNotes() { return this.getChildBranches() .filter(branch => !branch.isHidden) @@ -70,18 +81,16 @@ class Note extends AbstractEntity { .filter(childNote => !childNote.hasLabel('shareHiddenFromTree')); } + /** @returns {boolean} */ hasChildren() { return this.children && this.children.length > 0; } + /** @returns {boolean} */ hasVisibleChildren() { return this.getVisibleChildNotes().length > 0; } - getChildBranches() { - return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId)); - } - getContent(silentNotFoundError = false) { const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]); @@ -133,6 +142,7 @@ class Note extends AbstractEntity { } } + /** @returns {Attribute[]} */ getCredentials() { this.__getAttributes([]); @@ -203,10 +213,12 @@ class Note extends AbstractEntity { return this.inheritableAttributeCache; } + /** @returns {boolean} */ hasAttribute(type, name) { return !!this.getAttributes().find(attr => attr.type === type && attr.name === name); } + /** @returns {Note|null} */ getRelationTarget(name) { const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name); @@ -411,22 +423,27 @@ class Note extends AbstractEntity { return attrs.length > 0 ? attrs[0] : null; } + /** @returns {boolean} */ get isArchived() { return this.hasAttribute('label', 'archived'); } + /** @returns {boolean} */ hasInheritableOwnedArchivedLabel() { return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable); } + /** @returns {boolean} */ isTemplate() { return !!this.targetRelations.find(rel => rel.name === 'template'); } + /** @returns {Attribute[]} */ getTargetRelations() { return this.targetRelations; } + /** @returns {string} */ get shareId() { if (this.hasOwnedLabel('shareRoot')) { return ""; @@ -437,6 +454,10 @@ class Note extends AbstractEntity { return sharedAlias || this.noteId; } + get escapedTitle() { + return escape(this.title); + } + getPojoWithAttributes() { return { noteId: this.noteId, diff --git a/src/share/shaca/shaca.js b/src/share/shaca/shaca.js index 56c77778b..360b1b169 100644 --- a/src/share/shaca/shaca.js +++ b/src/share/shaca/shaca.js @@ -23,14 +23,17 @@ class Shaca { this.loaded = false; } + /** @returns {Note|null} */ getNote(noteId) { return this.notes[noteId]; } + /** @returns {boolean} */ hasNote(noteId) { return noteId in this.notes; } + /** @returns {Note[]} */ getNotes(noteIds, ignoreMissing = false) { const filteredNotes = []; @@ -51,18 +54,21 @@ class Shaca { return filteredNotes; } + /** @returns {Branch|null} */ getBranch(branchId) { return this.branches[branchId]; } - getAttribute(attributeId) { - return this.attributes[attributeId]; - } - + /** @returns {Branch|null} */ getBranchFromChildAndParent(childNoteId, parentNoteId) { return this.childParentToBranch[`${childNoteId}-${parentNoteId}`]; } + /** @returns {Attribute|null} */ + getAttribute(attributeId) { + return this.attributes[attributeId]; + } + getEntity(entityName, entityId) { if (!entityName || !entityId) { return null;