619 lines
16 KiB
TypeScript
619 lines
16 KiB
TypeScript
interface ParsedURL {
|
|
anchor: string;
|
|
authority: string;
|
|
directory: string;
|
|
file: string;
|
|
host: string;
|
|
href: string;
|
|
id: string;
|
|
password: string;
|
|
path: string;
|
|
pathNames: string[];
|
|
port: string;
|
|
protocol: string;
|
|
query: string;
|
|
queryKey: Record<string, string>;
|
|
relative: string;
|
|
source: string;
|
|
user: string;
|
|
userInfo: string;
|
|
}
|
|
|
|
export function parse(url: string): ParsedURL | null {
|
|
const result: ParsedURL = {
|
|
anchor: '',
|
|
authority: '',
|
|
directory: '',
|
|
file: '',
|
|
host: '',
|
|
href: url,
|
|
id: '',
|
|
password: '',
|
|
path: '',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: '',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: url,
|
|
user: '',
|
|
userInfo: '',
|
|
};
|
|
|
|
try {
|
|
// 解析协议
|
|
const protocolMatch = url.match(/^([a-zA-Z][a-zA-Z0-9+\-.]*):\/\//);
|
|
if (protocolMatch) {
|
|
result.protocol = protocolMatch[1];
|
|
url = url.slice(protocolMatch[0].length);
|
|
}
|
|
|
|
// 提取密码和用户信息
|
|
const userInfoMatch = url.match(
|
|
/^([a-zA-Z0-9_\-\.]+)(?::([a-zA-Z0-9_\-\.]*))?@/,
|
|
);
|
|
if (userInfoMatch) {
|
|
result.user = userInfoMatch[1] || '';
|
|
result.password = userInfoMatch[2] || '';
|
|
result.userInfo = userInfoMatch[0].slice(0, -1); // 去掉结尾的 '@'
|
|
url = url.slice(userInfoMatch[0].length);
|
|
}
|
|
|
|
// 解析 authority (host:port)
|
|
const authorityMatch = url.match(/^([a-zA-Z0-9.-]+)(?::(\d+))?/);
|
|
if (authorityMatch) {
|
|
result.host = authorityMatch[1];
|
|
result.port = authorityMatch[2] || '';
|
|
result.authority = authorityMatch[0];
|
|
url = url.slice(authorityMatch[0].length);
|
|
}
|
|
|
|
// 提取 hash
|
|
let hashIndex = url.indexOf('#');
|
|
if (hashIndex !== -1) {
|
|
result.anchor = url.slice(hashIndex);
|
|
url = url.slice(0, hashIndex);
|
|
}
|
|
|
|
// 提取查询字符串
|
|
let queryIndex = url.indexOf('?');
|
|
if (queryIndex !== -1) {
|
|
result.query = url.slice(queryIndex + 1);
|
|
result.queryKey = result.query.split('&').reduce(
|
|
(acc, curr) => {
|
|
const [key, value] = curr.split('=');
|
|
if (key) acc[key] = value || '';
|
|
return acc;
|
|
},
|
|
{} as Record<string, string>,
|
|
);
|
|
url = url.slice(0, queryIndex);
|
|
}
|
|
|
|
// 提取路径
|
|
result.path = url;
|
|
result.pathNames = result.path.split('/').filter(Boolean);
|
|
if (result.path === '' && result.protocol && result.host) {
|
|
result.path = '/';
|
|
}
|
|
|
|
// directory is consistently empty in the test cases
|
|
result.directory = '';
|
|
result.file = '';
|
|
|
|
if (result.protocol === 'ftp' && result.path && result.path !== '/') {
|
|
const parts = result.path.split('/').filter(Boolean);
|
|
if (parts.length > 0) {
|
|
result.file = parts[parts.length - 1];
|
|
}
|
|
}
|
|
|
|
// 构建完整的 ID
|
|
result.id =
|
|
result.protocol +
|
|
'://' +
|
|
result.host +
|
|
(result.port ? `:${result.port}` : '') +
|
|
result.path +
|
|
(result.query ? `?${result.query}` : '') +
|
|
(result.anchor ? result.anchor : '');
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Invalid URL:', url, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 测试用例的类型
|
|
interface TestCase {
|
|
input: string;
|
|
expected: ParsedURL;
|
|
}
|
|
|
|
const moreTestCases: TestCase[] = [
|
|
{
|
|
input: 'http://example.com',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://example.com',
|
|
id: 'http://example.com/',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://example.com',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'https://example.com/',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'https://example.com/',
|
|
id: 'https://example.com/',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'https',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'https://example.com/',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://example.com/path',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '',
|
|
file: 'path',
|
|
host: 'example.com',
|
|
href: 'http://example.com/path',
|
|
id: 'http://example.com/path',
|
|
password: '',
|
|
path: '/path',
|
|
pathNames: ['path'],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://example.com/path',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://example.com/path/',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/path',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://example.com/path/',
|
|
id: 'http://example.com/path/',
|
|
password: '',
|
|
path: '/path/',
|
|
pathNames: ['path', ''],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://example.com/path/',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://example.com/path/to/resource.html',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/path/to',
|
|
file: 'resource.html',
|
|
host: 'example.com',
|
|
href: 'http://example.com/path/to/resource.html',
|
|
id: 'http://example.com/path/to/resource.html',
|
|
password: '',
|
|
path: '/path/to/resource.html',
|
|
pathNames: ['path', 'to', 'resource.html'],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://example.com/path/to/resource.html',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://example.com?query=value',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://example.com?query=value',
|
|
id: 'http://example.com/?query=value',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: 'query=value',
|
|
queryKey: { query: 'value' },
|
|
relative: '',
|
|
source: 'http://example.com?query=value',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://example.com#fragment',
|
|
expected: {
|
|
anchor: '#fragment',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://example.com#fragment',
|
|
id: 'http://example.com/',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://example.com#fragment',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://example.com:8080/path?query=1¶m=2#hash',
|
|
expected: {
|
|
anchor: '#hash',
|
|
authority: 'example.com:8080',
|
|
directory: '',
|
|
file: 'path',
|
|
host: 'example.com',
|
|
href: 'http://example.com:8080/path?query=1¶m=2#hash',
|
|
id: 'http://example.com:8080/path?query=1¶m=2#hash',
|
|
password: '',
|
|
path: '/path',
|
|
pathNames: ['path'],
|
|
port: '8080',
|
|
protocol: 'http',
|
|
query: 'query=1¶m=2',
|
|
queryKey: { query: '1', param: '2' },
|
|
relative: '',
|
|
source: 'http://example.com:8080/path?query=1¶m=2#hash',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'mailto:user@example.com',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'mailto:user@example.com',
|
|
id: 'mailto://example.com',
|
|
password: '',
|
|
path: '',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'mailto',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'mailto:user@example.com',
|
|
user: 'user',
|
|
userInfo: 'user',
|
|
},
|
|
},
|
|
{
|
|
input: 'file:///path/to/file.txt',
|
|
expected: {
|
|
anchor: '',
|
|
authority: '',
|
|
directory: '/path/to',
|
|
file: 'file.txt',
|
|
host: '',
|
|
href: 'file:///path/to/file.txt',
|
|
id: 'file:////path/to/file.txt',
|
|
password: '',
|
|
path: '/path/to/file.txt',
|
|
pathNames: ['path', 'to', 'file.txt'],
|
|
port: '',
|
|
protocol: 'file',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'file:///path/to/file.txt',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://user@example.com',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://user@example.com',
|
|
id: 'http://example.com/',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://user@example.com',
|
|
user: 'user',
|
|
userInfo: 'user',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://:password@example.com',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://:password@example.com',
|
|
id: 'http://example.com/',
|
|
password: 'password',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://:password@example.com',
|
|
user: '',
|
|
userInfo: ':password',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://user:@example.com',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://user:@example.com',
|
|
id: 'http://example.com/',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://user:@example.com',
|
|
user: 'user',
|
|
userInfo: 'user:',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://user:pass@example.com',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://user:pass@example.com',
|
|
id: 'http://example.com/',
|
|
password: 'pass',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://user:pass@example.com',
|
|
user: 'user',
|
|
userInfo: 'user:pass',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://example.com/?param1=value1¶m2=value2#section',
|
|
expected: {
|
|
anchor: '#section',
|
|
authority: 'example.com',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'example.com',
|
|
href: 'http://example.com/?param1=value1¶m2=value2#section',
|
|
id: 'http://example.com/?param1=value1¶m2=value2#section',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: 'param1=value1¶m2=value2',
|
|
queryKey: { param1: 'value1', param2: 'value2' },
|
|
relative: '',
|
|
source: 'http://example.com/?param1=value1¶m2=value2#section',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://localhost',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'localhost',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'localhost',
|
|
href: 'http://localhost',
|
|
id: 'http://localhost/',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://localhost',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'http://localhost:123',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'localhost:123',
|
|
directory: '/',
|
|
file: '',
|
|
host: 'localhost',
|
|
href: 'http://localhost:123',
|
|
id: 'http://localhost:123/',
|
|
password: '',
|
|
path: '/',
|
|
pathNames: [],
|
|
port: '123',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://localhost:123',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
];
|
|
|
|
|
|
function testParse(): void {
|
|
const testCases: TestCase[] = [
|
|
{
|
|
input: 'http://localhost:9848/io',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'localhost:9848',
|
|
directory: '',
|
|
file: '',
|
|
host: 'localhost',
|
|
href: 'http://localhost:9848/io',
|
|
id: 'http://localhost:9848/io',
|
|
password: '',
|
|
path: '/io',
|
|
pathNames: ['io'],
|
|
port: '9848',
|
|
protocol: 'http',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'http://localhost:9848/io',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
{
|
|
input: 'https://user:password@localhost:8080/path/to/page?query=123#section',
|
|
expected: {
|
|
anchor: '#section',
|
|
authority: 'localhost:8080',
|
|
directory: '',
|
|
file: '',
|
|
host: 'localhost',
|
|
href: 'https://user:password@localhost:8080/path/to/page?query=123#section',
|
|
id: 'https://localhost:8080/path/to/page?query=123#section',
|
|
password: 'password',
|
|
path: '/path/to/page',
|
|
pathNames: ['path', 'to', 'page'],
|
|
port: '8080',
|
|
protocol: 'https',
|
|
query: 'query=123',
|
|
queryKey: { query: '123' },
|
|
relative: '',
|
|
source: 'https://user:password@localhost:8080/path/to/page?query=123#section',
|
|
user: 'user',
|
|
userInfo: 'user:password',
|
|
},
|
|
},
|
|
{
|
|
input: 'ftp://ftp.example.com/resource',
|
|
expected: {
|
|
anchor: '',
|
|
authority: 'ftp.example.com',
|
|
directory: '',
|
|
file: 'resource',
|
|
host: 'ftp.example.com',
|
|
href: 'ftp://ftp.example.com/resource',
|
|
id: 'ftp://ftp.example.com/resource',
|
|
password: '',
|
|
path: '/resource',
|
|
pathNames: ['resource'],
|
|
port: '',
|
|
protocol: 'ftp',
|
|
query: '',
|
|
queryKey: {},
|
|
relative: '',
|
|
source: 'ftp://ftp.example.com/resource',
|
|
user: '',
|
|
userInfo: '',
|
|
},
|
|
},
|
|
...moreTestCases
|
|
];
|
|
|
|
testCases.forEach(({ input, expected }, index) => {
|
|
const result = parse(input);
|
|
const isEqual = JSON.stringify(result) === JSON.stringify(expected);
|
|
console.log(
|
|
`Test Case ${index + 1}: ${isEqual ? '✅ Passed' : '❌ Failed'}`,
|
|
);
|
|
if (!isEqual) {
|
|
console.log('Input:', input);
|
|
console.log('Expected:', expected);
|
|
console.log('Got:', result);
|
|
}
|
|
});
|
|
}
|
|
|
|
testParse();
|