refactor(core): CORE-01 migrate utility contracts to TypeScript

This commit is contained in:
Harvey Zhao committed 2026-09-10 23:50:47 +08:00
1 parent e005e5f249
commit d3c695024a
33 files changed
+1269 -305

No files matched your search

-25
View File
@@ -1,25 +0,0 @@
export class ArtPlayerError extends Error {
constructor(message, context) {
super(message)
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, context || this.constructor)
}
this.name = 'ArtPlayerError'
}
}
export function errorHandle(condition, msg) {
if (!condition) {
throw new ArtPlayerError(msg)
}
return condition
}
// Handle internal play rejections without changing the public play/toggle promises.
// Values without catch (including synchronous pause results) pass through unchanged.
export function silencePromise(value) {
if (value && typeof value.catch === 'function') {
return value.catch(() => {})
}
return value
}
+29
View File
@@ -0,0 +1,29 @@
type StackFrame = ((...args: never[]) => unknown) | (abstract new (...args: never[]) => object)
export class ArtPlayerError extends Error {
constructor(message?: string, context?: StackFrame) {
super(message)
if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, context || this.constructor)
}
this.name = 'ArtPlayerError'
}
}
export function errorHandle<T>(condition: T, msg?: string): T {
if (!condition) {
throw new ArtPlayerError(msg)
}
return condition
}
// Handle internal play rejections without changing the public play/toggle promises.
// Values without catch (including synchronous pause results) pass through unchanged.
export function silencePromise<T>(value: T): T extends Promise<infer Result> ? Promise<Result | undefined> : T
export function silencePromise(value: unknown): unknown {
const candidate = value as { catch?: unknown } | null | undefined
if (candidate && typeof candidate.catch === 'function') {
return candidate.catch(() => {})
}
return value
}
-21
View File
@@ -1,21 +0,0 @@
export function getExt(url) {
if (url.includes('?')) {
return getExt(url.split('?')[0])
}
if (url.includes('#')) {
return getExt(url.split('#')[0])
}
return url.trim().toLowerCase().split('.').pop()
}
export function download(url, name) {
const elink = document.createElement('a')
elink.style.display = 'none'
elink.href = url
elink.download = name
document.body.appendChild(elink)
elink.click()
document.body.removeChild(elink)
}
+19
View File
@@ -0,0 +1,19 @@
export function getExt(url: string): string {
const end = url.search(/[?#]/)
const clean = (end < 0 ? url : url.slice(0, end)).trim().toLowerCase()
return clean.slice(clean.lastIndexOf('.') + 1)
}
export function download(url: string, name: string): void {
const elink = document.createElement('a')
elink.style.display = 'none'
elink.href = url
elink.download = name
document.body.appendChild(elink)
try {
elink.click()
}
finally {
elink.remove()
}
}
-43
View File
@@ -1,43 +0,0 @@
export function clamp(num, a, b) {
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b))
}
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function secondToTime(second) {
if (!second)
return '00:00'
const add0 = num => (num < 10 ? `0${num}` : String(num))
const hour = Math.floor(second / 3600)
const min = Math.floor((second - hour * 3600) / 60)
const sec = Math.floor(second - hour * 3600 - min * 60)
return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(':')
}
export function escape(str) {
return str.replace(
/[&<>'"]/g,
tag =>
({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'\'': '&#39;',
'"': '&quot;',
})[tag] || tag,
)
}
export function unescape(str) {
const map = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#39;': '\'',
'&quot;': '"',
}
const reg = new RegExp(`(${Object.keys(map).join('|')})`, 'g')
return str.replace(reg, tag => map[tag] || tag)
}
+44
View File
@@ -0,0 +1,44 @@
export function clamp(num: number, a: number, b: number): number {
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b))
}
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function secondToTime(second: number): string {
if (!second)
return '00:00'
const add0 = (num: number) => (num < 10 ? `0${num}` : String(num))
const hour = Math.floor(second / 3600)
const min = Math.floor((second - hour * 3600) / 60)
const sec = Math.floor(second - hour * 3600 - min * 60)
return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(':')
}
const escapeMap: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'\'': '&#39;',
'"': '&quot;',
}
const unescapeMap: Record<string, string> = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&#39;': '\'',
'&quot;': '"',
}
const unescapePattern = /(&amp;|&lt;|&gt;|&#39;|&quot;)/g
export function escape(str: string): string {
return str.replace(
/[&<>'"]/g,
tag => escapeMap[tag] || tag,
)
}
export function unescape(str: string): string {
return str.replace(unescapePattern, tag => unescapeMap[tag] || tag)
}
File renamed without changes.
-30
View File
@@ -1,30 +0,0 @@
export const def = Object.defineProperty
const { hasOwnProperty } = Object.prototype
export function has(obj, name) {
return hasOwnProperty.call(obj, name)
}
export function get(obj, name) {
return Object.getOwnPropertyDescriptor(obj, name)
}
export function mergeDeep(...objects) {
const isObject = item => item && typeof item === 'object' && !Array.isArray(item)
return objects.reduce((prev, obj) => {
Object.keys(obj).forEach((key) => {
const pVal = prev[key]
const oVal = obj[key]
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat(...oVal)
}
else if (isObject(pVal) && isObject(oVal)) {
prev[key] = mergeDeep(pVal, oVal)
}
else {
prev[key] = oVal
}
})
return prev
}, {})
}
+35
View File
@@ -0,0 +1,35 @@
export const def = Object.defineProperty
const { hasOwnProperty } = Object.prototype
export function has(obj: object, name: PropertyKey): boolean {
return hasOwnProperty.call(obj, name)
}
export function get(obj: object, name: PropertyKey): PropertyDescriptor | undefined {
return Object.getOwnPropertyDescriptor(obj, name)
}
export function mergeDeep<T extends object[]>(...objects: T): T[number] {
const isObject = (item: unknown): item is Record<string, unknown> => !!item && typeof item === 'object' && !Array.isArray(item)
const result = objects.reduce<Record<string, unknown>>((prev, obj) => {
Object.keys(obj).forEach((key) => {
const pVal = prev[key]
const oVal: unknown = (obj as Record<string, unknown>)[key]
let value: unknown
if (Array.isArray(pVal) && Array.isArray(oVal)) {
value = pVal.concat(...oVal)
}
else if (isObject(pVal) && isObject(oVal)) {
value = mergeDeep(pVal, oVal)
}
else {
value = oVal
}
// Treat keys as data rather than invoking inherited setters such as __proto__.
def(prev, key, { value, enumerable: true, configurable: true, writable: true })
})
return prev
}, {})
// Preserve the existing generic API; dynamic keys are handled only inside this boundary.
return result as T[number]
}
@@ -1,5 +1,5 @@
function fixSrt(srt) {
return srt.replace(/(\d\d:\d\d:\d\d)[,.](\d+)/g, (_, $1, $2) => {
function fixSrt(srt: string): string {
return srt.replace(/(\d\d:\d\d:\d\d)[,.](\d+)/g, (_match: string, $1: string, $2: string) => {
let ms = $2.slice(0, 3)
if ($2.length === 1) {
ms = `${$2}00`
@@ -11,7 +11,7 @@ function fixSrt(srt) {
})
}
export function srtToVtt(srtText) {
export function srtToVtt(srtText: string): string {
return 'WEBVTT \r\n\r\n'.concat(
fixSrt(srtText)
.replace(/\{\\([ibu])\}/g, '</$1>')
@@ -24,7 +24,7 @@ export function srtToVtt(srtText) {
)
}
export function vttToBlob(vttText) {
export function vttToBlob(vttText: string): string {
return URL.createObjectURL(
new Blob([vttText], {
type: 'text/vtt',
@@ -32,7 +32,7 @@ export function vttToBlob(vttText) {
)
}
export function assToVtt(ass) {
export function assToVtt(ass: string): string {
const reAss = new RegExp(
'Dialogue:\\s\\d,'
+ '(\\d+:\\d\\d:\\d\\d.\\d\\d),'
@@ -76,10 +76,11 @@ export function assToVtt(ass) {
const m = line.match(reAss)
if (!m)
return null
const [, start = '', end = '', , , text = ''] = m
return {
start: fixTime(m[1].trim()),
end: fixTime(m[2].trim()),
text: m[5]
start: fixTime(start.trim()),
end: fixTime(end.trim()),
text: text
.replace(/\{[\s\S]*?\}/g, '')
.replace(/(\\N)/g, '\n')
.trim()
-32
View File
@@ -1,32 +0,0 @@
export function sleep(ms = 0) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export function debounce(func, duration) {
let timeout
return function (...args) {
const effect = () => {
timeout = null
return func.apply(this, args)
}
clearTimeout(timeout)
timeout = setTimeout(effect, duration)
}
}
export function throttle(func, duration) {
let shouldWait = false
return function (...args) {
if (!shouldWait) {
func.apply(this, args)
shouldWait = true
setTimeout(() => {
shouldWait = false
}, duration)
}
}
}
+32
View File
@@ -0,0 +1,32 @@
export function sleep(ms = 0): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
export function debounce<Receiver, Args extends unknown[]>(func: (this: Receiver, ...args: Args) => unknown, duration: number) {
let timeout: ReturnType<typeof setTimeout> | undefined
return function (this: Receiver, ...args: Args): void {
const effect = () => {
timeout = undefined
return func.apply(this, args)
}
clearTimeout(timeout)
timeout = setTimeout(effect, duration)
}
}
export function throttle<Receiver, Args extends unknown[]>(func: (this: Receiver, ...args: Args) => unknown, duration: number) {
let shouldWait = false
return function (this: Receiver, ...args: Args): void {
if (!shouldWait) {
func.apply(this, args)
shouldWait = true
setTimeout(() => {
shouldWait = false
}, duration)
}
}
}