markpaste 0.0.1 → 0.0.6

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.
@@ -0,0 +1,36 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: pnpm/action-setup@v4
19
+ with:
20
+ version: 10.22.0
21
+
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22.14.0 # Minimum for Trusted Publishing
25
+ registry-url: 'https://registry.npmjs.org'
26
+ cache: 'pnpm'
27
+
28
+ - run: pnpm install --frozen-lockfile
29
+ - run: pnpm exec playwright install
30
+
31
+ - run: pnpm test
32
+
33
+ - name: Publish to npm
34
+ run: |
35
+ npm install -g npm@latest
36
+ npm publish --provenance --access public --registry https://registry.npmjs.org/
package/.nojekyll ADDED
File without changes
package/AGENTS.md CHANGED
@@ -13,9 +13,10 @@ MarkPaste is a web-based utility for converting rich text to Markdown. The core
13
13
  - `src/cleaner.js`: The logic for cleaning the HTML.
14
14
  - `src/converter.js`: The logic for converting HTML to Markdown.
15
15
 
16
- - `src/markpaste.js`: The library entry point. (for node or bundling)
16
+ - `src/index.js`: The library entry point. (for node or bundling)
17
17
 
18
18
  - Also:
19
+ - `bin/markpaste`: The CLI tool for macOS clipboard integration.
19
20
  - `test/web/`: Playwright tests for core application functionality.
20
21
  - `test/node/`: Node.js unit tests for library logic.
