refactor: api

This commit is contained in:
Rongjian Zhang 2021-02-12 12:54:33 +08:00
parent c072f3a75b
commit 84bdd914bf
41 changed files with 353 additions and 777 deletions

View File

@ -7,10 +7,8 @@
import breaks from '@bytemd/plugin-breaks';
import mermaid from '@bytemd/plugin-mermaid';
import footnotes from '@bytemd/plugin-footnotes';
import importImage from '@bytemd/plugin-import-image';
import frontmatter from '@bytemd/plugin-frontmatter';
import mediumZoom from '@bytemd/plugin-medium-zoom';
import importHtml from '@bytemd/plugin-import-html';
import en from 'bytemd/lib/locales/en-US';
import zh from 'bytemd/lib/locales/zh-CN';
@ -49,53 +47,24 @@
let enabled = {
breaks: false,
footnotes: true,
frontmatter: true,
gfm: true,
highlight: true,
math: true,
mermaid: true,
frontmatter: true,
footnotes: true,
'import-html': true,
'import-image': true,
'medium-zoom': true,
mermaid: true,
};
$: plugins = [
enabled.breaks && breaks(),
enabled.footnotes && footnotes(),
enabled.frontmatter && frontmatter(),
enabled.gfm && gfm({ locale: currentLocale.gfm }),
enabled.highlight && highlight(),
enabled.math && math({ locale: currentLocale.math }),
enabled.mermaid && mermaid({ locale: currentLocale.mermaid }),
enabled.footnotes && footnotes(),
enabled['import-image'] &&
importImage({
upload(files) {
return Promise.all(
files.map((file) => {
return ['https://picsum.photos/300'];
})
);
},
}),
enabled.frontmatter && frontmatter(),
enabled['import-html'] && importHtml(),
enabled['medium-zoom'] && mediumZoom(),
// For test:
// {
// editorEffect(cm, el) {
// console.log('on', cm, el);
// return () => {
// console.log('off', cm, el);
// };
// },
// effect(el, result) {
// console.log('on', el, result);
// return () => {
// console.log('off', el, result);
// };
// },
// },
enabled.mermaid && mermaid({ locale: currentLocale.mermaid }),
].filter((x) => x);
</script>
@ -122,6 +91,14 @@
{mode}
{plugins}
locale={currentLocale.bytemd}
uploadImages={(files) => {
return Promise.all(
files.map((file) => {
// TODO:
return 'https://picsum.photos/300';
})
);
}}
on:change={handleChange}
/>
</div>

View File

