Skip to content

Instantly share code, notes, and snippets.

@gcoda
Created September 6, 2022 13:15
Show Gist options
  • Save gcoda/5918cef4a34dfc109cacf365336ae925 to your computer and use it in GitHub Desktop.
Save gcoda/5918cef4a34dfc109cacf365336ae925 to your computer and use it in GitHub Desktop.
keep-takeout-to-markdown
import path from 'node:path'
import fs from 'node:fs/promises'
const sourceDirectory = './Keep'
const destDirectory = './Markdown'
const attachmentsDirectory = './Markdown/Attachments'
const filenames = await fs.readdir(sourceDirectory)
const formatTag = (s = '') =>
s
.replaceAll(/[\@\$\#\?]/g, '')
.trim()
.replaceAll(/[\-\_\.\ \\\/\=\?\:\s\*\+\&\|\(\)]/g, '-')
.replaceAll(/[\-]+/g, '-')
.toLowerCase()
const formatDate = (
date = new Date(),
offset = date.getTimezoneOffset(),
offsetAbs = Math.abs(offset)
) =>
`${new Date(date.getTime() - offset * 60 * 1000).toISOString().slice(0, -5)}${
offset > 0 ? '-' : '+'
}${String(Math.floor(offsetAbs / 60)).padStart(2, '0')}:${String(
offsetAbs % 60
).padStart(2, '0')}`
const getTitle = (note = {}) => {
if (note.title) return note.title
if (!note.textContent) {
return `${note.listContent ? 'list-' : ''}${new Date(
note.createdTimestampUsec / 1000
).toJSON()}`
}
return note.textContent
.split(/[\n\.]/)
.find(w => w.trim().length)
.trim()
}
const formatFilename = (note = {}) => {
const title = getTitle(note)
const formatted = note.isPinned
? `_pin_${formatTag(title)}`
: note.isArchived
? path.join('archive', `_archive_${formatTag(title)}`)
: note.isTrashed
? path.join('trash', formatTag(title))
: formatTag(title)
return formatted.slice(0, 64)
}
const formatAttachment = (a = { filePath: '' }) => {
const filePath = a.filePath
const relativeDirectory = path.relative(destDirectory, attachmentsDirectory)
const relativePath = path.join(relativeDirectory, filePath)
return `![[${relativePath}]]`
}
const formatListItem = (i = { text: '', isChecked: false }) =>
`- [${i.isChecked ? 'x' : ' '}] ${i.text}`.trim()
const formatAnnotation = (
a = { description: '', source: 'WEBLINK', title: '', url: '' }
) =>
a.source === 'WEBLINK'
? `- [${a.title}](${a.url}) - ${a.description}`
: ['', '```json', JSON.stringify(a, null, 2), '```', ''].join('\n')
for (let jsonFilename of filenames.filter(f => f.endsWith('.json'))) {
const noteJson = await fs.readFile(
path.resolve(sourceDirectory, jsonFilename),
'utf8'
)
const note = JSON.parse(noteJson)
const {
isPinned,
isTrashed,
isArchived,
textContent,
createdTimestampUsec,
userEditedTimestampUsec,
color,
labels,
annotations,
listContent,
attachments,
} = note
note.jsonFilename = jsonFilename
const created = formatDate(new Date(createdTimestampUsec / 1000))
const modified = formatDate(new Date(userEditedTimestampUsec / 1000))
const tags = [
...(labels ? labels.map(l => l.name).map(formatTag) : []),
...(isPinned ? ['Pinned'] : []),
...(isTrashed ? ['Trashed'] : []),
...(isArchived ? ['Archived'] : []),
...(color !== 'DEFAULT' ? [`highlight-${color.toLowerCase()}`] : []),
]
const title = getTitle(note)
const filename = `${formatFilename({ ...note })}.md`
const content = [
'---',
`title: ${title.replaceAll(/\s+/g, ' ')}`,
`created: ${created}`,
`modified: ${modified}`,
tags.length ? `tags: ${tags.join(', ')}` : null,
'---',
'',
`# ${title.replaceAll(/\s+/g, ' ')}\n`,
'',
labels
? textContent
?.split(' ')
.map(word =>
labels.find(l => `#${l.name}` == word)
? `#${formatTag(word)}`
: word
)
.join(' ')
: textContent,
...(listContent ? listContent.map(formatListItem) : []),
'',
...(annotations ? annotations.map(formatAnnotation) : []),
...(attachments ? attachments.map(formatAttachment) : []),
'',
]
.filter(l => l !== null)
.join('\n')
.replaceAll(/[\n]{2,}/g, '\n\n')
if (attachments) {
await fs.mkdir(attachmentsDirectory, { recursive: true })
for (const file of attachments) {
const attachmentSourcePath = path.resolve(sourceDirectory, file.filePath)
const attachmentDestPath = path.resolve(
attachmentsDirectory,
file.filePath
)
if (attachmentSourcePath.endsWith('.jpeg'))
try {
await fs.copyFile(
attachmentSourcePath.replace(/\.jpeg$/, '.jpg'),
attachmentDestPath
)
} catch (e) {
//
}
else {
try {
await fs.copyFile(attachmentSourcePath, attachmentDestPath)
} catch (err) {
console.error(err)
console.dir(
{ note, attachmentSourcePath, attachmentDestPath },
{ depth: 9 }
)
console.error('Error while copying attachment')
}
}
}
}
const destPath = path.resolve(destDirectory, filename)
const destDir = path.dirname(destPath)
await fs.mkdir(destDir, { recursive: true })
await fs.writeFile(destPath, content, 'utf-8')
}
{
"name": "takeout",
"version": "1.0.0",
"type": "module",
"description": "",
"main": "conver.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment