mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			231 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import type { AttributeType, FAttributeRow } from "../entities/fattribute.js";
 | 
						|
import utils from "./utils.js";
 | 
						|
 | 
						|
interface Token {
 | 
						|
    text: string;
 | 
						|
    startIndex: number;
 | 
						|
    endIndex: number;
 | 
						|
}
 | 
						|
 | 
						|
export interface Attribute {
 | 
						|
    attributeId?: string;
 | 
						|
    type: AttributeType;
 | 
						|
    name: string;
 | 
						|
    isInheritable?: boolean;
 | 
						|
    value?: string;
 | 
						|
    startIndex?: number;
 | 
						|
    endIndex?: number;
 | 
						|
    noteId?: string;
 | 
						|
}
 | 
						|
 | 
						|
function lex(str: string) {
 | 
						|
    str = str.trim();
 | 
						|
 | 
						|
    const tokens: Token[] = [];
 | 
						|
 | 
						|
    let quotes: boolean | string = false;
 | 
						|
    let currentWord = "";
 | 
						|
 | 
						|
    function isOperatorSymbol(chr: string) {
 | 
						|
        return ["=", "*", ">", "<", "!"].includes(chr);
 | 
						|
    }
 | 
						|
 | 
						|
    function previousOperatorSymbol() {
 | 
						|
        if (currentWord.length === 0) {
 | 
						|
            return false;
 | 
						|
        } else {
 | 
						|
            return isOperatorSymbol(currentWord[currentWord.length - 1]);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param endIndex - index of the last character of the token
 | 
						|
     */
 | 
						|
    function finishWord(endIndex: number) {
 | 
						|
        if (currentWord === "") {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        tokens.push({
 | 
						|
            text: currentWord,
 | 
						|
            startIndex: endIndex - currentWord.length + 1,
 | 
						|
            endIndex: endIndex
 | 
						|
        });
 | 
						|
 | 
						|
        currentWord = "";
 | 
						|
    }
 | 
						|
 | 
						|
    for (let i = 0; i < str.length; i++) {
 | 
						|
        const chr = str[i];
 | 
						|
 | 
						|
        if (chr === "\\") {
 | 
						|
            if (i + 1 < str.length) {
 | 
						|
                i++;
 | 
						|
 | 
						|
                currentWord += str[i];
 | 
						|
            } else {
 | 
						|
                currentWord += chr;
 | 
						|
            }
 | 
						|
 | 
						|
            continue;
 | 
						|
        } else if (['"', "'", "`"].includes(chr)) {
 | 
						|
            if (!quotes) {
 | 
						|
                if (previousOperatorSymbol()) {
 | 
						|
                    finishWord(i - 1);
 | 
						|
                }
 | 
						|
 | 
						|
                quotes = chr;
 | 
						|
            } else if (quotes === chr) {
 | 
						|
                quotes = false;
 | 
						|
 | 
						|
                finishWord(i - 1);
 | 
						|
            } else {
 | 
						|
                // it's a quote, but within other kind of quotes, so it's valid as a literal character
 | 
						|
                currentWord += chr;
 | 
						|
            }
 | 
						|
            continue;
 | 
						|
        } else if (!quotes) {
 | 
						|
            if (currentWord.length === 0 && (chr === "#" || chr === "~")) {
 | 
						|
                currentWord = chr;
 | 
						|
 | 
						|
                continue;
 | 
						|
            } else if (chr === " ") {
 | 
						|
                finishWord(i - 1);
 | 
						|
                continue;
 | 
						|
            } else if (["(", ")"].includes(chr)) {
 | 
						|
                finishWord(i - 1);
 | 
						|
 | 
						|
                currentWord = chr;
 | 
						|
 | 
						|
                finishWord(i);
 | 
						|
 | 
						|
                continue;
 | 
						|
            } else if (previousOperatorSymbol() !== isOperatorSymbol(chr)) {
 | 
						|
                finishWord(i - 1);
 | 
						|
 | 
						|
                currentWord += chr;
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        currentWord += chr;
 | 
						|
    }
 | 
						|
 | 
						|
    finishWord(str.length - 1);
 | 
						|
 | 
						|
    return tokens;
 | 
						|
}
 | 
						|
 | 
						|
function checkAttributeName(attrName: string) {
 | 
						|
    if (attrName.length === 0) {
 | 
						|
        throw new Error("Attribute name is empty, please fill the name.");
 | 
						|
    }
 | 
						|
 | 
						|
    if (!utils.isValidAttributeName(attrName)) {
 | 
						|
        throw new Error(`Attribute name "${attrName}" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function parse(tokens: Token[], str: string, allowEmptyRelations = false) {
 | 
						|
    const attrs: Attribute[] = [];
 | 
						|
 | 
						|
    function context(i: number) {
 | 
						|
        let { startIndex, endIndex } = tokens[i];
 | 
						|
        startIndex = Math.max(0, startIndex - 20);
 | 
						|
        endIndex = Math.min(str.length, endIndex + 20);
 | 
						|
 | 
						|
        return `"${startIndex !== 0 ? "..." : ""}${str.substr(startIndex, endIndex - startIndex)}${endIndex !== str.length ? "..." : ""}"`;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let i = 0; i < tokens.length; i++) {
 | 
						|
        const { text, startIndex } = tokens[i];
 | 
						|
 | 
						|
        function isInheritable() {
 | 
						|
            if (tokens.length > i + 3 && tokens[i + 1].text === "(" && tokens[i + 2].text === "inheritable" && tokens[i + 3].text === ")") {
 | 
						|
                i += 3;
 | 
						|
 | 
						|
                return true;
 | 
						|
            } else {
 | 
						|
                return false;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (text.startsWith("#")) {
 | 
						|
            const labelName = text.substr(1);
 | 
						|
 | 
						|
            checkAttributeName(labelName);
 | 
						|
 | 
						|
            const attr: Attribute = {
 | 
						|
                type: "label",
 | 
						|
                name: labelName,
 | 
						|
                isInheritable: isInheritable(),
 | 
						|
                startIndex: startIndex,
 | 
						|
                endIndex: tokens[i].endIndex // i could be moved by isInheritable
 | 
						|
            };
 | 
						|
 | 
						|
            if (i + 1 < tokens.length && tokens[i + 1].text === "=") {
 | 
						|
                if (i + 2 >= tokens.length) {
 | 
						|
                    throw new Error(`Missing value for label "${text}" in ${context(i)}`);
 | 
						|
                }
 | 
						|
 | 
						|
                i += 2;
 | 
						|
 | 
						|
                attr.value = tokens[i].text;
 | 
						|
                attr.endIndex = tokens[i].endIndex;
 | 
						|
            }
 | 
						|
 | 
						|
            attrs.push(attr);
 | 
						|
        } else if (text.startsWith("~")) {
 | 
						|
            const relationName = text.substr(1);
 | 
						|
 | 
						|
            checkAttributeName(relationName);
 | 
						|
 | 
						|
            const attr: Attribute = {
 | 
						|
                type: "relation",
 | 
						|
                name: relationName,
 | 
						|
                isInheritable: isInheritable(),
 | 
						|
                startIndex: startIndex,
 | 
						|
                endIndex: tokens[i].endIndex // i could be moved by isInheritable
 | 
						|
            };
 | 
						|
 | 
						|
            attrs.push(attr);
 | 
						|
 | 
						|
            if (i + 2 >= tokens.length || tokens[i + 1].text !== "=") {
 | 
						|
                if (allowEmptyRelations) {
 | 
						|
                    break;
 | 
						|
                } else {
 | 
						|
                    throw new Error(`Relation "${text}" in ${context(i)} should point to a note.`);
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            i += 2;
 | 
						|
 | 
						|
            let notePath = tokens[i].text;
 | 
						|
            if (notePath.startsWith("#")) {
 | 
						|
                notePath = notePath.substr(1);
 | 
						|
            }
 | 
						|
 | 
						|
            const noteId = notePath.split("/").pop();
 | 
						|
 | 
						|
            attr.value = noteId;
 | 
						|
            attr.endIndex = tokens[i].endIndex;
 | 
						|
        } else {
 | 
						|
            throw new Error(`Invalid attribute "${text}" in ${context(i)}`);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return attrs;
 | 
						|
}
 | 
						|
 | 
						|
function lexAndParse(str: string, allowEmptyRelations = false) {
 | 
						|
    const tokens = lex(str);
 | 
						|
 | 
						|
    return parse(tokens, str, allowEmptyRelations);
 | 
						|
}
 | 
						|
 | 
						|
export default {
 | 
						|
    lex,
 | 
						|
    parse,
 | 
						|
    lexAndParse
 | 
						|
};
 |