Skip to content

Commit 3e0ad4b

Browse files
committed
Initial commit
0 parents  commit 3e0ad4b

27 files changed

+12829
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 MarkEdit.app
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# MarkEdit-preview
2+
3+
Markdown preview for MarkEdit that leverages [markedit-api](https://github.com/MarkEdit-app/MarkEdit-api).
4+
5+
## Installation
6+
7+
Copy [dist/markedit-preview.js](dist/markedit-preview.js) to `~/Library/Containers/app.cyan.markedit/Data/Documents/scripts/`. Details [here](https://github.com/MarkEdit-app/MarkEdit/wiki/Customization#entries).
8+
9+
Use [dist/lite/markedit-preview.js](dist/lite/markedit-preview.js) if you don't need [mermaid](https://mermaid.js.org/), [katex](https://katex.org/) and [highlight.js](https://highlightjs.org/), it's much smaller (3.7 MB vs 160 KB).
10+
11+
> Once installed, restart MarkEdit to apply the changes.
12+
>
13+
> This extension automatically checks for updates and notifies you when a new version is available.
14+
15+
## Building
16+
17+
Run `yarn install && yarn build` to build and deploy the script.
18+
19+
To build the lite version, run `yarn build:lite` instead.
20+
21+
## How to Use
22+
23+
Access it from the `Extensions` menu in the menu bar, or use the keyboard shortcut <kbd>Shift–Command–V</kbd>.
24+
25+
<img src="./screenshot.png" width="520" alt="Using MarkEdit-preview">
26+
27+
To display local images, please ensure you're using MarkEdit 1.24.0 or later and follow [the guide](https://github.com/MarkEdit-app/MarkEdit/wiki/Customization#grant-folder-access) to grant file access.
28+
29+
## Styling
30+
31+
This extension applies the [github-markdown](https://github.com/sindresorhus/github-markdown-css) styling. You can customize the appearance by following the [customization](https://github.com/MarkEdit-app/MarkEdit/wiki/Customization) guidelines.
32+
33+
The preview pane can be styled using the `markdown-body` CSS class.
34+
35+
## Settings
36+
37+
In [settings.json](https://github.com/MarkEdit-app/MarkEdit/wiki/Customization#advanced-settings), you can define a settings node named `extension.markeditPreview` to configure this extension, default settings are:
38+
39+
```json
40+
{
41+
"extension.markeditPreview": {
42+
"autoUpdate": true,
43+
"syncScroll": true,
44+
"changeMode": {
45+
"modes": ["side-by-side", "preview"],
46+
"hotKey": {
47+
"key": "V",
48+
"modifiers": ["Command"]
49+
}
50+
},
51+
"markdownIt": {
52+
"preset": "default",
53+
"options": {}
54+
}
55+
}
56+
}
57+
```
58+
59+
- `autoUpdate`: Whether to enable automatic update checks.
60+
- `syncScroll`: Whether to enable scroll synchronization.
61+
- `changeMode.modes`: Define available preview modes for the "Change Mode" feature.
62+
- `changeMode.hotKey`: Assign keyboard shortcuts for mode switching. See the specification [here](https://github.com/MarkEdit-app/MarkEdit/wiki/Customization#generalmainwindowhotkey).
63+
- `markdownIt.preset`: Override the default [markdown-it](https://markdown-it.github.io/markdown-it/#MarkdownIt.new) preset.
64+
- `markdownIt.options`: Customize [markdown-it](https://markdown-it.github.io/markdown-it/#MarkdownIt.new) options.
65+
66+
> Extension settings require MarkEdit 1.24.0 or later.

dist/lite/markedit-preview.js

Lines changed: 1306 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/markedit-preview.js

Lines changed: 5461 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

eslint.config.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import eslintPluginTs from '@typescript-eslint/eslint-plugin';
2+
import parserTs from '@typescript-eslint/parser';
3+
4+
export default [
5+
{
6+
files: ['**/*.ts'],
7+
languageOptions: {
8+
parser: parserTs,
9+
parserOptions: {
10+
ecmaVersion: 2020,
11+
sourceType: 'module',
12+
project: './tsconfig.json',
13+
tsconfigRootDir: process.cwd(),
14+
},
15+
},
16+
plugins: {
17+
'@typescript-eslint': eslintPluginTs,
18+
},
19+
rules: {
20+
// --- Recommended ---
21+
...eslintPluginTs.configs.recommended.rules,
22+
23+
// --- Safety ---
24+
'no-implicit-coercion': 'error',
25+
'no-console': 'error',
26+
'eqeqeq': ['error', 'always'],
27+
'curly': 'error',
28+
'no-fallthrough': 'error',
29+
'default-case': 'error',
30+
31+
// --- Style ---
32+
'quotes': ['error', 'single', { avoidEscape: true }],
33+
'comma-dangle': ['error', 'always-multiline'],
34+
'no-trailing-spaces': 'error',
35+
'no-multi-spaces': 'error',
36+
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
37+
38+
// --- Clean code ---
39+
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
40+
'@typescript-eslint/prefer-nullish-coalescing': 'error',
41+
'@typescript-eslint/prefer-optional-chain': 'error',
42+
43+
// --- Customization ---
44+
'@typescript-eslint/consistent-type-imports': 'error',
45+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
46+
'@typescript-eslint/no-explicit-any': 'error',
47+
'@typescript-eslint/explicit-function-return-type': 'off',
48+
49+
// --- Others ---
50+
'no-var': 'error',
51+
'prefer-const': 'error',
52+
'no-shadow': 'error',
53+
},
54+
},
55+
];

main.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { EditorView } from '@codemirror/view';
2+
import { MarkEdit } from 'markedit-api';
3+
import type { MenuItem } from 'markedit-api';
4+
5+
import {
6+
setUp,
7+
ViewMode,
8+
setViewMode,
9+
changeViewMode,
10+
restoreViewMode,
11+
currentViewMode,
12+
renderHtmlPreview,
13+
saveCleanHtml,
14+
saveStyledHtml,
15+
copyHtml,
16+
copyRichText,
17+
getEditPane,
18+
getPreviewPane,
19+
} from './src/view';
20+
21+
import { startObserving } from './src/scroll';
22+
import { checkForUpdates } from './src/updater';
23+
import { keyboardShortcut } from './src/settings';
24+
import { localized } from './src/strings';
25+
26+
setUp();
27+
setTimeout(checkForUpdates, 4000);
28+
29+
MarkEdit.addMainMenuItem({
30+
title: localized('viewMode'),
31+
children: [
32+
{
33+
title: localized('changeMode'),
34+
action: changeViewMode,
35+
key: (keyboardShortcut['key'] ?? 'V') as string,
36+
modifiers: (keyboardShortcut['modifiers'] ?? ['Command']) as MenuItem['modifiers'],
37+
},
38+
{ separator: true },
39+
createModeItem(localized('editMode'), ViewMode.edit),
40+
createModeItem(localized('sideBySideMode'), ViewMode.sideBySide),
41+
createModeItem(localized('previewMode'), ViewMode.preview),
42+
{ separator: true },
43+
...createHtmlItems(),
44+
{ separator: true },
45+
{ title: `${localized('version')} ${__PKG_VERSION__}` },
46+
{
47+
title: `${localized('checkReleases')} (GitHub)`,
48+
action: () => open('https://github.com/MarkEdit-app/MarkEdit-preview/releases/latest'),
49+
},
50+
],
51+
});
52+
53+
MarkEdit.addExtension(EditorView.updateListener.of(update => {
54+
if (!update.docChanged) {
55+
return;
56+
}
57+
58+
if (states.renderUpdater !== undefined) {
59+
clearTimeout(states.renderUpdater);
60+
}
61+
62+
states.renderUpdater = setTimeout(renderHtmlPreview, 500);
63+
}));
64+
65+
MarkEdit.onEditorReady(() => {
66+
if (states.isInitiating) {
67+
states.isInitiating = false;
68+
restoreViewMode();
69+
}
70+
71+
renderHtmlPreview();
72+
startObserving(getEditPane(), getPreviewPane());
73+
});
74+
75+
function createModeItem(title: string, mode: ViewMode): MenuItem {
76+
return {
77+
title,
78+
action: () => setViewMode(mode),
79+
// state requires MarkEdit 1.24.0+
80+
state: () => ({ isSelected: currentViewMode() === mode }),
81+
};
82+
}
83+
84+
function createHtmlItems(): MenuItem[] {
85+
const copyItems = [
86+
{
87+
title: localized('copyHtml'),
88+
action: copyHtml,
89+
},
90+
{
91+
title: localized('copyRichText'),
92+
action: copyRichText,
93+
},
94+
];
95+
96+
// showSavePanel requires MarkEdit 1.24.0+
97+
if (typeof MarkEdit.showSavePanel === 'undefined') {
98+
return copyItems;
99+
}
100+
101+
return [
102+
{
103+
title: localized('saveCleanHtml'),
104+
action: saveCleanHtml,
105+
},
106+
{
107+
title: localized('saveStyledHtml'),
108+
action: saveStyledHtml,
109+
},
110+
...copyItems,
111+
];
112+
}
113+
114+
const states: {
115+
isInitiating: boolean;
116+
renderUpdater: ReturnType<typeof setTimeout> | undefined;
117+
} = {
118+
isInitiating: true,
119+
renderUpdater: undefined,
120+
};

package.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "markedit-preview",
3+
"version": "1.0.0",
4+
"description": "Markdown preview for MarkEdit.",
5+
"scripts": {
6+
"lint": "eslint . --config eslint.config.mjs",
7+
"build": "yarn lint && vite build",
8+
"build:lite": "cross-env LITE_BUILD=true vite build",
9+
"reload": "osascript -e 'quit app \"MarkEdit\"' -e 'delay 1' -e 'launch app \"MarkEdit\"'",
10+
"uninstall": "rm ~/Library/Containers/app.cyan.markedit/Data/Documents/scripts/$(node -p \"require('./package.json').name\").js"
11+
},
12+
"license": "MIT",
13+
"devDependencies": {
14+
"@codemirror/language": "^6.0.0",
15+
"@codemirror/state": "^6.0.0",
16+
"@codemirror/view": "^6.0.0",
17+
"@lezer/markdown": "^1.0.0",
18+
"@types/markdown-it": "^14.1.2",
19+
"@types/markdown-it-footnote": "^3.0.4",
20+
"@types/markdown-it-link-attributes": "^3.0.5",
21+
"@types/node": "^22.0.0",
22+
"@typescript-eslint/eslint-plugin": "^8.32.1",
23+
"@typescript-eslint/parser": "^8.32.1",
24+
"cross-env": "^7.0.3",
25+
"eslint": "^9.27.0",
26+
"markedit-api": "https://github.com/MarkEdit-app/MarkEdit-api#v0.9.0",
27+
"markedit-vite": "https://github.com/MarkEdit-app/MarkEdit-vite#v0.1.0",
28+
"typescript": "^5.0.0",
29+
"vite": "^5.0.0",
30+
"vite-plugin-singlefile": "^2.2.0"
31+
},
32+
"dependencies": {
33+
"@vscode/markdown-it-katex": "^1.1.1",
34+
"katex": "^0.16.22",
35+
"markdown-it": "^14.1.0",
36+
"markdown-it-footnote": "^4.0.0",
37+
"markdown-it-highlightjs": "^4.2.0",
38+
"markdown-it-link-attributes": "^4.0.1",
39+
"markdown-it-task-lists": "^2.1.1",
40+
"mermaid": "^11.6.0",
41+
"split-grid": "^1.0.11"
42+
}
43+
}

screenshot.png

638 KB
Loading

src/image.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const parser = new DOMParser();
2+
const scheme = 'image-loader';
3+
4+
/**
5+
* Replace localhost urls with a url scheme that can be used in the client.
6+
*
7+
* We don't use a markdown-it rule here because it's hard to handle inline html.
8+
*/
9+
export function replaceImageURLs(html: string) {
10+
const doc = parser.parseFromString(html, 'text/html');
11+
const images = doc.querySelectorAll<HTMLImageElement>('img');
12+
13+
images.forEach(image => {
14+
// Don't use image.src, which includes the host
15+
const url = image.getAttribute('src');
16+
if (url === null) {
17+
return;
18+
}
19+
20+
// Image with a remote url or base64 data
21+
if (url.includes('://') || url.startsWith('data:image/')) {
22+
return;
23+
}
24+
25+
// Image with a local file path
26+
image.src = `${scheme}://${url}`;
27+
});
28+
29+
return doc.body.innerHTML;
30+
}

0 commit comments

Comments
 (0)