Skip to content

Commit 0f03daa

Browse files
CopilotTechQuery
andauthored
[add] Base64 encoder & decoder with Unicode support (#30)
Co-authored-by: TechQuery <[email protected]>
1 parent a9e330d commit 0f03daa

File tree

8 files changed

+412
-375
lines changed

8 files changed

+412
-375
lines changed

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web-utility",
3-
"version": "4.5.1",
3+
"version": "4.5.3",
44
"license": "LGPL-3.0",
55
"author": "[email protected]",
66
"description": "Web front-end toolkit based on TypeScript",
@@ -36,20 +36,20 @@
3636
"@parcel/transformer-typescript-types": "~2.15.4",
3737
"@peculiar/webcrypto": "^1.5.0",
3838
"@types/jest": "^29.5.14",
39-
"@types/node": "^22.17.0",
39+
"@types/node": "^22.18.0",
4040
"@webcomponents/webcomponentsjs": "^2.8.0",
41-
"core-js": "^3.44.0",
41+
"core-js": "^3.45.1",
4242
"husky": "^9.1.7",
4343
"intersection-observer": "^0.12.2",
4444
"jest": "^29.7.0",
4545
"jest-environment-jsdom": "^29.7.0",
46-
"lint-staged": "^16.1.4",
46+
"lint-staged": "^16.1.5",
4747
"open-cli": "^8.0.0",
4848
"parcel": "~2.15.4",
4949
"prettier": "^3.6.2",
5050
"ts-jest": "^29.4.1",
51-
"typedoc": "^0.28.9",
52-
"typedoc-plugin-mdn-links": "^5.0.7",
51+
"typedoc": "^0.28.11",
52+
"typedoc-plugin-mdn-links": "^5.0.9",
5353
"typescript": "~5.9.2"
5454
},
5555
"pnpm": {

pnpm-lock.yaml

Lines changed: 301 additions & 301 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/DOM.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { URLData } from './URL';
22
import { HTMLProps, HTMLField, CSSStyles, CSSObject } from './DOM-type';
3-
import { Constructor, isEmpty, assertInheritance, toHyphenCase } from './data';
3+
import {
4+
Constructor,
5+
isEmpty,
6+
assertInheritance,
7+
toHyphenCase,
8+
likeArray
9+
} from './data';
410
import { toJSValue } from './parser';
511

612
export const XMLNamespace = {
@@ -35,11 +41,9 @@ export function elementTypeOf(tagName: string) {
3541
: 'xml';
3642
}
3743

38-
export function isHTMLElementClass<T extends Constructor<HTMLElement>>(
44+
export const isHTMLElementClass = <T extends Constructor<HTMLElement>>(
3945
Class: any
40-
): Class is T {
41-
return assertInheritance(Class, HTMLElement);
42-
}
46+
): Class is T => assertInheritance(Class, HTMLElement);
4347

4448
const nameMap = new WeakMap<Constructor<HTMLElement>, string>();
4549

@@ -92,11 +96,10 @@ export function parseDOM(HTML: string) {
9296
});
9397
}
9498

95-
export function stringifyDOM(node: Node) {
96-
return new XMLSerializer()
99+
export const stringifyDOM = (node: Node) =>
100+
new XMLSerializer()
97101
.serializeToString(node)
98102
.replace(/ xmlns="http:\/\/www.w3.org\/1999\/xhtml"/g, '');
99-
}
100103

