Release 1.1.2: Fixed Reading Mode icons and resolved strict validator warnings

This commit is contained in:
JLDiaz (m) 2026-05-13 22:06:01 +02:00
parent f31f9ed5eb
commit 4983397c7f
6 changed files with 87 additions and 59 deletions

View file

@ -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",

4
package-lock.json generated
View file

@ -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"

View file

@ -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"
}
}

View file

@ -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();
}
},

View file

@ -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 {

View file

@ -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",