feat: add electron app.

This commit is contained in:
jaywcjlove
2022-09-04 22:09:04 +08:00
parent a66e906eef
commit 76a6f48d0a
62 changed files with 483 additions and 98 deletions

17
website/src/App.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { Routes, Route } from 'react-router-dom';
import { Layout } from './components/Layout';
import { HomePage } from './pages/home';
import { EditorPage } from './pages/theme/editor';
import { DocsPage } from './pages/docs';
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="/editor/theme" element={<EditorPage />} />
<Route path="/doc" element={<DocsPage />} />
</Route>
</Routes>
);
}

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g id="Layer_2">
<g id="color-palette">
<path d="M19.54 5.08A10.61 10.61 0 0 0 11.91 2a10 10 0 0 0-.05 20 2.58 2.58 0 0 0 2.53-1.89 2.52 2.52 0 0 0-.57-2.28.5.5 0 0 1 .37-.83h1.65A6.15 6.15 0 0 0 22 11.33a8.48 8.48 0 0 0-2.46-6.25Zm-12.7 9.66a1.5 1.5 0 1 1 .4-2.08 1.49 1.49 0 0 1-.4 2.08ZM8.3 9.25a1.5 1.5 0 1 1-.55-2 1.5 1.5 0 0 1 .55 2ZM11 7a1.5 1.5 0 1 1 1.5-1.5A1.5 1.5 0 0 1 11 7Zm5.75.8a1.5 1.5 0 1 1 .55-2 1.5 1.5 0 0 1-.55 2Z" style="fill:#231f20" id="color-palette-2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 1024 1024" height="1em" width="1em">
<path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0 1 38.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 784 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1aad1a" viewBox="0 0 1024 1024" height="1em" width="1em">
<path d="M690.1 377.4c5.9 0 11.8.2 17.6.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6a21.5 21.5 0 0 1 9.1 17.6c0 2.4-.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-.1 17.8-.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8zm-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1zm-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1zm586.8 415.6c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7a9 9 0 0 0 6.4-2.6 9 9 0 0 0 2.6-6.4c0-2.2-.9-4.4-1.4-6.6-.3-1.2-7.6-28.3-12.2-45.3-.5-1.9-.9-3.8-.9-5.7.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9zm179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9a36.08 36.08 0 0 1-36 35.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 105 105" fill="currentColor">
<circle cx="12.5" cy="12.5" r="12.5">
<animate attributeName="fill-opacity" begin="0s" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="12.5" cy="52.5" r="12.5" fill-opacity=".5">
<animate attributeName="fill-opacity" begin="100ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="52.5" cy="12.5" r="12.5">
<animate attributeName="fill-opacity" begin="300ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="52.5" cy="52.5" r="12.5">
<animate attributeName="fill-opacity" begin="600ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="92.5" cy="12.5" r="12.5">
<animate attributeName="fill-opacity" begin="800ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="92.5" cy="52.5" r="12.5">
<animate attributeName="fill-opacity" begin="400ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="12.5" cy="92.5" r="12.5">
<animate attributeName="fill-opacity" begin="700ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="52.5" cy="92.5" r="12.5">
<animate attributeName="fill-opacity" begin="500ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="92.5" cy="92.5" r="12.5">
<animate attributeName="fill-opacity" begin="200ms" dur="1s" values="1;.2;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { ICommand, IMarkdownEditor, ToolBarProps } from '@uiw/react-markdown-editor';
import toast from 'react-hot-toast';
import styled from 'styled-components';
const Button = styled.button`
white-space: nowrap;
width: initial !important;
display: flex;
align-items: center;
padding: 0 0.4rem !important;
`;
const CopyView: React.FC<{ command: ICommand; editorProps: IMarkdownEditor & ToolBarProps }> = (props) => {
const { editorProps } = props;
const handleClick = () => {
const dom = editorProps.preview.current;
dom?.focus();
window.getSelection()?.removeAllRanges();
let range = document.createRange();
range.setStartBefore(dom?.firstChild!);
range.setEndAfter(dom?.lastChild!);
window.getSelection()?.addRange(range);
document.execCommand(`copy`);
window.getSelection()?.removeAllRanges();
toast.success(<div></div>);
};
return (
<Button type="button" onClick={handleClick}>
{props.command.icon}
</Button>
);
};
export const copy: ICommand = {
name: 'copy',
keyCommand: 'copy',
button: (command, props, opts) => <CopyView command={command} editorProps={{ ...props, ...opts }} />,
icon: (
<svg fill="currentColor" viewBox="0 0 24 24" height="16" width="16">
<path d="M20 2H10a2 2 0 0 0-2 2v2h8a2 2 0 0 1 2 2v8h2a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" />
<path d="M4 22h10c1.103 0 2-.897 2-2V10c0-1.103-.897-2-2-2H4c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2zm2-10h6v2H6v-2zm0 4h6v2H6v-2z" />
</svg>
),
};

