mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-29 11:02:28 +08:00
578 lines
17 KiB
JavaScript
578 lines
17 KiB
JavaScript
![]() |
"use strict";
|
||
|
|
||
|
const sql = require('../sql');
|
||
|
const utils = require('../../services/utils');
|
||
|
|
||
|
const LABEL = 'label';
|
||
|
const RELATION = 'relation';
|
||
|
|
||
|
class Note {
|
||
|
constructor(row) {
|
||
|
this.updateFromRow(row);
|
||
|
this.init();
|
||
|
}
|
||
|
|
||
|
updateFromRow(row) {
|
||
|
this.update([
|
||
|
row.noteId,
|
||
|
row.title,
|
||
|
row.type,
|
||
|
row.mime
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
update([noteId, title, type, mime]) {
|
||
|
/** @param {string} */
|
||
|
this.noteId = noteId;
|
||
|
/** @param {string} */
|
||
|
this.title = title;
|
||
|
/** @param {string} */
|
||
|
this.type = type;
|
||
|
/** @param {string} */
|
||
|
this.mime = mime;
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
init() {
|
||
|
/** @param {Branch[]} */
|
||
|
this.parentBranches = [];
|
||
|
/** @param {Note[]} */
|
||
|
this.parents = [];
|
||
|
/** @param {Note[]} */
|
||
|
this.children = [];
|
||
|
/** @param {Attribute[]} */
|
||
|
this.ownedAttributes = [];
|
||
|
|
||
|
/** @param {Attribute[]|null} */
|
||
|
this.__attributeCache = null;
|
||
|
/** @param {Attribute[]|null} */
|
||
|
this.inheritableAttributeCache = null;
|
||
|
|
||
|
/** @param {Attribute[]} */
|
||
|
this.targetRelations = [];
|
||
|
|
||
|
this.becca.addNote(this.noteId, this);
|
||
|
|
||
|
/** @param {Note[]|null} */
|
||
|
this.ancestorCache = null;
|
||
|
}
|
||
|
|
||
|
getParentBranches() {
|
||
|
return this.parentBranches;
|
||
|
}
|
||
|
|
||
|
getBranches() {
|
||
|
return this.parentBranches;
|
||
|
}
|
||
|
|
||
|
getParentNotes() {
|
||
|
return this.parents;
|
||
|
}
|
||
|
|
||
|
getChildNotes() {
|
||
|
return this.children;
|
||
|
}
|
||
|
|
||
|
hasChildren() {
|
||
|
return this.children && this.children.length > 0;
|
||
|
}
|
||
|
|
||
|
getChildBranches() {
|
||
|
return this.children.map(childNote => this.becca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
|
||
|
}
|
||
|
|
||
|
getContent(silentNotFoundError = false) {
|
||
|
const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]);
|
||
|
|
||
|
if (!row) {
|
||
|
if (silentNotFoundError) {
|
||
|
return undefined;
|
||
|
}
|
||
|
else {
|
||
|
throw new Error("Cannot find note content for noteId=" + this.noteId);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let content = row.content;
|
||
|
|
||
|
if (this.isStringNote()) {
|
||
|
return content === null
|
||
|
? ""
|
||
|
: content.toString("UTF-8");
|
||
|
}
|
||
|
else {
|
||
|
return content;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @returns {*} */
|
||
|
getJsonContent() {
|
||
|
const content = this.getContent();
|
||
|
|
||
|
if (!content || !content.trim()) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return JSON.parse(content);
|
||
|
}
|
||
|
|
||
|
/** @returns {boolean} true if this note is of application/json content type */
|
||
|
isJson() {
|
||
|
return this.mime === "application/json";
|
||
|
}
|
||
|
|
||
|
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
|
||
|
isJavaScript() {
|
||
|
return (this.type === "code" || this.type === "file")
|
||
|
&& (this.mime.startsWith("application/javascript")
|
||
|
|| this.mime === "application/x-javascript"
|
||
|
|| this.mime === "text/javascript");
|
||
|
}
|
||
|
|
||
|
/** @returns {boolean} true if this note is HTML */
|
||
|
isHtml() {
|
||
|
return ["code", "file", "render"].includes(this.type)
|
||
|
&& this.mime === "text/html";
|
||
|
}
|
||
|
|
||
|
/** @returns {boolean} true if the note has string content (not binary) */
|
||
|
isStringNote() {
|
||
|
return utils.isStringNote(this.type, this.mime);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [type] - (optional) attribute type to filter
|
||
|
* @param {string} [name] - (optional) attribute name to filter
|
||
|
* @returns {Attribute[]} all note's attributes, including inherited ones
|
||
|
*/
|
||
|
getAttributes(type, name) {
|
||
|
this.__getAttributes([]);
|
||
|
|
||
|
if (type && name) {
|
||
|
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
|
||
|
}
|
||
|
else if (type) {
|
||
|
return this.__attributeCache.filter(attr => attr.type === type);
|
||
|
}
|
||
|
else if (name) {
|
||
|
return this.__attributeCache.filter(attr => attr.name === name);
|
||
|
}
|
||
|
else {
|
||
|
return this.__attributeCache.slice();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
__getAttributes(path) {
|
||
|
if (path.includes(this.noteId)) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
if (!this.__attributeCache) {
|
||
|
const parentAttributes = this.ownedAttributes.slice();
|
||
|
const newPath = [...path, this.noteId];
|
||
|
|
||
|
if (this.noteId !== 'root') {
|
||
|
for (const parentNote of this.parents) {
|
||
|
parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const templateAttributes = [];
|
||
|
|
||
|
for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
|
||
|
if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') {
|
||
|
const templateNote = this.becca.notes[ownedAttr.value];
|
||
|
|
||
|
if (templateNote) {
|
||
|
templateAttributes.push(...templateNote.__getAttributes(newPath));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.__attributeCache = [];
|
||
|
|
||
|
const addedAttributeIds = new Set();
|
||
|
|
||
|
for (const attr of parentAttributes.concat(templateAttributes)) {
|
||
|
if (!addedAttributeIds.has(attr.attributeId)) {
|
||
|
addedAttributeIds.add(attr.attributeId);
|
||
|
|
||
|
this.__attributeCache.push(attr);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.inheritableAttributeCache = [];
|
||
|
|
||
|
for (const attr of this.__attributeCache) {
|
||
|
if (attr.isInheritable) {
|
||
|
this.inheritableAttributeCache.push(attr);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return this.__attributeCache;
|
||
|
}
|
||
|
|
||
|
/** @return {Attribute[]} */
|
||
|
__getInheritableAttributes(path) {
|
||
|
if (path.includes(this.noteId)) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
if (!this.inheritableAttributeCache) {
|
||
|
this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
|
||
|
}
|
||
|
|
||
|
return this.inheritableAttributeCache;
|
||
|
}
|
||
|
|
||
|
hasAttribute(type, name) {
|
||
|
return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
|
||
|
}
|
||
|
|
||
|
getAttributeCaseInsensitive(type, name, value) {
|
||
|
name = name.toLowerCase();
|
||
|
value = value ? value.toLowerCase() : null;
|
||
|
|
||
|
return this.getAttributes().find(
|
||
|
attr => attr.type === type
|
||
|
&& attr.name.toLowerCase() === name
|
||
|
&& (!value || attr.value.toLowerCase() === value));
|
||
|
}
|
||
|
|
||
|
getRelationTarget(name) {
|
||
|
const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name);
|
||
|
|
||
|
return relation ? relation.targetNote : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - label name
|
||
|
* @returns {boolean} true if label exists (including inherited)
|
||
|
*/
|
||
|
hasLabel(name) { return this.hasAttribute(LABEL, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - label name
|
||
|
* @returns {boolean} true if label exists (excluding inherited)
|
||
|
*/
|
||
|
hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - relation name
|
||
|
* @returns {boolean} true if relation exists (including inherited)
|
||
|
*/
|
||
|
hasRelation(name) { return this.hasAttribute(RELATION, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - relation name
|
||
|
* @returns {boolean} true if relation exists (excluding inherited)
|
||
|
*/
|
||
|
hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - label name
|
||
|
* @returns {Attribute|null} label if it exists, null otherwise
|
||
|
*/
|
||
|
getLabel(name) { return this.getAttribute(LABEL, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - label name
|
||
|
* @returns {Attribute|null} label if it exists, null otherwise
|
||
|
*/
|
||
|
getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - relation name
|
||
|
* @returns {Attribute|null} relation if it exists, null otherwise
|
||
|
*/
|
||
|
getRelation(name) { return this.getAttribute(RELATION, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - relation name
|
||
|
* @returns {Attribute|null} relation if it exists, null otherwise
|
||
|
*/
|
||
|
getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - label name
|
||
|
* @returns {string|null} label value if label exists, null otherwise
|
||
|
*/
|
||
|
getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - label name
|
||
|
* @returns {string|null} label value if label exists, null otherwise
|
||
|
*/
|
||
|
getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - relation name
|
||
|
* @returns {string|null} relation value if relation exists, null otherwise
|
||
|
*/
|
||
|
getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} name - relation name
|
||
|
* @returns {string|null} relation value if relation exists, null otherwise
|
||
|
*/
|
||
|
getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
|
||
|
|
||
|
/**
|
||
|
* @param {string} type - attribute type (label, relation, etc.)
|
||
|
* @param {string} name - attribute name
|
||
|
* @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
|
||
|
*/
|
||
|
hasOwnedAttribute(type, name) {
|
||
|
return !!this.getOwnedAttribute(type, name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} type - attribute type (label, relation, etc.)
|
||
|
* @param {string} name - attribute name
|
||
|
* @returns {Attribute} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
|
||
|
*/
|
||
|
getAttribute(type, name) {
|
||
|
const attributes = this.getAttributes();
|
||
|
|
||
|
return attributes.find(attr => attr.type === type && attr.name === name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} type - attribute type (label, relation, etc.)
|
||
|
* @param {string} name - attribute name
|
||
|
* @returns {string|null} attribute value of given type and name or null if no such attribute exists.
|
||
|
*/
|
||
|
getAttributeValue(type, name) {
|
||
|
const attr = this.getAttribute(type, name);
|
||
|
|
||
|
return attr ? attr.value : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} type - attribute type (label, relation, etc.)
|
||
|
* @param {string} name - attribute name
|
||
|
* @returns {string|null} attribute value of given type and name or null if no such attribute exists.
|
||
|
*/
|
||
|
getOwnedAttributeValue(type, name) {
|
||
|
const attr = this.getOwnedAttribute(type, name);
|
||
|
|
||
|
return attr ? attr.value : null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [name] - label name to filter
|
||
|
* @returns {Attribute[]} all note's labels (attributes with type label), including inherited ones
|
||
|
*/
|
||
|
getLabels(name) {
|
||
|
return this.getAttributes(LABEL, name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [name] - label name to filter
|
||
|
* @returns {string[]} all note's label values, including inherited ones
|
||
|
*/
|
||
|
getLabelValues(name) {
|
||
|
return this.getLabels(name).map(l => l.value);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [name] - label name to filter
|
||
|
* @returns {Attribute[]} all note's labels (attributes with type label), excluding inherited ones
|
||
|
*/
|
||
|
getOwnedLabels(name) {
|
||
|
return this.getOwnedAttributes(LABEL, name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [name] - label name to filter
|
||
|
* @returns {string[]} all note's label values, excluding inherited ones
|
||
|
*/
|
||
|
getOwnedLabelValues(name) {
|
||
|
return this.getOwnedAttributes(LABEL, name).map(l => l.value);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [name] - relation name to filter
|
||
|
* @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
|
||
|
*/
|
||
|
getRelations(name) {
|
||
|
return this.getAttributes(RELATION, name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [name] - relation name to filter
|
||
|
* @returns {Attribute[]} all note's relations (attributes with type relation), excluding inherited ones
|
||
|
*/
|
||
|
getOwnedRelations(name) {
|
||
|
return this.getOwnedAttributes(RELATION, name);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} [type] - (optional) attribute type to filter
|
||
|
* @param {string} [name] - (optional) attribute name to filter
|
||
|
* @returns {Attribute[]} note's "owned" attributes - excluding inherited ones
|
||
|
*/
|
||
|
getOwnedAttributes(type, name) {
|
||
|
// it's a common mistake to include # or ~ into attribute name
|
||
|
if (name && ["#", "~"].includes(name[0])) {
|
||
|
name = name.substr(1);
|
||
|
}
|
||
|
|
||
|
if (type && name) {
|
||
|
return this.ownedAttributes.filter(attr => attr.type === type && attr.name === name);
|
||
|
}
|
||
|
else if (type) {
|
||
|
return this.ownedAttributes.filter(attr => attr.type === type);
|
||
|
}
|
||
|
else if (name) {
|
||
|
return this.ownedAttributes.filter(attr => attr.name === name);
|
||
|
}
|
||
|
else {
|
||
|
return this.ownedAttributes.slice();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {Attribute} attribute belonging to this specific note (excludes inherited attributes)
|
||
|
*
|
||
|
* This method can be significantly faster than the getAttribute()
|
||
|
*/
|
||
|
getOwnedAttribute(type, name) {
|
||
|
const attrs = this.getOwnedAttributes(type, name);
|
||
|
|
||
|
return attrs.length > 0 ? attrs[0] : null;
|
||
|
}
|
||
|
|
||
|
get isArchived() {
|
||
|
return this.hasAttribute('label', 'archived');
|
||
|
}
|
||
|
|
||
|
hasInheritableOwnedArchivedLabel() {
|
||
|
return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
|
||
|
}
|
||
|
|
||
|
// will sort the parents so that non-search & non-archived are first and archived at the end
|
||
|
// this is done so that non-search & non-archived paths are always explored as first when looking for note path
|
||
|
resortParents() {
|
||
|
this.parentBranches.sort((a, b) =>
|
||
|
a.branchId.startsWith('virt-')
|
||
|
|| a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1);
|
||
|
|
||
|
this.parents = this.parentBranches.map(branch => branch.parentNote);
|
||
|
}
|
||
|
|
||
|
isTemplate() {
|
||
|
return !!this.targetRelations.find(rel => rel.name === 'template');
|
||
|
}
|
||
|
|
||
|
/** @return {Note[]} */
|
||
|
getSubtreeNotesIncludingTemplated() {
|
||
|
const arr = [[this]];
|
||
|
|
||
|
for (const childNote of this.children) {
|
||
|
arr.push(childNote.getSubtreeNotesIncludingTemplated());
|
||
|
}
|
||
|
|
||
|
for (const targetRelation of this.targetRelations) {
|
||
|
if (targetRelation.name === 'template') {
|
||
|
const note = targetRelation.note;
|
||
|
|
||
|
if (note) {
|
||
|
arr.push(note.getSubtreeNotesIncludingTemplated());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return arr.flat();
|
||
|
}
|
||
|
|
||
|
/** @return {Note[]} */
|
||
|
getSubtreeNotes(includeArchived = true) {
|
||
|
const noteSet = new Set();
|
||
|
|
||
|
function addSubtreeNotesInner(note) {
|
||
|
if (!includeArchived && note.isArchived) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
noteSet.add(note);
|
||
|
|
||
|
for (const childNote of note.children) {
|
||
|
addSubtreeNotesInner(childNote);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
addSubtreeNotesInner(this);
|
||
|
|
||
|
return Array.from(noteSet);
|
||
|
}
|
||
|
|
||
|
/** @return {String[]} */
|
||
|
getSubtreeNoteIds() {
|
||
|
return this.getSubtreeNotes().map(note => note.noteId);
|
||
|
}
|
||
|
|
||
|
getDescendantNoteIds() {
|
||
|
return this.getSubtreeNoteIds();
|
||
|
}
|
||
|
|
||
|
getAncestors() {
|
||
|
if (!this.ancestorCache) {
|
||
|
const noteIds = new Set();
|
||
|
this.ancestorCache = [];
|
||
|
|
||
|
for (const parent of this.parents) {
|
||
|
if (!noteIds.has(parent.noteId)) {
|
||
|
this.ancestorCache.push(parent);
|
||
|
noteIds.add(parent.noteId);
|
||
|
}
|
||
|
|
||
|
for (const ancestorNote of parent.getAncestors()) {
|
||
|
if (!noteIds.has(ancestorNote.noteId)) {
|
||
|
this.ancestorCache.push(ancestorNote);
|
||
|
noteIds.add(ancestorNote.noteId);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return this.ancestorCache;
|
||
|
}
|
||
|
|
||
|
getTargetRelations() {
|
||
|
return this.targetRelations;
|
||
|
}
|
||
|
|
||
|
/** @return {Note[]} - returns only notes which are templated, does not include their subtrees
|
||
|
* in effect returns notes which are influenced by note's non-inheritable attributes */
|
||
|
getTemplatedNotes() {
|
||
|
const arr = [this];
|
||
|
|
||
|
for (const targetRelation of this.targetRelations) {
|
||
|
if (targetRelation.name === 'template') {
|
||
|
const note = targetRelation.note;
|
||
|
|
||
|
if (note) {
|
||
|
arr.push(note);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param ancestorNoteId
|
||
|
* @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
|
||
|
*/
|
||
|
isDescendantOfNote(ancestorNoteId) {
|
||
|
const notePaths = this.getAllNotePaths();
|
||
|
|
||
|
return notePaths.some(path => path.includes(ancestorNoteId));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Note;
|