Skip to content

Commit

Permalink
feat: add smart copy command
Browse files Browse the repository at this point in the history
Copies selection or entire document depending on whether editor has a
selection active.
  • Loading branch information
mvdkwast committed Jul 10, 2023
1 parent c36b954 commit 9eae697
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 56 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,25 @@

Plugin for [Obsidian](https://obsidian.md) that copies the current document to the clipboard, so it can be pasted into HTML aware application like gmail.

This plugin exposes the `Copy document as HTML: Copy the current document to clipboard` command, which can be bound to a keyboard shortcut.
This plugin exposes the `Copy document as HTML` command, which can be bound to keyboard shortcuts (see below).

![image](https://user-images.githubusercontent.com/2441349/202304790-aea2a29e-2ed8-4ba2-bfb6-caaeb823e6f0.png)

## Features

Simple styling is applied to the document.
### Commands

The commands can be bound to keyboard shortcuts from the hotkeys menu, or run using the commands menu (Ctrl+P)

**Copy selection or document to clipboard** : If text is selected, it will copied as HTML into the clipboard. If no text
is selected, the entire document is copied. This should probably be your default keyboard shortcut. (suggestion:
`Ctrl+Shift+C`)

**Copy entire document to clipboard** : Copy the entire document

**Copy current selection to clipboard** : Copy the selected text only

### Media support

Currently working with :

Expand All @@ -20,6 +32,10 @@ Currently working with :
- ✅ Excalidraw - rendering as bitmap solves pasting in gmail
- ✅ Mermaid

### Styling

By default, simple styling is applied to the document. The stylesheet can be customized through the plugin settings.

## Advanced

- You may choose whether you want to embed external links (http, https) or not. If you don't (default), you will need internet access to view the document, and the linked image may be taken offline. If you do your documents will be larger.
Expand All @@ -38,6 +54,8 @@ The plugin converts image references to data urls, so no references to the vault
- data-uris can use a lot of memory for big/many pictures
- transclusions with block references are not supported (transclusions with headings are)

Also see the issues section on github.

## Install

Look for *Copy document as HTML* in the community plugin section in the Obsidian settings.
Expand Down
106 changes: 52 additions & 54 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,7 @@ class DocumentRenderer {
private readonly vaultUriPrefix: string;

constructor(private view: MarkdownView, private app: App,
private options: DocumentRendererOptions = documentRendererDefaults)
{
private options: DocumentRendererOptions = documentRendererDefaults) {
this.vaultPath = (this.app.vault.getRoot().vault.adapter as FileSystemAdapter).getBasePath()
.replace(/\\/g, '/');

Expand Down Expand Up @@ -404,7 +403,7 @@ class DocumentRenderer {
continue;
}

const { path, extension, heading, blockReference } = this.getLinkParts(src);
const {path, extension, heading, blockReference} = this.getLinkParts(src);
if (extension === '' || extension === 'md') {
const file = this.getEmbeddedFile(path);
if (file) {
Expand All @@ -416,8 +415,7 @@ class DocumentRenderer {
// if section not found
if (heading) {
markdown = this.extractSection(markdown, heading) ?? markdown;
}
else if (blockReference) {
} else if (blockReference) {
// TODO: implement block-reference matching
}

Expand Down Expand Up @@ -520,8 +518,7 @@ class DocumentRenderer {
}
if (this.options.footnoteHandling == FootnoteHandling.REMOVE_LINK) {
this.removeFootnoteLinks(node);
}
else if (this.options.footnoteHandling == FootnoteHandling.TITLE_ATTRIBUTE) {
} else if (this.options.footnoteHandling == FootnoteHandling.TITLE_ATTRIBUTE) {
// not supported yet
}

Expand Down Expand Up @@ -638,8 +635,7 @@ class DocumentRenderer {
if (text === '↩︎') {
// remove back-link
link.parentNode!.removeChild(link);
}
else {
} else {
// remove from reference
const span = link.parentNode!.createEl('span', {text: link.getText(), cls: 'footnote-link'})
link.parentNode!.replaceChild(span, link);
Expand Down Expand Up @@ -667,8 +663,7 @@ class DocumentRenderer {
if (this.externalSchemes.includes(scheme.toLowerCase())) {
// don't touch external images
return;
}
else {
} else {
// not an external image, continue processing below
}
}
Expand Down Expand Up @@ -830,11 +825,11 @@ class DocumentRenderer {
* - heading is present if link ends with `#some-header`
* - blockReference is present if link ends with `#^tag` (tag is a hex string)
*/
private getLinkParts(path: string): { path: string, extension: string, heading?: string, blockReference?: string} {
private getLinkParts(path: string): { path: string, extension: string, heading?: string, blockReference?: string } {
// split at right-most occurence
const hashIndex = path.lastIndexOf("#");
const [file, anchor] = hashIndex > 0
? [path.slice(0, hashIndex), path.slice(hashIndex+1)]
? [path.slice(0, hashIndex), path.slice(hashIndex + 1)]
: [path, ''];

return {
Expand Down Expand Up @@ -979,7 +974,7 @@ class CopyDocumentAsHTMLSettingsTab extends PluginSettingTab {
.addOption(FootnoteHandling.LEAVE_LINK.toString(), 'Display and link')
.setValue(this.plugin.settings.footnoteHandling.toString())
.onChange(async (value) => {
switch(value) {
switch (value) {
case FootnoteHandling.TITLE_ATTRIBUTE.toString():
this.plugin.settings.footnoteHandling = FootnoteHandling.TITLE_ATTRIBUTE;
break;
Expand Down Expand Up @@ -1077,49 +1072,22 @@ export default class CopyDocumentAsHTMLPlugin extends Plugin {
await this.loadSettings();

this.addCommand({
id: 'copy-as-html',
name: 'Copy current document to clipboard',

checkCallback: (checking: boolean): boolean => {
if (copyIsRunning) {
console.log('Document is already being copied');
return false;
}

const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
console.log('Nothing to copy: No active markdown view');
return false;
}

if (!checking) {
this.doCopy(activeView, false);
}
id: 'smart-copy-as-html',
name: 'Copy selection or document to clipboard',
checkCallback: this.buildCheckCallback(
view => this.doCopy(view, view.editor.somethingSelected()))
})

return true;
},
this.addCommand({
id: 'copy-as-html',
name: 'Copy entire document to clipboard',
checkCallback: this.buildCheckCallback(view => this.doCopy(view, false))
});

this.addCommand({
id: 'copy-selection-as-html',
name: 'Copy current selection to clipboard',
checkCallback: (checking: boolean): boolean => {
if (copyIsRunning) {
console.log('Selection is already being copied');
return false;
}

const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
console.log('Nothing to copy: No active markdown view');
return false;
}

if (!checking) {
this.doCopy(activeView, true);
}

return true;
}
checkCallback: this.buildCheckCallback(view => this.doCopy(view, true))
});

// Register post-processors that keep track of the blocks being rendered. For explanation,
Expand Down Expand Up @@ -1154,6 +1122,27 @@ export default class CopyDocumentAsHTMLPlugin extends Plugin {
await this.saveData(this.settings);
}

private buildCheckCallback(action: (activeView: MarkdownView) => void) {
return (checking: boolean): boolean => {
if (copyIsRunning) {
console.log('Document is already being copied');
return false;
}

const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
console.log('Nothing to copy: No active markdown view');
return false;
}

if (!checking) {
action(activeView);
}

return true;
}
}

private async doCopy(activeView: MarkdownView, onlySelected: boolean) {
console.log(`Copying "${activeView.file.path}" to clipboard...`);
const copier = new DocumentRenderer(activeView, this.app, this.settings);
Expand All @@ -1179,8 +1168,8 @@ export default class CopyDocumentAsHTMLPlugin extends Plugin {
});

await navigator.clipboard.write([data]);
console.log('Copied document to clipboard');
new Notice('document copied to clipboard')
console.log(`Copied ${onlySelected ? "selection" : "document"} to clipboard`);
new Notice(`${onlySelected ? "selection" : "document"} copied to clipboard`)
} catch (error) {
new Notice(`copy failed: ${error}`);
console.error('copy failed', error);
Expand All @@ -1196,6 +1185,15 @@ export default class CopyDocumentAsHTMLPlugin extends Plugin {
item
.setTitle("Copy as HTML")
.setIcon("clipboard-copy")
.onClick(async () => {
// @ts-ignore
this.app.commands.executeCommandById('copy-document-as-html:smart-copy-as-html');
});
});
menu.addItem((item) => {
item
.setTitle("Copy entire document as HTML")
.setIcon("clipboard-copy")
.onClick(async () => {
// @ts-ignore
this.app.commands.executeCommandById('copy-document-as-html:copy-as-html');
Expand Down

0 comments on commit 9eae697

Please sign in to comment.