21
22
  - `third_party/`: External assets like `pandoc.wasm`.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # MarkPaste
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/markpaste.svg)](https://www.npmjs.com/package/markpaste)
4
+
3
5
  MarkPaste is an isomorphic tool that converts rich text to Markdown. It works seamlessly in both the **browser** as a web application and in **Node.js** as a library.
4
6
 
5
7
  ## Features
@@ -13,16 +15,18 @@ MarkPaste is an isomorphic tool that converts rich text to Markdown. It works se
13
15
  ## Usage
14
16
 
15
17
  ### Web Application
18
+
16
19
  1. **Open the app:** Run locally or host the `index.html`.
17
20
  2. **Paste:** Copy rich text from a webpage or document and paste it into the input area.
18
21
  3. **Get Markdown:** View the generated Markdown from multiple converters simultaneously.
19
22
  4. **Copy:** Click to copy the Markdown to your clipboard.
20
23
 
21
24
  ### Node.js Library
25
+
22
26
  MarkPaste can be used programmatically in Node.js environments. It uses `linkedom` to provide a lightweight DOM implementation for the conversion logic.
23
27
 
24
28
  ```javascript
25
- import { convert } from './src/markpaste.js';
29
+ import {convert} from './src/index.js';
26
30
 
27
31
  const html = '<h1>Hello World</h1><p>This is <b>bold</b> text.</p>';
28
32
 
@@ -31,11 +35,11 @@ const markdown = await convert(html);
31
35
  console.log(markdown);
32
36
 
33
37
  // Using Pandoc
34
- const pandocMarkdown = await convert(html, { converter: 'pandoc' });
38
+ const pandocMarkdown = await convert(html, {converter: 'pandoc'});
35
39
  console.log(pandocMarkdown);
36
40
 
37
41
  // Disabling HTML cleaning
38
- const rawMarkdown = await convert(html, { clean: false });
42
+ const rawMarkdown = await convert(html, {clean: false});
39
43
  ```
40
44
 
41
45
  ## Development
@@ -43,6 +47,7 @@ const rawMarkdown = await convert(html, { clean: false });
43
47
  To run MarkPaste locally:
44
48
 
45
49
  1. **Clone & Install:**
50
+
46
51
  ```bash
47
52
  git clone https://github.com/paulirish/markpaste.git
48
53
  cd markpaste
@@ -56,11 +61,14 @@ To run MarkPaste locally:
56
61
  Access the application at `http://localhost:7025`.
57
62
 
58
63
  ### Testing
64
+
59
65
  The project uses a dual-testing strategy:
66
+
60
67
  - **Node.js tests:** Unit tests for isomorphic modules (`pnpm test:node`).
61
68
  - **Playwright tests:** End-to-end browser testing (`pnpm test:web`).
62
69
 
63
70
  Run all tests with:
71
+
64
72
  ```bash
65
73
  pnpm test
66
74
  ```
package/bin/markpaste ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * markpaste
5
+ *
6
+ * 1. Reads HTML from the clipboard (via raw hex from osascript)
7
+ * 2. Cleans and converts it to Markdown using MarkPaste
8
+ * 3. Saves Markdown as Plain Text and Cleaned HTML as HTML back to clipboard
9
+ * 4. Verifies the set content.
10
+ */
11
+
12
+ import { execSync, spawnSync } from 'node:child_process';
13
+ import { writeFileSync, unlinkSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import os from 'node:os';
17
+
18
+ import { convert, cleanHTML, isProbablyMarkdown } from '../src/index.js';
19
+ import { marked } from 'marked';
20
+
21
+ /**
22
+ * Formats a string for preview in the logs (truncated, escaped, and dimmed).
23
+ */
24
+ function textToPreview(text, labelWidth = 0) {
25
+ const dimmedStart = '\x1b[2m';
26
+ const dimmedEnd = '\x1b[22m';
27
+ const columns = process.stdout.columns || 80;
28
+ // maxLength = columns - labelWidth - 4 (for " ...") - 1 (safety)
29
+ const maxLength = Math.max(10, columns - labelWidth - 5);
30
+ const preview = text.substring(0, maxLength).replace(/\n/g, '\\n');
31
+ return `${dimmedStart}${preview}${dimmedEnd}...`;
32
+ }
33
+
34
+ /**
35
+ * Reads the HTML flavor from the macOS clipboard.
36
+ * Returns null if no HTML flavor is present.
37
+ */
38
+ function readHtmlFromClipboard() {
39
+ try {
40
+ const script = 'try\n the clipboard as «class HTML»\non error\n return ""\nend try';
41
+ const hexData = execSync(`osascript -e '${script}'`, { encoding: 'utf8' }).trim();
42
+ const match = hexData.match(/«data HTML([0-9A-F]*)»/i);
43
+ if (match && match[1]) {
44
+ return Buffer.from(match[1], 'hex').toString('utf8');
45
+ }
46
+ } catch (e) {
47
+ // Silently fail to return null
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Reads the plain text flavor from the macOS clipboard.
54
+ */
55
+ function readTextFromClipboard() {
56
+ return execSync('pbpaste', { encoding: 'utf8' });
57
+ }
58
+
59
+ /**
60
+ * Sets both the HTML and Plain Text flavors on the macOS clipboard.
61
+ */
62
+ function setClipboard(html, markdown) {
63
+ const mdTmp = path.join(os.tmpdir(), `mp_md_${Date.now()}.txt`);
64
+ writeFileSync(mdTmp, markdown);
65
+
66
+ const htmlHex = Buffer.from(html, 'utf8').toString('hex');
67
+ const appleScript = `
68
+ set theText to (read (POSIX file "${mdTmp}") as «class utf8»)
69
+ set the clipboard to {«class HTML»:«data HTML${htmlHex}», text:theText}
70
+ `;
71
+
72
+ const result = spawnSync('osascript', [], { input: appleScript, encoding: 'utf8' });
73
+ unlinkSync(mdTmp);
74
+
75
+ if (result.status !== 0) {
76
+ throw new Error(`osascript failed: ${result.stderr}`);
77
+ }
78
+ }
79
+
80
+ async function main() {
81
+ try {
82
+ const rawHtml = readHtmlFromClipboard();
83
+ const hasHtmlFlavor = !!rawHtml;
84
+ const input = rawHtml || readTextFromClipboard();
85
+
86
+ if (!input || input.trim() === '') {
87
+ console.error('No content found in clipboard.');
88
+ process.exit(1);
89
+ }
90
+
91
+ const isMarkdown = isProbablyMarkdown(input, hasHtmlFlavor);
92
+
93
+ console.log('Read from clipboard:');
94
+ const typeLabel = isMarkdown ? 'Markdown ' : 'HTML ';
95
+ const inputLabel = `${typeLabel}(${input.length} chars): `;
96
+ console.log(` ${inputLabel}${textToPreview(input, inputLabel.length + 2)}`);
97
+
98
+ let cleanedHtml;
99
+ let markdown;
100
+
101
+ if (isMarkdown) {
102
+ markdown = input;
103
+ cleanedHtml = await marked.parse(input);
104
+ } else {
105
+ cleanedHtml = await cleanHTML(input);
106
+ markdown = await convert(input, { clean: true, isMarkdown: false });
107
+ }
108
+
109
+ setClipboard(cleanedHtml, markdown);
110
+
111
+ const mdLabel = `Markdown (${markdown.length} chars): `;
112
+ const htmlLabel = `HTML (${cleanedHtml.length} chars): `;
113
+ const labelWidth = Math.max(mdLabel.length, htmlLabel.length) + 2;
114
+
115
+ console.log('\nClipboard updated with:');
116
+ console.log(` ${mdLabel.padEnd(labelWidth - 2)}${textToPreview(markdown, labelWidth)}`);
117
+ console.log(` ${htmlLabel.padEnd(labelWidth - 2)}${textToPreview(cleanedHtml, labelWidth)}`);
118
+
119
+ } catch (err) {
120
+ console.error('Error during conversion:', err);
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ main();
package/index.html CHANGED
@@ -56,6 +56,10 @@
56
56
 
57
57
  <!-- Bottom: 2 Columns Output -->
58
58
  <section class="section-outputs two-column-grid">
59
+ <div id="loadingOverlay" class="loading-overlay hidden">
60
+ <div class="spinner"></div>
61
+ <span>Converting...</span>
62
+ </div>
59
63
  <!-- Turndown -->
60
64
  <div class="output-column">
61
65
  <div class="panel output-panel">
@@ -135,10 +139,10 @@
135
139
  <script type="importmap">
136
140
  {
137
141
  "imports": {
138
- "marked": "/node_modules/marked/lib/marked.esm.js",
139
- "turndown": "/node_modules/turndown/lib/turndown.es.js",
140
- "turndown-plugin-gfm": "/node_modules/turndown-plugin-gfm/lib/turndown-plugin-gfm.es.js",
141
- "@bjorn3/browser_wasi_shim": "/node_modules/@bjorn3/browser_wasi_shim/dist/index.js"
142
+ "marked": "https://unpkg.com/marked@15.0.0/lib/marked.esm.js",
143
+ "turndown": "https://unpkg.com/turndown@7.2.0/lib/turndown.es.js",
144
+ "turndown-plugin-gfm": "https://unpkg.com/turndown-plugin-gfm@1.0.2/lib/turndown-plugin-gfm.es.js",
145
+ "@bjorn3/browser_wasi_shim": "https://unpkg.com/@bjorn3/browser_wasi_shim@0.4.0/dist/index.js"
142
146
  }
143
147
  }
144
148
  </script>
package/package.json CHANGED
@@ -1,23 +1,31 @@
1
1
  {
2
2
  "name": "markpaste",
3
- "version": "0.0.1",
3
+ "version": "0.0.6",
4
4
  "description": "HTML and Markdown, perfected for pasting into documents, emails, and more. So isomorphic. lol.",
5
- "main": "src/markpaste.js",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "markpaste": "bin/markpaste"
8
+ },
6
9
  "scripts": {
7
10
  "start": "npx statikk --port 7025",
8
11
  "typecheck": "tsc --noEmit",
9
- "format": "prettier --write .",
12
+ "format": "prettier --write \"**/*.{js,ts,json,md}\"",
10
13
  "test": "pnpm run typecheck && pnpm run test:node && pnpm run test:web",
11
14
  "test:web": "playwright test",
12
15
  "test:node": "node --test test/node/**/*.test.js"
13
16
  },
14
17
  "packageManager": "pnpm@10.22.0",
15
- "engines": {
16
- "npm": "Please use pnpm instead of NPM to install dependencies",
17
- "pnpm": "10.22.0"
18
+ "keywords": [
19
+ "markdown",
20
+ "html",
21
+ "clipboard",
22
+ "isomorphic"
23
+ ],
24
+ "author": "Paul Irish <npm@paul.irish>",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/paulirish/markpaste"
18
28
  },
19
- "keywords": [],
20
- "author": "",
21
29
  "license": "Apache-2.0",
22
30
  "type": "module",
23
31
  "devDependencies": {
@@ -17,8 +17,8 @@ export default defineConfig({
17
17
  },
18
18
  ],
19
19
  webServer: {
20
- command: 'npx http-server -p 8081',
21
- url: 'http://127.0.0.1:8081',
20
+ command: 'npx http-server -p 7025',
21
+ url: 'http://127.0.0.1:7025',
22
22
  reuseExistingServer: !process.env.CI,
23
23
  },
24
24
  });
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
4
+ import { spawnSync } from 'node:child_process';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { getConverter } from '../src/converter.js';
8
+ import { cleanHTML } from '../src/cleaner.js';
9
+
10
+ const testCases = [
11
+ {
12
+ name: 'Basic text',
13
+ html: '<p>Hello <b>world</b> and <i>everyone</i>!</p>'
14
+ },
15
+ {
16
+ name: 'Headings',
17
+ html: '<h1>Main Heading</h1><h2>Sub Heading</h2><h3>Third level</h3>'
18
+ },
19
+ {
20
+ name: 'Lists',
21
+ html: '<ul><li>Item 1</li><li>Item 2</li></ul><ol><li>First</li><li>Second</li></ol>'
22
+ },
23
+ {
24
+ name: 'Links',
25
+ html: '<p>Check out <a href="https://example.com">this link</a>.</p>'
26
+ },
27
+ {
28
+ name: 'Code',
29
+ html: '<p>Use <code>console.log()</code> to debug.</p><pre><code>function hello() {\n console.log("world");\n}</code></pre>'
30
+ },
31
+ {
32
+ name: 'Tables',
33
+ html: `
34
+ <table>
35
+ <thead>
36
+ <tr><th>Col 1</th><th>Col 2</th></tr>
37
+ </thead>
38
+ <tbody>
39
+ <tr><td>Data 1</td><td>Data 2</td></tr>
40
+ </tbody>
41
+ </table>
42
+ `
43
+ },
44
+ {
45
+ name: 'Tricky Span',
46
+ html: `
47
+ <p>The<span> </span>
48
+ <code class="w3-codespan">debugger</code>
49
+ <span> </span>keyword stops the execution of JavaScript.</p>
50
+ `
51
+ },
52
+ {
53
+ name: 'Nested formatting',
54
+ html: '<p><b>Bold and <i>italic and <u>underlined</u></i></b></p>'
55
+ },
56
+ {
57
+ name: 'Images',
58
+ html: '<img src="https://example.com/image.png" alt="An image" title="Image title">'
59
+ },
60
+ {
61
+ name: 'Blockquotes',
62
+ html: '<blockquote><p>This is a quote.</p><footer>— Someone</footer></blockquote>'
63
+ },
64
+ {
65
+ name: 'Horizontal Rule',
66
+ html: '<p>Before</p><hr><p>After</p>'
67
+ },
68
+ {
69
+ name: 'Task Lists',
70
+ html: '<ul><li>[ ] Todo</li><li>[x] Done</li></ul>'
71
+ }
72
+ ];
73
+
74
+ async function runComparison() {
75
+ const turndown = await getConverter('turndown');
76
+ const pandoc = await getConverter('pandoc');
77
+
78
+ console.log('Comparing Turndown vs Pandoc...\n');
79
+
80
+ for (const tc of testCases) {
81
+ console.log(`Test Case: ${tc.name}`);
82
+
83
+ const cleaned = await cleanHTML(tc.html);
84
+ const tOut = (await turndown.convert(cleaned)).trim();
85
+ const pOut = (await pandoc.convert(cleaned)).trim();
86
+
87
+ if (tOut === pOut) {
88
+ console.log(' ✅ Match');
89
+ } else {
90
+ console.log(' ❌ Mismatch - Diffing...');
91
+
92
+ const tmpDir = path.join(os.tmpdir(), 'markpaste-comp');
93
+ if (!existsSync(tmpDir)) mkdirSync(tmpDir);
94
+
95
+ const tFile = path.join(tmpDir, 'turndown.md');
96
+ const pFile = path.join(tmpDir, 'pandoc.md');
97
+
98
+ writeFileSync(tFile, tOut + '\n');
99
+ writeFileSync(pFile, pOut + '\n');
100
+
101
+ // Use git diff --no-index | delta
102
+ const diffCmd = `git --no-pager diff --no-index --color=always ${tFile} ${pFile} | delta`;
103
+ const result = spawnSync('sh', ['-c', diffCmd], { encoding: 'utf8', stdio: 'inherit' });
104
+
105
+ // cleanup
106
+ unlinkSync(tFile);
107
+ unlinkSync(pFile);
108
+ }
109
+ console.log('-'.repeat(40));
110
+ }
111
+
112
+ // Cleanup pandoc worker if necessary
113
+ if (pandoc.dispose) pandoc.dispose();
114
+ }
115
+
116
+ runComparison().catch(err => {
117
+ console.error(err);
118
+ process.exit(1);
119
+ });