@ -149,7 +149,7 @@ editor.on('change', (e) => {
| --- | --- | --- |
| `value` | `string` (required) | Markdown text |
| `plugins` | `BytemdPlugin[]` | ByteMD plugin list |
| `sanitize` | `{ allowStyle?: boolean }` or `(schema: Schema) => Schema` | Sanitize strategy |
| `sanitize` | `{ allowInlineStyle?: boolean }` or `(schema: Schema) => Schema` | Sanitize strategy |
### Editor
@ -157,10 +157,11 @@ editor.on('change', (e) => {
| Key | Type | Description |
| --- | --- | --- |
| `mode` | `'split'` or `'tab'` | Editor display mode |
| `mode` | `split`, `tab`, `auto` | Editor display mode |
| `previewDebounce` | `number` | Debounce time (ms) for preview, default: `300` |
| `placeholder` | `string` | Editor placeholder |
| `editorConfig` | [documentation](https://codemirror.net/doc/manual.html#config) | CodeMirror editor config |
| `locale` | i18n locale. Available locales could be found at `bytemd/lib/locales` |
## Style customization
@ -203,15 +204,12 @@ The 2,5,7 steps are designed for user customization via ByteMD plugin API.
| Package | Status | Description |
| --- | --- | --- |
| [@bytemd/plugin-breaks](./packages/plugin-breaks) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-breaks.svg)](https://npm.im/@bytemd/plugin-breaks) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-breaks/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-breaks) | Support breaks |
| [@bytemd/plugin-external-links](./packages/plugin-external-links) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-external-links.svg)](https://npm.im/@bytemd/plugin-external-links) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-external-links/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-external-links) | Open external links in new window |
| [@bytemd/plugin-footnotes](./packages/plugin-footnotes) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-footnotes.svg)](https://npm.im/@bytemd/plugin-footnotes) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-footnotes/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-footnotes) | Support footnotes |
| [@bytemd/plugin-frontmatter](./packages/plugin-frontmatter) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-frontmatter.svg)](https://npm.im/@bytemd/plugin-frontmatter) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-frontmatter/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-frontmatter) | Parse frontmatter |
| [@bytemd/plugin-gemoji](./packages/plugin-gemoji) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-gemoji.svg)](https://npm.im/@bytemd/plugin-gemoji) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-gemoji/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-gemoji) | Support Gemoji shortcodes |
| [@bytemd/plugin-gfm](./packages/plugin-gfm) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-gfm.svg)](https://npm.im/@bytemd/plugin-gfm) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-gfm/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-gfm) | Support GFM (autolink literals, strikethrough, tables, tasklists) |
| [@bytemd/plugin-highlight](./packages/plugin-highlight) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-highlight.svg)](https://npm.im/@bytemd/plugin-highlight) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-highlight/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-highlight) | Highlight code blocks |
| [@bytemd/plugin-highlight-ssr](./packages/plugin-highlight-ssr) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-highlight-ssr.svg)](https://npm.im/@bytemd/plugin-highlight-ssr) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-highlight-ssr/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-highlight-ssr) | Highlight code blocks (SSR compatible) |
| [@bytemd/plugin-import-html](./packages/plugin-import-html) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-import-html.svg)](https://npm.im/@bytemd/plugin-import-html) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-import-html/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-import-html) | Import HTML by pasting or dropping |
| [@bytemd/plugin-import-image](./packages/plugin-import-image) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-import-image.svg)](https://npm.im/@bytemd/plugin-import-image) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-import-image/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-import-image) | Import image by pasting or dropping |
| [@bytemd/plugin-math](./packages/plugin-math) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-math.svg)](https://npm.im/@bytemd/plugin-math) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-math/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-math) | Support math formula |
| [@bytemd/plugin-math-ssr](./packages/plugin-math-ssr) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-math-ssr.svg)](https://npm.im/@bytemd/plugin-math-ssr) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-math-ssr/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-math-ssr) | Support math formula (SSR compatible) |
| [@bytemd/plugin-medium-zoom](./packages/plugin-medium-zoom) | [![npm](https://img.shields.io/npm/v/@bytemd/plugin-medium-zoom.svg)](https://npm.im/@bytemd/plugin-medium-zoom) [![gzip size](https://img.badgesize.io/https://unpkg.com/@bytemd/plugin-medium-zoom/dist/index.min.js?compression=gzip)](https://unpkg.com/@bytemd/plugin-medium-zoom) | Zoom images like Medium |

View File

@ -39,6 +39,7 @@
"@types/classnames": "^2.2.11",
"@types/lodash-es": "^4.17.4",
"classnames": "^2.2.6",
"lodash-es": "^4.17.15"
"lodash-es": "^4.17.15",
"select-files": "^1.0.1"
}
}

View File

@ -10,7 +10,11 @@
import Toolbar from './toolbar.svelte';
import Viewer from './viewer.svelte';
import Toc from './toc.svelte';
import { createUtils, findStartIndex, getBuiltinItems } from './editor';
import {
createEditorUtils,
findStartIndex,
getBuiltinActions,
} from './editor';
import Status from './status.svelte';
import { icons } from './icons';
import enUS from './locales/en-US';
@ -24,13 +28,14 @@
export let placeholder: EditorProps['placeholder'];
export let editorConfig: EditorProps['editorConfig'];
export let locale: NonNullable<EditorProps['locale']> = enUS;
export let uploadImages: EditorProps['uploadImages'];
const dispatch = createEventDispatcher();
$: toolbarItems = getBuiltinItems(locale, plugins);
$: actions = getBuiltinActions(locale, plugins, uploadImages);
$: split = mode === 'split' || (mode === 'auto' && containerWidth >= 800);
let el: HTMLElement;
let root: HTMLElement;
let previewEl: HTMLElement;
let textarea: HTMLTextAreaElement;
let containerWidth = Infinity;
@ -65,7 +70,7 @@
return { edit, preview };
})();
$: context = { editor, $el: el, utils: createUtils(editor) };
$: context = { editor, root, ...createEditorUtils(editor) };
let cbs: ReturnType<NonNullable<BytemdPlugin['editorEffect']>>[] = [];
let keyMap: KeyMap = {};
@ -74,9 +79,9 @@
// console.log('on', plugins);
cbs = plugins.map((p) => p.editorEffect?.(context));
keyMap = {};
toolbarItems.forEach((item) => {
if (item.shortcut) {
keyMap[item.shortcut] = () => item.onClick(context);
actions.forEach(({ shortcut, handler }) => {
if (shortcut && handler) {
keyMap[shortcut] = () => handler(context);
}
});
editor.addKeyMap(keyMap);
@ -97,7 +102,7 @@
editor.setValue(value);
}
$: if (editor && el && plugins && hast) {
$: if (editor && root && plugins && hast) {
off();
tick().then(() => {
on();
@ -243,11 +248,32 @@
passive: true,
});
// handle image drop and paste
const handleImages = async (itemList: DataTransferItemList | undefined) => {
if (!uploadImages) return;
const files = Array.from(itemList ?? [])
.map((item) => {
if (item.type.startsWith('image/')) {
return item.getAsFile();
}
})
.filter((f): f is File => f != null);
const urls = await uploadImages(files);
context.appendBlock(urls.map((url) => `![](${url})`).join('\n\n'));
};
editor.on('drop', async (_, e) => {
handleImages(e.dataTransfer?.items);
});
editor.on('paste', async (_, e) => {
handleImages(e.clipboardData?.items);
});
// @ts-ignore
new ResizeObserver((entries) => {
containerWidth = entries[0].borderBoxSize[0].inlineSize;
// console.log(containerWidth);
}).observe(el, { box: 'border-box' });
}).observe(root, { box: 'border-box' });
// No need to call `on` because cm instance would change once after init
});
@ -259,7 +285,7 @@
'bytemd-mode-split': split,
'bytemd-fullscreen': fullscreen,
})}
bind:this={el}
bind:this={root}
>
<Toolbar
{context}
@ -268,7 +294,7 @@
{sidebar}
{fullscreen}
{locale}
{toolbarItems}
{actions}
on:tab={(e) => {
activeTab = e.detail;
if (activeTab === 0 && editor) {
@ -300,10 +326,13 @@
}}
/>
<div class="bytemd-body">
<div class="bytemd-editor" style={styles.edit}>
<span class="bytemd-editor" style={styles.edit}>
<textarea bind:this={textarea} style="display:none" />
</div>
<div bind:this={previewEl} class="bytemd-preview" style={styles.preview}>
</span><span
bind:this={previewEl}
class="bytemd-preview"
style={styles.preview}
>
<Viewer
value={debouncedValue}
{plugins}
@ -312,8 +341,10 @@
hast = e.detail;
}}
/>
</div>
<div class="bytemd-sidebar" style={sidebar ? undefined : 'display:none'}>
</span><span
class="bytemd-sidebar"
style={sidebar ? undefined : 'display:none'}
>
<div
class="bytemd-sidebar-close"
on:click={() => {
@ -323,7 +354,7 @@
{@html icons.close}
</div>
{#if sidebar === 'help'}
<Help {locale} {toolbarItems} />
<Help {locale} {actions} />
{:else if sidebar === 'toc'}
<Toc
{hast}
@ -335,7 +366,7 @@
}}
/>
{/if}
</div>
</span>
</div>
<Status
{locale}

View File

@ -1,11 +1,12 @@
import type { Editor } from 'codemirror';
import type { BytemdPlugin, BytemdToolbarItem } from './types';
import type { BytemdPlugin, BytemdAction, EditorProps } from './types';
import type { BytemdLocale } from './locales/en-US';
import { icons } from './icons';
import selectFiles from 'select-files';
export type EditorUtils = ReturnType<typeof createUtils>;
export type EditorUtils = ReturnType<typeof createEditorUtils>;
export function createUtils(editor: Editor) {
export function createEditorUtils(editor: Editor) {
return {
/**
* Wrap text with decorators, for example:
@ -102,106 +103,117 @@ const getShortcutWithPrefix = (key: string) => {
}
};
export function getBuiltinItems(
export function getBuiltinActions(
locale: BytemdLocale,
plugins: BytemdPlugin[]
): BytemdToolbarItem[] {
const items: BytemdToolbarItem[] = [
plugins: BytemdPlugin[],
uploadImages: EditorProps['uploadImages']
): BytemdAction[] {
const items: BytemdAction[] = [
{
icon: icons.heading,
onClick({ utils }) {
utils.replaceLines((lines) => lines.map((line) => '# ' + line));
},
...locale.heading,
icon: icons.heading,
handler({ replaceLines }) {
replaceLines((lines) => lines.map((line) => '# ' + line));
},
},
{
...locale.bold,
icon: icons.bold,
shortcut: getShortcutWithPrefix('B'),
onClick({ utils }) {
utils.wrapText('**');
handler({ wrapText }) {
wrapText('**');
},
...locale.bold,
},
{
...locale.italic,
icon: icons.italic,
shortcut: getShortcutWithPrefix('I'),
onClick({ utils }) {
utils.wrapText('_');
handler({ wrapText }) {
wrapText('_');
},
...locale.italic,
},
{
icon: icons.quote,
onClick({ utils }) {
utils.replaceLines((lines) => lines.map((line) => '> ' + line));
},
...locale.quote,
icon: icons.quote,
handler({ replaceLines }) {
replaceLines((lines) => lines.map((line) => '> ' + line));
},
},
{
...locale.link,
icon: icons.link,
shortcut: getShortcutWithPrefix('K'),
onClick({ editor, utils }) {
handler({ editor, wrapText }) {
if (editor.somethingSelected()) {
utils.wrapText('[', '](url)');
wrapText('[', '](url)');
const cursor = editor.getCursor();
editor.setSelection(
{ line: cursor.line, ch: cursor.ch + 2 },
{ line: cursor.line, ch: cursor.ch + 5 }
);
} else {
utils.wrapText('[', '](url)');
wrapText('[', '](url)');
}
},
...locale.link,
},
{
icon: icons.code,
onClick({ utils }) {
utils.wrapText('`');
},
...locale.image,
icon: icons.image,
handler: uploadImages
? async ({ appendBlock }) => {
const fileList = await selectFiles({
accept: 'image/*',
multiple: true,
});
const files = Array.from(fileList ?? []);
const urls = await uploadImages(files);
appendBlock(urls.map((url) => `![](${url})`).join('\n\n'));
}
: undefined,
},
{
...locale.code,
icon: icons.code,
handler({ wrapText }) {
wrapText('`');
},
},
{
...locale.pre,
icon: icons.codeBlock,
onClick({ editor, utils }) {
const { startLine } = utils.appendBlock('```js\n```');
handler({ editor, appendBlock }) {
const { startLine } = appendBlock('```js\n```');
editor.setSelection(
{ line: startLine, ch: 3 },
{ line: startLine, ch: 5 }
);
},
...locale.pre,
},
{
icon: icons.ul,
onClick({ utils }) {
utils.replaceLines((lines) => lines.map((line) => '- ' + line));
},
...locale.ul,
icon: icons.ul,
handler({ replaceLines }) {
replaceLines((lines) => lines.map((line) => '- ' + line));
},
},
{
icon: icons.ol,
onClick({ utils }) {
utils.replaceLines((lines) =>
lines.map((line, i) => `${i + 1}. ${line}`)
);
},
...locale.ol,
icon: icons.ol,
handler({ replaceLines }) {
replaceLines((lines) => lines.map((line, i) => `${i + 1}. ${line}`));
},
},
{
icon: icons.hr,
onClick({ utils }) {
utils.appendBlock('---');
},
...locale.hr,
icon: icons.hr,
},
];
plugins.forEach((p) => {
if (Array.isArray(p.toolbar)) {
items.push(...p.toolbar);
} else if (p.toolbar) {
items.push(p.toolbar);
if (Array.isArray(p.action)) {
items.push(...p.action);
} else if (p.action) {
items.push(p.action);
}
});
return items;

View File

@ -1,14 +1,15 @@
<script lang="ts">
import type { BytemdLocale } from './locales/en-US';
import type { BytemdToolbarItem } from './types';
export let toolbarItems: BytemdToolbarItem[];
import type { BytemdAction } from './types';
export let actions: BytemdAction[];
export let locale: BytemdLocale;
</script>
<div class="bytemd-help">
<h2>{locale.sidebar.cheatsheet}</h2>
<ul>
{#each toolbarItems as item}
{#each actions as item}
{#if item.cheatsheet}
<li>
<span class="bytemd-help-icon">{@html item.icon}</span><span
@ -22,7 +23,7 @@
</ul>
<h2>{locale.sidebar.shortcuts}</h2>
<ul>
{#each toolbarItems as item}
{#each actions as item}
{#if item.shortcut}
<li>
<span class="bytemd-help-icon">{@html item.icon}</span><span

View File

@ -2,6 +2,10 @@
export const icons = {
heading:
'<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 1v13.33M12.997 1v13.33M3 7.665h9.998" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/></svg>',
h1:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 8v32M25 8v32M6 24h19M34.226 24L39 19.017V40" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
h2:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 8v32M24 8v32M7 24h16M32 25c0-3.167 2.667-5 5-5s5 1.833 5 5c0 5.7-10 9.933-10 15h10" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
bold:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="M24 24c5.506 0 9.969-4.477 9.969-10S29.506 4 24 4H11v20h13zM28.031 44C33.537 44 38 39.523 38 34s-4.463-10-9.969-10H11v20h17.031z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
italic:
@ -10,6 +14,8 @@ export const icons = {
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M18.853 9.116c-7.53 4.836-11.714 10.465-12.55 16.887C5 36 13.94 40.893 18.47 36.497 23 32.1 20.285 26.52 17.005 24.994c-3.28-1.525-5.286-.994-4.936-3.032.35-2.039 5.016-7.69 9.116-10.323a.749.749 0 00.114-1.02L20.285 9.3c-.44-.572-.862-.55-1.432-.185zM38.679 9.116c-7.53 4.836-11.714 10.465-12.55 16.887-1.303 9.997 7.637 14.89 12.167 10.494 4.53-4.397 1.815-9.977-1.466-11.503-3.28-1.525-5.286-.994-4.936-3.032.35-2.039 5.017-7.69 9.117-10.323a.749.749 0 00.113-1.02L40.11 9.3c-.44-.572-.862-.55-1.431-.185z" fill="currentColor"/></svg>',
link:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M26.24 16.373l-9.14-9.14c-2.661-2.661-7.035-2.603-9.769.131-2.733 2.734-2.792 7.107-.13 9.768l7.935 7.936M32.903 23.003l7.935 7.936c2.661 2.66 2.603 7.034-.13 9.768-2.735 2.734-7.108 2.792-9.77.131l-9.14-9.14M26.11 26.142c2.733-2.734 2.791-7.108.13-9.769M21.799 21.799c-2.734 2.733-2.793 7.107-.131 9.768" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
image:
'<svg width="1em" height="1em" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" fill-opacity=".01" fill-rule="evenodd" d="M0 0h48v48H0z"/><g transform="translate(5 8)" stroke-width="4" stroke-linejoin="round" stroke="currentColor" fill="none"><path d="M2 0h34a2 2 0 012 2v28a2 2 0 01-2 2H2a2 2 0 01-2-2V2a2 2 0 012-2z" stroke-linecap="round"/><circle stroke-linecap="round" cx="9.5" cy="8.5" r="1.5"/><path d="M10 16l5 4 6-7 17 13v4a2 2 0 01-2 2H2a2 2 0 01-2-2v-4l10-10z"/></g></svg>',
code:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 13L4 25.432 16 37M32 13l12 12.432L32 37" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 4l-7 40" stroke="currentColor" stroke-width="4" stroke-linecap="round"/></svg>',
codeBlock:

View File

@ -9,7 +9,7 @@ const locale = {
},
sidebar: {
toc: 'Table of contents',
cheatsheet: 'Cheat Sheet',
cheatsheet: 'Markdown Cheat Sheet',
shortcuts: 'Shortcuts',
},
status: {
@ -38,6 +38,10 @@ const locale = {
title: 'Link',
cheatsheet: '[text](url)',
},
image: {
title: 'Image',
cheatsheet: '![alt](url)',
},
code: {
title: 'Code',
cheatsheet: '`code`',

View File

@ -38,7 +38,11 @@ const locale: BytemdLocale = {
},
link: {
title: '链接',
cheatsheet: '[链接](url)',
cheatsheet: '[文本](url)',
},
image: {
title: '图片',
cheatsheet: '![alt](url)',
},
code: {
title: '代码',

View File

@ -28,14 +28,12 @@
let el: HTMLElement;
export let tooltip: string;
export let icon: string;
export let style: string | undefined;
export let active: boolean;
</script>
<span
bind:this={el}
on:click={() => dispatch('click')}
{style}
class="bytemd-toolbar-icon"
class:bytemd-toolbar-icon-active={active}
>

View File

@ -4,77 +4,77 @@
import { createEventDispatcher } from 'svelte';
import ToolbarButton from './toolbar-button.svelte';
import { capitalize } from 'lodash-es';
import type { EditorProps, EditorContext, BytemdToolbarItem } from './types';
import type { EditorProps, BytemdEditorContext, BytemdAction } from './types';
import { icons } from './icons';
const dispatch = createEventDispatcher();
export let context: EditorContext;
export let context: BytemdEditorContext;
export let split: boolean;
export let activeTab: number;
export let fullscreen: boolean;
export let sidebar: false | 'help' | 'toc';
export let locale: NonNullable<EditorProps['locale']>;
export let toolbarItems: BytemdToolbarItem[];
export let actions: BytemdAction[];
</script>
<div class="bytemd-toolbar">
{#if !split}
<div class="bytemd-tabs">
<div class="bytemd-toolbar-left">
{#if split}
{#each actions as item}
{#if item.handler}
<ToolbarButton
tooltip={item.title + (item.shortcut ? ` <${item.shortcut}>` : '')}
icon={item.icon}
active={false}
on:click={() => item.handler && item.handler(context)}
/>
{/if}
{/each}
{:else}
<span
on:click={() => dispatch('tab', 0)}
class:bytemd-tab-active={activeTab === 0}>{locale.toolbar.write}</span
class="bytemd-toolbar-tab"
class:bytemd-toolbar-tab-active={activeTab === 0}
>{locale.toolbar.write}</span
><span
on:click={() => dispatch('tab', 1)}
class:bytemd-tab-active={activeTab === 1}
class="bytemd-toolbar-tab"
class:bytemd-toolbar-tab-active={activeTab === 1}
>{capitalize(locale.toolbar.preview)}</span
>
</div>
{/if}
{/if}
</div>
{#if split || activeTab === 0}
{#each toolbarItems as item}
<ToolbarButton
tooltip={item.title}
icon={item.icon}
style={undefined}
active={false}
on:click={() => item.onClick(context)}
/>
{/each}
{/if}
<ToolbarButton
tooltip={locale.toolbar.about}
icon={icons.info}
style="float:right"
active={false}
on:click={() => {
window.open('https://github.com/bytedance/bytemd');
}}
/><ToolbarButton
tooltip={locale.toolbar.fullscreen}
icon={fullscreen ? icons.fullscreenOff : icons.fullscreenOn}
style="float:right"
active={false}
on:click={() => {
dispatch('click', 'fullscreen');
}}
/><ToolbarButton
tooltip={locale.toolbar.help}
icon={icons.help}
style="float:right"
active={sidebar === 'help'}
on:click={() => {
dispatch('click', 'help');
}}
/><ToolbarButton
tooltip={locale.toolbar.toc}
icon={icons.toc}
style="float:right"
active={sidebar === 'toc'}
on:click={() => {
dispatch('click', 'toc');
}}
/>
<div class="bytemd-toolbar-right">
<ToolbarButton
tooltip={locale.toolbar.toc}
icon={icons.toc}
active={sidebar === 'toc'}
on:click={() => {
dispatch('click', 'toc');
}}
/><ToolbarButton
tooltip={locale.toolbar.help}
icon={icons.help}
active={sidebar === 'help'}
on:click={() => {
dispatch('click', 'help');
}}
/><ToolbarButton
tooltip={locale.toolbar.fullscreen}
icon={fullscreen ? icons.fullscreenOff : icons.fullscreenOn}
active={false}
on:click={() => {
dispatch('click', 'fullscreen');
}}
/><ToolbarButton
tooltip={locale.toolbar.about}
icon={icons.info}
active={false}
on:click={() => {
window.open('https://github.com/bytedance/bytemd');
}}
/>
</div>
</div>

View File

@ -5,50 +5,55 @@ import type { Editor, EditorConfiguration } from 'codemirror';
import type { EditorUtils } from './editor';
import type { BytemdLocale } from './locales/en-US';
export interface EditorContext {
export interface BytemdContext {
/**
* The root element of the viewer
*/
markdownBody: HTMLElement;
/**
* Virtual file format used in [unified](https://unifiedjs.com/)
*
* Get the HTML output by calling `vfile.toString()`
*/
vfile: VFile;
}
export interface BytemdEditorContext extends EditorUtils {
/**
* CodeMirror editor instance
*/
editor: Editor;
/**
* Root element, `$('.bytemd')`
* The root element
*/
$el: HTMLElement;
/**
* Utilities for Editor
*/
utils: EditorUtils;
root: HTMLElement;
}
export interface ViewerContext {
export interface BytemdAction {
/**
* Root element of the Viewer, `$('.markdown-body')`
*/
$el: HTMLElement;
vfile: VFile;
}
export interface BytemdToolbarItem {
/**
* Toolbar Icon (16x16), could be <img> or inline svg
* Action icon (16x16), could be <img> or inline svg
*/
icon: string;
/**
* Tooltip of toolbar item
* Action title
*/
title: string;
/**
* Toolbar icon click handler
* Action handler, used for toolbar icon click and shortcut trigger
*/
onClick(context: EditorContext): void;
handler?(context: BytemdEditorContext): void;
/**
* If specified, this record will be added to the Markdown cheat sheet
* Markdown syntax cheat sheet
*
* If specified, this record will be added to the Markdown cheat sheet section
*/
cheatsheet?: string;
/**
* shortcut handler
* Keyboard shortcut
*
* If specified, this record will be added to the Keyboard shortcut
* If specified, this record will be added to the Keyboard shortcut section
*
* https://codemirror.net/doc/manual.html#keymaps
*/
shortcut?: string;
}
@ -67,17 +72,17 @@ export interface BytemdPlugin {
*/
rehype?: (p: Processor) => Processor;
/**
* Register toolbar items
* Side effect for viewer, triggers when viewer props changes
*/
toolbar?: BytemdToolbarItem | BytemdToolbarItem[];
effect?(context: BytemdContext): void | (() => void);
/**
* Side effect for editor, triggers when plugin list changes
* Register actions in toolbar, cheatsheet and shortcuts
*/
editorEffect?(context: EditorContext): void | (() => void);
action?: BytemdAction | BytemdAction[];
/**
* Side effect for viewer, triggers when HTML or plugin list changes
* Side effect for editor, triggers when editor props changes
*/
effect?(context: ViewerContext): void | (() => void);
editorEffect?(context: BytemdEditorContext): void | (() => void);
}
export interface EditorProps extends ViewerProps {
@ -108,9 +113,13 @@ export interface EditorProps extends ViewerProps {
*/
editorConfig?: Omit<EditorConfiguration, 'value' | 'mode' | 'placeholder'>;
/**
* Locale
* i18n locale
*/
locale?: BytemdLocale;
/**
* Handle image uplodaer
*/
uploadImages?(files: File[]): Promise<string[]>;
}
export interface ViewerProps {
@ -134,7 +143,7 @@ export interface ViewerProps {
/**
* Allow inline styles. Default: `false`
*/
allowStyle?: boolean;
allowInlineStyle?: boolean;
}
| ((schema: Schema) => Schema);
}

View File

@ -30,7 +30,7 @@ export function getProcessor({
if (typeof sanitize === 'function') {
schema = sanitize(schema);
} else if (sanitize?.allowStyle) {
} else if (sanitize?.allowInlineStyle) {
schema.attributes!['*'].push('style');
}

View File

@ -21,12 +21,12 @@
return h;
}
let el: HTMLElement;
let markdownBody: HTMLElement;
let cbs: ReturnType<NonNullable<BytemdPlugin['effect']>>[] = [];
function on() {
// console.log('von');
cbs = plugins.map((p) => p.effect?.({ $el: el, vfile }));
cbs = plugins.map((p) => p.effect?.({ markdownBody, vfile }));
}
function off() {
// console.log('voff');
@ -34,14 +34,16 @@
}
onMount(() => {
el.addEventListener('click', (e) => {
markdownBody.addEventListener('click', (e) => {
const $ = e.target as HTMLElement;
if ($.tagName !== 'A') return;
const href = $.getAttribute('href');
if (!href?.startsWith('#')) return;
el.querySelector('#user-content-' + href.slice(1))?.scrollIntoView();
markdownBody
.querySelector('#user-content-' + href.slice(1))
?.scrollIntoView();
});
});
@ -75,6 +77,6 @@
$: html = `<!--${hashCode(value)}-->${vfile.toString()}`; // trigger re-render every time the value changes
</script>
<div bind:this={el} class="markdown-body">
<div bind:this={markdownBody} class="markdown-body">
{@html html}
</div>

View File

@ -17,11 +17,32 @@ $sidebar-width: 280px;
&-toolbar {
padding: 4px 12px;
border-bottom: 1px solid $border-color;
font-size: 0;
background-color: $gray-000;
user-select: none;
overflow: hidden;
&-left {
float: left;
}
&-right {
float: right;
}
&-tab {
cursor: pointer;
padding-left: 8px;
padding-right: 8px;
line-height: 24px;
font-size: 14px;
&-active {
color: $primary;
}
}
&-icon {
display: inline-block;
vertical-align: top;
cursor: pointer;
border-radius: 4px;
margin-left: 6px;
@ -40,29 +61,14 @@ $sidebar-width: 280px;
}
}
&-tabs {
display: inline-block;
vertical-align: top;
span {
cursor: pointer;
padding-left: 8px;
padding-right: 8px;
line-height: 24px;
font-size: 14px;
&.bytemd-tab-active {
color: $primary;
}
}
}
&-body {
height: calc(100% - 33px - 25px);
overflow: auto;
font-size: 0;
}
&-editor {
display: inline-block;
vertical-align: top;
height: 100%;
overflow: hidden;
.CodeMirror {
@ -86,6 +92,7 @@ $sidebar-width: 280px;
&-preview {
display: inline-block;
vertical-align: top;
height: 100%;
overflow: auto;
.markdown-body {
@ -95,6 +102,7 @@ $sidebar-width: 280px;
&-sidebar {
display: inline-block;
vertical-align: top; // Safari
height: 100%;
overflow: auto;
font-size: 16px;
@ -103,7 +111,6 @@ $sidebar-width: 280px;
width: $sidebar-width;
position: relative;
padding: 16px;
vertical-align: top; // Safari
&-close {
position: absolute;
padding: 16px;
@ -128,21 +135,24 @@ $sidebar-width: 280px;
&-help {
ul {
line-height: 20px;
font-size: 0;
svg {
font-size: 16px;
width: 16px;
height: 16px;
display: block;
}
span {
font-size: 13px;
display: inline-block;
vertical-align: middle;
vertical-align: top;
}
}
li {
list-style: none;
margin-bottom: 10px;
}
&-icon {
padding: 2px 0;
}
&-title {
padding-left: 8px;
}

View File

@ -1,26 +0,0 @@
# @bytemd/plugin-external-links
[![npm](https://img.shields.io/npm/v/@bytemd/plugin-external-links.svg)](https://npm.im/@bytemd/plugin-external-links)
ByteMD plugin to open external links in new window
## Usage
```js
import { Editor } from 'bytemd';
import externalLinks from '@bytemd/plugin-external-links';
new Editor({
target: document.body,
props: {
plugins: [
externalLinks(),
// ... other plugins
],
},
});
```
## License
MIT

View File

@ -1,30 +0,0 @@
{
"name": "@bytemd/plugin-external-links",
"version": "1.3.0",
"description": "ByteMD plugin to open external links in new window",
"author": "Rongjian Zhang <pd4d10@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/bytedance/bytemd.git",
"directory": "packages/plugin-external-links"
},
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"unpkg": "dist/index.min.js",
"jsdelivr": "dist/index.min.js",
"types": "lib/index.d.ts",
"files": [
"dist",
"lib"
],
"peerDependencies": {
"bytemd": "*"
},
"dependencies": {
"unist-util-visit": "^2.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,46 +0,0 @@
import type { BytemdPlugin } from 'bytemd';
import type { Element } from 'hast';
import visit from 'unist-util-visit';
type AnchorProps = Partial<Omit<HTMLAnchorElement, 'href'>>;
export interface ExternalLinksOptions {
/**
* Test if it is an external url
*/
test(href: string): boolean;
/**
* Internal links props
*/
internalProps?: AnchorProps;
/**
* External links props
*/
externalProps?: AnchorProps;
}
export default function externalLinks({
test,
internalProps = {},
externalProps = {
target: '_blank',
rel: 'nofollow noopener noreferrer',
},
}: ExternalLinksOptions): BytemdPlugin {
return {
rehype: (p) =>
p.use(() => (tree) => {
visit<Element>(tree, 'element', (node) => {
if (node.tagName !== 'a' || !node.properties?.href) return;
const href = node.properties.href as string;
if (!/https?:\/\//.test(href)) return; // only handle http and https
Object.assign(
node.properties,
test(href) ? externalProps : internalProps
);
});
}),
};
}

View File

@ -3,7 +3,7 @@ export const icons = {
strikethrough:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5 24h38M24 24c16 6 10 20 0 20s-12-8-12-8M36 12s-3-8-12-8-12.564 7.6-8.39 14" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 36s4 8 12 8 12.564-7.6 8.39-14" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
task:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#icon-05f6552097271bd)" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M42 20v19a3 3 0 01-3 3H9a3 3 0 01-3-3V9a3 3 0 013-3h21"/><path d="M16 20l10 8L41 7"/></g><defs><clipPath id="icon-05f6552097271bd"><path fill="currentColor" d="M0 0h48v48H0z"/></clipPath></defs></svg>',
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#icon-5e4ef1fb097271bd)" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M42 20v19a3 3 0 01-3 3H9a3 3 0 01-3-3V9a3 3 0 013-3h21"/><path d="M16 20l10 8L41 7"/></g><defs><clipPath id="icon-5e4ef1fb097271bd"><path fill="currentColor" d="M0 0h48v48H0z"/></clipPath></defs></svg>',
table:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M40.15 5H7.85A2.85 2.85 0 005 7.85v32.3A2.85 2.85 0 007.85 43h32.3A2.85 2.85 0 0043 40.15V7.85A2.85 2.85 0 0040.15 5z" stroke="currentColor" stroke-width="4"/><path d="M17 5v38M31 5v38M5 17h38M5 31h38" stroke="currentColor" stroke-width="4" stroke-linecap="round"/></svg>',
};

View File

@ -14,25 +14,26 @@ export default function gfm({
}: BytemdPluginGfmOptions = {}): BytemdPlugin {
return {
remark: (p) => p.use(remarkGfm, remarkGfmOptions),
toolbar: [
action: [
{
icon: icons.strikethrough,
onClick({ utils }) {
utils.wrapText('~~');
},
...locale.strike,
},
{
icon: icons.task,
onClick({ utils }) {
utils.replaceLines((lines) => lines.map((line) => '- [ ] ' + line));
icon: icons.strikethrough,
handler({ wrapText }) {
wrapText('~~');
},
...locale.task,
},
{
...locale.task,
icon: icons.task,
handler({ replaceLines }) {
replaceLines((lines) => lines.map((line) => '- [ ] ' + line));
},
},
{
...locale.table,
icon: icons.table,
onClick({ editor, utils }) {
const { startLine } = utils.appendBlock(
handler({ editor, appendBlock }) {
const { startLine } = appendBlock(
'| heading | |\n| --- | --- |\n| | |\n'
);
editor.setSelection(
@ -40,7 +41,6 @@ export default function gfm({
{ line: startLine, ch: 9 }
);
},
...locale.table,
},
],
};

View File

@ -10,9 +10,9 @@ export default function highlight({
}: BytemdPluginHighlightOptions = {}): BytemdPlugin {
let hljs: typeof H;
return {
effect({ $el }) {
effect({ markdownBody }) {
(async () => {
const els = $el.querySelectorAll<HTMLElement>('pre>code');
const els = markdownBody.querySelectorAll<HTMLElement>('pre>code');
if (els.length === 0) return;
if (!hljs) {

View File

@ -1,26 +0,0 @@
# @bytemd/plugin-import-html
[![npm](https://img.shields.io/npm/v/@bytemd/plugin-import-html.svg)](https://npm.im/@bytemd/plugin-import-html)
ByteMD plugin to import HTML by pasting or dropping
## Usage
```js
import { Editor } from 'bytemd';
import importHtml from '@bytemd/plugin-import-html';
new Editor({
target: document.body,
props: {
plugins: [
importHtml(),
// ... other plugins
],
},
});
```
## License
MIT

View File

@ -1,35 +0,0 @@
{
"name": "@bytemd/plugin-import-html",
"version": "1.3.0",
"description": "ByteMD plugin to import HTML by pasting or dropping",
"author": "Rongjian Zhang <pd4d10@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/bytedance/bytemd.git",
"directory": "packages/plugin-import-html"
},
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"unpkg": "dist/index.min.js",
"jsdelivr": "dist/index.min.js",
"types": "lib/index.d.ts",
"files": [
"dist",
"lib"
],
"dependencies": {
"mdast-util-gfm": "^0.1.0",
"rehype-parse": "^7.0.0",
"rehype-remark": "^8.0.0",
"remark-gfm": "^1.0.0",
"remark-stringify": "^9.0.0",
"unified": "^9.0.0"
},
"peerDependencies": {
"bytemd": "*"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,109 +0,0 @@
import type { BytemdPlugin } from 'bytemd';
import type { Processor } from 'unified';
import type { RemarkStringifyOptions } from 'remark-stringify';
export interface ImportHtmlOptions {
/**
* Process HTML before being converted to markdown
*/
rehype?: (p: Processor) => Processor;
/**
* Output markdown text format
*
* https://github.com/syntax-tree/mdast-util-to-markdown#tomarkdowntree-options
*/
markdownFormat?: RemarkStringifyOptions;
}
export default function importHtml({
rehype,
markdownFormat = {
fences: true,
listItemIndent: 'one',
},
}: ImportHtmlOptions = {}): BytemdPlugin {
const handler = async (
editor: CodeMirror.Editor,
e: ClipboardEvent | DragEvent
) => {
const items = Array.from(
(e instanceof ClipboardEvent
? e.clipboardData?.items
: e.dataTransfer?.items) ?? []
);
// fix: text copied from VSCode would have a `vscode-editor-data` type, exclude this case
if (items.length !== 2) return;
const htmlItem = items.find((item) => item.type === 'text/html');
if (!htmlItem) return;
e.preventDefault();
let html: string;
switch (htmlItem.kind) {
case 'string': {
html = await new Promise<string>((resolve) =>
htmlItem.getAsString((v) => {
resolve(v);
})
);
break;
}
case 'file': {
html = await htmlItem.getAsFile()!.text();
break;
}
default: {
throw new Error();
}
}
// console.log(html);
const [
{ default: unified },
{ default: rehypeParse },
{ default: rehypeRemark },
{ default: remarkStringify },
{ default: remarkGfm },
{ toMarkdown: gfmExt },
] = await Promise.all([
import('unified'),
import('rehype-parse'),
// @ts-ignore
import('rehype-remark'),
import('remark-stringify'),
import('remark-gfm'),
// @ts-ignore
import('mdast-util-gfm'),
]);
let processor = unified().use(rehypeParse);
if (rehype) {
processor = rehype(processor);
}
processor = processor
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify, {
...markdownFormat,
extensions: [gfmExt()],
});
const result = await processor.process(html);
editor.replaceSelection(result.toString());
};
return {
editorEffect({ editor }) {
editor.on('paste', handler);
editor.on('drop', handler);
return () => {
editor.off('paste', handler);
editor.off('drop', handler);
};
},
};
}

View File

@ -1,26 +0,0 @@
# @bytemd/plugin-import-image
[![npm](https://img.shields.io/npm/v/@bytemd/plugin-import-image.svg)](https://npm.im/@bytemd/plugin-import-image)
ByteMD plugin to import image by pasting or dropping
## Usage
```js
import { Editor } from 'bytemd';
import importImage from '@bytemd/plugin-import-image';
new Editor({
target: document.body,
props: {
plugins: [
importImage(),
// ... other plugins
],
},
});
```
## License
MIT

View File

@ -1,27 +0,0 @@
{
"name": "@bytemd/plugin-import-image",
"version": "1.3.2",
"description": "ByteMD plugin to import image by pasting or dropping",
"author": "Rongjian Zhang <pd4d10@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/bytedance/bytemd.git",
"directory": "packages/plugin-import-image"
},
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"unpkg": "dist/index.min.js",
"jsdelivr": "dist/index.min.js",
"types": "lib/index.d.ts",
"files": [
"dist",
"lib"
],
"peerDependencies": {
"bytemd": "*"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,5 +0,0 @@
// DO NOT EDIT, generated by scripts/icon.js
export const icons = {
image:
'<svg width="1em" height="1em" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" fill-opacity=".01" fill-rule="evenodd" d="M0 0h48v48H0z"/><g transform="translate(5 8)" stroke-width="4" stroke-linejoin="round" stroke="currentColor" fill="none"><path d="M2 0h34a2 2 0 012 2v28a2 2 0 01-2 2H2a2 2 0 01-2-2V2a2 2 0 012-2z" stroke-linecap="round"/><circle stroke-linecap="round" cx="9.5" cy="8.5" r="1.5"/><path d="M10 16l5 4 6-7 17 13v4a2 2 0 01-2 2H2a2 2 0 01-2-2v-4l10-10z"/></g></svg>',
};

View File

@ -1,74 +0,0 @@
import { BytemdPlugin, EditorContext } from 'bytemd';
import { icons } from './icons';
export interface ImportImageOptions {
/**
* Upload the file and return a URL
*/
upload(files: File[]): Promise<string[]>;
}
export default function importImage({
upload,
}: ImportImageOptions): BytemdPlugin {
const handleFiles = async (files: File[], utils: EditorContext['utils']) => {
const urls = await upload(files);
utils.appendBlock(urls.map((url) => `![](${url})`).join('\n\n'));
};
return {
toolbar: {
title: 'Image',
icon: icons.image,
onClick({ utils }) {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.accept = 'image/*';
input.addEventListener('input', (e) => {
const files = Array.from(input.files ?? []).filter(
// input accept would not work if 'all files' is selected
(item) => item.type.startsWith('image/')
);
if (files?.length) {
handleFiles(files, utils);
}
});
input.click();
},
},
editorEffect({ editor, utils }) {
const handler = async (
_: CodeMirror.Editor,
e: ClipboardEvent | DragEvent
) => {
const itemList =
e instanceof ClipboardEvent
? e.clipboardData?.items
: e.dataTransfer?.items;
const files = Array.from(itemList ?? [])
.map((item) => {
if (item.type.startsWith('image/')) {
return item.getAsFile();
}
})
.filter((f): f is File => f != null);
if (files.length) {
e.preventDefault();
await handleFiles(files, utils);
}
};
editor.on('paste', handler);
editor.on('drop', handler);
return () => {
editor.off('paste', handler);
editor.off('drop', handler);
};
},
};
}

View File

@ -1,7 +1,7 @@
// DO NOT EDIT, generated by scripts/icon.js
export const icons = {
inline:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" fill-opacity=".01" d="M0 0h48v48H0z"/><path d="M24 2v44M35 6H20a9 9 0 100 18M13 42h15a9 9 0 100-18h-8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M37 9l-3-3H8l17 18L8 42h26l3-3M5 24h10M33 24h10" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
display:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M40 9l-3-3H8l18 18L8 42h29l3-3" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
};

View File

@ -16,24 +16,21 @@ export default function math({
return {
remark: (u) => u.use(remarkMath),
rehype: (u) => u.use(rehypeKatex, katexOptions),
toolbar: [
action: [
{
icon: icons.inline,
onClick({ utils }) {
utils.wrapText('$');
},
...locale.inline,
icon: icons.inline,
},
{
...locale.display,
icon: icons.display,
onClick({ editor, utils }) {
const { startLine } = utils.appendBlock('$$\n\\TeX\n$$');
handler({ editor, appendBlock }) {
const { startLine } = appendBlock('$$\n\\TeX\n$$');
editor.setSelection(
{ line: startLine + 1, ch: 0 },
{ line: startLine + 1, ch: 4 }
);
},
...locale.display,
},
],
};

View File

@ -1,10 +1,10 @@
const locale = {
inline: {
title: 'Math formula',
title: 'Formula',
cheatsheet: '$\\TeX$',
},
display: {
title: 'Math formula block',
title: 'Formula block',
cheatsheet: '$$↵\\TeX↵$$',
},
};

View File

@ -1,7 +1,7 @@
// DO NOT EDIT, generated by scripts/icon.js
export const icons = {
inline:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" fill-opacity=".01" d="M0 0h48v48H0z"/><path d="M24 2v44M35 6H20a9 9 0 100 18M13 42h15a9 9 0 100-18h-8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M37 9l-3-3H8l17 18L8 42h26l3-3M5 24h10M33 24h10" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
display:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M40 9l-3-3H8l18 18L8 42h29l3-3" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
};

View File

@ -17,9 +17,9 @@ export default function math({
return {
remark: (p) => p.use(remarkMath),
effect({ $el }) {
effect({ markdownBody }) {
const renderMath = async (selector: string, displayMode: boolean) => {
const els = $el.querySelectorAll<HTMLElement>(selector);
const els = markdownBody.querySelectorAll<HTMLElement>(selector);
if (els.length === 0) return;
if (!katex) {
@ -38,24 +38,21 @@ export default function math({
renderMath('.math.math-inline', false);
renderMath('.math.math-display', true);
},
toolbar: [
action: [
{
icon: icons.inline,
onClick({ utils }) {
utils.wrapText('$');
},
...locale.inline,
icon: icons.inline,
},
{
...locale.display,
icon: icons.display,
onClick({ editor, utils }) {
const { startLine } = utils.appendBlock('$$\n\\TeX\n$$');
handler({ editor, appendBlock }) {
const { startLine } = appendBlock('$$\n\\TeX\n$$');
editor.setSelection(
{ line: startLine + 1, ch: 0 },
{ line: startLine + 1, ch: 4 }
);
},
...locale.display,
},
],
};

View File

@ -1,10 +1,10 @@
const locale = {
inline: {
title: 'Math formula',
title: 'Formula',
cheatsheet: '$\\TeX$',
},
display: {
title: 'Math formula block',
title: 'Formula block',
cheatsheet: '$$↵\\TeX↵$$',
},
};

View File

@ -5,11 +5,11 @@ export default function mediumZoom(options?: M.ZoomOptions): BytemdPlugin {
let m: typeof M;
return {
effect({ $el }) {
const imgs = [...$el.querySelectorAll('img')].filter((e) => {
effect({ markdownBody }) {
const imgs = [...markdownBody.querySelectorAll('img')].filter((e) => {
// Exclude images with anchor parent
let $: HTMLElement | null = e;
while ($ && $ !== $el) {
while ($ && $ !== markdownBody) {
if ($.tagName === 'A') return false;
$ = $.parentElement;
}

View File

@ -1,5 +1,5 @@
// DO NOT EDIT, generated by scripts/icon.js
export const icons = {
mermaid:
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 28a4 4 0 100-8 4 4 0 000 8zM42 8a2 2 0 100-4 2 2 0 000 4zM42 26a2 2 0 100-4 2 2 0 000 4zM42 44a2 2 0 100-4 2 2 0 000 4z" stroke="currentColor" stroke-width="4" stroke-linejoin="round"/><path d="M32 6H20v36h12M12 24h20" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
'<svg width="1em" height="1em" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" fill-opacity=".01" d="M0 0h48v48H0z"/><path stroke="currentColor" stroke-width="4" stroke-linejoin="round" d="M17 6h14v9H17zM6 33h14v9H6zM28 33h14v9H28z"/><path d="M24 16v8M13 33v-9h22v9" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
};

View File

@ -16,9 +16,9 @@ export default function mermaid({
let m: Mermaid;
return {
effect({ $el }) {
effect({ markdownBody }) {
(async () => {
const els = $el.querySelectorAll<HTMLElement>(
const els = markdownBody.querySelectorAll<HTMLElement>(
'pre>code.language-mermaid'
);
if (els.length === 0) return;
@ -54,12 +54,10 @@ export default function mermaid({
});
})();
},
toolbar: {
action: {
icon: icons.mermaid,
onClick({ editor, utils }) {
const { startLine } = utils.appendBlock(
'```mermaid\ngraph LR\nA--->B\n```'
);
handler({ editor, appendBlock }) {
const { startLine } = appendBlock('```mermaid\ngraph LR\nA--->B\n```');
editor.setSelection(
{ line: startLine + 1, ch: 0 }, // @ts-ignore
{ line: startLine + 2 }

View File

@ -14,10 +14,10 @@ export const Viewer: FC<ViewerProps> = ({ value, sanitize, plugins }) => {
}, [value, sanitize, plugins]);
useEffect(() => {
const $el = elRef.current;
if (!$el || !vfile) return;
const markdownBody = elRef.current;
if (!markdownBody || !vfile) return;
const cbs = plugins?.map(({ effect }) => effect?.({ $el, vfile }));
const cbs = plugins?.map(({ effect }) => effect?.({ markdownBody, vfile }));
return () => {
cbs?.forEach((cb) => cb && cb());
};

View File

@ -40,7 +40,9 @@ export default {
on() {
if (this.plugins && this.vfile) {
this.cbs = this.plugins.map(
({ effect }) => effect && effect({ $el: this.$el, vfile: this.vfile })
({ effect }) =>
effect &&
effect({ markdownBody: this.markdownBody, vfile: this.vfile })
);
}
},
@ -56,7 +58,9 @@ export default {
const href = $.getAttribute('href');
if (!href || !href.startsWith('#')) return;
const dest = this.$el.querySelector('#user-content-' + href.slice(1));
const dest = this.markdownBody.querySelector(
'#user-content-' + href.slice(1)
);
if (dest) dest.scrollIntoView();
},
},

View File

@ -8,15 +8,15 @@ const svgo = new Svgo();
const meta = {
bytemd: {
heading: () => `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 1V14.33" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.9971 1V14.33" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 7.66498H12.9975" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`,
heading: () =>
`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 1V14.33" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.9971 1V14.33" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 7.66498H12.9975" stroke="currentColor" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
h1: icons.H1,
h2: icons.H2,
bold: icons.TextBold,
italic: icons.TextItalic,
quote: icons.Quote,
link: icons.LinkOne,
image: icons.Pic,
code: icons.Code,
codeBlock: icons.CodeBrackets,
ol: icons.OrderedList,
@ -34,19 +34,16 @@ const meta = {
task: icons.CheckCorrect,
table: icons.InsertTable,
},
'plugin-import-image': {
image: icons.Pic,
},
'plugin-math': {
inline: icons.Dollar,
inline: icons.Inline,
display: icons.Formula,
},
'plugin-math-ssr': {
inline: icons.Dollar,
inline: icons.Inline,
display: icons.Formula,
},
'plugin-mermaid': {
mermaid: icons.MindMapping,
mermaid: icons.ChartGraph,
},
};

View File

@ -5321,13 +5321,6 @@ hast-to-hyperscript@^9.0.0:
unist-util-is "^4.0.0"
web-namespaces "^1.0.0"
hast-util-embedded@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/hast-util-embedded/-/hast-util-embedded-1.0.6.tgz#ea7007323351cc43e19e1d6256b7cde66ad1aa03"
integrity sha512-JQMW+TJe0UAIXZMjCJ4Wf6ayDV9Yv3PBDPsHD4ExBpAspJ6MOcCX+nzVF+UJVv7OqPcg852WEMSHQPoRA+FVSw==
dependencies:
hast-util-is-element "^1.1.0"
hast-util-from-parse5@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a"
@ -5340,12 +5333,7 @@ hast-util-from-parse5@^6.0.0:
vfile-location "^3.2.0"
web-namespaces "^1.0.0"
hast-util-has-property@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-1.0.4.tgz#9f137565fad6082524b382c1e7d7d33ca5059f36"
integrity sha512-ghHup2voGfgFoHMGnaLHOjbYFACKrRh9KFttdCzMCbFoBMJXiNi2+XTrPP8+q6cDJM/RSqlCfVWrjp1H201rZg==
hast-util-is-element@^1.0.0, hast-util-is-element@^1.1.0:
hast-util-is-element@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz#3b3ed5159a2707c6137b48637fbfe068e175a425"
integrity sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==
@ -5394,24 +5382,6 @@ hast-util-to-html@^7.1.1:
unist-util-is "^4.0.0"
xtend "^4.0.0"
hast-util-to-mdast@^7.0.0:
version "7.1.3"
resolved "https://registry.yarnpkg.com/hast-util-to-mdast/-/hast-util-to-mdast-7.1.3.tgz#e4ad9098929355501773aed5e66c8181559eee04"
integrity sha512-3vER9p8B8mCs5b2qzoBiWlC9VnTkFmr8Ufb1eKdcvhVY+nipt52YfMRshk5r9gOE1IZ9/xtlSxebGCv1ig9uKA==
dependencies:
extend "^3.0.0"
hast-util-has-property "^1.0.0"
hast-util-is-element "^1.1.0"
hast-util-to-text "^2.0.0"
mdast-util-phrasing "^2.0.0"
mdast-util-to-string "^1.0.0"
rehype-minify-whitespace "^4.0.3"
repeat-string "^1.6.1"
trim-trailing-lines "^1.1.0"
unist-util-is "^4.0.0"
unist-util-visit "^2.0.0"
xtend "^4.0.1"
hast-util-to-parse5@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479"
@ -5432,7 +5402,7 @@ hast-util-to-text@^2.0.0:
repeat-string "^1.0.0"
unist-util-find-after "^3.0.0"
hast-util-whitespace@^1.0.0, hast-util-whitespace@^1.0.4:
hast-util-whitespace@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41"
integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==
@ -7217,13 +7187,6 @@ mdast-util-math@^0.1.0:
mdast-util-to-markdown "^0.6.0"
repeat-string "^1.0.0"
mdast-util-phrasing@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-2.0.0.tgz#57e61f2be908be9f5fce54fcc2fa593687986267"
integrity sha512-G1rNlW/sViwzbBYD7+k3mKGtoWV2v4GBFky66OYHfktHe7Hg9R+hH4xpeoOtjYiwTvle8C8wlKMpgqPCkaeK8Q==
dependencies:
unist-util-is "^4.0.0"
mdast-util-to-hast@^10.0.0:
version "10.1.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.1.1.tgz#4dce367abdc57311a87cf95da54a4d115b9d25da"
@ -7250,11 +7213,6 @@ mdast-util-to-markdown@^0.6.0, mdast-util-to-markdown@^0.6.1, mdast-util-to-mark
repeat-string "^1.0.0"
zwitch "^1.0.0"
mdast-util-to-string@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527"
integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==
mdast-util-to-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b"
@ -9402,16 +9360,6 @@ rehype-katex@^4.0.0:
unified "^9.0.0"
unist-util-visit "^2.0.0"
rehype-minify-whitespace@^4.0.3:
version "4.0.5"
resolved "https://registry.yarnpkg.com/rehype-minify-whitespace/-/rehype-minify-whitespace-4.0.5.tgz#5b4781786116216f6d5d7ceadf84e2489dd7b3cd"
integrity sha512-QC3Z+bZ5wbv+jGYQewpAAYhXhzuH/TVRx7z08rurBmh9AbG8Nu8oJnvs9LWj43Fd/C7UIhXoQ7Wddgt+ThWK5g==
dependencies:
hast-util-embedded "^1.0.0"
hast-util-is-element "^1.0.0"
hast-util-whitespace "^1.0.4"
unist-util-is "^4.0.0"
rehype-parse@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-7.0.1.tgz#58900f6702b56767814afc2a9efa2d42b1c90c57"
@ -9427,13 +9375,6 @@ rehype-raw@^5.0.0:
dependencies:
hast-util-raw "^6.0.0"
rehype-remark@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/rehype-remark/-/rehype-remark-8.0.0.tgz#66233e5b6e096419353f4c5c0fb6808f7924dd57"
integrity sha512-d1EmgsqWc1v9E/URuzozU8pa4AYHIcfOMLhgzQRHeaxYyMHJKIrpBMdRhl+IbqcHLD699Ho/vO+DpSZgKsGM8Q==
dependencies:
hast-util-to-mdast "^7.0.0"
rehype-sanitize@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-4.0.0.tgz#b5241cf66bcedc49cd4e924a5f7a252f00a151ad"
@ -9512,13 +9453,6 @@ remark-rehype@^8.0.0:
dependencies:
mdast-util-to-hast "^10.0.0"
remark-stringify@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-9.0.1.tgz#576d06e910548b0a7191a71f27b33f1218862894"
integrity sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==
dependencies:
mdast-util-to-markdown "^0.6.0"
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@ -9881,6 +9815,11 @@ scss-tokenizer@^0.2.3:
js-base64 "^2.1.8"
source-map "^0.4.2"
select-files@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/select-files/-/select-files-1.0.1.tgz#974991a69c5dba26cfa57dcbc6d27f0917741acb"
integrity sha512-8h4DSpjfFa0hyMP3z3ye4SxyhdaE5RgaXeScRpH7xl4YblnZSHwexmLdLNdSKwTO8H9ccDKj7Votz0io+18+BQ==
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -10808,11 +10747,6 @@ trim-off-newlines@^1.0.0:
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
trim-trailing-lines@^1.1.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0"
integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==
trough@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
@ -11495,7 +11429,7 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
xtend@^4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==