forked from alangrainger/share-note
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathobsidian-share.js
205 lines (195 loc) · 8.65 KB
/
obsidian-share.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
const UPLOAD_LOCATION = 'https://example.com/somepath/' // the web root where the files will be uploaded. End in a trailing slash.
const UPLOAD_ENDPOINT = 'upload.php' // path to the upload endpoint, relative to UPLOAD_LOCATION
const YAML_FIELD = 'share'
const SECRET = 'some_fancy_secret'
const WIDTH = 700
const SHOW_FOOTER = true
/*
* Obsidian Share
*
* Created by Alan Grainger
* https://github.com/alangrainger/obsidian-share/
*
* v1.2.0
*/
const fs = require('fs')
const leaf = app.workspace.activeLeaf
const startMode = leaf.getViewState()
// Switch to Preview mode
const previewMode = leaf.getViewState()
previewMode.state.mode = 'preview'
leaf.setViewState(previewMode)
await new Promise(resolve => { setTimeout(() => { resolve() }, 200) })
// Parse the current document
let content, body, previewView, css
try {
content = leaf.view.modes.preview.renderer.sections.reduce((p, c) => p + c.el.innerHTML, '')
body = document.getElementsByTagName('body')[0]
previewView = document.getElementsByClassName('markdown-preview-view markdown-rendered')[0]
css = [...document.styleSheets].map(x => {
try { return [...x.cssRules].map(x => x.cssText).join('') }
catch (e) { }
}).filter(Boolean).join('').replace(/\n/g, '')
} catch (e) {
console.log(e)
new Notice('Failed to parse current note, check console for details', 5000)
}
// Revert to the original view mode
setTimeout(() => { leaf.setViewState(startMode) }, 200)
if (!previewView) return // Failed to parse current note
const status = new Notice('Sharing note...', 60000)
async function sha256(text) {
const encoder = new TextEncoder();
const data = encoder.encode(text)
const hash = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, '0')).join('')
}
const getHash = async (path) => { return (await sha256(path)).slice(0, 32) }
function updateFrontmatter(contents, field, value) {
const f = contents.match(/^---\r?\n(.*?)\n---\r?\n(.*)$/s),
v = `${field}: ${value}`,
x = new RegExp(`^${field}:.*$`, 'm'),
[s, e] = f ? [`${f[1]}\n`, f[2]] : ['', contents]
return f && f[1].match(x) ? contents.replace(x, v) : `---\n${s}${v}\n---\n${e}`
}
/**
* Upload to web server
* Will add two additional properties to the POST data:
* 'nonce' - here using millisecond timestamp
* 'auth' - SHA256 of nonce + SECRET
* @param {Object} data - An object with the following properties:
* @param {string} data.filename - Filename for the destination file
* @param {string} data.content - File content
* @param {string} [data.encoding] - Optional encoding type, accepts only 'base64'
*/
async function upload(data) {
data.nonce = Date.now().toString()
data.auth = await sha256(data.nonce + SECRET)
status.setMessage(`Uploading ${data.filename}...`)
return requestUrl({ url: UPLOAD_LOCATION + UPLOAD_ENDPOINT, method: 'POST', body: JSON.stringify(data) })
}
/**
* Convert mime-type to file extension
* If you want any additional base64 encoded files to be extracted from your CSS,
* add the extension and mime-type(s) here.
* @param {string} mimeType
* @returns {string} File extension
*/
function extension(mimeType) {
const mimes = {
ttf: ['font/ttf', 'application/x-font-ttf', 'application/x-font-truetype', 'font/truetype'],
otf: ['font/otf', 'application/x-font-opentype'],
woff: ['font/woff', 'application/font-woff', 'application/x-font-woff'],
woff2: ['font/woff2', 'application/font-woff2', 'application/x-font-woff2'],
}
return Object.keys(mimes).find(x => mimes[x].includes((mimeType || '').toLowerCase()))
}
const file = app.workspace.getActiveFile()
const footer = '<div class="status-bar"><div class="status-bar-item"><span class="status-bar-item-segment">Published with <a href="https://obsidianshare.com/" target="_blank">Obsidian Share</a></span></div></div>'
let html = `
<!DOCTYPE HTML>
<html>
<head>
<title>${file.basename}</title>
<meta property="og:title" content="${file.basename}" />
<meta id="head-description" name="description" content="">
<meta id="head-og-description" property="og:description" content="">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<style>
html, body {
overflow: visible !important;
}
.view-content {
height: 100% !important;
}
</style>
</head>
<body class="${body.className}" style="${body.style.cssText.replace(/"/g, `'`)}">
<div class="app-container">
<div class="horizontal-main-container">
<div class="workspace">
<div class="workspace-split mod-vertical mod-root">
<div class="workspace-leaf mod-active">
<div class="workspace-leaf-content">
<div class="view-content">
<div class="markdown-reading-view" style="max-width:${WIDTH}px;margin: 0 auto;">
<div class="${previewView.className}">
<div class="markdown-preview-sizer markdown-preview-section">
${content}
</div></div></div></div></div></div></div></div></div>${SHOW_FOOTER ? footer : ''}</div></body></html>`
try {
// Generate the HTML file for uploading
const dom = new DOMParser().parseFromString(html, 'text/html')
// Remove frontmatter to avoid sharing unwanted data
dom.querySelector('pre.frontmatter')?.remove()
dom.querySelector('div.frontmatter-container')?.remove()
// Set the meta description and OG description
const meta = app.metadataCache.getFileCache(file)
try {
const desc = Array.from(dom.querySelectorAll("p")).map(x => x.innerText).filter(x => !!x).join(' ').slice(0, 200) + '...'
dom.querySelector('#head-description').content = desc
dom.querySelector('#head-og-description').content = desc
} catch (e) { }
// Replace links
for (const el of dom.querySelectorAll("a.internal-link")) {
if (href = el.getAttribute('href').match(/^([^#]+)/)) {
const file = app.metadataCache.getFirstLinkpathDest(href[1], '')
if (meta?.frontmatter?.[YAML_FIELD + '_link']) {
// This file is shared, so update the link with the share URL
el.setAttribute('href', meta.frontmatter[YAML_FIELD + '_link'])
el.removeAttribute('target')
continue
}
}
// This file is not shared, so remove the link and replace with plain-text
el.replaceWith(el.innerText)
}
// Upload local images
for (const el of dom.querySelectorAll('img')) {
const src = el.getAttribute('src')
if (!src.startsWith('app://')) continue
try {
const localFile = window.decodeURIComponent(src.match(/app:\/\/local\/([^?#]+)/)[1])
const url = (await getHash(localFile)) + '.' + localFile.split('.').pop()
el.setAttribute('src', url)
el.removeAttribute('alt')
await upload({ filename: url, content: fs.readFileSync(localFile, { encoding: 'base64' }), encoding: 'base64' })
} catch (e) {
console.log(e)
}
}
// Share the file
const shareName = meta?.frontmatter?.[YAML_FIELD + '_hash'] || await getHash(file.path)
const shareFile = shareName + '.html'
await upload({ filename: shareFile, content: dom.documentElement.innerHTML })
// Upload theme CSS, unless this file has previously been shared
// To force a CSS re-upload, just remove the `share_link` frontmatter field
if (!meta?.frontmatter?.[YAML_FIELD + '_link']) {
await upload({ filename: 'style.css', content: css })
// Extract any base64 encoded attachments from the CSS.
// Will use the mime-type list above to determine which attachments to extract.
const regex = /url\s*\(\W*data:([^;,]+)[^)]*?base64\s*,\s*([A-Za-z0-9/=+]+).?\)/
for (const attachment of css.match(new RegExp(regex, 'g')) || []) {
if (match = attachment.match(new RegExp(regex))) {
if (extension(match[1])) {
const filename = (await getHash(match[2])) + `.${extension(match[1])}`
css = css.replace(match[0], `url("${filename}")`)
await upload({ filename: filename, content: match[2], encoding: 'base64' })
}
}
}
}
// Update the frontmatter in the current note
let contents = await app.vault.read(file)
contents = updateFrontmatter(contents, YAML_FIELD + '_updated', moment().format())
contents = updateFrontmatter(contents, YAML_FIELD + '_link', `${UPLOAD_LOCATION}${shareFile}`)
app.vault.modify(file, contents)
status.hide()
new Notice('File has been shared', 4000)
} catch (e) {
console.log(e)
status.hide()
new Notice('Failed to share file', 4000)
}