]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1")
            .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode
        return $("").html(str).text();
    }
    async initEditor() {
        this.$widget.show();
        this.$editor.on("click", (e) => this.handleEditorClick(e));
        this.textEditor = await AttributeEditor.create(this.$editor[0], editorConfig);
        this.textEditor.model.document.on("change:data", () => this.dataChanged());
        this.textEditor.editing.view.document.on(
            "enter",
            (event, data) => {
                // disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
                data.preventDefault();
                event.stop();
            },
            { priority: "high" }
        );
        // disable spellcheck for attribute editor
        this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", this.textEditor.editing.view.document.getRoot()));
    }
    dataChanged() {
        this.lastUpdatedNoteId = this.noteId;
        if (this.lastSavedContent === this.textEditor.getData()) {
            this.$saveAttributesButton.fadeOut();
        } else {
            this.$saveAttributesButton.fadeIn();
        }
        if (this.$errors.is(":visible")) {
            // using .hide() instead of .slideUp() since this will also hide the error after confirming
            // mention for relation name which suits up. When using.slideUp() error will appear and the slideUp which is weird
            this.$errors.hide();
        }
    }
    async handleEditorClick(e: JQuery.ClickEvent) {
        const pos = this.textEditor.model.document.selection.getFirstPosition();
        if (pos && pos.textNode && pos.textNode.data) {
            const clickIndex = this.getClickIndex(pos);
            let parsedAttrs;
            try {
                parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true);
            } catch (e) {
                // the input is incorrect because the user messed up with it and now needs to fix it manually
                return null;
            }
            let matchedAttr: Attribute | null = null;
            for (const attr of parsedAttrs) {
                if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) {
                    matchedAttr = attr;
                    break;
                }
            }
            setTimeout(() => {
                if (matchedAttr) {
                    this.$editor.tooltip("hide");
                    this.attributeDetailWidget.showAttributeDetail({
                        allAttributes: parsedAttrs,
                        attribute: matchedAttr,
                        isOwned: true,
                        x: e.pageX,
                        y: e.pageY
                    });
                } else {
                    this.showHelpTooltip();
                }
            }, 100);
        } else {
            this.showHelpTooltip();
        }
    }
    showHelpTooltip() {
        this.attributeDetailWidget.hide();
        this.$editor.tooltip({
            trigger: "focus",
            html: true,
            title: HELP_TEXT,
            placement: "bottom",
            offset: "0,30"
        });
        this.$editor.tooltip("show");
    }
    getClickIndex(pos: TextPosition) {
        let clickIndex = pos.offset - pos.textNode.startOffset;
        let curNode = pos.textNode;
        while (curNode.previousSibling) {
            curNode = curNode.previousSibling;
            if (curNode.name === "reference") {
                clickIndex += curNode._attrs.get("notePath").length + 1;
            } else {
                clickIndex += curNode.data.length;
            }
        }
        return clickIndex;
    }
    async loadReferenceLinkTitle($el: JQuery, href: string) {
        const { noteId } = linkService.parseNavigationStateFromUrl(href);
        const note = noteId ? await froca.getNote(noteId, true) : null;
        const title = note ? note.title : "[missing]";
        $el.text(title);
    }
    async refreshWithNote(note: FNote) {
        await this.renderOwnedAttributes(note.getOwnedAttributes(), true);
    }
    async renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) {
        // attrs are not resorted if position changes after the initial load
        ownedAttributes.sort((a, b) => a.position - b.position);
        let htmlAttrs = (await attributeRenderer.renderAttributes(ownedAttributes, true)).html();
        if (htmlAttrs.length > 0) {
            htmlAttrs += " ";
        }
        this.textEditor.setData(htmlAttrs);
        if (saved) {
            this.lastSavedContent = this.textEditor.getData();
            this.$saveAttributesButton.fadeOut(0);
        }
    }
    async createNoteForReferenceLink(title: string) {
        let result;
        if (this.notePath) {
            result = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
                activate: false,
                title: title
            });
        }
        return result?.note?.getBestNotePathString();
    }
    async updateAttributeList(attributes: FAttribute[]) {
        await this.renderOwnedAttributes(attributes, false);
    }
    focus() {
        this.$editor.trigger("focus");
        this.textEditor.model.change((writer) => {
            const positionAt = writer.createPositionAt(this.textEditor.model.document.getRoot(), "end");
            writer.setSelection(positionAt);
        });
    }
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
        if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
            this.refresh();
        }
    }
}