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.
- package/.github/workflows/publish.yml +36 -0
- package/.nojekyll +0 -0
- package/AGENTS.md +2 -1
- package/README.md +11 -3
- package/bin/markpaste +125 -0
- package/index.html +8 -4
- package/package.json +16 -8
- package/playwright.config.ts +2 -2
- package/scripts/compare-converters.js +119 -0
- package/src/app.js +153 -39
- package/src/cleaner.js +71 -16
- package/src/converter.js +1 -1
- package/src/index.js +46 -0
- package/src/pandoc.js +2 -2
- package/src/renderer.js +4 -4
- package/src/style.css +43 -0
- package/test/node/cleaner.test.js +24 -5
- package/test/node/converter.test.js +10 -7
- package/test/node/index.test.js +17 -4
- package/test/node/markpaste.test.js +72 -0
- package/test/node/pandoc.test.js +3 -3
- package/test/web/basic-load.spec.ts +6 -6
- package/test/web/cleaner.spec.ts +1 -1
- package/test/web/pasting.spec.ts +31 -4
- package/types/globals.d.ts +10 -7
- package/src/markpaste.js +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {test} from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import {
|
|
3
|
+
import {cleanHTML, removeStyleAttributes} from '../../src/cleaner.js';
|
|
4
4
|
|
|
5
5
|
test('cleaner: cleanHTML should remove disallowed tags', async () => {
|
|
6
6
|
const html = '<div><p>Hello</p><script>alert(1)</script><span>World</span></div>';
|
|
@@ -36,9 +36,7 @@ test('cleaner: removeStyleAttributes should strip style attributes', async () =>
|
|
|
36
36
|
assert.strictEqual(stripped.toLowerCase().includes('<p>hello</p>'), true);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
|
|
40
39
|
test('cleaner: handles leading OL/UL tags correctly', async () => {
|
|
41
|
-
|
|
42
40
|
const html = `<meta charset='utf-8'><ol style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px; padding-left: 40px; list-style: outside decimal; color: rgb(32, 33, 36); font-family: Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><h1 class="devsite-page-title" tabindex="-1" style="box-sizing: inherit; font: 500 36px / 44px "Google Sans", "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; color: rgb(32, 33, 36); letter-spacing: normal; margin: 0px 0px 24px; overflow: visible; text-overflow: ellipsis; display: inline; vertical-align: middle;">Manage Python packages<devsite-actions data-nosnippet="" style="box-sizing: inherit; font-feature-settings: "dgun"; display: inline-flex; gap: 8px; padding-inline: 8px;"><devsite-feature-tooltip ack-key="AckCollectionsBookmarkTooltipDismiss" analytics-category="Site-Wide Custom Events" analytics-action-show="Callout Profile displayed" analytics-action-close="Callout Profile dismissed" analytics-label="Create Collection Callout" class="devsite-page-bookmark-tooltip nocontent inline-block" dismiss-button="true" id="devsite-collections-dropdown" dismiss-button-text="Dismiss" close-button-text="Got it" rendered="" current-step="0" style="--devsite-popout-top: calc(100% + 17px); --devsite-popout-width: min(50vw,320px); --devsite-feature-tooltip-text-color: #fff; position: relative; box-sizing: inherit; font-feature-settings: "dgun"; display: inline-block; --devsite-popout-offset-x: 32px;"><slot><devsite-bookmark class="show" style="box-sizing: inherit; font-feature-settings: "dgun"; --devsite-bookmark-background: 0; --devsite-bookmark-background-focus-legacy: #e8eaed; --devsite-bookmark-background-hover-legacy: #f1f3f4; --devsite-bookmark-icon-color: #5f6368; --devsite-bookmark-icon-color-saved: #1a73e8; --devsite-bookmark-icon-color-saved-hover: #174ea6; --devsite-dropdown-list-toggle-background-hover: #f1f3f4; --devsite-dropdown-list-toggle-border: 1px solid #dadce0; --devsite-dropdown-list-toggle-border-hover: 1px solid #dadce0; --devsite-dropdown-list-toggle-height: 36px; display: inline-flex; -webkit-box-align: center; align-items: center; background: none 0px 50% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px; box-shadow: none; cursor: pointer; height: 36px; -webkit-box-pack: center; justify-content: center; margin: 0px; padding: 0px; vertical-align: middle;"><devsite-dropdown-list aria-label="Bookmark collections drop down" ellipsis="" checkboxes="" fetchingitems="true" writable="" additemtext="New Collection" rendered="" style="--devsite-checkbox-icon-canvas-offset-x: -10px; --devsite-checkbox-icon-canvas-offset-y: -8px; --devsite-checkbox-offset-x: 4px; --devsite-checkbox-offset-y: -2px; --devsite-mdc-line-height: 50px; display: inline-flex; position: relative; vertical-align: middle; box-sizing: inherit; font-feature-settings: "dgun"; --devsite-button-box-shadow: none; visibility: visible;"><button class="toggle-button button" aria-haspopup="menu" id="dropdown-list-0-toggle" aria-controls="dropdown-list-0-dropdown" aria-expanded="false" aria-label="Open dropdown" style="align-self: auto; appearance: none; background: 0px center; border-color: rgb(218, 220, 224); border-style: solid; border-width: 1px; border-image: none 100% / 1 / 0 stretch; border-radius: 4px; box-shadow: none; box-sizing: border-box; color: rgb(95, 99, 104); cursor: pointer; display: flex; font: 500 14px / 34px "Google Sans", "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; height: 36px; letter-spacing: normal; max-width: none; min-width: auto; outline: 0px; overflow: hidden; padding: 0px 3px; text-align: center; text-decoration: none; text-overflow: ellipsis; text-transform: none; transition: background-color 0.2s, border 0.2s, box-shadow 0.2s; vertical-align: middle; white-space: nowrap; width: auto; margin: 0px; margin-inline-end: 0px; -webkit-box-align: center; -webkit-box-pack: center; align-items: center; justify-content: center;"><slot name="toggle"><span data-label="devsite-bookmark-direct-action" data-title="Save page" class="material-icons bookmark-icon bookmark-action" slot="toggle" style="-webkit-font-smoothing: antialiased; text-rendering: optimizelegibility; overflow-wrap: normal; font-style: normal; font-variant: normal; font-size-adjust: none; font-language-override: normal; font-kerning: auto; font-optical-sizing: auto; font-feature-settings: "liga"; font-variation-settings: normal; font-weight: normal; font-stretch: normal; font-size: 24px; line-height: 1; font-family: "Material Icons"; text-transform: none; box-sizing: inherit; letter-spacing: normal; display: inline-block; white-space: nowrap; direction: ltr; color: rgb(95, 99, 104); transition: color 0.2s; vertical-align: bottom;">bookmark_border</span></slot></button></devsite-dropdown-list></devsite-bookmark></slot></devsite-feature-tooltip></devsite-actions></h1><devsite-toc class="devsite-nav devsite-toc-embedded" depth="2" devsite-toc-embedded="" visible="" style="box-sizing: inherit; font-feature-settings: "dgun"; font-size: 13px; display: block; margin: 28px 0px 24px;"><ul class="devsite-nav-list" style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px; padding: 0px; list-style: outside none; border-inline-start: 4px solid rgb(25, 103, 210); width: auto; padding-inline-start: 12px;"><li class="devsite-nav-item devsite-nav-heading devsite-toc-toggle" style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px; padding: 0px; line-height: 16px; display: flex;"></li><li class="devsite-nav-item" visible="" style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px; padding: 0px; line-height: 16px; display: block;"></li><li class="devsite-toc-toggle" style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px; padding: 0px; display: flex;"></li></ul></devsite-toc><div class="devsite-article-body clearfix" style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 16px 0px 0px; padding: 0px;"><p style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px 0px 16px; padding: 0px; color: rgb(32, 33, 36); font-family: Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><a href="https://pypi.org/" class="external" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">Python Package Index</a><span> </span>(PyPI) is a public repository for Python packages. You can use Artifact Registry to create private repositories for your Python packages. These private repositories use the canonical Python repository implementation, the<span> </span><a href="https://www.python.org/dev/peps/pep-0503/" class="external" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">simple repository API</a><span> </span>(PEP 503), and work with installation tools like<span> </span><code translate="no" dir="ltr" style="box-sizing: inherit; font-feature-settings: "dgun"; background: none 0% 0% / auto repeat scroll padding-box border-box rgb(241, 243, 244); color: rgb(55, 71, 79); font-style: normal; font-variant: normal; font-weight: 500; font-stretch: 100%; font-size: 14.4px; line-height: 14.4px; font-family: "Roboto Mono", monospace; font-optical-sizing: auto; font-size-adjust: none; font-kerning: auto; font-variation-settings: normal; font-language-override: normal; padding: 1px 4px; direction: ltr !important; text-align: left !important; border-color: rgb(55, 71, 79); border-style: none; border-width: 0px; border-image: none 100% / 1 / 0 stretch; border-radius: 0px; word-break: break-word;">pip</code>.</p><h2 id="overview" data-text="Overview" tabindex="-1" role="presentation" style="box-sizing: inherit; font: 400 24px / 32px "Google Sans", "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; letter-spacing: normal; margin: 48px 0px 24px; overflow-x: clip; text-overflow: ellipsis; border-bottom: 0px none rgb(32, 33, 36); padding: 0px; color: rgb(32, 33, 36); orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="devsite-heading" role="heading" aria-level="2" style="box-sizing: inherit; font-feature-settings: "dgun";">Overview</span><button type="button" class="devsite-heading-link button-flat material-icons" aria-label="Copy link to this section: Overview" data-title="Copy link to this section: Overview" data-id="overview" style="box-sizing: border-box; font-feature-settings: "liga"; appearance: none; align-self: auto; background: none 0px 50% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); border: 0px; border-radius: 4px; box-shadow: none; color: rgb(95, 99, 104); cursor: pointer; display: inline-block; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: 100%; font-size: 24px; font-family: "Material Icons"; font-optical-sizing: auto; font-size-adjust: none; font-kerning: auto; font-variation-settings: normal; font-language-override: normal; height: 24px; letter-spacing: normal; line-height: 24px; margin: 0px; margin-inline-end: 0px; max-width: none; min-width: 36px; outline: 0px; overflow: hidden; padding: 0px 24px; text-align: center; text-decoration: none; text-overflow: ellipsis; text-transform: none; transition: background-color 0.2s, border 0.2s, box-shadow 0.2s; vertical-align: middle; white-space: nowrap; width: auto; overflow-wrap: normal; direction: ltr; -webkit-font-smoothing: antialiased; padding-inline: 8px; --devsite-button-white-line-height: 24px; --devsite-button-white-background-hover: transparent; opacity: 0;"></button></h2><p style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 16px 0px; padding: 0px; color: rgb(32, 33, 36); font-family: Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">To get familiar with Python packages in Artifact Registry, you can try the<span> </span><a href="https://docs.cloud.google.com/artifact-registry/docs/python/quickstart" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">quickstart</a>.</p><p style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 16px 0px; padding: 0px; color: rgb(32, 33, 36); font-family: Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">When you are ready to learn more, read the following information:</p><ol style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px; padding-left: 40px; list-style: outside decimal; color: rgb(32, 33, 36); font-family: Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><li style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 12px 0px; padding: 0px;"><a href="https://docs.cloud.google.com/artifact-registry/docs/repositories/create-repos" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">Create a Python package repository</a><span> </span>for your packages.</li><li style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 12px 0px; padding: 0px;"><a href="https://docs.cloud.google.com/artifact-registry/docs/access-control" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">Grant permissions</a><span> </span>to the account that will connect with the repository. Service accounts for common integrations with Artifact Registry have default<span> </span><a href="https://docs.cloud.google.com/artifact-registry/docs/access-control#gcp" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">permissions</a><span> </span>for repositories in the same project.</li><li style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 12px 0px; padding: 0px;">Configure your tools:<ul style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 0px; padding-left: 40px; list-style: outside disc;"><li style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 12px 0px; padding: 0px;"><a href="https://docs.cloud.google.com/artifact-registry/docs/python/authentication" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">Configure authentication</a><span> </span>for Python clients that interact with the repository.</li><li style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 12px 0px; padding: 0px;"><a href="https://docs.cloud.google.com/artifact-registry/docs/configure-cloud-build" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">Configure Cloud Build</a><span> </span>to upload and download packages.</li><li style="box-sizing: inherit; font-feature-settings: "dgun"; margin: 12px 0px; padding: 0px;">Learn about<span> </span><a href="https://docs.cloud.google.com/artifact-registry/docs/deploy" style="box-sizing: inherit; font-feature-settings: "dgun"; color: rgb(26, 115, 232); outline: 0px; text-decoration: rgb(26, 115, 232); word-break: break-word;">deploying</a><span> </span>to Google Cloud runtime environments.</li></ul></li></ol><br class="Apple-interchange-newline">`;
|
|
43
41
|
|
|
44
42
|
const cleaned = await cleanHTML(html);
|
|
@@ -47,4 +45,25 @@ test('cleaner: handles leading OL/UL tags correctly', async () => {
|
|
|
47
45
|
assert.strictEqual(cleaned.toLowerCase().startsWith('<ol'), false);
|
|
48
46
|
assert.strictEqual(cleaned.toLowerCase().includes('<h1'), true);
|
|
49
47
|
assert.strictEqual(cleaned.toLowerCase().includes('manage python packages'), true);
|
|
50
|
-
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('cleaner: should unwrap <b style="font-weight: normal"> and equivalents', async () => {
|
|
51
|
+
const cases = [
|
|
52
|
+
['<b style="font-weight: normal">Not bold</b>', 'Not bold'],
|
|
53
|
+
['<strong style="font-weight: normal">Not bold</strong>', 'Not bold'],
|
|
54
|
+
['<b style="font-weight: 400">Not bold</b>', 'Not bold'],
|
|
55
|
+
['<b style="FONT-WEIGHT: NORMAL">Not bold</b>', 'Not bold'],
|
|
56
|
+
['<b style="font-weight: lighter">Not bold</b>', 'Not bold'],
|
|
57
|
+
['<i style="font-style: normal">Not italic</i>', 'Not italic'],
|
|
58
|
+
['<em style="font-style: normal">Not italic</em>', 'Not italic'],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const [input, expected] of cases) {
|
|
62
|
+
const cleaned = await cleanHTML(input);
|
|
63
|
+
assert.strictEqual(cleaned, expected, `Failed for input: ${input}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Should keep normal <b>
|
|
67
|
+
const bold = await cleanHTML('<b>Bold</b>');
|
|
68
|
+
assert.ok(bold.toUpperCase().includes('<B>BOLD</B>'));
|
|
69
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {test} from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import {
|
|
3
|
+
import {getConverter} from '../../src/converter.js';
|
|
4
4
|
|
|
5
5
|
test('converter: turndown should convert HTML to Markdown', async () => {
|
|
6
6
|
const converter = await getConverter('turndown');
|
|
@@ -10,9 +10,12 @@ test('converter: turndown should convert HTML to Markdown', async () => {
|
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
test('converter: unknown converter should throw error', async () => {
|
|
13
|
-
await assert.rejects(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
await assert.rejects(
|
|
14
|
+
async () => {
|
|
15
|
+
await getConverter('non-existent');
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
message: 'Unknown converter: non-existent. Available converters: turndown, pandoc',
|
|
19
|
+
}
|
|
20
|
+
);
|
|
18
21
|
});
|
package/test/node/index.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {test} from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import {
|
|
3
|
+
import {convert} from '../../src/index.js';
|
|
4
4
|
|
|
5
5
|
test('library: convert should use turndown by default', async () => {
|
|
6
6
|
const html = '<h1>Hello</h1>';
|
|
@@ -10,7 +10,7 @@ test('library: convert should use turndown by default', async () => {
|
|
|
10
10
|
|
|
11
11
|
test('library: convert should support pandoc', async () => {
|
|
12
12
|
const html = '<p>Hello <b>World</b></p>';
|
|
13
|
-
const markdown = await convert(html, {
|
|
13
|
+
const markdown = await convert(html, {converter: 'pandoc'});
|
|
14
14
|
assert.strictEqual(markdown.includes('Hello'), true);
|
|
15
15
|
assert.strictEqual(markdown.includes('World'), true);
|
|
16
16
|
});
|
|
@@ -18,6 +18,19 @@ test('library: convert should support pandoc', async () => {
|
|
|
18
18
|
test('library: convert should support disabling cleaning', async () => {
|
|
19
19
|
const html = '<div style="color: red;"><p>Hello</p></div>';
|
|
20
20
|
// If clean is false, it uses removeStyleAttributes which unwraps but might keep some structure
|
|
21
|
-
const markdown = await convert(html, {
|
|
21
|
+
const markdown = await convert(html, {clean: false});
|
|
22
22
|
assert.strictEqual(markdown.includes('Hello'), true);
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
test('library: convert should short-circuit if markdown is detected', async () => {
|
|
26
|
+
const markdownInput = '# Already Markdown\n\n- item';
|
|
27
|
+
const result = await convert(markdownInput);
|
|
28
|
+
assert.strictEqual(result, markdownInput);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('library: convert should NOT short-circuit if isMarkdown: false is passed', async () => {
|
|
32
|
+
const input = '# Not Markdown'; // Looks like MD, but we force it not to be
|
|
33
|
+
const result = await convert(input, {isMarkdown: false});
|
|
34
|
+
// Turndown escapes # if it's not a real header
|
|
35
|
+
assert.strictEqual(result.trim(), '\\# Not Markdown');
|
|
36
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {test} from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {execSync, spawnSync} from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const CLIP_TOOL = path.resolve(__dirname, '../../bin/markpaste');
|
|
10
|
+
|
|
11
|
+
// This test only works on macOS
|
|
12
|
+
if (os.platform() === 'darwin') {
|
|
13
|
+
test('markpaste cli: round-trip conversion', async () => {
|
|
14
|
+
const testContent = '<h1>Test</h1><p><b>Bold</b></p>';
|
|
15
|
+
|
|
16
|
+
// 1. Set initial clipboard state
|
|
17
|
+
const htmlHex = Buffer.from(testContent, 'utf8').toString('hex');
|
|
18
|
+
const setScript = `set the clipboard to {«class HTML»:«data HTML${htmlHex}», text:"${testContent}"}`;
|
|
19
|
+
spawnSync('osascript', ['-e', setScript]);
|
|
20
|
+
|
|
21
|
+
// 2. Run markpaste
|
|
22
|
+
execSync(`"${CLIP_TOOL}"`, {encoding: 'utf8'});
|
|
23
|
+
|
|
24
|
+
// 3. Verify Plain Text flavor (should be Markdown)
|
|
25
|
+
const plainText = execSync('osascript -e "get (the clipboard as string)"', {encoding: 'utf8'}).trim();
|
|
26
|
+
assert.strictEqual(plainText.includes('# Test'), true);
|
|
27
|
+
assert.strictEqual(plainText.includes('**Bold**'), true);
|
|
28
|
+
|
|
29
|
+
// 4. Verify HTML flavor (should be cleaned HTML)
|
|
30
|
+
const htmlData = execSync('osascript -e "get (the clipboard as «class HTML»)"', {encoding: 'utf8'}).trim();
|
|
31
|
+
assert.strictEqual(htmlData.includes('«data HTML'), true);
|
|
32
|
+
|
|
33
|
+
// Extract hex and convert back to string
|
|
34
|
+
const match = htmlData.match(/«data HTML([0-9A-F]*)»/i);
|
|
35
|
+
if (match && match[1]) {
|
|
36
|
+
const resultHtml = Buffer.from(match[1], 'hex').toString('utf8');
|
|
37
|
+
assert.strictEqual(resultHtml.toLowerCase().includes('<h1>test</h1>'), true);
|
|
38
|
+
assert.strictEqual(resultHtml.toLowerCase().includes('<b>bold</b>'), true);
|
|
39
|
+
} else {
|
|
40
|
+
assert.fail('Could not extract HTML hex from clipboard');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('markpaste cli: markdown to html conversion', async () => {
|
|
45
|
+
const markdownInput = '# MD Test\n\n- item 1';
|
|
46
|
+
|
|
47
|
+
// 1. Set clipboard to plain text only (no HTML flavor)
|
|
48
|
+
execSync('pbcopy', {input: markdownInput});
|
|
49
|
+
// Verify no HTML flavor exists (or at least we want to simulate that state)
|
|
50
|
+
// Actually pbcopy only sets text flavor, so this is perfect.
|
|
51
|
+
|
|
52
|
+
// 2. Run markpaste
|
|
53
|
+
execSync(`"${CLIP_TOOL}"`, {encoding: 'utf8'});
|
|
54
|
+
|
|
55
|
+
// 3. Verify HTML flavor (should be rendered HTML)
|
|
56
|
+
const htmlData = execSync('osascript -e "get (the clipboard as «class HTML»)"', {encoding: 'utf8'}).trim();
|
|
57
|
+
assert.strictEqual(htmlData.includes('«data HTML'), true);
|
|
58
|
+
|
|
59
|
+
const match = htmlData.match(/«data HTML([0-9A-F]*)»/i);
|
|
60
|
+
const resultHtml = Buffer.from(match[1], 'hex').toString('utf8');
|
|
61
|
+
assert.strictEqual(resultHtml.toLowerCase().includes('<h1>md test</h1>'), true);
|
|
62
|
+
assert.strictEqual(resultHtml.toLowerCase().includes('<li>item 1</li>'), true);
|
|
63
|
+
|
|
64
|
+
// 4. Verify Plain Text flavor (should be original markdown)
|
|
65
|
+
const plainText = execSync('osascript -e "get (the clipboard as string)"', {encoding: 'utf8'}).trim();
|
|
66
|
+
assert.strictEqual(plainText, markdownInput);
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
test('markpaste cli: skipped on non-macOS', () => {
|
|
70
|
+
// Pass
|
|
71
|
+
});
|
|
72
|
+
}
|
package/test/node/pandoc.test.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {test} from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import {
|
|
3
|
+
import {getConverter} from '../../src/converter.js';
|
|
4
4
|
|
|
5
5
|
test('pandoc: converter should convert HTML to Markdown', async () => {
|
|
6
6
|
const converter = await getConverter('pandoc');
|
|
7
7
|
const html = '<h1>Hello</h1>';
|
|
8
8
|
const markdown = converter.convert(html);
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
assert.strictEqual(markdown.includes('Hello'), true);
|
|
11
11
|
// Pandoc usually adds its own flavor of Markdown
|
|
12
12
|
assert.strictEqual(markdown.includes('#'), true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {test, expect} from '@playwright/test';
|
|
2
2
|
|
|
3
|
-
test('should load the page without errors or 404s', async ({
|
|
3
|
+
test('should load the page without errors or 404s', async ({page}) => {
|
|
4
4
|
const failedRequests: string[] = [];
|
|
5
5
|
const consoleErrors: string[] = [];
|
|
6
6
|
|
|
@@ -27,7 +27,7 @@ test('should load the page without errors or 404s', async ({ page }) => {
|
|
|
27
27
|
consoleErrors.push(error.message);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
await page.goto('http://127.0.0.1:
|
|
30
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
31
31
|
|
|
32
32
|
// Verify no network failures
|
|
33
33
|
expect(failedRequests, `Found failed network requests: ${failedRequests.join(', ')}`).toHaveLength(0);
|
|
@@ -39,14 +39,14 @@ test('should load the page without errors or 404s', async ({ page }) => {
|
|
|
39
39
|
await expect(page.locator('h1')).toHaveText('MarkPaste');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
test('should paste HTML and update all 2 markdown outputs', async ({
|
|
43
|
-
await page.goto('http://127.0.0.1:
|
|
42
|
+
test('should paste HTML and update all 2 markdown outputs', async ({page}) => {
|
|
43
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
44
44
|
|
|
45
45
|
const inputArea = page.locator('#inputArea');
|
|
46
46
|
const testHtml = '<h1>Test Title</h1><p>Hello <strong>world</strong></p>';
|
|
47
47
|
|
|
48
48
|
// Simulate paste event
|
|
49
|
-
await page.evaluate(
|
|
49
|
+
await page.evaluate(html => {
|
|
50
50
|
const el = document.querySelector('#inputArea');
|
|
51
51
|
if (!el) return;
|
|
52
52
|
const dataTransfer = new DataTransfer();
|
package/test/web/cleaner.spec.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {test, expect} from '@playwright/test';
|
|
|
2
2
|
|
|
3
3
|
test.describe('Cleaner functionality', () => {
|
|
4
4
|
test.beforeEach(async ({page}) => {
|
|
5
|
-
await page.goto('http://127.0.0.1:
|
|
5
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
test('should remove MDN copy button and play links', async ({page}) => {
|
package/test/web/pasting.spec.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {test, expect} from '@playwright/test';
|
|
|
2
2
|
|
|
3
3
|
test.describe('MarkPaste functionality', () => {
|
|
4
4
|
test.beforeEach(async ({page}) => {
|
|
5
|
-
await page.goto('http://127.0.0.1:
|
|
5
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
test('should convert basic rich text to markdown', async ({page}) => {
|
|
@@ -56,8 +56,7 @@ test.describe('MarkPaste functionality', () => {
|
|
|
56
56
|
await expect(pandocOutput).toContainText('Hello World');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
test.skip('should toggle HTML cleaning', async ({page}) => {
|
|
59
|
+
test('should toggle HTML cleaning', async ({page}) => {
|
|
61
60
|
const html = '<div><p>Hello</p><style>body{color:red;}</style><script>alert("xss")</script></div>';
|
|
62
61
|
|
|
63
62
|
await page.evaluate(html => {
|
|
@@ -77,10 +76,11 @@ test.describe('MarkPaste functionality', () => {
|
|
|
77
76
|
// Uncheck "Clean HTML"
|
|
78
77
|
await page.locator('#cleanHtmlToggle').uncheck();
|
|
79
78
|
// Wait for update
|
|
80
|
-
await page.waitForTimeout(100);
|
|
79
|
+
await page.waitForTimeout(100);
|
|
81
80
|
|
|
82
81
|
expect(await htmlCode.textContent()).toContain('<div>');
|
|
83
82
|
expect(await htmlCode.textContent()).not.toContain('<script>');
|
|
83
|
+
expect(await htmlCode.textContent()).not.toContain('<style>');
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
test('should retain table structure', async ({page}) => {
|
|
@@ -114,4 +114,31 @@ test.describe('MarkPaste functionality', () => {
|
|
|
114
114
|
await expect(htmlCode).toContainText('<td');
|
|
115
115
|
await expect(htmlCode).toContainText('<th');
|
|
116
116
|
});
|
|
117
|
+
|
|
118
|
+
test('should handle pasting markdown and pipe it through', async ({page}) => {
|
|
119
|
+
const markdown = '# This is Markdown\n\n- List item 1\n- List item 2';
|
|
120
|
+
|
|
121
|
+
// Simulate paste event
|
|
122
|
+
await page.evaluate(text => {
|
|
123
|
+
const inputArea = document.getElementById('inputArea');
|
|
124
|
+
const dataTransfer = new DataTransfer();
|
|
125
|
+
dataTransfer.setData('text/plain', text);
|
|
126
|
+
const event = new ClipboardEvent('paste', {
|
|
127
|
+
clipboardData: dataTransfer,
|
|
128
|
+
bubbles: true,
|
|
129
|
+
cancelable: true,
|
|
130
|
+
});
|
|
131
|
+
inputArea.dispatchEvent(event);
|
|
132
|
+
}, markdown);
|
|
133
|
+
|
|
134
|
+
const outputCode = page.locator('#outputCodeTurndown');
|
|
135
|
+
await expect(outputCode).toHaveText(markdown);
|
|
136
|
+
|
|
137
|
+
const htmlCode = page.locator('#htmlCode');
|
|
138
|
+
// It should be rendered HTML in the preview
|
|
139
|
+
const htmlText = await htmlCode.innerText();
|
|
140
|
+
expect(htmlText).toContain('<h1>This is Markdown</h1>');
|
|
141
|
+
expect(htmlText).toContain('<ul>');
|
|
142
|
+
expect(htmlText).toContain('<li>List item 1</li>');
|
|
143
|
+
});
|
|
117
144
|
});
|
package/types/globals.d.ts
CHANGED
|
@@ -29,19 +29,22 @@ declare global {
|
|
|
29
29
|
on(name: string, fn: EventListenerOrEventListenerObject): void;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
// idle detector
|
|
34
33
|
|
|
35
34
|
interface IdleDetector {
|
|
36
|
-
addEventListener(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
addEventListener(
|
|
36
|
+
type: 'change',
|
|
37
|
+
listener: (this: IdleDetector, ev: {userState: 'active' | 'idle'; screenState: 'locked' | 'unlocked'}) => unknown,
|
|
38
|
+
options?: boolean | AddEventListenerOptions
|
|
39
|
+
): void;
|
|
40
|
+
start(options: {threshold: number; signal?: AbortSignal}): Promise<void>;
|
|
41
|
+
screenState: 'locked' | 'unlocked';
|
|
42
|
+
userState: 'active' | 'idle';
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
declare const IdleDetector: {
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
new (): IdleDetector;
|
|
47
|
+
requestPermission(): Promise<'granted' | 'denied'>;
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
50
|
/// <reference path="pandoc-wasm.d.ts" />
|
package/src/markpaste.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* markpaste.js
|
|
3
|
-
* MarkPaste Library Entry Point
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { cleanHTML, removeStyleAttributes } from './cleaner.js';
|
|
7
|
-
import { getConverter } from './converter.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Converts HTML to Markdown using the specified converter.
|
|
11
|
-
* @param {string} html The HTML string to convert.
|
|
12
|
-
* @param {Object} options Configuration options.
|
|
13
|
-
* @param {string} [options.converter='turndown'] The converter to use ('turndown', 'pandoc').
|
|
14
|
-
* @param {boolean} [options.clean=true] Whether to clean the HTML before conversion.
|
|
15
|
-
* @returns {Promise<string>} The resulting Markdown string.
|
|
16
|
-
*/
|
|
17
|
-
export async function convert(html, options = {}) {
|
|
18
|
-
const { converter: converterName = 'turndown', clean = true } = options;
|
|
19
|
-
|
|
20
|
-
const cleanedHtml = clean ? await cleanHTML(html) : await removeStyleAttributes(html);
|
|
21
|
-
const converter = await getConverter(converterName);
|
|
22
|
-
|
|
23
|
-
return converter.convert(cleanedHtml);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export { cleanHTML, removeStyleAttributes, getConverter };
|