View File

@@ -0,0 +1,27 @@
import { NavLink } from 'react-router-dom';
import { ICommand } from '@uiw/react-markdown-editor';
import styled from 'styled-components';
const Link = styled(NavLink)`
font-size: 0.8rem;
line-height: 0.8rem;
text-decoration: none;
padding: 0.18rem 0.3rem;
&:hover {
color: var(--color-accent-fg);
background-color: var(--color-neutral-muted);
border-radius: 0.2rem;
}
`;
export const cssCommand: ICommand = {
name: 'previewTtheme',
keyCommand: 'previewTtheme',
button: () => <Link to="/editor/theme"></Link>,
};
export const previousCommand: ICommand = {
name: 'previous',
keyCommand: 'previous',
button: () => <Link to="/"></Link>,
};

View File

@@ -0,0 +1,73 @@
import React, { useContext } from 'react';
import { ICommand, IMarkdownEditor, ToolBarProps } from '@uiw/react-markdown-editor';
import styled from 'styled-components';
import { Context, previewThemes, PreviewThemeValue, themes as editorThemes, ThemeValue } from '../store/context';
const Select = styled.select`
max-width: 4rem;
padding: 0;
appearance: none;
background-color: var(--color-border-muted);
border: none;
padding: 0 0.2rem 0 0.2rem;
margin: 0;
font-family: inherit;
font-size: 0.8rem;
outline: none;
height: 1.15rem;
cursor: inherit;
line-height: inherit;
border-radius: 0.2rem;
&::-ms-expand {
display: none;
}
`;
const ThemeView: React.FC<{ command: ICommand; editorProps: IMarkdownEditor & ToolBarProps }> = (props) => {
const { theme, setTheme } = useContext(Context);
const handleChange = (ev: React.ChangeEvent<HTMLSelectElement>) => setTheme(ev.target.value as any);
return (
<Select value={theme} onChange={handleChange}>
{(Object.keys(editorThemes) as Array<ThemeValue>).map((name, key) => {
return (
<option key={key} value={name}>
{editorThemes[name].label}
</option>
);
})}
</Select>
);
};
export const theme: ICommand = {
name: 'theme',
keyCommand: 'theme',
button: (command, props, opts) => <ThemeView command={command} editorProps={{ ...props, ...opts }} />,
};
const ThemePreviewView: React.FC<{}> = () => {
const { setCss, previewTheme, setPreviewTheme } = useContext(Context);
const handleChange = (ev: React.ChangeEvent<HTMLSelectElement>) => {
const value = ev.target.value as PreviewThemeValue;
console.log('vvvv');
setPreviewTheme(value);
setCss(previewThemes[value].value);
};
return (
<Select value={previewTheme} onChange={handleChange}>
{(Object.keys(previewThemes) as Array<PreviewThemeValue>).map((name, key) => {
return (
<option value={name} key={key}>
{previewThemes[name].label}
</option>
);
})}
</Select>
);
};
export const previeTheme: ICommand = {
name: 'previewTtheme',
keyCommand: 'previewTtheme',
button: () => <ThemePreviewView />,
};

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { ICommand } from '@uiw/react-markdown-editor';
import styled from 'styled-components';
import { ReactComponent as ColorIcon } from '../assets/color.svg';
const Title = styled.div`
font-size: 0.9rem;
font-weight: bold;
display: flex;
align-items: center;
line-height: 1;
padding-right: 0.5rem;
padding-left: 0.2rem;
`;
export const themeTitle: ICommand = {
name: 'themeTitle',
keyCommand: 'themeTitle',
button: () => (
<Title>
<ColorIcon width={16} height={16} />
</Title>
),
};

View File

