@quark.clip/quark 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/cli/bin/quark.js +120 -0
- package/cli/package-lock.json +1225 -0
- package/cli/package.json +23 -0
- package/cli/src/clipboard.js +81 -0
- package/cli/src/daemon.js +104 -0
- package/cli/src/installer.js +125 -0
- package/cli/src/mcp.js +90 -0
- package/cli/src/network.js +99 -0
- package/cli/src/transformers.js +222 -0
- package/dist/404.html +23 -0
- package/dist/assets/index-CaferGuT.js +79 -0
- package/dist/assets/index-Dn1mbcJe.css +1 -0
- package/dist/index.html +29 -0
- package/dist/logo.svg +5 -0
- package/package.json +57 -0
- package/server.ts +60 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const { marked } = require('marked');
|
|
2
|
+
|
|
3
|
+
function isExcelTSV(text) {
|
|
4
|
+
const lines = text.trim().split('\n').filter(l => l.trim().length > 0);
|
|
5
|
+
if (lines.length < 2) return false;
|
|
6
|
+
const tabs = lines[0].split('\t').length;
|
|
7
|
+
return tabs > 1 && lines.every(l => l.split('\t').length === tabs);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isCSV(text) {
|
|
11
|
+
const lines = text.trim().split('\n').filter(l => l.trim().length > 0);
|
|
12
|
+
if (lines.length < 2) return false;
|
|
13
|
+
const commas = lines[0].split(',').length;
|
|
14
|
+
return commas > 2 && lines.every(l => l.split(',').length === commas);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function convertToHTML(text, delimiter) {
|
|
18
|
+
const rows = text.trim().split('\n').filter(l => l.trim().length > 0).map(r => r.split(delimiter));
|
|
19
|
+
let html = '<table style="border-collapse: collapse; font-family: sans-serif; font-size: 14px;">\n';
|
|
20
|
+
rows.forEach((row, i) => {
|
|
21
|
+
html += ' <tr>\n';
|
|
22
|
+
row.forEach(cell => {
|
|
23
|
+
const tag = i === 0 ? 'th' : 'td';
|
|
24
|
+
const bg = i === 0 ? 'background-color: #f3f2f1;' : '';
|
|
25
|
+
const style = `border: 1px solid #d1d1d1; padding: 6px 12px; ${bg}`;
|
|
26
|
+
html += ` <${tag} style="${style}">${cell.trim()}</${tag}>\n`;
|
|
27
|
+
});
|
|
28
|
+
html += ' </tr>\n';
|
|
29
|
+
});
|
|
30
|
+
html += '</table>';
|
|
31
|
+
return html;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stripTrackingParams(url) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = new URL(url);
|
|
37
|
+
const paramsToRemove = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'si', 'igshid'];
|
|
38
|
+
let changed = false;
|
|
39
|
+
paramsToRemove.forEach(param => {
|
|
40
|
+
if (parsed.searchParams.has(param)) {
|
|
41
|
+
parsed.searchParams.delete(param);
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return changed ? parsed.toString() : url;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fixPDFLineBreaks(text) {
|
|
52
|
+
return text.replace(/([^\n])\n([^\n])/g, '$1 $2');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function prettifyJSON(text) {
|
|
56
|
+
try {
|
|
57
|
+
const obj = JSON.parse(text);
|
|
58
|
+
return JSON.stringify(obj, null, 2);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function applySmartQuotes(text) {
|
|
65
|
+
// Detect language based on character frequency
|
|
66
|
+
const frenchChars = (text.match(/[éèêëàâäôöûüçœ]/gi) || []).length;
|
|
67
|
+
const germanChars = (text.match(/[äöüß]/gi) || []).length;
|
|
68
|
+
const spanishChars = (text.match(/[ñáéíóú¿¡]/gi) || []).length;
|
|
69
|
+
|
|
70
|
+
// If French is dominant
|
|
71
|
+
if (frenchChars > 0 && frenchChars >= germanChars && frenchChars >= spanishChars) {
|
|
72
|
+
return text
|
|
73
|
+
.replace(/(^|\s)"(.*?)"(?=\s|$|[.,!?])/g, "$1« $2 »")
|
|
74
|
+
.replace(/(^|\s)'(.*?)'(?=\s|$|[.,!?])/g, "$1‹ $2 ›")
|
|
75
|
+
.replace(/--/g, "\u2014");
|
|
76
|
+
}
|
|
77
|
+
// If German is dominant
|
|
78
|
+
else if (germanChars > 0 && germanChars > frenchChars && germanChars >= spanishChars) {
|
|
79
|
+
return text
|
|
80
|
+
.replace(/(^|\s)"(.*?)"(?=\s|$|[.,!?])/g, "$1„$2“")
|
|
81
|
+
.replace(/(^|\s)'(.*?)'(?=\s|$|[.,!?])/g, "$1‚$2‘")
|
|
82
|
+
.replace(/--/g, "\u2014");
|
|
83
|
+
}
|
|
84
|
+
// If Spanish is dominant
|
|
85
|
+
else if (spanishChars > 0 && spanishChars > frenchChars && spanishChars > germanChars) {
|
|
86
|
+
return text
|
|
87
|
+
.replace(/(^|\s)"(.*?)"(?=\s|$|[.,!?])/g, "$1«$2»")
|
|
88
|
+
.replace(/(^|[-\u2014\s(\["])'/g, "$1\u2018")
|
|
89
|
+
.replace(/'/g, "\u2019")
|
|
90
|
+
.replace(/--/g, "\u2014");
|
|
91
|
+
}
|
|
92
|
+
// Default English
|
|
93
|
+
else {
|
|
94
|
+
return text
|
|
95
|
+
.replace(/(^|[-\u2014\s(\["])'/g, "$1\u2018")
|
|
96
|
+
.replace(/'/g, "\u2019")
|
|
97
|
+
.replace(/(^|[-\u2014/\[(\u2018\s])"/g, "$1\u201c")
|
|
98
|
+
.replace(/"/g, "\u201d")
|
|
99
|
+
.replace(/--/g, "\u2014");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderMathFormulas(text) {
|
|
104
|
+
let html = text;
|
|
105
|
+
const blockMathRegex = /\$\$\s*([\s\S]*?)\s*\$\$/g;
|
|
106
|
+
const inlineMathRegex = /\$(?!\s)([^$\n]+?)(?<!\s)\$/g;
|
|
107
|
+
|
|
108
|
+
let hasMath = false;
|
|
109
|
+
|
|
110
|
+
html = html.replace(blockMathRegex, (match, formula) => {
|
|
111
|
+
hasMath = true;
|
|
112
|
+
const encoded = encodeURIComponent(formula.trim());
|
|
113
|
+
return `<div style="text-align: center; margin: 1em 0;"><img src="https://latex.codecogs.com/svg.image?${encoded}" alt="${formula.replace(/"/g, '"')}" /></div>`;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
html = html.replace(inlineMathRegex, (match, formula) => {
|
|
117
|
+
hasMath = true;
|
|
118
|
+
const encoded = encodeURIComponent(formula.trim());
|
|
119
|
+
return `<img style="vertical-align: middle;" src="https://latex.codecogs.com/svg.image?${encoded}" alt="${formula.replace(/"/g, '"')}" />`;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return { hasMath, html };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isMarkdown(text) {
|
|
126
|
+
// Simple heuristic: contains headings, lists, bold, links, or tables
|
|
127
|
+
return /^#+\s/m.test(text) ||
|
|
128
|
+
/\*\*[^*]+\*\*/.test(text) ||
|
|
129
|
+
/\[.+?\]\(.+?\)/.test(text) ||
|
|
130
|
+
/^\s*[-*+]\s/m.test(text) ||
|
|
131
|
+
/^\s*\d+\.\s/m.test(text) ||
|
|
132
|
+
/`[^`]+`/.test(text) ||
|
|
133
|
+
/^> /m.test(text) ||
|
|
134
|
+
(text.includes('|') && /^[-:| ]+$/m.test(text));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function convertMarkdownToHTML(text) {
|
|
138
|
+
// First render math
|
|
139
|
+
const { html: mathProcessed } = renderMathFormulas(text);
|
|
140
|
+
// Then parse markdown
|
|
141
|
+
const html = await marked.parse(mathProcessed);
|
|
142
|
+
// Add some basic styling to tables and code blocks for clipboard pasting
|
|
143
|
+
return html
|
|
144
|
+
.replace(/<table>/g, '<table style="border-collapse: collapse; font-family: sans-serif; font-size: 14px; width: 100%;">')
|
|
145
|
+
.replace(/<th>/g, '<th style="border: 1px solid #d1d1d1; padding: 6px 12px; background-color: #f3f2f1; text-align: left;">')
|
|
146
|
+
.replace(/<td>/g, '<td style="border: 1px solid #d1d1d1; padding: 6px 12px;">')
|
|
147
|
+
.replace(/<code>/g, '<code style="background-color: #f4f4f4; padding: 2px 4px; border-radius: 4px; font-family: monospace;">')
|
|
148
|
+
.replace(/<pre><code.*?>/g, '<pre style="background-color: #f4f4f4; padding: 12px; border-radius: 8px; font-family: monospace; overflow-x: auto;"><code>');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeShouting(text) {
|
|
152
|
+
if (/^[A-Z\s.,!?'-]+$/.test(text) && text.length > 15 && text.includes(' ')) {
|
|
153
|
+
return text.charAt(0) + text.slice(1).toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The master pipeline
|
|
159
|
+
async function processClipboard(text, originalHtml) {
|
|
160
|
+
let result = { changed: false, text: text, html: originalHtml };
|
|
161
|
+
|
|
162
|
+
// 0. Case Normalization (Shouting)
|
|
163
|
+
const normalizedText = normalizeShouting(text);
|
|
164
|
+
if (normalizedText !== text) {
|
|
165
|
+
text = normalizedText;
|
|
166
|
+
result.text = text;
|
|
167
|
+
result.changed = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 0.5 Smart Quotes
|
|
171
|
+
if (text.includes('"') || text.includes("'")) {
|
|
172
|
+
if (!text.startsWith('{') && !text.startsWith('[') && !text.includes('function') && !text.includes('const ')) {
|
|
173
|
+
const smartText = applySmartQuotes(text);
|
|
174
|
+
if (smartText !== text) {
|
|
175
|
+
text = smartText;
|
|
176
|
+
result.text = text;
|
|
177
|
+
result.changed = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 1. URL Tracking Stripper
|
|
183
|
+
if (text.startsWith('http') && text.includes('utm_')) {
|
|
184
|
+
const clean = stripTrackingParams(text);
|
|
185
|
+
if (clean !== text) return { changed: true, text: clean, html: null };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 2. JSON Prettier
|
|
189
|
+
if ((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']'))) {
|
|
190
|
+
const pretty = prettifyJSON(text);
|
|
191
|
+
if (pretty !== text && pretty.includes('\n')) return { changed: true, text: pretty, html: null };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3. Excel TSV to HTML
|
|
195
|
+
if (isExcelTSV(text)) {
|
|
196
|
+
return { changed: true, text: text, html: convertToHTML(text, '\t') };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 4. CSV to HTML
|
|
200
|
+
if (isCSV(text) && !originalHtml) {
|
|
201
|
+
return { changed: true, text: text, html: convertToHTML(text, ',') };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 5. Full Markdown & Math to HTML
|
|
205
|
+
const mathResult = renderMathFormulas(text);
|
|
206
|
+
if ((isMarkdown(text) || mathResult.hasMath) && !originalHtml) {
|
|
207
|
+
const html = await convertMarkdownToHTML(text);
|
|
208
|
+
return { changed: true, text: text, html: html };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 6. PDF Line Breaks (Only apply if it looks like a broken paragraph, heuristic)
|
|
212
|
+
if (text.length > 100 && text.split('\n').length > 3 && !text.includes('\t') && !originalHtml) {
|
|
213
|
+
const fixed = fixPDFLineBreaks(text);
|
|
214
|
+
if (fixed !== text && fixed.length < text.length) {
|
|
215
|
+
return { changed: true, text: fixed, html: null };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { processClipboard };
|
package/dist/404.html
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Quark</title>
|
|
6
|
+
<script type="text/javascript">
|
|
7
|
+
// Single Page Apps for GitHub Pages
|
|
8
|
+
// MIT License
|
|
9
|
+
// https://github.com/rafgraph/spa-github-pages
|
|
10
|
+
var pathSegmentsToKeep = 1;
|
|
11
|
+
var l = window.location;
|
|
12
|
+
l.replace(
|
|
13
|
+
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
|
|
14
|
+
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
|
|
15
|
+
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
|
|
16
|
+
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
|
|
17
|
+
l.hash
|
|
18
|
+
);
|
|
19
|
+
</script>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|