mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
feat: add parseYamlSafe utility (#7706)
* feat: add parseYamlSafe utility function and refactor YAML parsing in components * fix * fix * fix: preserve malformed YAML in profile editors --------- Co-authored-by: wonfen <eiwonfen@gmail.com>
This commit is contained in:
@@ -65,6 +65,7 @@ import { useThemeMode } from '@/services/states'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import getSystem from '@/utils/get-system'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
interface Props {
|
||||
proxiesUid: string
|
||||
@@ -181,6 +182,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const [prependSeq, setPrependSeq] = useState<IProxyGroupConfig[]>([])
|
||||
const [appendSeq, setAppendSeq] = useState<IProxyGroupConfig[]>([])
|
||||
const [deleteSeq, setDeleteSeq] = useState<string[]>([])
|
||||
const hasLoadedSeqConfigRef = useRef(false)
|
||||
|
||||
const filteredPrependSeq = useMemo(
|
||||
() => prependSeq.filter((group) => match(group.name)),
|
||||
@@ -327,8 +329,17 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
const data = await readProfileFile(property)
|
||||
const obj = yaml.load(data) as ISeqProfileConfig | null
|
||||
const obj = parseYamlSafe(data) as ISeqProfileConfig | null | undefined
|
||||
|
||||
setPrevData(data)
|
||||
setCurrData(data)
|
||||
|
||||
if (obj === undefined) {
|
||||
setVisualization(false)
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(obj?.prepend || [])
|
||||
setAppendSeq(obj?.append || [])
|
||||
@@ -342,17 +353,22 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}
|
||||
return normalized
|
||||
})
|
||||
|
||||
setPrevData(data)
|
||||
setCurrData(data)
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
}, [property])
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === '' || visualization !== true) {
|
||||
const handleVisualizationToggle = () => {
|
||||
if (visualization) {
|
||||
setVisualization(false)
|
||||
return
|
||||
}
|
||||
|
||||
const obj = yaml.load(currData) as ISeqProfileConfig | null
|
||||
const obj = parseYamlSafe(currData) as ISeqProfileConfig | null | undefined
|
||||
if (obj === undefined) {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
startTransition(() => {
|
||||
setPrependSeq(obj?.prepend ?? [])
|
||||
setAppendSeq(obj?.append ?? [])
|
||||
@@ -367,12 +383,17 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
return normalized
|
||||
})
|
||||
})
|
||||
}, [currData, visualization])
|
||||
setVisualization(true)
|
||||
}
|
||||
|
||||
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||
useEffect(() => {
|
||||
if (prependSeq && appendSeq && deleteSeq) {
|
||||
if (hasLoadedSeqConfigRef.current && prependSeq && appendSeq && deleteSeq) {
|
||||
const serialize = () => {
|
||||
if (!hasLoadedSeqConfigRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setCurrData(buildGroupsYaml(prependSeq, appendSeq, deleteSeq))
|
||||
} catch (e) {
|
||||
@@ -391,13 +412,15 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const fetchProxyPolicy = useCallback(async () => {
|
||||
const data = await readProfileFile(profileUid)
|
||||
const proxiesData = await readProfileFile(proxiesUid)
|
||||
const originGroupsObj = yaml.load(data) as {
|
||||
const originGroupsObj = parseYamlSafe(data) as {
|
||||
'proxy-groups': IProxyGroupConfig[]
|
||||
} | null
|
||||
|
||||
const originProxiesObj = yaml.load(data) as { proxies: [] } | null
|
||||
const originProxiesObj = parseYamlSafe(data) as { proxies: [] } | null
|
||||
const originProxies = originProxiesObj?.proxies || []
|
||||
const moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null
|
||||
const moreProxiesObj = parseYamlSafe(
|
||||
proxiesData,
|
||||
) as ISeqProfileConfig | null
|
||||
const morePrependProxies = moreProxiesObj?.prepend || []
|
||||
const moreAppendProxies = moreProxiesObj?.append || []
|
||||
const moreDeleteProxies = normalizeDeleteSeq(moreProxiesObj?.delete)
|
||||
@@ -437,21 +460,21 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const mergeData = await readProfileFile(mergeUid)
|
||||
const globalMergeData = await readProfileFile('Merge')
|
||||
|
||||
const originGroupsObj = yaml.load(data) as {
|
||||
const originGroupsObj = parseYamlSafe(data) as {
|
||||
'proxy-groups': IProxyGroupConfig[]
|
||||
} | null
|
||||
|
||||
const originProviderObj = yaml.load(data) as {
|
||||
const originProviderObj = parseYamlSafe(data) as {
|
||||
'proxy-providers': Record<string, unknown>
|
||||
} | null
|
||||
const originProvider = originProviderObj?.['proxy-providers'] || {}
|
||||
|
||||
const moreProviderObj = yaml.load(mergeData) as {
|
||||
const moreProviderObj = parseYamlSafe(mergeData) as {
|
||||
'proxy-providers': Record<string, unknown>
|
||||
} | null
|
||||
const moreProvider = moreProviderObj?.['proxy-providers'] || {}
|
||||
|
||||
const globalProviderObj = yaml.load(globalMergeData) as {
|
||||
const globalProviderObj = parseYamlSafe(globalMergeData) as {
|
||||
'proxy-providers': Record<string, unknown>
|
||||
} | null
|
||||
const globalProvider = globalProviderObj?.['proxy-providers'] || {}
|
||||
@@ -536,9 +559,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setVisualization((prev) => !prev)
|
||||
}}
|
||||
onClick={handleVisualizationToggle}
|
||||
>
|
||||
{visualization
|
||||
? t('shared.editorModes.advanced')
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useThemeMode } from '@/services/states'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import getSystem from '@/utils/get-system'
|
||||
import parseUri from '@/utils/uri-parser'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
interface Props {
|
||||
profileUid: string
|
||||
@@ -72,6 +73,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const [prependSeq, setPrependSeq] = useState<IProxyConfig[]>([])
|
||||
const [appendSeq, setAppendSeq] = useState<IProxyConfig[]>([])
|
||||
const [deleteSeq, setDeleteSeq] = useState<string[]>([])
|
||||
const hasLoadedSeqConfigRef = useRef(false)
|
||||
|
||||
// 节点的 name 会被用作 SortableContext 的 item id、React key 以及拖拽排序的
|
||||
// 依据。当 name 为空/null(例如高级模式下粘贴了缺少 name 的节点)时,
|
||||
@@ -275,7 +277,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const fetchProfile = useCallback(async () => {
|
||||
const data = await readProfileFile(profileUid)
|
||||
|
||||
const originProxiesObj = yaml.load(data) as {
|
||||
const originProxiesObj = parseYamlSafe(data) as {
|
||||
proxies: IProxyConfig[]
|
||||
} | null
|
||||
|
||||
@@ -283,36 +285,58 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
}, [profileUid])
|
||||
|
||||
const fetchContent = useCallback(async () => {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
const data = await readProfileFile(property)
|
||||
const obj = yaml.load(data) as ISeqProfileConfig | null
|
||||
const obj = parseYamlSafe(data) as ISeqProfileConfig | null | undefined
|
||||
|
||||
setPrevData(data)
|
||||
setCurrData(data)
|
||||
|
||||
if (obj === undefined) {
|
||||
setVisualization(false)
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(obj?.prepend || [])
|
||||
setAppendSeq(obj?.append || [])
|
||||
setDeleteSeq(obj?.delete || [])
|
||||
|
||||
setPrevData(data)
|
||||
setCurrData(data)
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
}, [property])
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === '' || visualization !== true) {
|
||||
const handleVisualizationToggle = () => {
|
||||
if (visualization) {
|
||||
setVisualization(false)
|
||||
return
|
||||
}
|
||||
|
||||
const obj = yaml.load(currData) as ISeqProfileConfig | null
|
||||
const obj = parseYamlSafe(currData) as ISeqProfileConfig | null | undefined
|
||||
if (obj === undefined) {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
startTransition(() => {
|
||||
setPrependSeq(obj?.prepend ?? [])
|
||||
setAppendSeq(obj?.append ?? [])
|
||||
setDeleteSeq(obj?.delete ?? [])
|
||||
})
|
||||
}, [currData, visualization])
|
||||
setVisualization(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!(prependSeq && appendSeq && deleteSeq)) {
|
||||
if (
|
||||
!hasLoadedSeqConfigRef.current ||
|
||||
!(prependSeq && appendSeq && deleteSeq)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const serialize = () => {
|
||||
if (!hasLoadedSeqConfigRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setCurrData(
|
||||
yaml.dump(
|
||||
@@ -386,9 +410,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setVisualization((prev) => !prev)
|
||||
}}
|
||||
onClick={handleVisualizationToggle}
|
||||
>
|
||||
{visualization
|
||||
? t('shared.editorModes.advanced')
|
||||
|
||||
@@ -52,6 +52,7 @@ import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import getSystem from '@/utils/get-system'
|
||||
import { isValidIpCidr } from '@/utils/network'
|
||||
import { parseYamlSafe } from '@/utils/yaml'
|
||||
|
||||
interface Props {
|
||||
groupsUid: string
|
||||
@@ -409,29 +410,42 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const fetchContent = useCallback(async () => {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
const data = await readProfileFile(property)
|
||||
const obj = yaml.load(data) as ISeqProfileConfig | null
|
||||
const obj = parseYamlSafe(data) as ISeqProfileConfig | null | undefined
|
||||
|
||||
setPrevData(data)
|
||||
setCurrData(data)
|
||||
|
||||
if (obj === undefined) {
|
||||
setVisualization(false)
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(obj?.prepend || [])
|
||||
setAppendSeq(obj?.append || [])
|
||||
setDeleteSeq(obj?.delete || [])
|
||||
|
||||
setPrevData(data)
|
||||
setCurrData(data)
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
}, [property])
|
||||
|
||||
useEffect(() => {
|
||||
if (currData === '' || visualization !== true) {
|
||||
const handleVisualizationToggle = () => {
|
||||
if (visualization) {
|
||||
setVisualization(false)
|
||||
return
|
||||
}
|
||||
|
||||
const obj = yaml.load(currData) as ISeqProfileConfig | null
|
||||
const obj = parseYamlSafe(currData) as ISeqProfileConfig | null | undefined
|
||||
if (obj === undefined) {
|
||||
hasLoadedSeqConfigRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
hasLoadedSeqConfigRef.current = true
|
||||
startTransition(() => {
|
||||
setPrependSeq(obj?.prepend ?? [])
|
||||
setAppendSeq(obj?.append ?? [])
|
||||
setDeleteSeq(obj?.delete ?? [])
|
||||
})
|
||||
}, [currData, visualization])
|
||||
setVisualization(true)
|
||||
}
|
||||
|
||||
// 优化:异步处理大数据yaml.dump,避免UI卡死
|
||||
useEffect(() => {
|
||||
@@ -482,13 +496,13 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const mergeData = await readProfileFile(mergeUid) // merge配置文件
|
||||
const globalMergeData = await readProfileFile('Merge') // global merge配置文件
|
||||
|
||||
const rulesObj = yaml.load(data) as { rules: [] } | null
|
||||
const rulesObj = parseYamlSafe(data) as { rules: [] } | null
|
||||
|
||||
const originGroupsObj = yaml.load(data) as {
|
||||
const originGroupsObj = parseYamlSafe(data) as {
|
||||
'proxy-groups': IProxyGroupConfig[]
|
||||
} | null
|
||||
const originGroups = originGroupsObj?.['proxy-groups'] || []
|
||||
const moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null
|
||||
const moreGroupsObj = parseYamlSafe(groupsData) as ISeqProfileConfig | null
|
||||
const rawPrependGroups = moreGroupsObj?.['prepend']
|
||||
const morePrependGroups = Array.isArray(rawPrependGroups)
|
||||
? (rawPrependGroups as IProxyGroupConfig[])
|
||||
@@ -514,29 +528,29 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
moreAppendGroups,
|
||||
)
|
||||
|
||||
const originRuleSetObj = yaml.load(data) as {
|
||||
const originRuleSetObj = parseYamlSafe(data) as {
|
||||
'rule-providers': Record<string, unknown>
|
||||
} | null
|
||||
const originRuleSet = originRuleSetObj?.['rule-providers'] || {}
|
||||
const moreRuleSetObj = yaml.load(mergeData) as {
|
||||
const moreRuleSetObj = parseYamlSafe(mergeData) as {
|
||||
'rule-providers': Record<string, unknown>
|
||||
} | null
|
||||
const moreRuleSet = moreRuleSetObj?.['rule-providers'] || {}
|
||||
const globalRuleSetObj = yaml.load(globalMergeData) as {
|
||||
const globalRuleSetObj = parseYamlSafe(globalMergeData) as {
|
||||
'rule-providers': Record<string, unknown>
|
||||
} | null
|
||||
const globalRuleSet = globalRuleSetObj?.['rule-providers'] || {}
|
||||
const ruleSet = Object.assign({}, originRuleSet, moreRuleSet, globalRuleSet)
|
||||
|
||||
const originSubRuleObj = yaml.load(data) as {
|
||||
const originSubRuleObj = parseYamlSafe(data) as {
|
||||
'sub-rules': Record<string, unknown>
|
||||
} | null
|
||||
const originSubRule = originSubRuleObj?.['sub-rules'] || {}
|
||||
const moreSubRuleObj = yaml.load(mergeData) as {
|
||||
const moreSubRuleObj = parseYamlSafe(mergeData) as {
|
||||
'sub-rules': Record<string, unknown>
|
||||
} | null
|
||||
const moreSubRule = moreSubRuleObj?.['sub-rules'] || {}
|
||||
const globalSubRuleObj = yaml.load(globalMergeData) as {
|
||||
const globalSubRuleObj = parseYamlSafe(globalMergeData) as {
|
||||
'sub-rules': Record<string, unknown>
|
||||
} | null
|
||||
const globalSubRule = globalSubRuleObj?.['sub-rules'] || {}
|
||||
@@ -609,9 +623,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setVisualization((prev) => !prev)
|
||||
}}
|
||||
onClick={handleVisualizationToggle}
|
||||
>
|
||||
{visualization
|
||||
? t('shared.editorModes.advanced')
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { load } from 'js-yaml'
|
||||
|
||||
import { debugLog } from './debug'
|
||||
|
||||
/**
|
||||
* 空文档返回 null,解析失败返回 undefined,且不抛出错误。
|
||||
*/
|
||||
export function parseYamlSafe(input: string) {
|
||||
if (
|
||||
input
|
||||
.split(/\r?\n/u)
|
||||
.every((line) => line.trim() === '' || line.trimStart().startsWith('#'))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedYaml = load(input)
|
||||
return parsedYaml ?? null
|
||||
} catch (e) {
|
||||
debugLog('parseYamlSafe failed', e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user