@@ -0,0 +1,107 @@
import styled from 'styled-components';
import { Outlet, NavLink } from 'react-router-dom';
import '@wcj/dark-mode';
import { useContext } from 'react';
import { ReactComponent as LogoIcon } from '../assets/logo.svg';
import { ReactComponent as GithubIcon } from '../assets/github.svg';
import { ReactComponent as Loading } from '../assets/tail-spin.svg';
import { Context } from '../store/context';
const Warpper = styled.div``;
const Header = styled.header`
-webkit-app-region: drag;
display: flex;
flex-direction: row;
justify-content: space-between;
border-bottom: 1px solid var(--color-border-muted);
padding: 0.5rem 0.6rem 0.5rem 1rem;
`;
const Article = styled.article`
display: flex;
flex-direction: row;
align-items: center;
gap: 0.6rem;
`;
const Logo = styled(LogoIcon)`
max-width: 3.6rem;
`;
const Title = styled.h1`
font-size: 1rem;
margin: 0;
display: flex;
align-items: center;
user-select: none;
sup {
color: var(--color-fg-subtle);
margin-left: 0.4rem;
background-color: var(--color-border-muted);
border-radius: 0.1rem;
padding: 0 0.2rem 0 0.1rem;
font-weight: normal;
font-size: 0.7rem;
letter-spacing: -0.1rem;
}
`;
const Section = styled.section`
display: flex;
align-items: center;
gap: 0.5rem;
dark-mode {
font-size: 1.05rem;
display: block;
line-height: 12px;
margin-left: 0.6rem;
}
a svg {
display: block;
}
a {
text-decoration: none;
color: var(--color-theme-text);
padding: 0.1rem 0.3rem;
box-shadow: inset 0 0 0 var(--color-accent-fg);
transition: all 0.3s;
font-size: 0.9rem;
&.active {
box-shadow: inset 0 -0.3rem 0 var(--color-accent-fg);
}
&:hover:not(.active):not(:last-child) {
box-shadow: inset 0 -1.5rem 0 var(--color-accent-fg);
color: #fff;
border-radius: 0.2rem;
}
}
`;
export function Layout() {
const { isLoading } = useContext(Context);
return (
<Warpper className="wmde-markdown-color">
<Header>
<Article>
<Logo width={28} height={28} />
<Title>
<sup> v{VERSION} </sup>
</Title>
{isLoading && <Loading />}
</Article>
<Section>
<NavLink to="/"></NavLink>
<NavLink to="/editor/theme"></NavLink>
<NavLink to="/doc"></NavLink>
<dark-mode permanent dark="Dark" light="Light" />
<a href="https://github.com/jaywcjlove/wxmp" target="__blank">
<GithubIcon width={23} height={23} />
</a>
</Section>
</Header>
<Outlet />
</Warpper>
);
}

72
website/src/index.tsx Normal file
View File

@@ -0,0 +1,72 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import BackToUp from '@uiw/react-back-to-top';
import { Toaster } from 'react-hot-toast';
import { createGlobalStyle } from 'styled-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import { Provider } from './store/Provider';
export const GlobalStyle = createGlobalStyle`
[data-color-mode*='dark'], [data-color-mode*='dark'] body {
--color-fg-default: #c9d1d9;
--color-fg-muted: #8b949e;
--color-fg-subtle: #484f58;
--color-canvas-default: #0d1117;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-neutral-muted: rgba(110,118,129,0.4);
--color-accent-fg: #58a6ff;
--color-accent-emphasis: #1f6feb;
--color-attention-subtle: rgba(187,128,9,0.15);
--color-danger-fg: #f85149;
}
[data-color-mode*='light'], [data-color-mode*='light'] body {
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-canvas-default: #ffffff;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210,18%,87%,1);
--color-neutral-muted: rgba(175,184,193,0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
`;
const queryClient = new QueryClient();
const style: React.CSSProperties = { zIndex: 999 };
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<HashRouter>
<Toaster />
<BackToUp style={style}>Top</BackToUp>
<GlobalStyle />
<QueryClientProvider client={queryClient}>
<Provider>
<App />
</Provider>
</QueryClientProvider>
</HashRouter>,
);

View File

@@ -0,0 +1,17 @@
import MarkdownEditor from '@uiw/react-markdown-editor';
import styled from 'styled-components';
import { markdownString } from '../../store/context';
const Warpper = styled.div`
max-width: 59rem;
margin: 0 auto 0 auto;
padding: 0 1rem 3rem 1rem;
`;
export const DocsPage = () => {
return (
<Warpper>
<MarkdownEditor.Markdown source={markdownString} />
</Warpper>
);
};

View File

