diff --git a/src/commonmark/commands.ts b/src/commonmark/commands.ts index 25b3acf6..b1950bca 100644 --- a/src/commonmark/commands.ts +++ b/src/commonmark/commands.ts @@ -620,9 +620,9 @@ export function insertCommonmarkLinkCommand( } /** - * Inserts a tagLink at the cursor, optionally placing it around the currently selected text if able + * Inserts a tag_link at the cursor, optionally placing it around the currently selected text if able * @param validate The validation method that will be used to validate the selected text - * @param isMetaTag Whether or not the inserted tagLink is for a meta tag + * @param isMetaTag Whether or not the inserted tag_link is for a meta tag */ export function insertTagLinkCommand( validate: TagLinkOptions["validate"], diff --git a/src/rich-text/commands/index.ts b/src/rich-text/commands/index.ts index 741da2dd..a3d7c869 100644 --- a/src/rich-text/commands/index.ts +++ b/src/rich-text/commands/index.ts @@ -272,7 +272,7 @@ function getHeadingLevel(state: EditorState): number { } /** - * Creates a command that toggles tagLink formatting for a node + * Creates a command that toggles tag_link formatting for a node * @param validate The function to validate the tagName with * @param isMetaTag Whether the tag to be created is a meta tag or not */ @@ -294,7 +294,7 @@ export function toggleTagLinkCommand( } let tr = state.tr; - const nodeCheck = nodeTypeActive(state.schema.nodes.tagLink); + const nodeCheck = nodeTypeActive(state.schema.nodes.tag_link); if (nodeCheck(state)) { const selectedText = state.selection.content().content.firstChild .attrs["tagName"] as string; @@ -314,7 +314,7 @@ export function toggleTagLinkCommand( return false; } - const newTagNode = state.schema.nodes.tagLink.create({ + const newTagNode = state.schema.nodes.tag_link.create({ tagName: selectedText.trim(), tagType: isMetaTag ? "meta-tag" : "tag", }); diff --git a/src/rich-text/editor.ts b/src/rich-text/editor.ts index df8cfd4c..9c8f578e 100644 --- a/src/rich-text/editor.ts +++ b/src/rich-text/editor.ts @@ -29,7 +29,6 @@ import { stackOverflowMarkdownSerializer } from "../shared/markdown-serializer"; import { CodeBlockView } from "./node-views/code-block"; import { HtmlBlock, HtmlBlockContainer } from "./node-views/html-block"; import { ImageView } from "./node-views/image"; -import { TagLink } from "./node-views/tag-link"; import { richTextCodePasteHandler } from "../shared/prosemirror-plugins/code-paste-handler"; import { linkPasteHandler } from "./plugins/link-paste-handler"; import { linkPreviewPlugin, LinkPreviewProvider } from "./plugins/link-preview"; @@ -101,7 +100,6 @@ export class RichTextEditor extends BaseView { EditorType.RichText ); - const tagLinkOptions = this.options.parserFeatures.tagLinks; this.editorView = new EditorView( (node: HTMLElement) => { node.classList.add(...(this.options.classList || [])); @@ -162,9 +160,6 @@ export class RichTextEditor extends BaseView { ) { return new ImageView(node, view, getPos); }, - tagLink(node: ProseMirrorNode) { - return new TagLink(node, tagLinkOptions); - }, html_block: function (node: ProseMirrorNode) { return new HtmlBlock(node); }, diff --git a/src/rich-text/node-views/tag-link.ts b/src/rich-text/node-views/tag-link.ts deleted file mode 100644 index 6d63066f..00000000 --- a/src/rich-text/node-views/tag-link.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Node as ProsemirrorNode } from "prosemirror-model"; -import { NodeView } from "prosemirror-view"; -import { error } from "../../shared/logger"; -import { TagLinkOptions } from "../../shared/view"; - -// TODO instead of a NodeView, should we use marks and an editor like `link-tooltip`? -export class TagLink implements NodeView { - dom: HTMLElement | null; - - constructor(node: ProsemirrorNode, options: TagLinkOptions) { - this.dom = document.createElement("a"); - this.dom.setAttribute("href", "#"); - this.dom.setAttribute("rel", "tag"); - this.dom.classList.add("s-tag"); - this.dom.innerText = node.attrs.tagName as string; - - if (options?.render) { - const rendered = options.render( - node.attrs.tagName as string, - node.attrs.tagType === "meta-tag" - ); - - // the renderer failed to return the bare minimum necessary to link the tag - // log an error to the console, but don't crash the user input - if (!rendered || !rendered?.link) { - error( - "TagLink NodeView", - "Unable to render taglink due to invalid response from options.renderer: ", - rendered - ); - return; - } - - const additionalClasses = rendered.additionalClasses || []; - additionalClasses.forEach((c) => this.dom.classList.add(c)); - this.dom.setAttribute("href", rendered.link); - this.dom.setAttribute("title", rendered.linkTitle); - } - } -} diff --git a/src/rich-text/schema.ts b/src/rich-text/schema.ts index e7ead7bb..208f61a9 100644 --- a/src/rich-text/schema.ts +++ b/src/rich-text/schema.ts @@ -425,16 +425,45 @@ const nodes: { }, }, - // TODO should this be a mark instead? - tagLink: { + tag_link: { content: "text*", - marks: "", // TODO should it accept marks? + marks: "", atom: true, // TODO allow this to be editable inline: true, group: "inline", attrs: { tagName: { default: null }, tagType: { default: "tag" }, + href: { default: null }, + title: { default: null }, + additionalClasses: { default: "" }, + }, + parseDOM: [ + { + tag: "a.s-tag", + getAttrs(dom: HTMLElement) { + dom.classList.remove("s-tag"); + return { + href: dom.getAttribute("href"), + title: dom.getAttribute("title"), + additionalClasses: Array.from(dom.classList).join(" "), + tagType: dom.getAttribute("tagtype"), + tagName: dom.textContent, + }; + }, + }, + ], + toDOM(node) { + return [ + "a", + { + tagType: node.attrs.tagType as string, + href: node.attrs.href as string, + title: node.attrs.title as string, + class: `s-tag ${node.attrs.additionalClasses as string}`, + }, + node.attrs.tagName, + ]; }, }, }; @@ -481,7 +510,7 @@ const marks: { }, parseDOM: [ { - tag: "a[href]", + tag: "a[href]:not(.s-tag)", getAttrs(dom: HTMLElement) { return { href: dom.getAttribute("href"), diff --git a/src/shared/markdown-it/tag-link.ts b/src/shared/markdown-it/tag-link.ts index 0456eeb9..8037e719 100644 --- a/src/shared/markdown-it/tag-link.ts +++ b/src/shared/markdown-it/tag-link.ts @@ -1,5 +1,6 @@ import MarkdownIt from "markdown-it/lib"; import StateInline from "markdown-it/lib/rules_inline/state_inline"; +import { error } from "../logger"; import type { TagLinkOptions } from "../view"; function parse_tag_link( @@ -33,7 +34,7 @@ function parse_tag_link( const totalContent = state.src.slice(state.pos, labelEnd + 1); const isMeta = totalContent.slice(0, 10) === "[meta-tag:"; - const tagName = totalContent.slice(isMeta ? 10 : 5, -1); + const tagName = totalContent.slice(isMeta ? 10 : 5, -1).trim(); if (options.validate && !options.validate(tagName, isMeta, totalContent)) { return false; @@ -43,7 +44,24 @@ function parse_tag_link( let token = state.push("tag_link_open", "a", 1); token.attrSet("tagName", tagName); token.attrSet("tagType", isMeta ? "meta-tag" : "tag"); - token.content = totalContent; + + // call the renderer as if it exists + if (options.render) { + const rendered = options.render(tagName, isMeta); + + if (rendered && rendered.link) { + const additionalClasses = rendered.additionalClasses || []; + token.attrSet("href", rendered.link); + token.attrSet("title", rendered.linkTitle); + token.attrSet("additionalClasses", additionalClasses.join(" ")); + } else { + // We don't want to crash the parsing process here since we can still display a passable version of the tag link. + // However, we should at least log a console error. + error( + `Unable to fully render taglink for [${tagName}] due to invalid response from options.renderer.` + ); + } + } token = state.push("text", "", 0); token.content = tagName; diff --git a/src/shared/markdown-parser.ts b/src/shared/markdown-parser.ts index 298c6447..9223ee66 100644 --- a/src/shared/markdown-parser.ts +++ b/src/shared/markdown-parser.ts @@ -136,10 +136,13 @@ const customMarkdownParserTokens: MarkdownParser["tokens"] = { }, tag_link: { - block: "tagLink", + block: "tag_link", getAttrs: (tok: Token) => ({ tagName: tok.attrGet("tagName"), tagType: tok.attrGet("tagType"), + href: tok.attrGet("href"), + additionalClasses: tok.attrGet("additionalClasses"), + title: tok.attrGet("title"), }), }, diff --git a/src/shared/markdown-serializer.ts b/src/shared/markdown-serializer.ts index e2d76e1c..4f27dbb3 100644 --- a/src/shared/markdown-serializer.ts +++ b/src/shared/markdown-serializer.ts @@ -462,7 +462,7 @@ const customMarkdownSerializerNodes: MarkdownSerializerNodes = { state.closeBlock(node); }, - tagLink(state, node) { + tag_link(state, node) { const isMeta = node.attrs.tagType === "meta-tag"; const prefix = isMeta ? "meta-tag" : "tag"; const tag = node.attrs.tagName as string; diff --git a/src/shared/menu/entries.ts b/src/shared/menu/entries.ts index adb27037..afd22bce 100644 --- a/src/shared/menu/entries.ts +++ b/src/shared/menu/entries.ts @@ -171,7 +171,7 @@ const moreFormattingDropdown = (schema: Schema, options: CommonViewOptions) => options.parserFeatures?.tagLinks?.validate, false ), - active: nodeTypeActive(schema.nodes.tagLink), + active: nodeTypeActive(schema.nodes.tag_link), }, commonmark: insertTagLinkCommand( options.parserFeatures?.tagLinks?.validate, @@ -188,7 +188,7 @@ const moreFormattingDropdown = (schema: Schema, options: CommonViewOptions) => options.parserFeatures?.tagLinks?.validate, true ), - active: nodeTypeActive(schema.nodes.tagLink), + active: nodeTypeActive(schema.nodes.tag_link), }, commonmark: insertTagLinkCommand( options.parserFeatures?.tagLinks?.validate, diff --git a/test/rich-text/commands/index.test.ts b/test/rich-text/commands/index.test.ts index eb6b9438..c8a07a6f 100644 --- a/test/rich-text/commands/index.test.ts +++ b/test/rich-text/commands/index.test.ts @@ -475,7 +475,7 @@ describe("commands", () => { let containsTagLink = false; newState.doc.nodesBetween(0, newState.doc.content.size, (node) => { - containsTagLink = node.type.name === "tagLink"; + containsTagLink = node.type.name === "tag_link"; return !containsTagLink; }); @@ -497,7 +497,7 @@ describe("commands", () => { let containsTagLink = false; newState.doc.nodesBetween(0, newState.doc.content.size, (node) => { - containsTagLink = node.type.name === "tagLink"; + containsTagLink = node.type.name === "tag_link"; return !containsTagLink; }); @@ -533,7 +533,7 @@ describe("commands", () => { 0, tagLinkResult.newState.doc.content.size, (node) => { - containsTagLink = node.type.name === "tagLink"; + containsTagLink = node.type.name === "tag_link"; return !containsTagLink; } @@ -543,7 +543,7 @@ describe("commands", () => { } ); - it("should replace selected text with tagLink", () => { + it("should replace selected text with tag_link", () => { let state = createState("this is my state", []); state = applySelection(state, 5, 7); //"is" @@ -566,7 +566,7 @@ describe("commands", () => { text: "this ", }, { - "type.name": "tagLink", + "type.name": "tag_link", }, { isText: true, @@ -578,7 +578,7 @@ describe("commands", () => { }); }); - it("should untoggle tagLink when selected", () => { + it("should untoggle tag_link when selected", () => { let state = createState("someText", []); state = applySelection(state, 0, 8); // cursor is inside the tag @@ -597,7 +597,7 @@ describe("commands", () => { "type.name": "paragraph", "content": [ { - "type.name": "tagLink", + "type.name": "tag_link", }, ], }, diff --git a/test/shared/markdown-parser.test.ts b/test/shared/markdown-parser.test.ts index e192584c..8db137ec 100644 --- a/test/shared/markdown-parser.test.ts +++ b/test/shared/markdown-parser.test.ts @@ -198,7 +198,7 @@ console.log("test"); "childCount": 1, "content": [ { - "type.name": "tagLink", + "type.name": "tag_link", "childCount": 1, "content": [{ text: "python" }], },