101104
export function* walkDOM<T extends Node = Node>(
102105
root: Node,
@@ -253,12 +256,12 @@ export interface ScrollEvent {
253256
links: (HTMLAnchorElement | HTMLAreaElement)[];
254257
}
255258

256-
export function watchScroll(
259+
export const watchScroll = (
257260
box: HTMLElement,
258261
handler: (event: ScrollEvent) => any,
259262
depth = 6
260-
) {
261-
return Array.from(
263+
) =>
264+
Array.from(
262265
box.querySelectorAll<HTMLHeadingElement>(
263266
Array.from(new Array(depth), (_, index) => `h${++index}`) + ''
264267
),
@@ -288,8 +291,6 @@ export function watchScroll(
288291
};
289292
}
290293
);
291-
}
292-
293294
export function watchVisible(
294295
root: Element,
295296
handler: (visible: boolean) => any
@@ -314,9 +315,10 @@ export function watchVisible(
314315
export function formToJSON<T extends object = URLData<File>>(
315316
form: HTMLFormElement | HTMLFieldSetElement
316317
) {
317-
const data = {} as T;
318+
const { elements } = form,
319+
data = {} as T;
318320

319-
for (const field of form.elements) {
321+
for (const field of elements) {
320322
let { name, value, checked, defaultValue, selectedOptions, files } =
321323
field as HTMLField;
322324
const type = (field as HTMLField).type as string;
@@ -333,8 +335,9 @@ export function formToJSON<T extends object = URLData<File>>(
333335
case 'radio':
334336
case 'checkbox':
335337
if (checked)
336-
parsedValue = defaultValue ? toJSValue(defaultValue) : true;
337-
else continue;
338+
parsedValue = !defaultValue || toJSValue(defaultValue);
339+
else if (likeArray(elements.namedItem(name))) continue;
340+
else parsedValue = false;
338341
break;
339342
case 'select-multiple':
340343
parsedValue = Array.from(selectedOptions, ({ value }) =>

source/URL.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { isEmpty, likeArray, makeArray } from './data';
22
import { parseJSON } from './parser';
33

4-
export function isXDomain(URI: string) {
5-
return new URL(URI, document.baseURI).origin !== location.origin;
6-
}
4+
export const isXDomain = (URI: string) =>
5+
new URL(URI, document.baseURI).origin !== location.origin;
76

87
export type JSONValue = number | boolean | string | null;
98
export interface URLData<E = unknown> {
109
[key: string]: JSONValue | JSONValue[] | URLData | URLData[] | E;
1110
}
1211

1312
export function parseURLData(
14-
raw = globalThis.location?.search,
13+
raw = globalThis.location?.search || '',
1514
toBuiltIn = true
1615
): URLData {
1716
const rawData = raw
@@ -54,25 +53,21 @@ export function buildURLData(map: string[][] | Record<string, any>) {
5453
return new URLSearchParams(list);
5554
}
5655

57-
export async function blobOf(URI: string | URL) {
58-
return (await fetch(URI + '')).blob();
59-
}
56+
export const blobOf = async (URI: string | URL) =>
57+
(await fetch(URI + '')).blob();
6058

6159
const DataURI = /^data:(.+?\/(.+?))?(;base64)?,([\s\S]+)/;
6260
/**
6361
* Blob logic forked from axes's
6462
*
65-
* @see http://www.cnblogs.com/axes/p/4603984.html
63+
* @see {@link http://www.cnblogs.com/axes/p/4603984.html}
6664
*/
6765
export function blobFrom(URI: string) {
6866
var [_, type, __, base64, data] = DataURI.exec(URI) || [];
6967

7068
data = base64 ? atob(data) : data;
7169

72-
const aBuffer = new ArrayBuffer(data.length);
73-
const uBuffer = new Uint8Array(aBuffer);
74-
75-
for (let i = 0; data[i]; i++) uBuffer[i] = data.charCodeAt(i);
70+
const aBuffer = Uint8Array.from(data, char => char.charCodeAt(0));
7671

7772
return new Blob([aBuffer], { type });
7873
}

source/data.ts

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,21 @@ export type PickData<T> = Omit<T, TypeKeys<T, Function>>;
1616

1717
export type DataKeys<T> = Exclude<keyof T, TypeKeys<T, Function>>;
1818

19-
export function likeNull(value?: any) {
20-
return !(value != null) || Number.isNaN(value);
21-
}
19+
export const likeNull = (value?: any) =>
20+
!(value != null) || Number.isNaN(value);
2221

23-
export function isEmpty(value?: any) {
24-
return (
25-
likeNull(value) ||
26-
(typeof value === 'object' ? !Object.keys(value).length : value === '')
27-
);
28-
}
22+
export const isEmpty = (value?: any) =>
23+
likeNull(value) ||
24+
(typeof value === 'object' ? !Object.keys(value).length : value === '');
2925

3026
/**
3127
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag}
3228
*/
3329
export const classNameOf = (data: any): string =>
3430
Object.prototype.toString.call(data).slice(8, -1);
3531

36-
export function assertInheritance(Sub: Function, Super: Function) {
37-
return Sub.prototype instanceof Super;
38-
}
32+
export const assertInheritance = (Sub: Function, Super: Function) =>
33+
Sub.prototype instanceof Super;
3934

4035
export function proxyPrototype<T extends object>(
4136
target: T,
@@ -62,29 +57,24 @@ export function proxyPrototype<T extends object>(
6257
Object.setPrototypeOf(target, prototypeProxy);
6358
}
6459

65-
export function isUnsafeNumeric(raw: string) {
66-
return (
67-
/^[\d.]+$/.test(raw) &&
68-
raw.localeCompare(Number.MAX_SAFE_INTEGER + '', undefined, {
69-
numeric: true
70-
}) > 0
71-
);
72-
}
60+
export const isUnsafeNumeric = (raw: string) =>
61+
/^[\d.]+$/.test(raw) &&
62+
raw.localeCompare(Number.MAX_SAFE_INTEGER + '', undefined, {
63+
numeric: true
64+
}) > 0;
7365

74-
export function byteLength(raw: string) {
75-
return raw.replace(/[^\u0021-\u007e\uff61-\uffef]/g, 'xx').length;
76-
}
66+
export const byteLength = (raw: string) =>
67+
raw.replace(/[^\u0021-\u007e\uff61-\uffef]/g, 'xx').length;
7768

7869
export type HyphenCase<T extends string> = T extends `${infer L}${infer R}`
7970
? `${L extends Uppercase<L> ? `-${Lowercase<L>}` : L}${HyphenCase<R>}`
8071
: T;
81-
export function toHyphenCase(raw: string) {
82-
return raw.replace(
72+
export const toHyphenCase = (raw: string) =>
73+
raw.replace(
8374
/[A-Z]+|[^A-Za-z][A-Za-z]/g,
8475
(match, offset) =>
8576
`${offset ? '-' : ''}${(match[1] || match[0]).toLowerCase()}`
8677
);
87-
}
8878

8979
export type CamelCase<
9080
Raw extends string,
@@ -94,23 +84,40 @@ export type CamelCase<
9484
? `${Capitalize<L>}${Capitalize<CamelCase<R>>}`
9585
: `${Capitalize<Raw>}`
9686
>;
97-
export function toCamelCase(raw: string, large = false) {
98-
return raw.replace(/^[A-Za-z]|[^A-Za-z][A-Za-z]/g, (match, offset) =>
87+
export const toCamelCase = (raw: string, large = false) =>
88+
raw.replace(/^[A-Za-z]|[^A-Za-z][A-Za-z]/g, (match, offset) =>
9989
offset || large
10090
? (match[1] || match[0]).toUpperCase()
10191
: match.toLowerCase()
10292
);
103-
}
10493

105-
export function uniqueID() {
106-
return (Date.now() + parseInt((Math.random() + '').slice(2))).toString(36);
107-
}
94+
export const uniqueID = () =>
95+
(Date.now() + parseInt((Math.random() + '').slice(2))).toString(36);
10896

109-
export function objectFrom<V, K extends string>(values: V[], keys: K[]) {
110-
return Object.fromEntries(
97+
/**
98+
* Encode string to Base64 with Unicode support
99+
*
100+
* @param input - String to encode
101+
* @returns Base64 encoded string
102+
*/
103+
export const encodeBase64 = (input: string) =>
104+
btoa(String.fromCharCode(...new TextEncoder().encode(input)));
105+
106+
/**
107+
* Decode Base64 string with Unicode support
108+
*
109+
* @param input - Base64 encoded string to decode
110+
* @returns Decoded Unicode string
111+
*/
112+
export const decodeBase64 = (input: string) =>
113+
new TextDecoder().decode(
114+
Uint8Array.from(atob(input), char => char.charCodeAt(0))
115+
);
116+
117+
export const objectFrom = <V, K extends string>(values: V[], keys: K[]) =>
118+
Object.fromEntries(
111119
values.map((value, index) => [keys[index], value])
112120
) as Record<K, V>;
113-
}
114121

115122
export enum DiffStatus {
116123
Old = -1,

test/DOM.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ object-fit: contain;`);
181181
it('should convert a Form to JSON', () => {
182182
document.body.innerHTML = `
183183
<form>
184-
<input type="checkbox" name="switch" checked>
184+
<input type="checkbox" name="switch">
185185
186186
<input type="checkbox" name="list" value="01" checked>
187187
<input type="checkbox" name="list" value="02">
@@ -215,7 +215,7 @@ object-fit: contain;`);
215215

216216
expect(data).toEqual(
217217
expect.objectContaining({
218-
switch: true,
218+
switch: false,
219219
list: ['01', '03'],
220220
array: [2, 3],
221221
test: { example: '123', other: 2 },

test/data.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './polyfill';
12
import 'core-js/proposals/promise-with-resolvers';
23
import {
34
likeNull,
@@ -8,6 +9,8 @@ import {
89
byteLength,
910
toHyphenCase,
1011
toCamelCase,
12+
encodeBase64,
13+
decodeBase64,
1114
objectFrom,
1215
DiffStatus,
1316
diffKeys,
@@ -103,6 +106,35 @@ describe('Data', () => {
103106
expect(toCamelCase('Small Camel')).toBe('smallCamel');
104107
});
105108

109+
it('should encode and decode Base64 with Unicode support', () => {
110+
// Test basic ASCII
111+
const ascii = 'Hello World';
112+
expect(decodeBase64(encodeBase64(ascii))).toBe(ascii);
113+
114+
// Test Unicode characters
115+
const unicode = 'Hello 世界 🌍 😀';
116+
expect(decodeBase64(encodeBase64(unicode))).toBe(unicode);
117+
118+
// Test various Unicode ranges
119+
const emoji = '🚀🎉🌟💖';
120+
expect(decodeBase64(encodeBase64(emoji))).toBe(emoji);
121+
122+
// Test mathematical symbols
123+
const math = '∑∏∫∆∇∂';
124+
expect(decodeBase64(encodeBase64(math))).toBe(math);
125+
126+
// Test empty string
127+
expect(decodeBase64(encodeBase64(''))).toBe('');
128+
129+
// Test known Base64 encoding
130+
expect(encodeBase64('Hello')).toBe('SGVsbG8=');
131+
expect(decodeBase64('SGVsbG8=')).toBe('Hello');
132+
133+
// Test known Unicode encoding
134+
expect(encodeBase64('世界')).toBe('5LiW55WM');
135+
expect(decodeBase64('5LiW55WM')).toBe('世界');
136+
});
137+
106138
it('should build an Object with Key & Value arrays', () => {
107139
expect(objectFrom([1, '2'], ['x', 'y'])).toStrictEqual({
108140
x: 1,

test/polyfill.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter';
2-
import { TextEncoder } from 'util';
2+
import { TextEncoder, TextDecoder } from 'util';
33
import { Crypto } from '@peculiar/webcrypto';
44
import 'intersection-observer';
55

6-
const polyfill = { TextEncoder, crypto: new Crypto() };
6+
const polyfill = { TextEncoder, TextDecoder, crypto: new Crypto() };
77

88
for (const [key, value] of Object.entries(polyfill))
99
Object.defineProperty(globalThis, key, { value });

0 commit comments

Comments
 (0)