@@ -0,0 +1,19 @@
import { MarkdownPreviewProps } from '@uiw/react-markdown-preview';
import styled from 'styled-components';
import { useContext } from 'react';
import { Context } from '../../store/context';
import { markdownToHTML } from '../../utils/markdownToHTML';
const Warpper = styled.div`
width: 375px;
padding: 20px;
box-shadow: 0 0 60px rgb(0 0 0 / 10%);
min-height: 100%;
`;
export const Preview = (props: MarkdownPreviewProps) => {
const { css } = useContext(Context);
const html = markdownToHTML(props.source || '', css);
return <Warpper contentEditable spellCheck={false} dangerouslySetInnerHTML={{ __html: html }} />;
};

View File

@@ -0,0 +1,36 @@
import MarkdownEditor, { getCommands } from '@uiw/react-markdown-editor';
import { useContext } from 'react';
import { EditorView } from '@codemirror/view';
import styled from 'styled-components';
import { Preview } from './Preview';
import { copy } from '../../commands/copy';
import { theme as themeCommand, previeTheme } from '../../commands/theme';
import { cssCommand } from '../../commands/css';
import { Context, themes } from '../../store/context';
export const Warpper = styled.div`
height: calc(100vh - 2.9rem);
`;
export const HomePage = () => {
const commands = [...getCommands(), themeCommand];
const { theme, markdown, isLoading, setMarkdown } = useContext(Context);
const themeValue = themes[theme].value;
const handleChange = (value: string) => setMarkdown(value);
return (
<Warpper>
<MarkdownEditor
value={markdown}
toolbars={commands}
theme={themeValue}
readOnly={isLoading}
toolbarsMode={[cssCommand, previeTheme, copy, 'fullscreen', 'preview']}
extensions={[EditorView.lineWrapping]}
renderPreview={Preview}
onChange={handleChange}
visible={true}
height="calc(100vh - 4.70rem)"
/>
</Warpper>
);
};

View File

@@ -0,0 +1,19 @@
import { MarkdownPreviewProps } from '@uiw/react-markdown-preview';
import styled from 'styled-components';
import { useContext } from 'react';
import { Context } from '../../store/context';
import { markdownToHTML } from '../../utils/markdownToHTML';
const Warpper = styled.div`
width: 375px;
padding: 20px;
box-shadow: 0 0 60px rgb(0 0 0 / 10%);
min-height: 100%;
`;
export const Preview = (props: MarkdownPreviewProps) => {
const { css, markdown } = useContext(Context);
const html = markdownToHTML(markdown, css);
return <Warpper contentEditable spellCheck={false} dangerouslySetInnerHTML={{ __html: html }} />;
};

View File

@@ -0,0 +1,35 @@
import MarkdownEditor, { IMarkdownEditor } from '@uiw/react-markdown-editor';
import { useContext } from 'react';
import { EditorView } from '@codemirror/view';
import { css as cssLang } from '@codemirror/lang-css';
import { Preview } from './Preview';
import { copy } from '../../commands/copy';
import { previousCommand } from '../../commands/css';
import { themeTitle } from '../../commands/title';
import { theme as themeCommand, previeTheme } from '../../commands/theme';
import { Context, themes } from '../../store/context';
import { Warpper } from '../home';
export const EditorPage = () => {
const commands = [themeTitle, themeCommand, previousCommand];
const toolbarsMode: IMarkdownEditor['toolbarsMode'] = [previeTheme, copy, 'fullscreen', 'preview'];
const { theme, css, setCss, isLoading } = useContext(Context);
const value = themes[theme].value;
const handleChange = (value: string) => setCss(value);
return (
<Warpper>
<MarkdownEditor
value={css}
theme={value}
readOnly={isLoading}
toolbars={commands}
toolbarsMode={toolbarsMode}
reExtensions={[EditorView.lineWrapping, cssLang()]}
renderPreview={Preview}
onChange={handleChange}
visible={true}
height="calc(100vh - 4.92rem)"
/>
</Warpper>
);
};

19
website/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
/// <reference types="react-scripts" />
declare module '*.less' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare var VERSION: string;
declare module '*.md' {
import { CodeBlockData } from 'markdown-react-code-preview-loader';
const src: CodeBlockData;
export default src;
}
declare module '*.md.css' {
const src: string;
export default src;
}

View File

