Skip to content

Commit ab0a952

Browse files
高亮缺少的空格
1 parent bce7987 commit ab0a952

File tree

11 files changed

+229
-154
lines changed

11 files changed

+229
-154
lines changed
Lines changed: 5 additions & 0 deletions
Loading

imports.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ declare module '*.webp' {
55
const path: string;
66
export default path;
77
}
8+
declare module '*.svg' {
9+
const path: string;
10+
export default path;
11+
}

src/app/features/article/articleViewer/getProcessor.ts

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import rehypeReact from 'rehype-react';
33
import rehypeHighlight from 'rehype-highlight';
44
import { visit, SKIP } from 'unist-util-visit';
55
import luoguMarkdownProcessor from 'lg-markdown-processor';
6+
import { Element, ElementContent, Text } from 'hast';
7+
8+
function checkClass(element: Element, target: string) {
9+
let className = element.properties['className'];
10+
if (className === undefined || className === null) return false;
11+
if (typeof className === 'number' || typeof className === 'boolean')
12+
element.properties['className'] = className = className.toString();
13+
if (typeof className === 'string')
14+
element.properties['className'] = className = className.split(' ');
15+
return className.includes(target);
16+
}
617

718
const rehypeReactConfig: import('hast-util-to-jsx-runtime').Options = {
819
Fragment: 'article',
@@ -14,40 +25,84 @@ const rehypeReactConfig: import('hast-util-to-jsx-runtime').Options = {
1425

1526
export default function getProcessor() {
1627
return luoguMarkdownProcessor({
17-
rehypePlugins: [hastHeilightSpace, rehypeHighlight]
28+
rehypePlugins: [hastHighlightSpace, rehypeHighlight]
1829
})
1930
.use(rehypeReact, rehypeReactConfig)
2031
.freeze();
2132
}
22-
function hastHeilightSpace() {
23-
const heilightSpaceElement: import('hast').Element = {
33+
34+
namespace CharTest {
35+
export const ChineseChar = /\p{sc=Han}/u;
36+
export const Punctuation = /\p{P}/u;
37+
export const EnglishChar = /[a-zA-Z]/;
38+
}
39+
namespace HighlightElement {
40+
export const Space: Element = {
2441
type: 'element',
2542
tagName: 'span',
26-
properties: { className: ['articleViewer-heilightSpace'] },
43+
properties: { className: ['articleHighlight', 'Space'] },
2744
children: [{ type: 'text', value: ' ' }]
2845
};
46+
export const NeedSpaceBetweenEnglishCharAndChineseChar: Element = {
47+
type: 'element',
48+
tagName: 'span',
49+
properties: {
50+
className: [
51+
'articleHighlight',
52+
'NeedSpaceBetweenPunctuationAndChineseChar'
53+
]
54+
},
55+
children: [
56+
{
57+
type: 'element',
58+
tagName: 'div',
59+
properties: {},
60+
children: []
61+
}
62+
]
63+
};
64+
}
65+
function hastHighlightSpace() {
2966
return (tree: import('hast').Root) =>
3067
visit(tree, 'element', element => {
31-
if (element === heilightSpaceElement) return SKIP;
3268
if (element.tagName === 'code') return SKIP;
33-
let className = element.properties['className'];
34-
if (typeof className === 'string') className = [className];
35-
if (
36-
Array.isArray(className) &&
37-
(className.includes('katex') || className.includes('katex-display'))
38-
)
69+
if (checkClass(element, 'katex') || checkClass(element, 'katex-display'))
3970
return SKIP;
40-
const newChildren = new Array<import('hast').ElementContent>();
41-
for (let i of element.children) {
42-
if (i.type !== 'text') {
43-
newChildren.push(i);
44-
continue;
45-
}
46-
i.value.split(' ').forEach((s, i) => {
47-
if (i) newChildren.push(heilightSpaceElement);
48-
newChildren.push({ type: 'text', value: s });
71+
if (checkClass(element, 'articleHighlight')) return SKIP;
72+
const tmp = element.children
73+
.flatMap<Exclude<ElementContent, Text> | string>(child =>
74+
child.type === 'text' ? Array.from(child.value) : child
75+
)
76+
.flatMap<Exclude<ElementContent, Text> | string>((child, idx, arr) => {
77+
if (typeof child !== 'string') return child;
78+
if (child === ' ') return HighlightElement.Space;
79+
const prevElement = idx !== 0 ? arr[idx - 1] : undefined;
80+
if (
81+
typeof prevElement === 'string' &&
82+
((CharTest.ChineseChar.test(prevElement) &&
83+
CharTest.EnglishChar.test(child)) ||
84+
(CharTest.EnglishChar.test(prevElement) &&
85+
CharTest.ChineseChar.test(child)))
86+
)
87+
return [
88+
HighlightElement.NeedSpaceBetweenEnglishCharAndChineseChar,
89+
child
90+
];
91+
return child;
4992
});
93+
element.children = [];
94+
const chars = new Array<string>();
95+
for (const child of tmp) {
96+
if (typeof child === 'string') chars.push(child);
97+
else {
98+
if (chars.length > 0) {
99+
element.children.push({ type: 'text', value: chars.join('') });
100+
chars.length = 0;
101+
}
102+
element.children.push(child);
103+
}
50104
}
51-
element.children = newChildren;
105+
if (chars.length > 0)
106+
element.children.push({ type: 'text', value: chars.join('') });
52107
});
53108
}
Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1-
.articleViewer-heilightSpace {
2-
background-color: #9f9;
1+
.articleHighlight.Space {
2+
background-color: #00ff2d;
3+
}
4+
.articleHighlight.NeedSpaceBetweenPunctuationAndChineseChar {
5+
position: relative;
6+
display: inline-block;
7+
width: 2px;
8+
> :first-child {
9+
position: absolute;
10+
left: -2px;
11+
top: -18px;
12+
width: 6px;
13+
height: 24px;
14+
background-image: url(assets/needSpaceBetweenPunctuationAndChineseChar.svg);
15+
background-size: 100% 100%;
16+
}
317
}

src/app/style.css

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@ a:hover {
4242
font-weight: bold;
4343
text-align: center;
4444
text-decoration: none;
45-
}
46-
.username-badge {
47-
display: inline-block;
48-
padding: 0 0.5em;
49-
box-sizing: border-box;
50-
font-weight: 400;
51-
line-height: 1.5;
52-
border-width: 1px;
53-
border-style: solid;
54-
border-radius: 2px;
55-
color: white;
56-
font-size: 12px;
45+
gap: 4px;
46+
> .username-badge {
47+
display: inline-block;
48+
padding: 0 0.5em;
49+
box-sizing: border-box;
50+
font-weight: 400;
51+
line-height: 1.5;
52+
border-width: 1px;
53+
border-style: solid;
54+
border-radius: 2px;
55+
color: white;
56+
font-size: 12px;
57+
}
5758
}
5859
.errorDiv {
5960
height: 100%;

src/app/utils.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,16 @@ export function UserName({ children }: { children: UserSummary }) {
2121
color: UsernameColor[children.color as keyof typeof UsernameColor]
2222
}}
2323
>
24-
<>{children.name}</>
25-
{children.ccfLevel > 2 && (
26-
<span style={{ marginLeft: '4px' }}>
27-
<CcfLevelSvg level={children.ccfLevel} />
28-
</span>
29-
)}
24+
<span>{children.name}</span>
25+
{children.ccfLevel > 2 && <CcfLevelSvg level={children.ccfLevel} />}
3026
{children.badge && (
3127
<span
3228
className="username-badge"
3329
style={{
3430
backgroundColor:
3531
UsernameColor[children.color as keyof typeof UsernameColor],
3632
borderColor:
37-
UsernameColor[children.color as keyof typeof UsernameColor],
38-
marginLeft: '4px'
33+
UsernameColor[children.color as keyof typeof UsernameColor]
3934
}}
4035
>
4136
{children.badge}

test/articleViewer/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ function MarkdownTestComponent() {
5555
overflowY: 'auto',
5656
overflowX: 'hidden'
5757
}}
58+
className="articleViewer"
5859
>
5960
<ArticleViewer>{markdownText}</ArticleViewer>
6061
</div>

test/articleViewer/testcase/format.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
中文English 中文 English

test/articleViewer/webpack.config.js

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,29 @@
33
import path from 'path';
44
import HtmlWebpackPlugin from 'html-webpack-plugin';
55
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
6+
import Base from '../../webpack.config.base.js';
67

78
const dirname = import.meta.dirname;
89

9-
export default {
10-
mode: 'development',
11-
entry: path.join(dirname, 'index.tsx'),
12-
plugins: [
10+
/**
11+
* @param {Record<string,string|boolean>} env
12+
* @param {{ mode: 'production' | 'development' | 'none' | undefined; }} argv
13+
* @returns { Promise<import('webpack').Configuration> }
14+
*/
15+
export default async function (env, argv) {
16+
const base = await Base(env, argv);
17+
(base.plugins || (base.plugins = [])).push(
1318
new HtmlWebpackPlugin({
1419
filename: 'index.html'
15-
}),
16-
new MiniCssExtractPlugin()
17-
],
18-
devServer: {
20+
})
21+
);
22+
base.devServer = {
1923
port: 22552,
2024
static: {
2125
directory: path.join(dirname, 'testcase'),
2226
publicPath: '/'
2327
}
24-
},
25-
resolve: {
26-
extensions: ['.tsx', '.ts', '.js']
27-
},
28-
module: {
29-
rules: [
30-
{
31-
test: /\.tsx?$/,
32-
use: 'ts-loader'
33-
},
34-
{
35-
test: /\.css$/i,
36-
use: [
37-
MiniCssExtractPlugin.loader,
38-
'css-loader',
39-
{
40-
loader: 'postcss-loader',
41-
options: { postcssOptions: { plugins: [['postcss-preset-env']] } }
42-
}
43-
]
44-
}
45-
]
46-
},
47-
devtool: 'eval-source-map'
48-
};
28+
};
29+
base.entry = path.join(dirname, 'index.tsx');
30+
return base;
31+
}

webpack.config.base.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//@ts-check
2+
3+
import { resolve } from 'path';
4+
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
5+
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
6+
import terser from 'terser-webpack-plugin';
7+
import WebpackBarPlugin from 'webpackbar';
8+
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
9+
10+
/**
11+
* @param {Record<string,string|boolean>} env
12+
* @param {{ mode: 'production' | 'development' | 'none' | undefined; }} argv
13+
* @returns { Promise<import('webpack').Configuration> }
14+
*/
15+
export default async function (env, argv) {
16+
const mode = argv.mode || 'development';
17+
return {
18+
mode,
19+
devtool: argv.mode === 'development' && 'eval-source-map',
20+
module: {
21+
rules: [
22+
{
23+
test: /\.tsx?$/,
24+
use: 'ts-loader'
25+
},
26+
{
27+
test: /\.css$/i,
28+
use: [
29+
MiniCssExtractPlugin.loader,
30+
'css-loader',
31+
{
32+
loader: 'postcss-loader',
33+
options: { postcssOptions: { plugins: [['postcss-preset-env']] } }
34+
}
35+
]
36+
},
37+
{
38+
test: /.(woff2?|eot|ttf|otf)|(webp|svg)$/,
39+
type: 'asset/inline'
40+
}
41+
]
42+
},
43+
resolve: {
44+
extensions: ['.ts', '.tsx', '.js', '.json', '.css', '.webp', '.svg'],
45+
alias: {
46+
'luogu-api': resolve('luogu-api-docs', 'luogu-api'),
47+
assets: resolve('assets')
48+
}
49+
},
50+
optimization:
51+
mode == 'production'
52+
? {
53+
minimize: true,
54+
usedExports: true,
55+
innerGraph: true,
56+
minimizer: [
57+
new terser({
58+
parallel: true,
59+
terserOptions: {
60+
format: { comments: false }
61+
},
62+
extractComments: false
63+
}),
64+
new CssMinimizerPlugin()
65+
]
66+
}
67+
: {},
68+
plugins: [
69+
new WebpackBarPlugin(),
70+
...(env.analyze ? [new BundleAnalyzerPlugin()] : []),
71+
new MiniCssExtractPlugin()
72+
]
73+
};
74+
}

0 commit comments

Comments
 (0)