Skip to content

Commit a0656d9

Browse files
committed
refactor
1 parent adb6765 commit a0656d9

File tree

2 files changed

+173
-166
lines changed

2 files changed

+173
-166
lines changed
Lines changed: 6 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -4,177 +4,17 @@ import { Plugin } from '@/types/plugin';
44
import { NovelStatus } from '@libs/novelStatus';
55
import { FilterTypes, Filters } from '@libs/filterInputs';
66
import { storage } from '@libs/storage';
7-
8-
const BASE64_ALPHABET =
9-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
10-
11-
const decodeBase64ToBytes = (encoded: string): number[] => {
12-
const normalized = encoded
13-
.replace(/-/g, '+')
14-
.replace(/_/g, '/')
15-
.replace(/\s+/g, '');
16-
const padded =
17-
normalized.length % 4 === 0
18-
? normalized
19-
: normalized + '='.repeat(4 - (normalized.length % 4));
20-
const bytes: number[] = [];
21-
22-
for (let i = 0; i < padded.length; i += 4) {
23-
const c1 = BASE64_ALPHABET.indexOf(padded.charAt(i));
24-
const c2 = BASE64_ALPHABET.indexOf(padded.charAt(i + 1));
25-
const c3Char = padded.charAt(i + 2);
26-
const c4Char = padded.charAt(i + 3);
27-
const c3 = c3Char === '=' ? 0 : BASE64_ALPHABET.indexOf(c3Char);
28-
const c4 = c4Char === '=' ? 0 : BASE64_ALPHABET.indexOf(c4Char);
29-
30-
if (
31-
c1 < 0 ||
32-
c2 < 0 ||
33-
(c3Char !== '=' && c3 < 0) ||
34-
(c4Char !== '=' && c4 < 0)
35-
) {
36-
continue;
37-
}
38-
39-
const bitStream = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
40-
bytes.push((bitStream >> 16) & 255);
41-
if (c3Char !== '=') {
42-
bytes.push((bitStream >> 8) & 255);
43-
}
44-
if (c4Char !== '=') {
45-
bytes.push(bitStream & 255);
46-
}
47-
}
48-
49-
return bytes;
50-
};
51-
52-
const utf8BytesToString = (bytes: number[]): string => {
53-
let out = '';
54-
let i = 0;
55-
56-
while (i < bytes.length) {
57-
const c = bytes[i++];
58-
59-
if (c < 128) {
60-
out += String.fromCharCode(c);
61-
} else if (c < 224) {
62-
out += String.fromCharCode(((c & 31) << 6) | (bytes[i++] & 63));
63-
} else if (c < 240) {
64-
out += String.fromCharCode(
65-
((c & 15) << 12) | ((bytes[i++] & 63) << 6) | (bytes[i++] & 63),
66-
);
67-
} else {
68-
let codePoint =
69-
((c & 7) << 18) |
70-
((bytes[i++] & 63) << 12) |
71-
((bytes[i++] & 63) << 6) |
72-
(bytes[i++] & 63);
73-
codePoint -= 65536;
74-
out += String.fromCharCode(
75-
55296 + (codePoint >> 10),
76-
56320 + (codePoint & 1023),
77-
);
78-
}
79-
}
80-
81-
return out;
82-
};
83-
84-
const decodeBase64Utf8 = (encoded: string) =>
85-
utf8BytesToString(decodeBase64ToBytes(encoded));
86-
87-
const decodeXorChunk = (encoded: string, key: string): string => {
88-
const input = decodeBase64ToBytes(encoded);
89-
if (!key) {
90-
return utf8BytesToString(input);
91-
}
92-
93-
const output: number[] = [];
94-
for (let i = 0; i < input.length; i++) {
95-
output.push(input[i] ^ key.charCodeAt(i % key.length));
96-
}
97-
return utf8BytesToString(output);
98-
};
99-
100-
const parseProtectedChunks = (raw: string): string[] => {
101-
if (!raw) {
102-
return [];
103-
}
104-
105-
try {
106-
const parsed = JSON.parse(raw) as unknown;
107-
if (Array.isArray(parsed)) {
108-
return parsed.filter((item): item is string => typeof item === 'string');
109-
}
110-
} catch {
111-
// fallback to single-chunk payload
112-
}
113-
114-
return [raw];
115-
};
116-
117-
const decodeProtectedContent = (
118-
mode: string,
119-
key: string,
120-
chunks: string[],
121-
): string => {
122-
if (!chunks.length) {
123-
return '';
124-
}
125-
126-
const sortedChunks = [...chunks].sort((a, b) => {
127-
const ai = Number.parseInt(a.substring(0, 4), 10);
128-
const bi = Number.parseInt(b.substring(0, 4), 10);
129-
if (Number.isNaN(ai) || Number.isNaN(bi)) {
130-
return 0;
131-
}
132-
return ai - bi;
133-
});
134-
135-
let content = '';
136-
137-
for (const chunk of sortedChunks) {
138-
const payload = /^\d{4}/.test(chunk) ? chunk.substring(4) : chunk;
139-
140-
if (mode === 'xor_shuffle') {
141-
content += decodeXorChunk(payload, key);
142-
} else if (mode === 'base64_reverse') {
143-
content += decodeBase64Utf8(payload.split('').reverse().join(''));
144-
} else {
145-
content += decodeBase64Utf8(payload);
146-
}
147-
}
148-
149-
return content.replace(
150-
/\[note(\d+)]/gi,
151-
'<span id="anchor-note$1" class="note-icon none-print inline note-tooltip" data-tooltip-content="#note$1 .note-content" data-note-id="note$1"><i class="fas fa-sticky-note"></i></span><a id="anchor-note$1" class="inline-print none" href="#note$1">[note]</a>',
152-
);
153-
};
154-
155-
const parseDmyToIso = (value: string): string | undefined => {
156-
const matched = value.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
157-
if (!matched) {
158-
return undefined;
159-
}
160-
161-
const day = Number(matched[1]);
162-
const month = Number(matched[2]) - 1;
163-
const year = Number(matched[3]);
164-
const date = new Date(year, month, day);
165-
166-
if (Number.isNaN(date.getTime())) {
167-
return undefined;
168-
}
169-
170-
return date.toISOString();
171-
};
7+
import {
8+
parseDmyToIso,
9+
parseProtectedChunks,
10+
decodeProtectedContent,
11+
} from './utils';
17212

