mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
frontend validation of attribute name + other changes and fixes
This commit is contained in:
parent
f24e27dadd
commit
3670fbff49
@ -1,13 +1,6 @@
|
|||||||
import attributeParser from '../src/public/app/services/attribute_parser.js';
|
import attributeParser from '../src/public/app/services/attribute_parser.js';
|
||||||
import {describe, it, expect, execute} from './mini_test.js';
|
import {describe, it, expect, execute} from './mini_test.js';
|
||||||
|
|
||||||
describe("Preprocessor", () => {
|
|
||||||
it("relation with value", () => {
|
|
||||||
expect(attributeParser.preprocess('<p>~relation = <a class="reference-link" href="#root/RclIpMauTOKS/NFi2gL4xtPxM" some-attr="abc" data-note-path="root/RclIpMauTOKS/NFi2gL4xtPxM">note</a> </p>'))
|
|
||||||
.toEqual("~relation = #root/RclIpMauTOKS/NFi2gL4xtPxM ");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Lexer", () => {
|
describe("Lexer", () => {
|
||||||
it("simple label", () => {
|
it("simple label", () => {
|
||||||
expect(attributeParser.lexer("#label").map(t => t.text))
|
expect(attributeParser.lexer("#label").map(t => t.text))
|
||||||
@ -95,11 +88,16 @@ describe("Parser", () => {
|
|||||||
expect(attrs[0].name).toEqual("token");
|
expect(attrs[0].name).toEqual("token");
|
||||||
expect(attrs[0].value).toEqual('NFi2gL4xtPxM');
|
expect(attrs[0].value).toEqual('NFi2gL4xtPxM');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// it("error cases", () => {
|
describe("error cases", () => {
|
||||||
// expect(() => attributeParser.parser(["~token"].map(t => ({text: t})), "~token"))
|
it("error cases", () => {
|
||||||
// .toThrow('Relation "~token" should point to a note.');
|
expect(() => attributeParser.lexAndParse('~token'))
|
||||||
// });
|
.toThrow('Relation "~token" in "~token" should point to a note.');
|
||||||
|
|
||||||
|
expect(() => attributeParser.lexAndParse("#a&b/s"))
|
||||||
|
.toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
execute();
|
execute();
|
||||||
|
@ -131,7 +131,7 @@ export default class DesktopMainWindowLayout {
|
|||||||
.child(new FlexContainer('column').id('center-pane')
|
.child(new FlexContainer('column').id('center-pane')
|
||||||
.child(new FlexContainer('row').class('title-row')
|
.child(new FlexContainer('row').class('title-row')
|
||||||
.cssBlock('.title-row > * { margin: 5px; }')
|
.cssBlock('.title-row > * { margin: 5px; }')
|
||||||
.css('height', '55px')
|
.overflowing()
|
||||||
.child(new NoteTitleWidget())
|
.child(new NoteTitleWidget())
|
||||||
.child(new RunScriptButtonsWidget().hideInZenMode())
|
.child(new RunScriptButtonsWidget().hideInZenMode())
|
||||||
.child(new NoteTypeWidget().hideInZenMode())
|
.child(new NoteTypeWidget().hideInZenMode())
|
||||||
|
@ -1,17 +1,3 @@
|
|||||||
function preprocess(str) {
|
|
||||||
if (str.startsWith('<p>')) {
|
|
||||||
str = str.substr(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (str.endsWith('</p>')) {
|
|
||||||
str = str.substr(0, str.length - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
str = str.replace(/ /g, " ");
|
|
||||||
|
|
||||||
return str.replace(/<a[^>]+href="(#[A-Za-z0-9/]*)"[^>]*>[^<]*<\/a>/g, "$1");
|
|
||||||
}
|
|
||||||
|
|
||||||
function lexer(str) {
|
function lexer(str) {
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
|
|
||||||
@ -117,6 +103,14 @@ function lexer(str) {
|
|||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
|
||||||
|
|
||||||
|
function checkAttributeName(attrName) {
|
||||||
|
if (!attrNameMatcher.test(attrName)) {
|
||||||
|
throw new Error(`Attribute name "${attrName}" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parser(tokens, str, allowEmptyRelations = false) {
|
function parser(tokens, str, allowEmptyRelations = false) {
|
||||||
const attrs = [];
|
const attrs = [];
|
||||||
|
|
||||||
@ -149,9 +143,13 @@ function parser(tokens, str, allowEmptyRelations = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (text.startsWith('#')) {
|
if (text.startsWith('#')) {
|
||||||
|
const labelName = text.substr(1);
|
||||||
|
|
||||||
|
checkAttributeName(labelName);
|
||||||
|
|
||||||
const attr = {
|
const attr = {
|
||||||
type: 'label',
|
type: 'label',
|
||||||
name: text.substr(1),
|
name: labelName,
|
||||||
isInheritable: isInheritable(),
|
isInheritable: isInheritable(),
|
||||||
startIndex: startIndex,
|
startIndex: startIndex,
|
||||||
endIndex: tokens[i].endIndex // i could be moved by isInheritable
|
endIndex: tokens[i].endIndex // i could be moved by isInheritable
|
||||||
@ -171,9 +169,13 @@ function parser(tokens, str, allowEmptyRelations = false) {
|
|||||||
attrs.push(attr);
|
attrs.push(attr);
|
||||||
}
|
}
|
||||||
else if (text.startsWith('~')) {
|
else if (text.startsWith('~')) {
|
||||||
|
const relationName = text.substr(1);
|
||||||
|
|
||||||
|
checkAttributeName(relationName);
|
||||||
|
|
||||||
const attr = {
|
const attr = {
|
||||||
type: 'relation',
|
type: 'relation',
|
||||||
name: text.substr(1),
|
name: relationName,
|
||||||
isInheritable: isInheritable(),
|
isInheritable: isInheritable(),
|
||||||
startIndex: startIndex,
|
startIndex: startIndex,
|
||||||
endIndex: tokens[i].endIndex // i could be moved by isInheritable
|
endIndex: tokens[i].endIndex // i could be moved by isInheritable
|
||||||
@ -211,15 +213,12 @@ function parser(tokens, str, allowEmptyRelations = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function lexAndParse(str, allowEmptyRelations = false) {
|
function lexAndParse(str, allowEmptyRelations = false) {
|
||||||
str = preprocess(str);
|
|
||||||
|
|
||||||
const tokens = lexer(str);
|
const tokens = lexer(str);
|
||||||
|
|
||||||
return parser(tokens, str, allowEmptyRelations);
|
return parser(tokens, str, allowEmptyRelations);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
preprocess,
|
|
||||||
lexer,
|
lexer,
|
||||||
parser,
|
parser,
|
||||||
lexAndParse
|
lexAndParse
|
||||||
|
@ -158,6 +158,8 @@ const ATTR_TITLES = {
|
|||||||
"relation-definition": "Relation definition detail"
|
"relation-definition": "Relation definition detail"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
|
||||||
|
|
||||||
export default class AttributeDetailWidget extends TabAwareWidget {
|
export default class AttributeDetailWidget extends TabAwareWidget {
|
||||||
async refresh() {
|
async refresh() {
|
||||||
// this widget is not activated in a standard way
|
// this widget is not activated in a standard way
|
||||||
@ -280,7 +282,7 @@ export default class AttributeDetailWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("RENDERING");
|
|
||||||
this.attrType = this.getAttrType(attribute);
|
this.attrType = this.getAttrType(attribute);
|
||||||
|
|
||||||
const attrName =
|
const attrName =
|
||||||
@ -365,16 +367,16 @@ console.log("RENDERING");
|
|||||||
|
|
||||||
this.toggleInt(true);
|
this.toggleInt(true);
|
||||||
|
|
||||||
this.$widget.css("left", x - this.$widget.outerWidth() / 2);
|
const offset = this.parent.$widget.offset();
|
||||||
this.$widget.css("top", y + 25);
|
|
||||||
|
this.$widget.css("left", x - offset.left - this.$widget.outerWidth() / 2);
|
||||||
|
this.$widget.css("top", y - offset.top + 70);
|
||||||
|
|
||||||
// so that the detail window always fits
|
// so that the detail window always fits
|
||||||
this.$widget.css("max-height",
|
this.$widget.css("max-height",
|
||||||
this.$widget.outerHeight() + y > $(window).height() - 50
|
this.$widget.outerHeight() + y > $(window).height() - 50
|
||||||
? $(window).height() - y - 50
|
? $(window).height() - y - 50
|
||||||
: 10000);
|
: 10000);
|
||||||
|
|
||||||
console.log("RENDERING DONE");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateRelatedNotes() {
|
async updateRelatedNotes() {
|
||||||
@ -435,6 +437,13 @@ console.log("RENDERING");
|
|||||||
updateAttributeInEditor() {
|
updateAttributeInEditor() {
|
||||||
let attrName = this.$inputName.val();
|
let attrName = this.$inputName.val();
|
||||||
|
|
||||||
|
if (!ATTR_NAME_MATCHER.test(attrName)) {
|
||||||
|
// invalid characters are simply ignored (from user perspective they are not even entered)
|
||||||
|
attrName = attrName.replace(/[^\p{L}\p{N}_:]/ug, "");
|
||||||
|
|
||||||
|
this.$inputName.val(attrName);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.attrType === 'label-definition') {
|
if (this.attrType === 'label-definition') {
|
||||||
attrName = 'label:' + attrName;
|
attrName = 'label:' + attrName;
|
||||||
} else if (this.attrType === 'relation-definition') {
|
} else if (this.attrType === 'relation-definition') {
|
||||||
|
@ -293,15 +293,22 @@ export default class AttributeEditorWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
parseAttributes() {
|
parseAttributes() {
|
||||||
try {
|
try {
|
||||||
const attrs = attributesParser.lexAndParse(this.textEditor.getData());
|
const attrs = attributesParser.lexAndParse(this.getPreprocessedData());
|
||||||
|
|
||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
this.$errors.show().text(e.message);
|
this.$errors.text(e.message).slideDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreprocessedData() {
|
||||||
|
const str = this.textEditor.getData()
|
||||||
|
.replace(/<a[^>]+href="(#[A-Za-z0-9/]*)"[^>]*>[^<]*<\/a>/g, "$1");
|
||||||
|
|
||||||
|
return $("<div>").html(str).text();
|
||||||
|
}
|
||||||
|
|
||||||
async initEditor() {
|
async initEditor() {
|
||||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||||
|
|
||||||
@ -332,18 +339,18 @@ export default class AttributeEditorWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEditorClick(e) {
|
async handleEditorClick(e) {console.log("click")
|
||||||
const pos = this.textEditor.model.document.selection.getFirstPosition();
|
const pos = this.textEditor.model.document.selection.getFirstPosition();
|
||||||
|
|
||||||
if (pos && pos.textNode && pos.textNode.data) {
|
if (pos && pos.textNode && pos.textNode.data) {console.log(pos);
|
||||||
const clickIndex = this.getClickIndex(pos);
|
const clickIndex = this.getClickIndex(pos);
|
||||||
|
|
||||||
let parsedAttrs;
|
let parsedAttrs;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedAttrs = attributesParser.lexAndParse(this.textEditor.getData(), true);
|
parsedAttrs = attributesParser.lexAndParse(this.getPreprocessedData(), true);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {console.log(e);
|
||||||
// the input is incorrect because user messed up with it and now needs to fix it manually
|
// the input is incorrect because user messed up with it and now needs to fix it manually
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -357,13 +364,15 @@ export default class AttributeEditorWidget extends TabAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.attributeDetailWidget.showAttributeDetail({
|
setTimeout(() => {
|
||||||
allAttributes: parsedAttrs,
|
this.attributeDetailWidget.showAttributeDetail({
|
||||||
attribute: matchedAttr,
|
allAttributes: parsedAttrs,
|
||||||
isOwned: true,
|
attribute: matchedAttr,
|
||||||
x: e.pageX,
|
isOwned: true,
|
||||||
y: e.pageY
|
x: e.pageX,
|
||||||
});
|
y: e.pageY
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -636,7 +636,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
|||||||
}
|
}
|
||||||
|
|
||||||
.component {
|
.component {
|
||||||
contain: strict;
|
contain: layout size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
|
@ -4,6 +4,7 @@ const noteCache = require('./note_cache');
|
|||||||
const hoistedNoteService = require('../hoisted_note');
|
const hoistedNoteService = require('../hoisted_note');
|
||||||
const protectedSessionService = require('../protected_session');
|
const protectedSessionService = require('../protected_session');
|
||||||
const stringSimilarity = require('string-similarity');
|
const stringSimilarity = require('string-similarity');
|
||||||
|
const log = require('../log');
|
||||||
|
|
||||||
function isNotePathArchived(notePath) {
|
function isNotePathArchived(notePath) {
|
||||||
const noteId = notePath[notePath.length - 1];
|
const noteId = notePath[notePath.length - 1];
|
||||||
@ -62,6 +63,11 @@ function getNoteTitle(childNoteId, parentNoteId) {
|
|||||||
const childNote = noteCache.notes[childNoteId];
|
const childNote = noteCache.notes[childNoteId];
|
||||||
const parentNote = noteCache.notes[parentNoteId];
|
const parentNote = noteCache.notes[parentNoteId];
|
||||||
|
|
||||||
|
if (!childNote) {
|
||||||
|
log.info(`Cannot find note in cache for noteId ${childNoteId}`);
|
||||||
|
return "[error fetching title]";
|
||||||
|
}
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
|
||||||
if (childNote.isProtected) {
|
if (childNote.isProtected) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user