Skip to content

Commit

Permalink
feature: apply stylesheet to document
Browse files Browse the repository at this point in the history
- Provide a default stylesheet
- Provide a setting to provide a custom stylesheet
  • Loading branch information
Martijn authored and mvdkwast committed Nov 15, 2022
1 parent 7df1ca5 commit b1f8e0d
Show file tree
Hide file tree
Showing 7 changed files with 3,641 additions and 42 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ jobs:
asset_name: manifest.json
asset_content_type: application/json

# - name: Upload styles.css
# id: upload-css
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ./styles.css
# asset_name: styles.css
# asset_content_type: text/css
- name: Upload styles.css
id: upload-css
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./styles.css
asset_name: styles.css
asset_content_type: text/css
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
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: Copy the current document to clipboard` command, which can be bound to a
keyboard shortcut.

## Support
## Features

Simple styling is applied to the document.

Currently working with :

Expand All @@ -17,13 +19,19 @@ Currently working with :
- ✅ obsidian-dataview - for large dataview blocks the content may not be complete
- ✅ Excalidraw - rendering as bitmap solves pasting in gmail

### Advanced

- It is possible to customize or replace the stylesheet in the settings dialog.
- The default is to convert SVG to bitmap for better compatibility at the cost of potential quality loss. If you know
that the application you are going to paste into has good .svg support, you can toggle the `Convert SVG to bitmap`
setting.

## Implementation

The plugin converts image references to data urls, so no references to the vault are included in the HTML.

## Known issues

- No styling yet (next priority)
- No mobile support yet
- Special fields (front-matter, double-colon attributes, ...) are not removed.
- data-uris can use a lot of memory for big/many pictures
Expand All @@ -34,7 +42,9 @@ The plugin converts image references to data urls, so no references to the vault

## INSTALL

If you want to check this out before this plugin is approved as a community plugin, you may use the [Obsidian BRAT](https://github.com/TfTHacker/obsidian42-brat) plugin to install it. Point it to this url : https://github.com/mvdkwast/obsidian-copy-as-html
If you want to check this out before this plugin is approved as a community plugin, you may use the [Obsidian
BRAT](https://github.com/TfTHacker/obsidian42-brat) plugin to install it. Point it to this url :
https://github.com/mvdkwast/obsidian-copy-as-html

Don't be afraid to comment if anything seems wrong !

Expand Down
208 changes: 192 additions & 16 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,146 @@ async function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}

/**
* Static assets
*/

const DEFAULT_STYLESHEET =
`body,input {
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif
}
code, kbd, pre {
font-family: "Roboto Mono", "Courier New", Courier, monospace;
background-color: #f5f5f5;
}
pre {
padding: 1em 0.5em;
}
table {
background: white;
border: 1px solid #666;
border-collapse: collapse;
padding: 0.5em;
}
table thead th,
table tfoot th {
text-align: left;
background-color: #eaeaea;
color: black;
}
table th, table td {
border: 1px solid #ddd;
padding: 0.5em;
}
table td {
color: #222222;
}
.callout,
.callout[data-callout="abstract"] .callout-title,
.callout[data-callout="summary"] .callout-title,
.callout[data-callout="tldr"] .callout-title,
.callout[data-callout="faq"] .callout-title,
.callout[data-callout="info"] .callout-title,
.callout[data-callout="help"] .callout-title {
background-color: #4355dbaa
}
.callout[data-callout="tip"] .callout-title,
.callout[data-callout="hint"] .callout-title,
.callout[data-callout="important"] .callout-title {
background-color: #34bbe6;
}
.callout[data-callout="success"] .callout-title,
.callout[data-callout="check"] .callout-title,
.callout[data-callout="done"] .callout-title {
background-color: #a3e048;
}
.callout[data-callout="question"] .callout-title,
.callout[data-callout="todo"] .callout-title {
background-color: #49da9a;
}
.callout[data-callout="caution"] .callout-title,
.callout[data-callout="attention"] .callout-title {
background-color: #f7d038;
}
.callout[data-callout="warning"] .callout-title,
.callout[data-callout="missing"] .callout-title,
.callout[data-callout="bug"] .callout-title {
background-color: #eb7532;
}
.callout[data-callout="failure"] .callout-title,
.callout[data-callout="fail"] .callout-title,
.callout[data-callout="danger"] .callout-title,
.callout[data-callout="error"] .callout-title {
background-color: #e6261f;
}
.callout[data-callout="example"] .callout-title {
background-color: #d23be7;
}
.callout[data-callout="quote"] .callout-title,
.callout[data-callout="cite"] .callout-title {
background-color: #aaaaaa;
}
.callout-icon {
flex: 0 0 auto;
display: flex;
align-self: center;
}
svg.svg-icon {
height: 18px;
width: 18px;
stroke-width: 1.75px;
}
.callout {
overflow: hidden;
margin: 1em 0;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.callout-title {
padding: .5em;
display: flex;
gap: 8px;
font-size: inherit;
color: black;
line-height: 1.3em;
}
.callout-title-inner {
font-weight: bold;
color: black;
}
.callout-content {
overflow-x: auto;
padding: 0.25em .5em;
color: #222222;
background-color: white !important;
}
`;


const htmlTemplate = (stylesheet: string, body: string, title: string) => `<html>
<head>
<title>${title}</title>
<style>
${stylesheet}
</style>
</head>
<body>
${body}
</body>
</html>`;

/*
* Plugin code
Expand Down Expand Up @@ -90,17 +230,10 @@ class DocumentRenderer {
this.modal.open();

try {
// @ts-ignore
// this.app.commands.executeCommandById('markdown:toggle-preview');
const topNode = await this.renderMarkdown();

return await this.transformHTML(topNode!);
} finally {
this.modal.close();

// Return to edit view
// @ts-ignore
// this.app.commands.executeCommandById('markdown:toggle-preview');
}
}

Expand Down Expand Up @@ -306,7 +439,6 @@ class DocumentRenderer {

const dataUriPromise = new Promise<string>((resolve, reject) => {
image.onload = () => {
// TODO: resize image ?
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;

Expand All @@ -321,7 +453,7 @@ class DocumentRenderer {
// if we fail, leave the original url.
// This way images that we may not load from external sources (tainted) may still be accessed
// (eg. plantuml)
// TODO: attempt fallback with fetch ?
// TODO: should we attempt to fallback with fetch ?
resolve(url);
}

Expand Down Expand Up @@ -353,7 +485,7 @@ class DocumentRenderer {
private getExtension(filePath: string): string {
// avoid using the "path" library
const fileName = filePath.slice(filePath.lastIndexOf('/') + 1);
return fileName.slice(fileName.lastIndexOf('.')+1 || fileName.length)
return fileName.slice(fileName.lastIndexOf('.') + 1 || fileName.length)
.toLowerCase();
}

Expand All @@ -366,12 +498,12 @@ class DocumentRenderer {
* Modal to show progress during conversion
*/
class CopyingToHtmlModal extends Modal {
private _progress: HTMLElement;

constructor(app: App) {
super(app);
}

private _progress: HTMLElement;

get progress() {
return this._progress;
}
Expand Down Expand Up @@ -414,16 +546,54 @@ class CopyDocumentAsHTMLSettingsTab extends PluginSettingTab {
this.plugin.settings.convertSvgToBitmap = value;
await this.plugin.saveSettings();
}));

const useCustomStylesheetSetting = new Setting(containerEl)
.setName('Provide a custom stylesheet')
.setDesc('The default stylesheet provides minimalistic theming. You may want to customize it for better looks.');

const customStylesheetSetting = new Setting(containerEl)
.setName('Custom stylesheet')
.setDesc('Disabling the setting above will replace the custom stylesheet with the default.')
.setClass('custom-css-setting')
.addTextArea(textArea => textArea
.setValue(this.plugin.settings.styleSheet)
.onChange(async (value) => {
this.plugin.settings.styleSheet = value;
await this.plugin.saveSettings();
}));

useCustomStylesheetSetting.addToggle(toggle => {
customStylesheetSetting.settingEl.toggle(this.plugin.settings.useCustomStylesheet);

toggle
.setValue(this.plugin.settings.useCustomStylesheet)
.onChange(async (value) => {
this.plugin.settings.useCustomStylesheet = value;
customStylesheetSetting.settingEl.toggle(this.plugin.settings.useCustomStylesheet);
if (!value) {
this.plugin.settings.styleSheet = DEFAULT_STYLESHEET;
}
await this.plugin.saveSettings();
});
});
}
}

type CopyDocumentAsHTMLSettings = {
/** If set svg are converted to bitmap */
convertSvgToBitmap: boolean;

/** remember if the stylesheet was default or custom */
useCustomStylesheet: boolean;

/** Style-sheet */
styleSheet: string;
}

const DEFAULT_SETTINGS: CopyDocumentAsHTMLSettings = {
convertSvgToBitmap: true
convertSvgToBitmap: true,
useCustomStylesheet: false,
styleSheet: DEFAULT_STYLESHEET
}

export default class CopyDocumentAsHTMLPlugin extends Plugin {
Expand Down Expand Up @@ -477,6 +647,11 @@ export default class CopyDocumentAsHTMLPlugin extends Plugin {

async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());

// reload it so we may update it in a new release
if (!this.settings.useCustomStylesheet) {
this.settings.styleSheet = DEFAULT_STYLESHEET;
}
}

async saveSettings() {
Expand All @@ -493,15 +668,16 @@ export default class CopyDocumentAsHTMLPlugin extends Plugin {
ppLastBlockDate = Date.now();
ppIsProcessing = true;

const document = await copier.renderDocument();
const htmlBody = await copier.renderDocument();
const htmlDocument = htmlTemplate(this.settings.styleSheet, htmlBody.outerHTML, activeView.file.name);

const data =
new ClipboardItem({
"text/html": new Blob([document.outerHTML], {
"text/html": new Blob([htmlDocument], {
// @ts-ignore
type: ["text/html", 'text/plain']
}),
"text/plain": new Blob([document.outerHTML], {
"text/plain": new Blob([htmlDocument], {
type: "text/plain"
}),
});
Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"id": "copy-document-as-html",
"name": "Copy document as HTML",
"version": "0.1.2",
"version": "0.2.0",
"minAppVersion": "1.0.0",
"description": "Copy the current document to clipboard as HTML, including images",
"author": "mvdkwast",
"authorUrl": "https://github.com/mvdkwast",
"isDesktopOnly": true
"isDesktopOnly": false
}
Loading

0 comments on commit b1f8e0d

Please sign in to comment.