17313
class HakoPlugin implements Plugin.PluginBase {
17414
id = 'ln.hako.vn';
17515
name = 'Hako Novel';
17616
icon = 'src/vi/hakolightnovel/icon.png';
177-
version = '1.1.20';
17+
version = '1.1.21';
17818

17919
pluginSettings = {
18020
usingDocln: {

plugins/vietnamese/LNHako/utils.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
2+
const BASE64_ALPHABET =
3+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
4+
5+
const decodeBase64ToBytes = (encoded: string): number[] => {
6+
const normalized = encoded
7+
.replace(/-/g, '+')
8+
.replace(/_/g, '/')
9+
.replace(/\s+/g, '');
10+
const padded =
11+
normalized.length % 4 === 0
12+
? normalized
13+
: normalized + '='.repeat(4 - (normalized.length % 4));
14+
const bytes: number[] = [];
15+
16+
for (let i = 0; i < padded.length; i += 4) {
17+
const c1 = BASE64_ALPHABET.indexOf(padded.charAt(i));
18+
const c2 = BASE64_ALPHABET.indexOf(padded.charAt(i + 1));
19+
const c3Char = padded.charAt(i + 2);
20+
const c4Char = padded.charAt(i + 3);
21+
const c3 = c3Char === '=' ? 0 : BASE64_ALPHABET.indexOf(c3Char);
22+
const c4 = c4Char === '=' ? 0 : BASE64_ALPHABET.indexOf(c4Char);
23+
24+
if (
25+
c1 < 0 ||
26+
c2 < 0 ||
27+
(c3Char !== '=' && c3 < 0) ||
28+
(c4Char !== '=' && c4 < 0)
29+
) {
30+
continue;
31+
}
32+
33+
const bitStream = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4;
34+
bytes.push((bitStream >> 16) & 255);
35+
if (c3Char !== '=') {
36+
bytes.push((bitStream >> 8) & 255);
37+
}
38+
if (c4Char !== '=') {
39+
bytes.push(bitStream & 255);
40+
}
41+
}
42+
43+
return bytes;
44+
};
45+
46+
const utf8BytesToString = (bytes: number[]): string => {
47+
let out = '';
48+
let i = 0;
49+
50+
while (i < bytes.length) {
51+
const c = bytes[i++];
52+
53+
if (c < 128) {
54+
out += String.fromCharCode(c);
55+
} else if (c < 224) {
56+
out += String.fromCharCode(((c & 31) << 6) | (bytes[i++] & 63));
57+
} else if (c < 240) {
58+
out += String.fromCharCode(
59+
((c & 15) << 12) | ((bytes[i++] & 63) << 6) | (bytes[i++] & 63),
60+
);
61+
} else {
62+
let codePoint =
63+
((c & 7) << 18) |
64+
((bytes[i++] & 63) << 12) |
65+
((bytes[i++] & 63) << 6) |
66+
(bytes[i++] & 63);
67+
codePoint -= 65536;
68+
out += String.fromCharCode(
69+
55296 + (codePoint >> 10),
70+
56320 + (codePoint & 1023),
71+
);
72+
}
73+
}
74+
75+
return out;
76+
};
77+
78+
const decodeBase64Utf8 = (encoded: string) =>
79+
utf8BytesToString(decodeBase64ToBytes(encoded));
80+
81+
const decodeXorChunk = (encoded: string, key: string): string => {
82+
const input = decodeBase64ToBytes(encoded);
83+
if (!key) {
84+
return utf8BytesToString(input);
85+
}
86+
87+
const output: number[] = [];
88+
for (let i = 0; i < input.length; i++) {
89+
output.push(input[i] ^ key.charCodeAt(i % key.length));
90+
}
91+
return utf8BytesToString(output);
92+
};
93+
94+
const parseProtectedChunks = (raw: string): string[] => {
95+
if (!raw) {
96+
return [];
97+
}
98+
99+
try {
100+
const parsed = JSON.parse(raw) as unknown;
101+
if (Array.isArray(parsed)) {
102+
return parsed.filter((item): item is string => typeof item === 'string');
103+
}
104+
} catch {
105+
// fallback to single-chunk payload
106+
}
107+
108+
return [raw];
109+
};
110+
111+
const decodeProtectedContent = (
112+
mode: string,
113+
key: string,
114+
chunks: string[],
115+
): string => {
116+
if (!chunks.length) {
117+
return '';
118+
}
119+
120+
const sortedChunks = [...chunks].sort((a, b) => {
121+
const ai = Number.parseInt(a.substring(0, 4), 10);
122+
const bi = Number.parseInt(b.substring(0, 4), 10);
123+
if (Number.isNaN(ai) || Number.isNaN(bi)) {
124+
return 0;
125+
}
126+
return ai - bi;
127+
});
128+
129+
let content = '';
130+
131+
for (const chunk of sortedChunks) {
132+
const payload = /^\d{4}/.test(chunk) ? chunk.substring(4) : chunk;
133+
134+
if (mode === 'xor_shuffle') {
135+
content += decodeXorChunk(payload, key);
136+
} else if (mode === 'base64_reverse') {
137+
content += decodeBase64Utf8(payload.split('').reverse().join(''));
138+
} else {
139+
content += decodeBase64Utf8(payload);
140+
}
141+
}
142+
143+
return content.replace(
144+
/\[note(\d+)]/gi,
145+
'<span id="anchor-note$1" class="note-icon none-print inline note-tooltip" data-tooltip-content="#note$1 .note-content" data-note-id="note$1"><i class="fas fa-sticky-note"></i></span><a id="anchor-note$1" class="inline-print none" href="#note$1">[note]</a>',
146+
);
147+
};
148+
149+
const parseDmyToIso = (value: string): string | undefined => {
150+
const matched = value.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
151+
if (!matched) {
152+
return undefined;
153+
}
154+
155+
const day = Number(matched[1]);
156+
const month = Number(matched[2]) - 1;
157+
const year = Number(matched[3]);
158+
const date = new Date(year, month, day);
159+
160+
if (Number.isNaN(date.getTime())) {
161+
return undefined;
162+
}
163+
164+
return date.toISOString();
165+
};
166+
167+
export { parseDmyToIso, parseProtectedChunks, decodeProtectedContent };

0 commit comments

Comments
 (0)