diff --git a/manifest.json b/manifest.json index f3fb62b..71c2b82 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "copy-text-protocol", "name": "Copy Text Protocol", - "version": "1.1.1", + "version": "1.1.2", "minAppVersion": "0.15.0", "description": "Adds support for copying text to the clipboard using a custom protocol.", "author": "jldiaz", diff --git a/package-lock.json b/package-lock.json index cc97698..07e1976 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "copy-text-protocol", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "copy-text-protocol", - "version": "1.1.0", + "version": "1.1.1", "license": "0-BSD", "dependencies": { "obsidian": "latest" diff --git a/package.json b/package.json index b487f0a..80c052c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "copy-text-protocol", - "version": "1.1.1", + "version": "1.1.2", "description": "A plugin that adds support for copying text via a custom protocol.", "main": "main.js", "type": "module", @@ -27,6 +27,7 @@ "typescript-eslint": "8.35.1" }, "dependencies": { + "@lezer/common": "^1.2.3", "obsidian": "latest" } } diff --git a/src/main.ts b/src/main.ts index ca543ab..de67b58 100644 --- a/src/main.ts +++ b/src/main.ts @@ -58,54 +58,59 @@ const copyLinkPlugin = ViewPlugin.fromClass( const { from, to } = view.viewport; const selection = view.state.selection; - tree.iterate({ - from, - to, - enter(node: SyntaxNodeRef) { - if (!node.name.includes('string_url')) return; - const urlText = view.state.doc.sliceString(node.from, node.to); - if (!urlText.includes('obsidian://copy')) return; + for (const { from: lFrom, to: lTo } of view.visibleRanges) { + tree.iterate({ + from: lFrom, + to: lTo, + enter(node: SyntaxNodeRef) { + if (!node.name.includes('string_url')) return; + const urlText = view.state.doc.sliceString(node.from, node.to); + if (!urlText.includes('obsidian://copy')) return; - let textToCopy = ''; - try { - const cleanUrl = urlText.startsWith('<') ? urlText.slice(1, -1) : urlText; - const url = new URL(cleanUrl); - textToCopy = url.searchParams.get('text') ?? ''; - } catch { - const match = urlText.match(/[?&]text=([^&> ]+)/); - textToCopy = (match && match[1]) ? decodeURIComponent(match[1]) : ''; - } - - if (!textToCopy) return; - - let labelNode: SyntaxNodeRef | null = null; - let curr = node.node.prevSibling; - for (let i = 0; i < 6 && curr; i++) { - if (curr.name.includes('link') && !curr.name.includes('formatting')) { - labelNode = curr as unknown as SyntaxNodeRef; - break; + let textToCopy = ''; + try { + const cleanUrl = urlText.startsWith('<') ? urlText.slice(1, -1) : urlText; + const url = new URL(cleanUrl); + textToCopy = url.searchParams.get('text') ?? ''; + } catch { + const match = urlText.match(/[?&]text=([^&> ]+)/); + textToCopy = (match && match[1]) ? decodeURIComponent(match[1]) : ''; } - curr = curr.prevSibling; - } - if (labelNode) { - // Hide widget if cursor is anywhere near the link structure - // (from the start of label to the end of the URL) - const linkStart = labelNode.from - 1; // accounting for '[' - const linkEnd = node.to + 1; // accounting for ')' - const isEditing = selection.ranges.some(r => r.from <= linkEnd && r.to >= linkStart); + if (!textToCopy) return; - builder.add(labelNode.from, labelNode.to, Decoration.mark({ class: 'copy-protocol-link' })); - - if (!isEditing) { - builder.add(labelNode.to, labelNode.to, Decoration.widget({ - widget: new CopyIconWidget(textToCopy), - side: 1 - })); + let labelNode: SyntaxNodeRef | null = null; + let curr = node.node.prevSibling; + for (let i = 0; i < 6 && curr; i++) { + if (curr.name.includes('link') && !curr.name.includes('formatting')) { + labelNode = curr as unknown as SyntaxNodeRef; + break; + } + curr = curr.prevSibling; } - } - }, - }); + + if (labelNode) { + // Mark the line to hide external link icons via CSS + const line = view.state.doc.lineAt(node.from); + builder.add(line.from, line.from, Decoration.line({ class: 'has-copy-protocol-line' })); + + // Hide widget if cursor is anywhere near the link structure + const linkStart = labelNode.from - 1; + const linkEnd = node.to + 1; + const isEditing = selection.ranges.some(r => r.from <= linkEnd && r.to >= linkStart); + + builder.add(labelNode.from, labelNode.to, Decoration.mark({ class: 'copy-protocol-link' })); + + if (!isEditing) { + builder.add(labelNode.to, labelNode.to, Decoration.widget({ + widget: new CopyIconWidget(textToCopy), + side: 1 + })); + } + } + }, + }); + } return builder.finish(); } }, diff --git a/styles.css b/styles.css index 7b9050b..18cf386 100644 --- a/styles.css +++ b/styles.css @@ -4,7 +4,18 @@ /* ── Reading View (Standard Links) ── */ -a[href^="obsidian://copy"]::after { +/* + * We target the same pseudo-element Obsidian uses (::after) but with higher + * specificity to override it completely without !important. + */ +.markdown-rendered a.external-link[href^="obsidian://copy"], +.markdown-preview-view a.external-link[href^="obsidian://copy"] { + background-image: none; + padding-inline-end: 0; +} + +.markdown-rendered a.external-link[href^="obsidian://copy"]::after, +.markdown-preview-view a.external-link[href^="obsidian://copy"]::after { content: ""; display: inline-block; width: 0.85em; @@ -12,6 +23,7 @@ a[href^="obsidian://copy"]::after { margin-left: 5px; vertical-align: middle; background-color: currentColor; + background-image: none; /* Clears Obsidian's default icon if set via background-image */ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/svg%3E"); -webkit-mask-size: contain; @@ -21,34 +33,43 @@ a[href^="obsidian://copy"]::after { opacity: 0.8; } -a[href^="obsidian://copy"]:hover::after { +.markdown-rendered a.external-link[href^="obsidian://copy"]:hover::after, +.markdown-preview-view a.external-link[href^="obsidian://copy"]:hover::after { opacity: 1; } +/* Hide Obsidian's default external link icon elements in Reading Mode */ +.markdown-rendered a[href^="obsidian://copy"] .external-link, +.markdown-rendered a[href^="obsidian://copy"] + .external-link, +.markdown-preview-view a[href^="obsidian://copy"] .external-link, +.markdown-preview-view a[href^="obsidian://copy"] + .external-link { + display: none; +} + /* ── Live Preview (Managed by ViewPlugin Widget) ── */ /* - * Hide Obsidian's default external link icon on the same line as our copy links. - * We still use the .copy-protocol-link mark for this. + * Hide Obsidian's default external link icon on our copy links. + * Instead of :has(), we use the .has-copy-protocol-line class added in JS. */ -.cm-line:has(.copy-protocol-link) .external-link, -.cm-line:has(.copy-protocol-link) .cm-url.external-link, -.cm-line:has(.copy-protocol-link) .cm-widgetBuffer + .external-link { - display: none !important; +.markdown-source-view.mod-cm6 .has-copy-protocol-line .external-link, +.markdown-source-view.mod-cm6 .has-copy-protocol-line .cm-url.external-link { + display: none; } /* Ensure the link text looks like a link */ -.copy-protocol-link { +.markdown-source-view.mod-cm6 .copy-protocol-link { text-decoration: underline; text-underline-offset: 2px; } /* Copy cursor on hover */ -.copy-protocol-link:hover, -a[href^="obsidian://copy"]:hover { - cursor: copy !important; +.markdown-source-view.mod-cm6 .copy-protocol-link:hover, +.markdown-source-view.mod-cm6 a[href^="obsidian://copy"]:hover { + cursor: copy; } + /* ── Widget Styles (Live Preview) ── */ .copy-protocol-icon { diff --git a/versions.json b/versions.json index 197d03a..0abcd86 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,5 @@ { + "1.1.2": "0.15.0", "1.1.1": "0.15.0", "1.1.0": "0.15.0", "1.0.1": "0.15.0",