Merge branch 'preview-fixes'

This commit is contained in:
the1812 2024-03-09 20:59:23 +08:00
commit d2a3aa2873
29 changed files with 320 additions and 98 deletions

View File

@ -1,6 +1,54 @@
<!-- spell-checker: disable -->
# 更新日志
## v2.8.9-preview
`2024-03-09`
包含 [v2.8.9](https://github.com/the1812/Bilibili-Evolved/releases/tag/v2.8.9) 的所有更新内容.
✨新增
- `下载视频 - WASM 混流输出` 支持更持久的缓存方式. (PR #4667 by [WakelessSloth56](https://github.com/WakelessSloth56))
- `外置稍后再看``启用快速收藏` 增加了 `显示方式` 选项, 出现无法自动适配的排版问题时可以尝试手动调整此设置. (#4532)
- 新增插件 `动态过滤器 - 移除商品带货动态`. (#4425)
> 移除动态里的商品带货动态 (UP主的推荐 · 来自 XX), 装有 `动态过滤器` 时生效.
- 新增组件 `动态图片平铺展示`. (#4645)
> 将动态中左右切换式的图片改回传统的平铺展示. (在动态详情中可能稍有延迟)
- 更换了 `自定义顶栏` 的动态提醒数字来源. (#4427)
## v2.8.9
`2024-03-09`
✨新增
- `下载视频 - IDM 输出支持` 补充了保留文件名相关内容的描述. (PR #4664 by [LY](https://github.com/Young-Lord))
- 夜间模式更新对以下区域的支持:
- 新版评论区输入框 (#4605)
- 动态详情页
- 搜索页面 (#4354)
- `删除广告` 现在可以在新版首页中将删除后的空位移除. (#4610)
- `删除视频弹窗` 支持屏蔽 "请评价视频播放效果" 等小弹窗, 归属于投票类. (#4610)
🐛修复
- 修复 XML 弹幕下载遇到空弹幕时报错. (#4629)
- 修复播放前显示封面在中途暂停仍会出现. (#4491)
- 修复 `自定义顶栏` 的搜索结果描述溢出. (#4464)
- 修复自动关灯无效. (PR #4659 by [Waua](https://github.com/FoundTheWOUT), #4631)
- 修复 `网址参数清理` 导致搜索页出现 `https://search.bilibili.com/undefined`. (PR #4663 by [sunfkny](https://github.com/sunfkny), #4656)
- 修复 `禁止跳转动态详情` 导致动态中的查看图片按钮失效. (PR #4662 by [sunfkny](https://github.com/sunfkny))
- 修复 `展开动态内容` 对部分用户失效. (#4633)
- 修复 `高分辨率图片` 导致旧版首页 logo 比例出错. (#4480)
- 修复个人空间中搜索过动态后再返回导致常规动态里的相关功能失效. (#4458)
- 修复番剧区的 `扩展倍速` 失效. (#4571)
- 修复 `快速收起评论` 遮挡评论的表情等弹窗.
- 修复 `自定义顶栏` 导致网课分区内容错位. (#4610)
- 修复 `隐藏顶部横幅` 导致部分分区页面内容错位. (#4610)
☕开发者相关
- PlayerAgent 支持监听 `pause` 事件.
- `forEachFeedsCard` 现在不会在非动态页面执行.
## v2.8.8 / v2.8.8-preview
`2024-01-18`

View File

@ -6,7 +6,7 @@
## 爱发电
<a href="https://afdian.net/@the1812" target="_blank">
<a href="https://afdian.net/a/the1812" target="_blank">
<img alt="爱发电" src="../images/compressed/afdian.jpg" width="400">
</a>
@ -29,6 +29,14 @@ https://afdian.net/@the1812?tab=sponsor
| 时间 | 用户名 | 单号后4位 | 金额 |
| ------------------- | --------------------- | --------- | ------- |
| 2024.03.01 16:12:34 | 5*B | 0619 | ¥2.33 |
| 2024.02.29 08:53:14 | M*z | 8723 | ¥2.33 |
| 2024.02.20 14:47:10 | 匿名 | 4111 | ¥5.00 |
| 2024.01.31 01:19:43 | *易 | 6909 | ¥2.33 |
| 2024.01.29 21:03:27 | 匿名 | 0302 | ¥2.33 |
| 2024.01.29 21:02:54 | 匿名 | 6268 | ¥2.33 |
| 2024.01.29 21:01:19 | 匿名 | 9601 | ¥2.33 |
| 2024.01.19 01:11:57 | 远去之蓝 | 7860 | ¥5.00 |
| 2024.01.12 22:02:24 | 简式谐_LastofNESTS | 5700 | ¥22.33 |
| 2024.01.02 16:44:38 | O*h | 5101 | ¥10.00 |
| 2023.12.18 21:46:53 | *京 | 1206 | ¥20.00 |

View File

@ -1052,7 +1052,7 @@
"type": "plugin",
"name": "downloadVideo.outputs.idm",
"displayName": "下载视频 - IDM 输出支持",
"description": "为下载视频增加 IDM 输出支持.",
"description": "为下载视频增加 IDM 输出支持,建议配合 [ef2.exe](https://github.com/MotooriKashin/ef2) 以简化操作、保留文件名.",
"fullRelativePath": "../../registry/dist/plugins/video/download/idm-output.js",
"fullAbsolutePath": "registry/dist/plugins/video/download/idm-output.js"
},

View File

@ -1423,7 +1423,7 @@ by FoundTheWOUT
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/video/download/idm-output.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/video/download/idm-output.js)
为下载视频增加 IDM 输出支持.
为下载视频增加 IDM 输出支持,建议配合 [ef2.exe](https://github.com/MotooriKashin/ef2) 以简化操作、保留文件名.
### [下载视频 - 手动输入](../../registry/dist/plugins/video/download/manual-input.js)
`downloadVideo.inputs.manual`

View File

@ -32,6 +32,9 @@ const entry = async () => {
if (target.hasAttribute('click-title')) {
return
}
if (target.hasAttribute('data-pics')) {
return
}
if (
[
'bili-rich-text__action',

View File

@ -37,6 +37,6 @@
.at-panel,
.emoji-panel,
.reply-operation .operation-list {
z-index: 111;
z-index: 111 !important;
}
}

View File

@ -1,11 +1,17 @@
.dyn-card-opus__summary:first-child,
.bili-rich-text__content.folded {
-webkit-line-clamp: unset !important;
display: block !important;
height: auto !important;
max-height: unset !important;
~ .bili-rich-text__action {
display: none !important;
}
}
.card .main-content {
.bili-rich-text__action,
.expand-btn,
.content-ellipsis {
display: none !important;
}
.bili-rich-text__content,
.content-full {
-webkit-line-clamp: unset !important;
display: block !important;

View File

@ -22,4 +22,5 @@ export const urlExclude = [
// '//www.bilibili.com/blackboard/',
'//member.bilibili.com/v2',
'//game.bilibili.com/',
'//www.bilibili.com/mooc/',
]

View File

@ -474,6 +474,32 @@
}
}
}
.bili-dyn-item,
.bili-opus-view {
~ .bili-tabs {
.bili-tabs__header {
@include background-color('4');
@include border-color('5');
}
.bili-tabs__nav__item {
@include color('e');
&.is-active,
&:hover {
@include theme-color();
}
}
// .bili-tabs__content {
// .comment-wrap {
// @include background-color();
// }
// }
}
}
.reaction-list {
.reaction-item {
box-shadow: inset 0 -0.5px 0 0 #666 !important;
}
}
.bili-dyn-forward {
&__item {
@include border-color('6');

View File

@ -413,6 +413,10 @@
.reply-box-textarea::-webkit-input-placeholder {
@include color('8');
}
.reply-box-warp {
@include background-color('2');
@include border-color('5');
}
.fixed-reply-box {
.reply-box {
@include background-color('2');
@ -457,14 +461,29 @@
}
}
}
.image-btn {
.image-icon path {
@include fill('a');
}
&:hover {
.image-icon path {
@include fill('e');
}
}
}
.image-btn,
.emoji-btn,
.at-btn {
@include border-color('4');
@include background-color('3');
@include color('a');
&:hover {
@include color('e');
}
}
.reply-loading {
@include color('a');
}
.emoji-panel {
@include border-color('8884');
@include color('e');

View File

@ -495,6 +495,16 @@
@include theme-color();
}
}
.opus-module-topic {
@include theme-color();
}
.opus-more__btn {
@include color('a');
&:hover {
@include color('e');
@include background-color('3');
}
}
.opus-tabs {
.bili-tabs {
&__header {
@ -508,27 +518,28 @@
}
}
}
.side-toolbar {
&__box,
&__btn {
@include background-color('3');
&.backtop span::before {
@include to-white();
}
}
.side-toolbar {
&__box,
&__btn {
@include background-color('3');
&.backtop span::before {
@include to-white();
}
&__action {
}
&__action {
&,
& svg {
@include color('e');
}
&:hover,
&.is-active {
&,
.side-toolbar__action__text,
& svg {
@include color('e');
}
&:hover,
&.is-active {
&,
& svg {
@include theme-color();
path {
@include theme-fill();
}
@include theme-color();
path {
@include theme-fill();
}
}
}
@ -697,9 +708,7 @@
@include set-color('border-top-color', '4');
}
#{contains('mediainfo_mediaRight')} {
#{contains('mediainfo_media_desc_section')}
#{contains('mediainfo_display_area')}
#{contains('mediainfo_ellipsis')} {
#{contains('mediainfo_media_desc_section')} #{contains('mediainfo_display_area')} #{contains('mediainfo_ellipsis')} {
background: linear-gradient(90deg, hsla(0, 0%, 100%, 0), #222 20%, #222);
@include theme-color();
}
@ -795,3 +804,65 @@
}
}
}
#bili-header-container {
@include background-color('2');
}
.bili-button {
&.primary {
&.plain {
@include background-color();
}
}
}
.vui_button {
&.blue {
@include theme-background-color();
@include border-color();
@include foreground-color();
&:hover {
@include theme-background-color('80');
}
}
}
.be-shielding::after,
.shielding::after {
@include color('e');
@include background-color();
}
.activity-game-list-item {
.activity-card {
@include background-color();
.activity-card-content-head-title {
@include color('e');
}
.activity-card-content-head-desc {
@include color('a');
}
}
}
.activity-game-list .game-card {
.game-card-content-head-title {
@include color('e');
.tag {
@include color('a');
@include border-color('a');
}
}
.game-card-content-head-text {
@include color('a');
}
}
.bangumi-pgc-list {
.media-card {
.media-card-content-head-title {
@include color('e');
}
.media-card-content-head-cv,
.media-card-content-head-label {
@include color('a');
}
.media-card-content-head-desc {
@include color('8');
}
}
}

View File

@ -190,6 +190,7 @@
}
html:root {
--brand_blue: var(--theme-color);
--brand_blue_hover: var(--theme-color-80);
--brand_blue_thin: var(--theme-color-10);
--text_link: var(--theme-color);
}

View File

@ -7,6 +7,9 @@
#biliMainHeader {
min-height: unset !important;
}
.header-v3 .z-top-container {
min-height: 160px !important;
}
.bili-header {
padding-top: 50px !important;
min-height: 0 !important;

View File

@ -2,7 +2,22 @@ import { styledComponentEntry } from '@/components/styled-component'
import { Options } from '.'
const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/
/** 排除 */
const excludeSelectors = ['#certify-img1', '#certify-img2']
/** 需要设置 height */
const heightSelectors = [
// 动态头像框必须设高度
// https://github.com/the1812/Bilibili-Evolved/issues/2030
'.bili-avatar-img',
]
/** width 和 height 都得设置 */
const widthAndHeightSelectors = [
// 首页 Logo
// https://github.com/the1812/Bilibili-Evolved/issues/4480
'.logo-img',
]
const walk = (rootElement: Node, action: (node: HTMLElement) => void) => {
const walker = document.createNodeIterator(rootElement, NodeFilter.SHOW_ELEMENT)
let node = walker.nextNode()
@ -43,9 +58,10 @@ export const imageResolution = async (dpi: number, element: HTMLElement) => {
return
}
if (element.getAttribute('width') === null && element.getAttribute('height') === null) {
if (element.classList.contains('bili-avatar-img')) {
// 动态头像框必须设高度
// https://github.com/the1812/Bilibili-Evolved/issues/2030
if (widthAndHeightSelectors.some(selector => element.matches(selector))) {
element.setAttribute('height', currentHeight)
element.setAttribute('width', currentWidth)
} else if (heightSelectors.some(selector => element.matches(selector))) {
element.setAttribute('height', currentHeight)
} else {
element.setAttribute('width', currentWidth)

View File

@ -81,9 +81,12 @@ body.dark .blocked-ads.new {
pointer-events: none !important;
opacity: 0 !important;
}
.recommended-container_floor-aside .container > *,
body:not(.preserve-feed-goods) .bili-dyn-content__orig__additional:has(.dyn-goods) {
margin: 0 !important;
}
.feed-card:has(.bili-video-card a[href*='cm.bilibili.com']),
.bili-video-card.is-rcmd:has(a[href*='cm.bilibili.com']),
.bili-video-card__wrap:has(a[href*='cm.bilibili.com']) {
display: none !important;
}

View File

@ -141,6 +141,9 @@ const entry = async () => {
url: string,
...restArgs: unknown[]
) {
if (url === undefined || url === null) {
return original.call(this, data, unused, url, ...restArgs)
}
const resolvedUrl = (() => {
try {
return new URL(url, location.origin + location.pathname).toString()

View File

@ -17,22 +17,22 @@ import { convertToXPath, formatSpeedText, parseSpeedText, useShare } from './uti
export const PLAYER_AGENT = playerAgent.provideCustomQuery({
video: {
speedMenuList: '.bilibili-player-video-btn-speed-menu,.bpx-player-ctrl-playbackrate-menu',
speedMenuList: '.bilibili-player-video-btn-speed-menu, .bpx-player-ctrl-playbackrate-menu',
speedMenuItem:
'.bilibili-player-video-btn-speed-menu-list,.bpx-player-ctrl-playbackrate-menu-item',
speedNameBtn: '.bilibili-player-video-btn-speed-name,.bpx-player-ctrl-playbackrate-result',
speedContainer: '.bilibili-player-video-btn-speed,.bpx-player-ctrl-playbackrate',
active: '.bilibili-player-active,.bpx-state-active',
show: '.bilibili-player-speed-show,.bpx-state-show',
'.bilibili-player-video-btn-speed-menu-list, .bpx-player-ctrl-playbackrate-menu-item',
speedNameBtn: '.bilibili-player-video-btn-speed-name, .bpx-player-ctrl-playbackrate-result',
speedContainer: '.bilibili-player-video-btn-speed, .bpx-player-ctrl-playbackrate',
active: '.bilibili-player-active, .bpx-state-active',
show: '.bilibili-player-speed-show, .bpx-state-show',
},
bangumi: {
speedMenuList: '.squirtle-speed-select-list',
speedMenuItem: '.squirtle-select-item',
speedNameBtn: '.squirtle-speed-select-result',
speedContainer: '.squirtle-speed-wrap',
active: '.active',
speedMenuList: '.squirtle-speed-select-list, .bpx-player-ctrl-playbackrate-menu',
speedMenuItem: '.squirtle-select-item, .bpx-player-ctrl-playbackrate-menu-item',
speedNameBtn: '.squirtle-speed-select-result, .bpx-player-ctrl-playbackrate-result',
speedContainer: '.squirtle-speed-wrap, .bpx-player-ctrl-playbackrate',
active: '.active, .bpx-state-active',
// bangumi 那边没有这种 class, 随便填一个就行了
show: '.bilibili-player-speed-show',
show: '.bilibili-player-speed-show, .bpx-state-show',
},
})

View File

@ -39,7 +39,8 @@ body {
.bilibili-player-score,
.bpx-player-popup-dm-close,
.bpx-player-score-summary-wrap,
.bpx-player-score {
.bpx-player-score,
.bili-qoeFeedback {
display: none !important;
}
}

View File

@ -1,21 +1,17 @@
import { defineComponentMetadata } from '@/components/define'
import { playerAgent, PlayerAgentEventTypes } from '@/components/video/player-agent'
import { videoChange, VideoChangeCallback } from '@/core/observer'
import { createHook } from '@/core/utils'
import { playerUrls } from '@/core/utils/urls'
const entry = async () => {
let lastAid: string
const removeCover = () => document.documentElement.style.removeProperty('--cover-url')
videoChange(() => {
console.debug('isBpxPlayer')
const currentBpxVideo = dq('.bpx-player-video-wrap video') as HTMLVideoElement
if (!currentBpxVideo) {
console.warn('bpx player not found')
return
}
createHook(currentBpxVideo, 'play', () => {
playerAgent.addEventListener(PlayerAgentEventTypes.Pause, () => {
removeCover()
})
playerAgent.addEventListener(PlayerAgentEventTypes.Play, () => {
removeCover()
return true
})
})
const showCover: VideoChangeCallback = async ({ aid }) => {

View File

@ -7,7 +7,8 @@ import { DownloadVideoOutput } from '../../../../components/video/download/types
export const plugin: PluginMetadata = {
name: 'downloadVideo.outputs.idm',
displayName: '下载视频 - IDM 输出支持',
description: '为下载视频增加 IDM 输出支持.',
description:
'为下载视频增加 IDM 输出支持,建议配合 [ef2.exe](https://github.com/MotooriKashin/ef2) 以简化操作、保留文件名.',
setup: ({ addData }) => {
addData('downloadVideo.outputs', (outputs: DownloadVideoOutput[]) => {
outputs.push({

View File

@ -1,5 +1,5 @@
{
"version": "2.8.8",
"version": "2.8.9",
"author": "Grant Howard, Coulomb-G",
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
"license": "MIT",

View File

@ -31,7 +31,7 @@ export const component = defineComponentMetadata({
getActions: async () => [
{
name: '开始 / 继续组件二等分',
description: 'Start/Continue component bisection',
description: 'Start / Continue component bisection',
icon: 'mdi-view-split-horizontal',
action: async () => {
const bisector = await import('./api')

View File

@ -71,15 +71,12 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
if (vm.observer) {
return vm.observer
}
const newListPromise = select(
'.feed-card .content, .bili-dyn-list__items',
) as Promise<HTMLElement>
const newListPromise = select('.bili-dyn-list__items') as Promise<HTMLElement>
vm.observer = (async () => {
// const newList = await vm.listElement as HTMLElement
const newList = await newListPromise
if (newList !== (await vm.listElement)) {
if (vm.listElement) {
await stop()
stop()
}
vm.listElement = newListPromise
start()
@ -91,7 +88,7 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
return vm.observer
}
childListSubtree(container, async () => {
if (dq('.feed-card .content, .bili-dyn-list__items')) {
if (dq('.bili-dyn-list__items')) {
start()
} else {
stop()

View File

@ -1,4 +1,5 @@
import { getCookieValue, matchUrlPattern } from '@/core/utils'
import { feedsUrls } from '@/core/utils/urls'
import { FeedsCardCallback } from '../types'
import { feedsCardCallbacks } from './base'
import { FeedsCardsManagerV1 } from './v1'
@ -28,6 +29,10 @@ export const feedsCardsManager = (() => {
* @param callback
*/
export const forEachFeedsCard = async (callback: FeedsCardCallback) => {
if (feedsUrls.every(url => !matchUrlPattern(url))) {
return null
}
const success = await feedsCardsManager.startWatching()
if (!success) {
console.error('feedsCardsManager.startWatching() failed')

View File

@ -24,7 +24,7 @@
<div v-else class="suggest-item-name">
{{ action.displayName || action.name }}
</div>
<div v-if="action.description" class="suggest-item-description">
<div v-if="action.description" class="suggest-item-description" :title="action.description">
{{ action.description }}
</div>
</div>
@ -111,6 +111,7 @@ export default Vue.extend({
&-description {
opacity: 0.5;
font-size: smaller;
@include single-line();
}
&-delete {
opacity: 0.5;

View File

@ -8,6 +8,7 @@ import {
PlayerQuery,
CustomQuery,
CustomQueryProvider,
PlayerAgentEventTypes,
} from './types'
export const elementQuery = (selector: string): ElementQuery => {
@ -31,9 +32,6 @@ export const click = (target: ElementQuery) => {
return button
}
export enum PlayerAgentEventTypes {
Play = 'play',
}
export abstract class PlayerAgent
extends EventTarget
implements EnumEventTarget<`${PlayerAgentEventTypes}`>
@ -97,11 +95,11 @@ export abstract class PlayerAgent
}
// 关灯状态 && 要开灯 -> 开灯
if (on && isCurrentLightOff) {
this.nativeApi.setLightOff(true)
this.nativeApi.setLightOff(false)
return true
}
if (!on && !isCurrentLightOff) {
this.nativeApi.setLightOff(false)
this.nativeApi.setLightOff(true)
return false
}
return null
@ -128,59 +126,64 @@ export abstract class PlayerAgent
return unsafeWindow.nano
}
get nanoTypeMap(): Record<PlayerAgentEventTypes, string> {
return {
[PlayerAgentEventTypes.Play]: this.nanoApi.EventType.Player_Play,
[PlayerAgentEventTypes.Pause]: this.nanoApi.EventType.Player_Pause,
}
}
private eventHandlerMap = new Map<
EventListenerOrEventListenerObject,
{
handler: () => void
handler: EventListener
options: boolean | AddEventListenerOptions
}
>()
addEventListener(
type: `${PlayerAgentEventTypes}`,
callback: EventListenerOrEventListenerObject,
callback: EventListener,
options?: boolean | AddEventListenerOptions,
): void {
super.addEventListener(type, callback, options)
switch (type) {
default: {
break
}
case PlayerAgentEventTypes.Play: {
const handler = () => {
this.dispatchEvent(new Event(PlayerAgentEventTypes.Play))
}
if (typeof options === 'object' && options.once) {
this.nativeApi.once(this.nanoApi.EventType.Player_Initialized, handler)
} else {
this.nativeApi.on(this.nanoApi.EventType.Player_Initialized, handler)
}
this.eventHandlerMap.set(callback, {
handler,
options,
})
const registerHandler = (nanoType: string) => {
if (typeof options === 'object' && options.once) {
this.nativeApi.once(nanoType, callback)
} else {
this.nativeApi.on(nanoType, callback)
}
this.eventHandlerMap.set(callback, {
handler: callback,
options,
})
}
const nanoType = this.nanoTypeMap[type]
if (!nanoType) {
console.warn('[PlayerAgent] unknown event type', type)
return
}
registerHandler(nanoType)
}
removeEventListener(
type: `${PlayerAgentEventTypes}`,
callback: EventListenerOrEventListenerObject,
callback: EventListener,
options?: boolean | EventListenerOptions,
): void {
super.removeEventListener(type, callback, options)
switch (type) {
default: {
break
}
case PlayerAgentEventTypes.Play: {
const handlerData = this.eventHandlerMap.get(callback)
if (!handlerData || lodash.isEqual(options, handlerData.options)) {
break
}
this.nativeApi.off(this.nanoApi.EventType.Player_Initialized, handlerData.handler)
const unregisterHandler = (nanoType: string) => {
const handlerData = this.eventHandlerMap.get(callback)
if (!handlerData || lodash.isEqual(options, handlerData.options)) {
return
}
this.nativeApi.off(nanoType, handlerData.handler)
}
const nanoType = this.nanoTypeMap[type]
if (!nanoType) {
return
}
unregisterHandler(nanoType)
}
/** 获取是否静音 */

View File

@ -2,6 +2,8 @@ import { bangumiUrls, matchCurrentPage } from '@/core/utils/urls'
import { BangumiPlayerAgent } from './bangumi-player'
import { VideoPlayerBpxAgent } from './video-player'
export * from './types'
export const playerAgent = (() => {
if (matchCurrentPage(bangumiUrls)) {
return new BangumiPlayerAgent()

View File

@ -65,3 +65,8 @@ export interface PlayerQuery<QueryResult> extends CustomNestedQuery<QueryResult>
danmakuTipLayer: QueryResult
danmakuSwitch: QueryResult
}
export enum PlayerAgentEventTypes {
Play = 'play',
Pause = 'pause',
}

View File

@ -7,6 +7,9 @@ const normalizeMap = {
}
const escapeMap = Object.fromEntries(Object.entries(normalizeMap).map(entry => entry.reverse()))
const replace = (content: string, map: Record<string, string>) => {
if (!content) {
return ''
}
for (const [key, value] of Object.entries(map)) {
content = content.replace(new RegExp(key, 'g'), value)
}