Skip to content

Commit

Permalink
refactor: adopt preact and restructure the codebase
Browse files Browse the repository at this point in the history
  • Loading branch information
pionxzh committed Jan 11, 2023
1 parent 7881d89 commit c8626c0
Show file tree
Hide file tree
Showing 23 changed files with 1,188 additions and 498 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module.exports = {
root: true,
extends: [
'@pionxzh/eslint-config-ts',
'@pionxzh/eslint-config-react',
],
rules: {
'no-console': 'off',
'no-alert': 'off',
'react/prop-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
auto-install-peers=true
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"devDependencies": {
"@commitlint/cli": "^17.3.0",
"@commitlint/config-conventional": "^17.3.0",
"@pionxzh/eslint-config-ts": "^0.1.1",
"@pionxzh/eslint-config-react": "^0.1.1",
"@types/node": "^18.11.17",
"cpy-cli": "^4.2.0",
"eslint": "^8.30.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/userscript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
},
"dependencies": {
"html2canvas": "^1.4.1",
"preact": "^10.11.3",
"sentinel-js": "^0.0.5",
"vite-plugin-monkey": "^2.10.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.5.0",
"vite": "^4.0.3"
}
}
79 changes: 79 additions & 0 deletions packages/userscript/src/exporter/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ChatGPTAvatar } from '../icons'
import { getConversation } from '../parser'
import { timestamp } from '../utils/utils'
import { downloadFile } from '../utils/download'

import templateHtml from '../template.html?raw'

export function exportToHtml() {
const conversations = getConversation()
if (conversations.length === 0) return alert('No conversation found. Please send a message first.')

const lang = document.documentElement.lang ?? 'en'
const conversationHtml = conversations.map((item) => {
const { author: { name, avatar }, lines } = item

const avatarEl = name === 'ChatGPT'
? `${ChatGPTAvatar}`
: `<img src="${avatar}" alt="${name}" />`

const linesHtml = lines.map((line) => {
const lineHtml = line.map((item) => {
switch (item.type) {
case 'text':
return escapeHtml(item.text)
case 'image':
return `<img src="${item.src}" referrerpolicy="no-referrer" />`
case 'code':
return `<code>${escapeHtml(item.code)}</code>`
case 'code-block':
return `<pre><code class="language-${item.lang}">${escapeHtml(item.code)}</code></pre>`
case 'link':
return `<a href="${item.href}" target="_blank" rel="noopener noreferrer">${escapeHtml(item.text)}</a>`
case 'ordered-list-item':
return `<ol>${item.items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ol>`
case 'unordered-list-item':
return `<ul>${item.items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`
case 'table': {
const header = item.headers.map(item => `<th>${escapeHtml(item)}</th>`).join('')
const body = item.rows.map(row => `<tr>${row.map(item => `<td>${escapeHtml(item)}</td>`).join('')}</tr>`).join('')
return `<table><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table>`
}
default:
return ''
}
}).join('')

const skipTags = ['pre', 'ul', 'ol', 'table']
if (skipTags.some(tag => lineHtml.startsWith(`<${tag}>`))) return lineHtml
return `<p>${lineHtml}</p>`
}).join('')

return `
<div class="conversation-item">
<div class="author">
${avatarEl}
</div>
<div class="conversation-content">
${linesHtml}
</div>
</div>`
}).join('')

const html = templateHtml
.replace('{{time}}', new Date().toISOString())
.replace('{{lang}}', lang)
.replace('{{content}}', conversationHtml)

const fileName = `ChatGPT-${timestamp()}.html`
downloadFile(fileName, 'text/html', html)
}

function escapeHtml(html: string) {
return html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
28 changes: 28 additions & 0 deletions packages/userscript/src/exporter/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import html2canvas from 'html2canvas'
import { downloadUrl } from '../utils/download'
import { sleep, timestamp } from '../utils/utils'

export async function exportToPng() {
const thread = document.querySelector('main .group')?.parentElement as HTMLElement
if (!thread || thread.children.length === 0) return

// hide bottom bar
thread.children[thread.children.length - 1].classList.add('hidden')

await sleep(100)

const canvas = await html2canvas(thread, {
scrollX: -window.scrollX,
scrollY: -window.scrollY,
windowWidth: thread.scrollWidth,
windowHeight: thread.scrollHeight,
})

// restore the layout
thread.children[thread.children.length - 1].classList.remove('hidden')

const dataUrl = canvas.toDataURL('image/png', 1)
.replace(/^data:image\/[^;]/, 'data:application/octet-stream')
const fileName = `ChatGPT-${timestamp()}.png`
downloadUrl(fileName, dataUrl)
}
21 changes: 21 additions & 0 deletions packages/userscript/src/exporter/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getConversation } from '../parser'
import type { Conversation } from '../type'
import { downloadFile } from '../utils/download'
import { timestamp } from '../utils/utils'
import { lineToText } from './text'

export function exportToMarkdown() {
const conversations = getConversation()
if (conversations.length === 0) return alert('No conversation found. Please send a message first.')

const text = conversationToMarkdown(conversations)
downloadFile(`chatgpt-${timestamp()}.md`, 'text/markdown', text)
}

function conversationToMarkdown(conversation: Conversation[]) {
return conversation.map((item) => {
const { author: { name }, lines } = item
const text = lines.map(line => lineToText(line)).join('\r\n\r\n')
return `#### ${name}:\r\n${text}`
}).join('\r\n\r\n')
}
33 changes: 33 additions & 0 deletions packages/userscript/src/exporter/text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ConversationLine } from '../type'
import { getConversation } from '../parser'
import { copyToClipboard } from '../utils/clipboard'
import { tableToMarkdown } from '../utils/markdown'

export function exportToText() {
const conversations = getConversation()
if (conversations.length === 0) return alert('No conversation found. Please send a message first.')

const text = conversations.map((item) => {
const { author: { name }, lines } = item
const text = lines.map(line => lineToText(line)).join('\r\n\r\n')
return `${name}:\r\n${text}`
}).join('\r\n\r\n')

copyToClipboard(text)
}

export function lineToText(line: ConversationLine): string {
return line.map((item) => {
switch (item.type) {
case 'text': return item.text
case 'image': return '[image]'
case 'link': return `[${item.text}](${item.href})`
case 'ordered-list-item': return item.items.map((item, index) => `${index + 1}. ${item}`).join('\r\n')
case 'unordered-list-item': return item.items.map(item => `- ${item}`).join('\r\n')
case 'code': return `\`${item.code}\``
case 'code-block': return `\`\`\`${item.lang}\r\n${item.code}\`\`\``
case 'table': return tableToMarkdown(item.headers, item.rows)
default: return ''
}
}).join('')
}
19 changes: 0 additions & 19 deletions packages/userscript/src/icons.ts

This file was deleted.

Loading

0 comments on commit c8626c0

Please sign in to comment.