@@ -0,0 +1,47 @@
import React, { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { PreviewThemeValue, previewThemes, ThemeValue, Context, markdownString } from './context';
import { useMdSource } from './getMdSource';
export const Provider: React.FC<React.PropsWithChildren> = ({ children }) => {
const [searchParams, setSearchParams] = useSearchParams();
const paramPreviewTheme = searchParams.get('theme') as PreviewThemeValue;
const initPreviewTheme = paramPreviewTheme || 'underscore';
const mdurl = searchParams.get('md');
const [markdown, setMarkdown] = React.useState<string>(mdurl ? '' : markdownString);
const [css, setCss] = React.useState<string>(previewThemes[initPreviewTheme].value);
const [previewTheme, setPreviewTheme] = React.useState<PreviewThemeValue>(initPreviewTheme);
const [theme, setTheme] = React.useState<ThemeValue>('default');
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const { data: mddata, isLoading: loading } = useMdSource(mdurl);
useEffect(() => {
if (paramPreviewTheme !== previewTheme) {
searchParams.set('theme', previewTheme);
setSearchParams(searchParams);
}
}, [paramPreviewTheme, previewTheme, searchParams, setSearchParams]);
useEffect(() => {
if (mdurl) {
setMarkdown(mddata || '');
}
}, [mddata, mdurl]);
useEffect(() => setIsLoading(loading), [loading]);
return (
<Context.Provider
value={{
isLoading,
setIsLoading,
markdown,
setMarkdown,
css,
setCss,
previewTheme,
setPreviewTheme,
theme,
setTheme,
}}
>
{children}
</Context.Provider>
);
};

View File

@@ -0,0 +1,142 @@
import React from 'react';
import { defaultTheme } from '@uiw/react-markdown-editor';
import { abcdef } from '@uiw/codemirror-theme-abcdef';
import { androidstudio } from '@uiw/codemirror-theme-androidstudio';
import { atomone } from '@uiw/codemirror-theme-atomone';
import { bbedit } from '@uiw/codemirror-theme-bbedit';
import { bespin } from '@uiw/codemirror-theme-bespin';
import { darcula } from '@uiw/codemirror-theme-darcula';
import { dracula } from '@uiw/codemirror-theme-dracula';
import { duotoneLight, duotoneDark } from '@uiw/codemirror-theme-duotone';
import { eclipse } from '@uiw/codemirror-theme-eclipse';
import { githubLight, githubDark } from '@uiw/codemirror-theme-github';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { sublime } from '@uiw/codemirror-theme-sublime';
import { xcodeLight, xcodeDark } from '@uiw/codemirror-theme-xcode';
import defStyle from '../themes/default.md.css';
import simpleStyle from '../themes/simple.md.css';
import underscoreStyle from '../themes/underscore.md.css';
import baseStyle from '../themes/base.md.css';
import data from '../../../README.md';
export const markdownString = data.source;
export const themes = {
default: {
label: '默认主题',
value: defaultTheme,
},
abcdef: {
label: 'Abcdef Theme',
value: abcdef,
},
androidstudio: {
label: 'Android Studio Theme',
value: androidstudio,
},
atomone: {
label: 'Atomone Theme',
value: atomone,
},
bbedit: {
label: 'Bbedit Theme',
value: bbedit,
},
bespin: {
label: 'Bespin Theme',
value: bespin,
},
darcula: {
label: 'Darcula Theme',
value: darcula,
},
dracula: {
label: 'Dracula Theme',
value: dracula,
},
duotoneLight: {
label: 'Duotone Light Theme',
value: duotoneLight,
},
duotoneDark: {
label: 'Duotone Dark Theme',
value: duotoneDark,
},
eclipse: {
label: 'Eclipse Theme',
value: eclipse,
},
githubLight: {
label: 'Github Light Theme',
value: githubLight,
},
githubDark: {
label: 'Github Dark Theme',
value: githubDark,
},
okaidia: {
label: 'Okaidia Theme',
value: okaidia,
},
sublime: {
label: 'Sublime Theme',
value: sublime,
},
xcodeLight: {
label: 'Xcode Light Theme',
value: xcodeLight,
},
xcodeDark: {
label: 'Xcode Dark Theme',
value: xcodeDark,
},
};
export const previewThemes = {
default: {
label: '翡翠绿',
value: defStyle,
},
simple: {
label: '简洁蓝',
value: simpleStyle,
},
underscore: {
label: '下划线黄',
value: underscoreStyle,
},
base: {
label: '简洁',
value: baseStyle,
},
};
export type ThemeValue = keyof typeof themes;
export type PreviewThemeValue = keyof typeof previewThemes;
export interface CreateContext {
isLoading: boolean;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
markdown: string;
setMarkdown: React.Dispatch<React.SetStateAction<string>>;
css: string;
setCss: React.Dispatch<React.SetStateAction<string>>;
previewTheme: PreviewThemeValue;
setPreviewTheme: React.Dispatch<React.SetStateAction<PreviewThemeValue>>;
theme: ThemeValue;
setTheme: React.Dispatch<React.SetStateAction<ThemeValue>>;
}
export const Context = React.createContext<CreateContext>({
isLoading: true,
setIsLoading: () => {},
markdown: data.source,
setMarkdown: () => {},
css: previewThemes['underscore'].value,
setCss: () => {},
previewTheme: 'underscore',
setPreviewTheme: () => {},
theme: 'default',
setTheme: () => {},
});

View File

@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import styled from 'styled-components';
const Warpper = styled.div`
font-size: 0.8rem;
`;
export const useMdSource = (url: string | null) => {
return useQuery(['database-list', url], () => {
if (!url) return Promise.resolve('');
return fetch(url)
.then((response) => response.text())
.then((data) => {
return data;
})
.catch((err) => {
toast.error(
<Warpper>
<a href={url}>URL</a>
</Warpper>,
);
});
});
};

View File

@@ -0,0 +1,183 @@
a {
color: inherit;
text-decoration: none;
}
h1 {
color: inherit;
font-size: 1.5rem;
font-weight: bold;
}
h2 {
color: inherit;
margin: 2.5rem 0 1rem 0;
font-size: 1.3em;
font-weight: bold;
}
h3 {
color: inherit;
margin: 1em 0 1em 0;
font-weight: bold;
font-size: 1em;
}
h4 {
color: inherit;
margin: 0.6em 0 0.6em 0;
font-weight: bold;
font-size: 0.9em;
}
p {
color: initial;
font-size: 0.85em;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
font-size: 0.85em;
}
blockquote {
font-style: normal;
border-left: none;
margin: 1em 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-size: 0.85em;
font-weight: 400;
letter-spacing: normal;
word-spacing: 0px;
border-radius: 5px;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: initial;
background: rgba(27, 31, 35, 0.05);
padding: 0.1em 0.3em;
border-radius: 0.3em;
font-weight: bold;
font-size: 1em;
top: -0.1em;
position: relative;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3rem 0 0.6rem 0;
padding-left: 0.2rem;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

View File

@@ -0,0 +1,223 @@
a {
color: #576b95;
text-decoration: none;
font-size: 0.85em;
}
h1 {
display: table;
text-align: center;
color: #3f3f3f;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 2em auto 1em;
padding: 0 1em;
border-bottom: 2px solid #009874;
margin-top: 0;
}
h2 {
display: table;
text-align: center;
color: #fff;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.3em;
font-weight: bold;
margin: 4em auto 2em;
padding: 0 0.3em;
border-radius: 0.3em;
background: #009874;
}
p {
font-size: 0.85em;
}
h3 {
text-align: left;
color: #3f3f3f;
line-height: 1.2;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.1em;
font-weight: bold;
margin: 2em 8px 0.75em 0;
padding-left: 8px;
border-left: 3px solid #009874;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
line-height: 1.5em;
color: rgb(30 41 59);
font-size: 0.85em;
}
blockquote {
text-align: left;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 0.85em;
font-style: normal;
border-left: none;
padding: 0.1rem 1rem;
border-radius: 4px;
background: rgba(27, 31, 35, 0.05);
margin: 1rem 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-indent: 0px;
text-transform: none;
widows: 2;
word-spacing: 0px;
text-decoration-style: initial;
text-decoration-color: initial;
text-align: left;
line-height: 1.5;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
border-radius: 0.3em;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
line-height: 1.75;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: initial;
color: #009874;
background: rgba(27, 31, 35, 0.05);
padding: 0.1em 0.3em;
border-radius: 0.3em;
font-weight: bold;
font-size: 1em;
top: -0.1em;
position: relative;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3em 0 0.6em 0;
padding-left: 0.2em;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

View File

@@ -0,0 +1,223 @@
a {
color: #576b95;
text-decoration: none;
font-size: 0.85em;
}
h1 {
display: table;
text-align: center;
color: #3f3f3f;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.2em;
font-weight: bold;
margin: 2em auto 1em;
padding: 0 1em;
border-bottom: 2px solid #0f4c81;
margin-top: 0;
}
h2 {
display: table;
text-align: center;
color: #fff;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.2em;
font-weight: bold;
margin: 4em auto 2em;
padding: 0 0.3em;
border-radius: 0.3rem;
background: #0f4c81;
}
p {
font-size: 0.85em;
}
h3 {
text-align: left;
color: #3f3f3f;
line-height: 1.2;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.1em;
font-weight: bold;
margin: 2em 8px 0.75em 0;
padding-left: 8px;
border-left: 3px solid #0f4c81;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
line-height: 1.5em;
color: rgb(30 41 59);
font-size: 0.85em;
}
blockquote {
text-align: left;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 0.85em;
font-style: normal;
border-left: none;
padding: 0.1rem 1rem;
border-radius: 4px;
background: rgba(27, 31, 35, 0.05);
margin: 1rem 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-indent: 0px;
text-transform: none;
widows: 2;
word-spacing: 0px;
text-decoration-style: initial;
text-decoration-color: initial;
text-align: left;
line-height: 1.5;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
border-radius: 0.3em;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
line-height: 1.75;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: initial;
color: #0f4c81;
background: rgba(27, 31, 35, 0.05);
padding: 0.1em 0.3em;
border-radius: 0.3em;
font-weight: bold;
font-size: 1em;
top: -0.1em;
position: relative;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3em 0 0.6em 0;
padding-left: 0.2em;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

View File

@@ -0,0 +1,220 @@
a {
color: #576b95;
text-decoration: none;
font-size: 0.85em;
}
h1 {
display: table;
text-align: center;
color: #3f3f3f;
line-height: 1.15;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.3em;
font-weight: bold;
margin: 2em auto 1em;
padding: 0 1em 0.3em 1em;
margin-top: 0;
box-shadow: inset 0 -0.9rem 0 0 #ffb11b;
}
h2 {
display: table;
line-height: 1.35;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.2em;
font-weight: bold;
padding: 0 0.3em;
margin: 2em 0 1em 0;
box-shadow: inset 0 -0.7rem 0 0 #ffb11b;
}
p {
font-size: 0.85em;
}
h3 {
text-align: left;
color: #3f3f3f;
line-height: 1.2;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.1em;
font-weight: bold;
margin: 2em 8px 0.75em 0;
padding-left: 8px;
border-left: 5px solid #ffb11b;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
line-height: 1.5em;
color: rgb(30 41 59);
font-size: 0.85em;
}
blockquote {
text-align: left;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 0.85em;
font-style: normal;
border-left: none;
padding: 0.1rem 1rem;
border-radius: 4px;
background: rgba(27, 31, 35, 0.05);
margin: 1rem 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-indent: 0px;
text-transform: none;
widows: 2;
word-spacing: 0px;
text-decoration-style: initial;
text-decoration-color: initial;
text-align: left;
line-height: 1.5;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
border-radius: 0.3em;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
line-height: 1.75;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: initial;
color: #ffb11b;
background: rgba(27, 31, 35, 0.05);
padding: 0.1em 0.3em;
border-radius: 0.3em;
font-weight: bold;
font-size: 1em;
top: -0.1em;
position: relative;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3em 0 0.6em 0;
padding-left: 0.2em;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

113
website/src/utils/css.ts Normal file
View File

@@ -0,0 +1,113 @@
import { RootContent, Element, Text, Root } from 'hast';
export const getBlock = (data: any, str: string = '') => {
if (data && data.data && data.data.type === 'Declaration') {
str = `${data.data.property}: ${data.data.value.value}${data.data.important ? ' !important' : ''};`;
if (data.next) {
str += getBlock(data.next);
}
}
return str;
};
export const cssdata = (list: any, result: Record<string, string> = {}) => {
if (list.data && list.data.type === 'Rule') {
result[list.data.prelude.value] = getBlock(list.data.block.children.head);
if (list.next) {
result = cssdata(list.next, { ...result });
}
}
return result;
};
export const spaceEscape = (node: RootContent) => {
if (node.type === 'element' && node.children) {
const className = node.properties?.className as string[];
if (className) {
if (!node.properties) {
node.properties = {};
}
node.properties.className = className.filter((str: string) => !/(token|control-flow)/.test(str));
}
node.children.map((elm) => {
if (elm.type === 'element' && elm.children) {
spaceEscape(elm);
}
if (elm.type === 'text') {
elm.value = elm.value.replace(/\s/g, '\u00A0');
}
return elm;
});
}
};
type ChildContent = Element | Text;
const getNodeText = (node: ChildContent[]) => {
let str = '';
node.forEach((item) => {
if (item.type === 'text') str += item.value;
else if (item.type === 'element') {
str += getNodeText(item.children as ChildContent[]);
}
});
return str.replace(/↩/, '');
};
export const footnotes = (node: Element) => {
node.children.map((item) => {
if (item.type === 'element' && item.tagName === 'h2') {
if (!item.properties) item.properties = {};
item.properties.className = ['footnotes-title'];
item.children = [
{
type: 'text',
value: '参考',
},
];
}
if (item.type === 'element' && item.tagName === 'ol') {
item.children.map((li) => {
if (li.type === 'element' && li.tagName === 'li') {
if (!li.properties) li.properties = {};
li.properties.className = ['footnotes-list'];
li.children = [
{
type: 'text',
value: getNodeText(li.children as ChildContent[]),
},
];
}
return li;
});
}
return item;
});
};
export const footnotesLabel = (node: Element) => {
const label = getNodeText(node.children as ChildContent[]);
node.children = [
{
type: 'text',
value: `[${label}]`,
},
];
};
export const imagesStyle = (node: Element, parent: Root | Element | null) => {
if (
parent?.type === 'element' &&
/(p|a)/.test(parent.tagName) &&
node?.type === 'element' &&
node.tagName === 'img'
) {
if (parent.tagName === 'p') {
parent.tagName = 'figure';
}
if (!parent.properties) parent.properties = {};
parent.properties.className = ['image-warpper'];
if (!node.properties) node.properties = {};
node.properties.className = ['image'];
}
};

View File

@@ -0,0 +1,103 @@
import { VFile } from 'vfile';
import { unified } from 'unified';
import * as csstree from 'css-tree';
import { Element } from 'hast';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypePrism from 'rehype-prism-plus';
import rehypeRaw from 'rehype-raw';
import rehypeAttrs from 'rehype-attr';
import rehypeIgnore from 'rehype-ignore';
import rehypeRewrite from 'rehype-rewrite';
import stringify from 'rehype-stringify';
import { cssdata, spaceEscape, footnotes, footnotesLabel, imagesStyle } from './css';
export type MarkdownToHTMLOptions = {};
export function markdownToHTML(md: string, css: string, options: MarkdownToHTMLOptions = {}) {
const ast = csstree.parse(css, {
parseAtrulePrelude: false,
parseRulePrelude: false,
parseValue: false,
parseCustomProperty: false,
positions: false,
});
// @ts-ignore
const data = cssdata(ast.children.head);
const processor = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypePrism)
.use(rehypeRaw)
.use(rehypeIgnore, {})
.use(rehypeAttrs, { properties: 'attr' })
.use(rehypeRewrite, {
rewrite: (node, index, parent) => {
// @ts-ignore
if (
node?.type === 'element' &&
node?.tagName === 'code' &&
parent?.type === 'element' &&
parent?.tagName === 'pre'
) {
spaceEscape(node);
}
if (
node?.type === 'element' &&
node.tagName === 'section' &&
(node?.properties?.className as string[]).includes('footnotes')
) {
footnotes(node);
}
if (node?.type === 'element' && node.tagName === 'sup') {
footnotesLabel(node);
}
if (node?.type === 'element' && node.tagName === 'img') {
imagesStyle(node, parent);
}
// Code Spans style
if (
node?.type === 'element' &&
node?.tagName === 'code' &&
parent?.type === 'element' &&
parent?.tagName !== 'pre'
) {
if (!node.properties) node.properties = {};
node.properties!.className = ['code-spans'];
}
// List TODO style
if (parent?.type === 'element' && node?.type === 'element' && node?.tagName === 'input') {
if (parent && parent.type === 'element') {
parent.children = parent?.children.filter((elm) => (elm as Element).tagName !== 'input');
}
return;
}
// Support *.md.css
if (node?.type === 'element') {
if (!node.properties) {
node.properties = {};
}
const className = node.properties?.className as string[];
let style = '';
if (className) {
className.forEach((name) => {
if (data[`.${name}`]) {
style = data[`.${name}`];
}
});
}
if (!style) style = data[node.tagName];
if (style) {
node.properties.style = style + (node.properties.style || '');
}
}
},
})
.use(stringify);
const file = new VFile();
file.value = md;
const hastNode = processor.runSync(processor.parse(file), file);
return String(processor.stringify(hastNode, file));
}