This commit is contained in:
zadam
2023-02-17 16:30:14 +01:00
parent 97cadc3acf
commit 760c7b73ad
16 changed files with 5828 additions and 1 deletions

View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: entities/fattribute.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: entities/fattribute.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import promotedAttributeDefinitionParser from '../services/promoted_attribute_definition_parser.js';
/**
* Attribute is an abstract concept which has two real uses - label (key - value pair)
* and relation (representing named relationship between source and target note)
*/
class FAttribute {
constructor(froca, row) {
this.froca = froca;
this.update(row);
}
update(row) {
/** @type {string} */
this.attributeId = row.attributeId;
/** @type {string} */
this.noteId = row.noteId;
/** @type {string} */
this.type = row.type;
/** @type {string} */
this.name = row.name;
/** @type {string} */
this.value = row.value;
/** @type {int} */
this.position = row.position;
/** @type {boolean} */
this.isInheritable = !!row.isInheritable;
}
/** @returns {FNote} */
getNote() {
return this.froca.notes[this.noteId];
}
/** @returns {Promise&lt;FNote>} */
async getTargetNote() {
const targetNoteId = this.targetNoteId;
return await this.froca.getNote(targetNoteId, true);
}
get targetNoteId() { // alias
if (this.type !== 'relation') {
throw new Error(`Attribute ${this.attributeId} is not a relation`);
}
return this.value;
}
get isAutoLink() {
return this.type === 'relation' &amp;&amp; ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
}
get toString() {
return `FAttribute(attributeId=${this.attributeId}, type=${this.type}, name=${this.name}, value=${this.value})`;
}
isDefinition() {
return this.type === 'label' &amp;&amp; (this.name.startsWith('label:') || this.name.startsWith('relation:'));
}
getDefinition() {
return promotedAttributeDefinitionParser.parse(this.value);
}
isDefinitionFor(attr) {
return this.type === 'label' &amp;&amp; this.name === `${attr.type}:${attr.name}`;
}
get dto() {
const dto = Object.assign({}, this);
delete dto.froca;
return dto;
}
}
export default FAttribute;
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="FAttribute.html">FAttribute</a></li><li><a href="FBranch.html">FBranch</a></li><li><a href="FNote.html">FNote</a></li><li><a href="FNoteComplement.html">FNoteComplement</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.1</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: entities/fbranch.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: entities/fbranch.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
* parents.
*/
class FBranch {
constructor(froca, row) {
this.froca = froca;
this.update(row);
}
update(row) {
/**
* primary key
* @type {string}
*/
this.branchId = row.branchId;
/** @type {string} */
this.noteId = row.noteId;
/** @type {string} */
this.parentNoteId = row.parentNoteId;
/** @type {int} */
this.notePosition = row.notePosition;
/** @type {string} */
this.prefix = row.prefix;
/** @type {boolean} */
this.isExpanded = !!row.isExpanded;
/** @type {boolean} */
this.fromSearchNote = !!row.fromSearchNote;
}
/** @returns {FNote} */
async getNote() {
return this.froca.getNote(this.noteId);
}
/** @returns {FNote} */
getNoteFromCache() {
return this.froca.getNoteFromCache(this.noteId);
}
/** @returns {FNote} */
async getParentNote() {
return this.froca.getNote(this.parentNoteId);
}
/** @returns {boolean} true if it's top level, meaning its parent is root note */
isTopLevel() {
return this.parentNoteId === 'root';
}
get toString() {
return `FBranch(branchId=${this.branchId})`;
}
get pojo() {
const pojo = {...this};
delete pojo.froca;
return pojo;
}
}
export default FBranch;
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="FAttribute.html">FAttribute</a></li><li><a href="FBranch.html">FBranch</a></li><li><a href="FNote.html">FNote</a></li><li><a href="FNoteComplement.html">FNoteComplement</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.1</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@@ -0,0 +1,939 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: entities/fnote.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: entities/fnote.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import server from '../services/server.js';
import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js";
const LABEL = 'label';
const RELATION = 'relation';
const NOTE_TYPE_ICONS = {
"file": "bx bx-file",
"image": "bx bx-image",
"code": "bx bx-code",
"render": "bx bx-extension",
"search": "bx bx-file-find",
"relationMap": "bx bx-map-alt",
"book": "bx bx-book",
"noteMap": "bx bx-map-alt",
"mermaid": "bx bx-selection",
"canvas": "bx bx-pen",
"webView": "bx bx-globe-alt",
"launcher": "bx bx-link",
"doc": "bx bxs-file-doc",
"contentWidget": "bx bxs-widget"
};
class FNote {
/**
* @param {Froca} froca
* @param {Object.&lt;string, Object>} row
*/
constructor(froca, row) {
this.froca = froca;
/** @type {string[]} */
this.attributes = [];
/** @type {string[]} */
this.targetRelations = [];
/** @type {string[]} */
this.parents = [];
/** @type {string[]} */
this.children = [];
/** @type {Object.&lt;string, string>} */
this.parentToBranch = {};
/** @type {Object.&lt;string, string>} */
this.childToBranch = {};
this.update(row);
}
update(row) {
/** @type {string} */
this.noteId = row.noteId;
/** @type {string} */
this.title = row.title;
/** @type {boolean} */
this.isProtected = !!row.isProtected;
/**
* one of 'text', 'code', 'file' or 'render'
* @type {string}
*/
this.type = row.type;
/**
* content-type, e.g. "application/json"
* @type {string}
*/
this.mime = row.mime;
}
addParent(parentNoteId, branchId) {
if (parentNoteId === 'none') {
return;
}
if (!this.parents.includes(parentNoteId)) {
this.parents.push(parentNoteId);
}
this.parentToBranch[parentNoteId] = branchId;
}
addChild(childNoteId, branchId, sort = true) {
if (!(childNoteId in this.childToBranch)) {
this.children.push(childNoteId);
}
this.childToBranch[childNoteId] = branchId;
if (sort) {
this.sortChildren();
}
}
sortChildren() {
const branchIdPos = {};
for (const branchId of Object.values(this.childToBranch)) {
branchIdPos[branchId] = this.froca.getBranch(branchId).notePosition;
}
this.children.sort((a, b) => branchIdPos[this.childToBranch[a]] &lt; branchIdPos[this.childToBranch[b]] ? -1 : 1);
}
/** @returns {boolean} */
isJson() {
return this.mime === "application/json";
}
async getContent() {
// we're not caching content since these objects are in froca and as such pretty long-lived
const note = await server.get(`notes/${this.noteId}`);
return note.content;
}
async getJsonContent() {
const content = await this.getContent();
try {
return JSON.parse(content);
}
catch (e) {
console.log(`Cannot parse content of note '${this.noteId}': `, e.message);
return null;
}
}
/**
* @returns {string[]}
*/
getParentBranchIds() {
return Object.values(this.parentToBranch);
}
/**
* @returns {string[]}
* @deprecated use getParentBranchIds() instead
*/
getBranchIds() {
return this.getParentBranchIds();
}
/**
* @returns {FBranch[]}
*/
getParentBranches() {
const branchIds = Object.values(this.parentToBranch);
return this.froca.getBranches(branchIds);
}
/**
* @returns {FBranch[]}
* @deprecated use getParentBranches() instead
*/
getBranches() {
return this.getParentBranches();
}
/** @returns {boolean} */
hasChildren() {
return this.children.length > 0;
}
/** @returns {FBranch[]} */
getChildBranches() {
// don't use Object.values() to guarantee order
const branchIds = this.children.map(childNoteId => this.childToBranch[childNoteId]);
return this.froca.getBranches(branchIds);
}
/** @returns {string[]} */
getParentNoteIds() {
return this.parents;
}
/** @returns {FNote[]} */
getParentNotes() {
return this.froca.getNotesFromCache(this.parents);
}
// will sort the parents so that non-search &amp; non-archived are first and archived at the end
// this is done so that non-search &amp; non-archived paths are always explored as first when looking for note path
resortParents() {
this.parents.sort((aNoteId, bNoteId) => {
const aBranchId = this.parentToBranch[aNoteId];
if (aBranchId &amp;&amp; aBranchId.startsWith('virt-')) {
return 1;
}
const aNote = this.froca.getNoteFromCache([aNoteId]);
if (aNote.isArchived || aNote.isHiddenCompletely()) {
return 1;
}
return -1;
});
}
get isArchived() {
return this.hasAttribute('label', 'archived');
}
/** @returns {string[]} */
getChildNoteIds() {
return this.children;
}
/** @returns {Promise&lt;FNote[]>} */
async getChildNotes() {
return await this.froca.getNotes(this.children);
}
/**
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {FAttribute[]} all note's attributes, including inherited ones
*/
getOwnedAttributes(type, name) {
const attrs = this.attributes
.map(attributeId => this.froca.attributes[attributeId])
.filter(Boolean); // filter out nulls;
return this.__filterAttrs(attrs, type, name);
}
/**
* @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter
* @returns {FAttribute[]} all note's attributes, including inherited ones
*/
getAttributes(type, name) {
return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
}
__getCachedAttributes(path) {
// notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
// when template instance is a parent of template itself
if (path.includes(this.noteId)) {
return [];
}
if (!(this.noteId in noteAttributeCache.attributes)) {
const newPath = [...path, this.noteId];
const attrArrs = [ this.getOwnedAttributes() ];
// inheritable attrs on root are typically not intended to be applied to hidden subtree #3537
if (this.noteId !== 'root' &amp;&amp; this.noteId !== '_hidden') {
for (const parentNote of this.getParentNotes()) {
// these virtual parent-child relationships are also loaded into froca
if (parentNote.type !== 'search') {
attrArrs.push(parentNote.__getInheritableAttributes(newPath));
}
}
}
for (const templateAttr of attrArrs.flat().filter(attr => attr.type === 'relation' &amp;&amp; ['template', 'inherit'].includes(attr.name))) {
const templateNote = this.froca.notes[templateAttr.value];
if (templateNote &amp;&amp; templateNote.noteId !== this.noteId) {
attrArrs.push(
templateNote.__getCachedAttributes(newPath)
// template attr is used as a marker for templates, but it's not meant to be inherited
.filter(attr => !(attr.type === 'label' &amp;&amp; (attr.name === 'template' || attr.name === 'workspacetemplate')))
);
}
}
noteAttributeCache.attributes[this.noteId] = [];
const addedAttributeIds = new Set();
for (const attr of attrArrs.flat()) {
if (!addedAttributeIds.has(attr.attributeId)) {
addedAttributeIds.add(attr.attributeId);
noteAttributeCache.attributes[this.noteId].push(attr);
}
}
}
return noteAttributeCache.attributes[this.noteId];
}
isRoot() {
return this.noteId === 'root';
}
getAllNotePaths(encounteredNoteIds = null) {
if (this.noteId === 'root') {
return [['root']];
}
if (!encounteredNoteIds) {
encounteredNoteIds = new Set();
}
encounteredNoteIds.add(this.noteId);
const parentNotes = this.getParentNotes();
let paths;
if (parentNotes.length === 1) { // optimization for the most common case
if (encounteredNoteIds.has(parentNotes[0].noteId)) {
return [];
}
else {
paths = parentNotes[0].getAllNotePaths(encounteredNoteIds);
}
}
else {
paths = [];
for (const parentNote of parentNotes) {
if (encounteredNoteIds.has(parentNote.noteId)) {
continue;
}
const newSet = new Set(encounteredNoteIds);
paths.push(...parentNote.getAllNotePaths(newSet));
}
}
for (const path of paths) {
path.push(this.noteId);
}
return paths;
}
getSortedNotePaths(hoistedNotePath = 'root') {
const notePaths = this.getAllNotePaths().map(path => ({
notePath: path,
isInHoistedSubTree: path.includes(hoistedNotePath),
isArchived: path.find(noteId => froca.notes[noteId].isArchived),
isSearch: path.find(noteId => froca.notes[noteId].type === 'search'),
isHidden: path.includes('_hidden')
}));
notePaths.sort((a, b) => {
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
return a.isInHoistedSubTree ? -1 : 1;
} else if (a.isSearch !== b.isSearch) {
return a.isSearch ? 1 : -1;
} else if (a.isArchived !== b.isArchived) {
return a.isArchived ? 1 : -1;
} else if (a.isHidden !== b.isHidden) {
return a.isHidden ? 1 : -1;
} else {
return a.notePath.length - b.notePath.length;
}
});
return notePaths;
}
/**
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
*/
isHiddenCompletely() {
if (this.noteId === 'root') {
return false;
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.noteId === 'root') {
return false;
} else if (parentNote.noteId === '_hidden') {
continue;
}
if (!parentNote.isHiddenCompletely()) {
return false;
}
}
return true;
}
__filterAttrs(attributes, type, name) {
this.__validateTypeName(type, name);
if (!type &amp;&amp; !name) {
return attributes;
} else if (type &amp;&amp; name) {
return attributes.filter(attr => attr.name === name &amp;&amp; attr.type === type);
} else if (type) {
return attributes.filter(attr => attr.type === type);
} else if (name) {
return attributes.filter(attr => attr.name === name);
}
}
__getInheritableAttributes(path) {
const attrs = this.__getCachedAttributes(path);
return attrs.filter(attr => attr.isInheritable);
}
__validateTypeName(type, name) {
if (type &amp;&amp; type !== 'label' &amp;&amp; type !== 'relation') {
throw new Error(`Unrecognized attribute type '${type}'. Only 'label' and 'relation' are possible values.`);
}
if (name) {
const firstLetter = name.charAt(0);
if (firstLetter === '#' || firstLetter === '~') {
throw new Error(`Detect '#' or '~' in the attribute's name. In the API, attribute names should be set without these characters.`);
}
}
}
/**
* @param {string} [name] - label name to filter
* @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones
*/
getOwnedLabels(name) {
return this.getOwnedAttributes(LABEL, name);
}
/**
* @param {string} [name] - label name to filter
* @returns {FAttribute[]} all note's labels (attributes with type label), including inherited ones
*/
getLabels(name) {
return this.getAttributes(LABEL, name);
}
getIcon() {
const iconClassLabels = this.getLabels('iconClass');
const workspaceIconClass = this.getWorkspaceIconClass();
if (iconClassLabels.length > 0) {
return iconClassLabels[0].value;
}
else if (workspaceIconClass) {
return workspaceIconClass;
}
else if (this.noteId === 'root') {
return "bx bx-chevrons-right";
}
if (this.noteId === '_share') {
return "bx bx-share-alt";
}
else if (this.type === 'text') {
if (this.isFolder()) {
return "bx bx-folder";
}
else {
return "bx bx-note";
}
}
else if (this.type === 'code' &amp;&amp; this.mime.startsWith('text/x-sql')) {
return "bx bx-data";
}
else {
return NOTE_TYPE_ICONS[this.type];
}
}
getColorClass() {
const color = this.getLabelValue("color");
return cssClassManager.createClassForColor(color);
}
isFolder() {
return this.type === 'search'
|| this.getFilteredChildBranches().length > 0;
}
getFilteredChildBranches() {
let childBranches = this.getChildBranches();
if (!childBranches) {
ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`);
return;
}
if (options.is("hideIncludedImages_main")) {
const imageLinks = this.getRelations('imageLink');
// image is already visible in the parent note so no need to display it separately in the book
childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
}
// we're not checking hideArchivedNotes since that would mean we need to lazy load the child notes
// which would seriously slow down everything.
// we check this flag only once user chooses to expand the parent. This has the negative consequence that
// note may appear as folder but not contain any children when all of them are archived
return childBranches;
}
/**
* @param {string} [name] - relation name to filter
* @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones
*/
getOwnedRelations(name) {
return this.getOwnedAttributes(RELATION, name);
}
/**
* @param {string} [name] - relation name to filter
* @returns {FAttribute[]} all note's relations (attributes with type relation), including inherited ones
*/
getRelations(name) {
return this.getAttributes(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 (including inherited)
*/
hasAttribute(type, name) {
return !!this.getAttribute(type, 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 (including inherited)
*/
hasOwnedAttribute(type, name) {
return !!this.getOwnedAttribute(type, name);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {FAttribute} 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.
*/
getOwnedAttribute(type, name) {
const attributes = this.getOwnedAttributes();
return attributes.find(attr => attr.name === name &amp;&amp; attr.type === type);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {FAttribute} 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.name === name &amp;&amp; attr.type === type);
}
/**
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {string} 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} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
* @returns {string} 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} name - label name
* @returns {boolean} true if label exists (excluding inherited)
*/
hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {boolean} true if label exists (including inherited)
*/
hasLabel(name) { return this.hasAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {boolean} true if relation exists (excluding inherited)
*/
hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {boolean} true if relation exists (including inherited)
*/
hasRelation(name) { return this.hasAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {FAttribute} label if it exists, null otherwise
*/
getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
/**
* @param {string} name - label name
* @returns {FAttribute} label if it exists, null otherwise
*/
getLabel(name) { return this.getAttribute(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {FAttribute} relation if it exists, null otherwise
*/
getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {FAttribute} relation if it exists, null otherwise
*/
getRelation(name) { return this.getAttribute(RELATION, name); }
/**
* @param {string} name - label name
* @returns {string} label value if label exists, null otherwise
*/
getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
/**
* @param {string} name - label name
* @returns {string} label value if label exists, null otherwise
*/
getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
/**
* @param {string} name - relation name
* @returns {string} relation value if relation exists, null otherwise
*/
getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
/**
* @param {string} name - relation name
* @returns {string} relation value if relation exists, null otherwise
*/
getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
/**
* @param {string} name
* @returns {Promise&lt;FNote>|null} target note of the relation or null (if target is empty or note was not found)
*/
async getRelationTarget(name) {
const targets = await this.getRelationTargets(name);
return targets.length > 0 ? targets[0] : null;
}
/**
* @param {string} [name] - relation name to filter
* @returns {Promise&lt;FNote[]>}
*/
async getRelationTargets(name) {
const relations = this.getRelations(name);
const targets = [];
for (const relation of relations) {
targets.push(await this.froca.getNote(relation.value));
}
return targets;
}
/**
* @returns {FNote[]}
*/
getNotesToInheritAttributesFrom() {
const relations = [
...this.getRelations('template'),
...this.getRelations('inherit')
];
return relations.map(rel => this.froca.notes[rel.value]);
}
getPromotedDefinitionAttributes() {
if (this.hasLabel('hidePromotedAttributes')) {
return [];
}
const promotedAttrs = this.getAttributes()
.filter(attr => attr.isDefinition())
.filter(attr => {
const def = attr.getDefinition();
return def &amp;&amp; def.isPromoted;
});
// attrs are not resorted if position changes after initial load
promotedAttrs.sort((a, b) => a.position &lt; b.position ? -1 : 1);
return promotedAttrs;
}
hasAncestor(ancestorNoteId, followTemplates = false, visitedNoteIds = null) {
if (this.noteId === ancestorNoteId) {
return true;
}
if (!visitedNoteIds) {
visitedNoteIds = new Set();
} else if (visitedNoteIds.has(this.noteId)) {
// to avoid infinite cycle when template is descendent of the instance
return false;
}
visitedNoteIds.add(this.noteId);
if (followTemplates) {
for (const templateNote of this.getNotesToInheritAttributesFrom()) {
if (templateNote.hasAncestor(ancestorNoteId, followTemplates, visitedNoteIds)) {
return true;
}
}
}
for (const parentNote of this.getParentNotes()) {
if (parentNote.hasAncestor(ancestorNoteId, followTemplates, visitedNoteIds)) {
return true;
}
}
return false;
}
isInHiddenSubtree() {
return this.noteId === '_hidden' || this.hasAncestor('_hidden');
}
/**
* @deprecated NOOP
*/
invalidateAttributeCache() {}
/**
* Get relations which target this note
*
* @returns {FAttribute[]}
*/
getTargetRelations() {
return this.targetRelations
.map(attributeId => this.froca.attributes[attributeId]);
}
/**
* Get relations which target this note
*
* @returns {FNote[]}
*/
async getTargetRelationSourceNotes() {
const targetRelations = this.getTargetRelations();
return await this.froca.getNotes(targetRelations.map(tr => tr.noteId));
}
/**
* Return note complement which is most importantly note's content
*
* @returns {Promise&lt;FNoteComplement>}
*/
async getNoteComplement() {
return await this.froca.getNoteComplement(this.noteId);
}
toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
get dto() {
const dto = Object.assign({}, this);
delete dto.froca;
return dto;
}
getCssClass() {
const labels = this.getLabels('cssClass');
return labels.map(l => l.value).join(' ');
}
getWorkspaceIconClass() {
const labels = this.getLabels('workspaceIconClass');
return labels.length > 0 ? labels[0].value : "";
}
getWorkspaceTabBackgroundColor() {
const labels = this.getLabels('workspaceTabBackgroundColor');
return labels.length > 0 ? labels[0].value : "";
}
/** @returns {boolean} true if this note is JavaScript (code or file) */
isJavaScript() {
return (this.type === "code" || this.type === "file" || this.type === 'launcher')
&amp;&amp; (this.mime.startsWith("application/javascript")
|| this.mime === "application/x-javascript"
|| this.mime === "text/javascript");
}
/** @returns {boolean} true if this note is HTML */
isHtml() {
return (this.type === "code" || this.type === "file" || this.type === "render") &amp;&amp; this.mime === "text/html";
}
/** @returns {string|null} JS script environment - either "frontend" or "backend" */
getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() &amp;&amp; this.mime.endsWith('env=frontend'))) {
return "frontend";
}
if (this.type === 'render') {
return "frontend";
}
if (this.isJavaScript() &amp;&amp; this.mime.endsWith('env=backend')) {
return "backend";
}
return null;
}
async executeScript() {
if (!this.isJavaScript()) {
throw new Error(`Note ${this.noteId} is of type ${this.type} and mime ${this.mime} and thus cannot be executed`);
}
const env = this.getScriptEnv();
if (env === "frontend") {
const bundleService = (await import("../services/bundle.js")).default;
return await bundleService.getAndExecuteBundle(this.noteId);
}
else if (env === "backend") {
const resp = await server.post(`script/run/${this.noteId}`);
}
else {
throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`);
}
}
isShared() {
for (const parentNoteId of this.parents) {
if (parentNoteId === 'root' || parentNoteId === 'none') {
continue;
}
const parentNote = froca.notes[parentNoteId];
if (!parentNote || parentNote.type === 'search') {
continue;
}
if (parentNote.noteId === '_share' || parentNote.isShared()) {
return true;
}
}
return false;
}
isContentAvailable() {
return !this.isProtected || protectedSessionHolder.isProtectedSessionAvailable()
}
isLaunchBarConfig() {
return this.type === 'launcher' || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(this.noteId);
}
isOptions() {
return this.noteId.startsWith("_options");
}
}
export default FNote;
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="FAttribute.html">FAttribute</a></li><li><a href="FBranch.html">FBranch</a></li><li><a href="FNote.html">FNote</a></li><li><a href="FNoteComplement.html">FNoteComplement</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.1</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: entities/fnote_complement.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: entities/fnote_complement.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* Complements the FNote with the main note content and other extra attributes
*/
class FNoteComplement {
constructor(row) {
/** @type {string} */
this.noteId = row.noteId;
/**
* can either contain the whole content (in e.g. string notes), only part (large text notes) or nothing at all (binary notes, images)
* @type {string}
*/
this.content = row.content;
/** @type {int} */
this.contentLength = row.contentLength;
/** @type {string} */
this.dateCreated = row.dateCreated;
/** @type {string} */
this.dateModified = row.dateModified;
/** @type {string} */
this.utcDateCreated = row.utcDateCreated;
/** @type {string} */
this.utcDateModified = row.utcDateModified;
// "combined" date modified give larger out of note's and note_content's dateModified
/** @type {string} */
this.combinedDateModified = row.combinedDateModified;
/** @type {string} */
this.combinedUtcDateModified = row.combinedUtcDateModified;
}
}
export default FNoteComplement;
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="FAttribute.html">FAttribute</a></li><li><a href="FBranch.html">FBranch</a></li><li><a href="FNote.html">FNote</a></li><li><a href="FNoteComplement.html">FNoteComplement</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.1</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@@ -0,0 +1,590 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: services/frontend_script_api.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: services/frontend_script_api.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import server from './server.js';
import utils from './utils.js';
import toastService from './toast.js';
import linkService from './link.js';
import froca from './froca.js';
import noteTooltipService from './note_tooltip.js';
import protectedSessionService from './protected_session.js';
import dateNotesService from './date_notes.js';
import searchService from './search.js';
import RightPanelWidget from '../widgets/right_panel_widget.js';
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
import BasicWidget from "../widgets/basic_widget.js";
import SpacedUpdate from "./spaced_update.js";
import shortcutService from "./shortcuts.js";
/**
* &lt;p>This is the main frontend API interface for scripts. All the properties and methods are published in the "api" object
* available in the JS frontend notes. You can use e.g. &lt;code>api.showMessage(api.startNote.title);&lt;/code>&lt;/p>
*
* @constructor
*/
function FrontendScriptApi(startNote, currentNote, originEntity = null, $container = null) {
/** @property {jQuery} container of all the rendered script content */
this.$container = $container;
/** @property {object} note where script started executing */
this.startNote = startNote;
/** @property {object} note where script is currently executing */
this.currentNote = currentNote;
/** @property {object|null} entity whose event triggered this execution */
this.originEntity = originEntity;
/** @property {dayjs} day.js library for date manipulation. See {@link https://day.js.org} for documentation */
this.dayjs = dayjs;
/**
* @property {RightPanelWidget}
* @deprecated use api.RightPanelWidget instead
*/
this.CollapsibleWidget = RightPanelWidget;
/** @property {RightPanelWidget} */
this.RightPanelWidget = RightPanelWidget;
/** @property {NoteContextAwareWidget} */
this.NoteContextAwareWidget = NoteContextAwareWidget;
/**
* @property {NoteContextAwareWidget}
* @deprecated use NoteContextAwareWidget instead
*/
this.TabAwareWidget = NoteContextAwareWidget;
/**
* @property {NoteContextAwareWidget}
* @deprecated use NoteContextAwareWidget instead
*/
this.TabCachingWidget = NoteContextAwareWidget;
/**
* @property {NoteContextAwareWidget}
* @deprecated use NoteContextAwareWidget instead
*/
this.NoteContextCachingWidget = NoteContextAwareWidget;
/** @property {BasicWidget} */
this.BasicWidget = BasicWidget;
/**
* Activates note in the tree and in the note detail.
*
* @method
* @param {string} notePath (or noteId)
* @returns {Promise&lt;void>}
*/
this.activateNote = async notePath => {
await appContext.tabManager.getActiveContext().setNote(notePath);
};
/**
* Activates newly created note. Compared to this.activateNote() also makes sure that frontend has been fully synced.
*
* @param {string} notePath (or noteId)
* @returns {Promise&lt;void>}
*/
this.activateNewNote = async notePath => {
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath);
appContext.triggerEvent('focusAndSelectTitle');
};
/**
* Open a note in a new tab.
*
* @method
* @param {string} notePath (or noteId)
* @param {boolean} activate - set to true to activate the new tab, false to stay on the current tab
* @returns {Promise&lt;void>}
*/
this.openTabWithNote = async (notePath, activate) => {
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.openContextWithNote(notePath, { activate });
if (activate) {
appContext.triggerEvent('focusAndSelectTitle');
}
};
/**
* Open a note in a new split.
*
* @method
* @param {string} notePath (or noteId)
* @param {boolean} activate - set to true to activate the new split, false to stay on the current split
* @returns {Promise&lt;void>}
*/
this.openSplitWithNote = async (notePath, activate) => {
await ws.waitForMaxKnownEntityChangeId();
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const {ntxId} = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
if (activate) {
appContext.triggerEvent('focusAndSelectTitle');
}
};
/**
* Adds a new launcher to the launchbar. If the launcher (id) already exists, it will be updated.
*
* @method
* @deprecated you can now create/modify launchers in the top-left Menu -> Configure Launchbar
* for special needs there's also backend API's createOrUpdateLauncher()
* @param {object} opts
* @property {string} [opts.id] - id of the button, used to identify the old instances of this button to be replaced
* ID is optional because of BC, but not specifying it is deprecated. ID can be alphanumeric only.
* @property {string} opts.title
* @property {string} [opts.icon] - name of the boxicon to be used (e.g. "time" for "bx-time" icon)
* @property {function} opts.action - callback handling the click on the button
* @property {string} [opts.shortcut] - keyboard shortcut for the button, e.g. "alt+t"
*/
this.addButtonToToolbar = async opts => {
console.warn("api.addButtonToToolbar() has been deprecated since v0.58 and may be removed in the future. Use Menu -> Configure Launchbar to create/update launchers instead.");
const {action, ...reqBody} = opts;
reqBody.action = action.toString();
await server.put('special-notes/api-script-launcher', reqBody);
};
function prepareParams(params) {
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "function") {
return `!@#Function: ${p.toString()}`;
}
else {
return p;
}
});
}
/**
* Executes given anonymous function on the backend.
* Internally this serializes the anonymous function into string and sends it to backend via AJAX.
*
* @method
* @param {string} script - script to be executed on the backend
* @param {Array.&lt;?>} params - list of parameters to the anonymous function to be sent to backend
* @returns {Promise&lt;*>} return value of the executed function on the backend
*/
this.runOnBackend = async (script, params = []) => {
if (typeof script === "function") {
script = script.toString();
}
const ret = await server.post('script/exec', {
script: script,
params: prepareParams(params),
startNoteId: startNote.noteId,
currentNoteId: currentNote.noteId,
originEntityName: "notes", // currently there's no other entity on frontend which can trigger event
originEntityId: originEntity ? originEntity.noteId : null
}, "script");
if (ret.success) {
await ws.waitForMaxKnownEntityChangeId();
return ret.executionResult;
}
else {
throw new Error(`server error: ${ret.error}`);
}
};
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
*
* @method
* @param {string} searchString
* @returns {Promise&lt;FNote[]>}
*/
this.searchForNotes = async searchString => {
return await searchService.searchForNotes(searchString);
};
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "#dateModified =* MONTH AND #log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
*
* @method
* @param {string} searchString
* @returns {Promise&lt;FNote|null>}
*/
this.searchForNote = async searchString => {
const notes = await this.searchForNotes(searchString);
return notes.length > 0 ? notes[0] : null;
};
/**
* Returns note by given noteId. If note is missing from cache, it's loaded.
**
* @method
* @param {string} noteId
* @returns {Promise&lt;FNote>}
*/
this.getNote = async noteId => await froca.getNote(noteId);
/**
* Returns list of notes. If note is missing from cache, it's loaded.
*
* This is often used to bulk-fill the cache with notes which would have to be picked one by one
* otherwise (by e.g. createNoteLink())
*
* @method
* @param {string[]} noteIds
* @param {boolean} [silentNotFoundError] - don't report error if the note is not found
* @returns {Promise&lt;FNote[]>}
*/
this.getNotes = async (noteIds, silentNotFoundError = false) => await froca.getNotes(noteIds, silentNotFoundError);
/**
* Update frontend tree (note) cache from the backend.
*
* @method
* @param {string[]} noteIds
*/
this.reloadNotes = async noteIds => await froca.reloadNotes(noteIds);
/**
* Instance name identifies particular Trilium instance. It can be useful for scripts
* if some action needs to happen on only one specific instance.
*
* @method
* @returns {string}
*/
this.getInstanceName = () => window.glob.instanceName;
/**
* @method
* @param {Date} date
* @returns {string} date in YYYY-MM-DD format
*/
this.formatDateISO = utils.formatDateISO;
/**
* @method
* @param {string} str
* @returns {Date} parsed object
*/
this.parseDate = utils.parseDate;
/**
* Show info message to the user.
*
* @method
* @param {string} message
*/
this.showMessage = toastService.showMessage;
/**
* Show error message to the user.
*
* @method
* @param {string} message
*/
this.showError = toastService.showError;
/**
* Trigger command.
*
* @method
* @param {string} name
* @param {object} data
*/
this.triggerCommand = (name, data) => appContext.triggerCommand(name, data);
/**
* Trigger event.
*
* @method
* @param {string} name
* @param {object} data
*/
this.triggerEvent = (name, data) => appContext.triggerEvent(name, data);
/**
* Create note link (jQuery object) for given note.
*
* @method
* @param {string} notePath (or noteId)
* @param {object} [params]
* @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link
* @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link
* @param {boolean} [params.showNoteIcon=false] - show also note icon before the title
* @param {string} [params.title=] - custom link tile with note's title as default
*/
this.createNoteLink = linkService.createNoteLink;
/**
* Adds given text to the editor cursor
*
* @method
* @param {string} text - this must be clear text, HTML is not supported.
*/
this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text});
/**
* @method
* @returns {FNote} active note (loaded into right pane)
*/
this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote();
/**
* See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
*
* @method
* @returns {Promise&lt;CKEditor>} instance of CKEditor
*/
this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor();
/**
* See https://codemirror.net/doc/manual.html#api
*
* @method
* @returns {Promise&lt;CodeMirror>} instance of CodeMirror
*/
this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor();
/**
* Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the
* implementation of actual widget type.
*
* @method
* @returns {Promise&lt;NoteDetailWidget>}
*/
this.getActiveNoteDetailWidget = () => new Promise(resolve => appContext.triggerCommand('executeInActiveNoteDetailWidget', {callback: resolve}));
/**
* @method
* @returns {Promise&lt;string|null>} returns note path of active note or null if there isn't active note
*/
this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath();
/**
* Returns component which owns given DOM element (the nearest parent component in DOM tree)
*
* @method
* @param {Element} el - DOM element
* @returns {Component}
*/
this.getComponentByEl = el => appContext.getComponentByEl(el);
/**
* @method
* @param {object} $el - jquery object on which to set up the tooltip
* @returns {Promise&lt;void>}
*/
this.setupElementTooltip = noteTooltipService.setupElementTooltip;
/**
* @method
* @param {string} noteId
* @param {boolean} protect - true to protect note, false to unprotect
* @returns {Promise&lt;void>}
*/
this.protectNote = async (noteId, protect) => {
await protectedSessionService.protectNote(noteId, protect, false);
};
/**
* @method
* @param {string} noteId
* @param {boolean} protect - true to protect subtree, false to unprotect
* @returns {Promise&lt;void>}
*/
this.protectSubTree = async (noteId, protect) => {
await protectedSessionService.protectNote(noteId, protect, true);
};
/**
* Returns date-note for today. If it doesn't exist, it is automatically created.
*
* @method
* @returns {Promise&lt;FNote>}
*/
this.getTodayNote = dateNotesService.getTodayNote;
/**
* Returns day note for a given date. If it doesn't exist, it is automatically created.
*
* @method
* @param {string} date - e.g. "2019-04-29"
* @returns {Promise&lt;FNote>}
*/
this.getDayNote = dateNotesService.getDayNote;
/**
* Returns day note for the first date of the week of the given date. If it doesn't exist, it is automatically created.
*
* @method
* @param {string} date - e.g. "2019-04-29"
* @returns {Promise&lt;FNote>}
*/
this.getWeekNote = dateNotesService.getWeekNote;
/**
* Returns month-note. If it doesn't exist, it is automatically created.
*
* @method
* @param {string} month - e.g. "2019-04"
* @returns {Promise&lt;FNote>}
*/
this.getMonthNote = dateNotesService.getMonthNote;
/**
* Returns year-note. If it doesn't exist, it is automatically created.
*
* @method
* @param {string} year - e.g. "2019"
* @returns {Promise&lt;FNote>}
*/
this.getYearNote = dateNotesService.getYearNote;
/**
* Hoist note in the current tab. See https://github.com/zadam/trilium/wiki/Note-hoisting
*
* @method
* @param {string} noteId - set hoisted note. 'root' will effectively unhoist
* @returns {Promise&lt;void>}
*/
this.setHoistedNoteId = (noteId) => {
const activeNoteContext = appContext.tabManager.getActiveContext();
if (activeNoteContext) {
activeNoteContext.setHoistedNoteId(noteId);
}
};
/**
* @method
* @param {string} keyboardShortcut - e.g. "ctrl+shift+a"
* @param {function} handler
* @param {string} [namespace] - specify namespace of the handler for the cases where call for bind may be repeated.
* If a handler with this ID exists, it's replaced by the new handler.
* @returns {Promise&lt;void>}
*/
this.bindGlobalShortcut = shortcutService.bindGlobalShortcut;
/**
* Trilium runs in backend and frontend process, when something is changed on the backend from script,
* frontend will get asynchronously synchronized.
*
* This method returns a promise which resolves once all the backend -> frontend synchronization is finished.
* Typical use case is when new note has been created, we should wait until it is synced into frontend and only then activate it.
*
* @method
* @returns {Promise&lt;void>}
*/
this.waitUntilSynced = ws.waitForMaxKnownEntityChangeId;
/**
* This will refresh all currently opened notes which have included note specified in the parameter
*
* @param includedNoteId - noteId of the included note
* @returns {Promise&lt;void>}
*/
this.refreshIncludedNote = includedNoteId => appContext.triggerEvent('refreshIncludedNote', {noteId: includedNoteId});
/**
* Return randomly generated string of given length. This random string generation is NOT cryptographically secure.
*
* @method
* @param {number} length of the string
* @returns {string} random string
*/
this.randomString = utils.randomString;
this.logMessages = {};
this.logSpacedUpdates = {};
/**
* Log given message to the log pane in UI
*
* @param message
* @returns {void}
*/
this.log = message => {
const {noteId} = this.startNote;
message = `${utils.now()}: ${message}`;
console.log(`Script ${noteId}: ${message}`);
this.logMessages[noteId] = this.logMessages[noteId] || [];
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
appContext.triggerEvent("apiLogMessages", {noteId, messages});
}, 100);
this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate();
};
}
export default FrontendScriptApi;
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="FAttribute.html">FAttribute</a></li><li><a href="FBranch.html">FBranch</a></li><li><a href="FNote.html">FNote</a></li><li><a href="FNoteComplement.html">FNoteComplement</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.1</a>
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>