Markdown 基本语法

一级标题

二级标题

三级标题

四级标题

五级标题
六级标题

粗体 斜体 粗斜体 正常 删除线

单行块引用

多个段落的块引用

多个段落的块引用

嵌套块引用

嵌套块引用

带有其它元素的块引用

带有其它元素的块引用

带有其它元素的块引用

带有其它元素的块引用

  1. 有序列表
  2. 有序列表
  3. 有序列表
    1. 有序列表
    2. 有序列表
  4. 有序列表
  • 无序列表
  • 无序列表
  • 无序列表
    • 无序列表
    • 无序列表
  • 无序列表

转义反引号

分割线


链接

链接(带有标题)

https://www.lynx3.top/

email@example.com

带格式化的链接

图片

HTML 嵌入

Markdown 扩展语法

表格

左对齐居中对齐右对齐
/叫做斜线
|叫做管道符

代码框

编译语言

C++

来源:https://raw.githubusercontent.com/argvchs/fastio/master/fastio.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <string>
#include <string_view>
#include <type_traits>
namespace fastio {
namespace symbols {
enum symbol {
endl,
ends,
flush,
bin,
oct,
dec,
hex,
left,
right,
boolalpha,
noboolalpha,
showbase,
noshowbase,
showpoint,
noshowpoint,
showpos,
noshowpos,
ws,
uppercase,
lowercase,
fixed,
defaultfloat,
reset
};
struct setbase {
int base;
setbase(int n) : base(n) {}
};
struct setfill {
char fill;
setfill(char c) : fill(c) {}
};
struct setprecision {
int precision;
setprecision(int n) : precision(n) {}
};
struct setw {
int width;
setw(int n) : width(n) {}
};
} // namespace symbols
namespace interface {
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
template <typename T>
constexpr bool is_signed_v =
(std::is_integral_v<T> && std::is_signed_v<T>) || std::is_same_v<T, i128>;
template <typename T>
constexpr bool is_unsigned_v =
(std::is_integral_v<T> && std::is_unsigned_v<T>) || std::is_same_v<T, u128>;
template <typename T> constexpr bool is_integral_v = is_signed_v<T> || is_unsigned_v<T>;
template <typename T> constexpr bool is_floating_v = std::is_floating_point_v<T>;
template <typename T> struct make_unsigned : public std::make_unsigned<T> {};
template <> struct make_unsigned<i128> : public std::type_identity<u128> {};
template <> struct make_unsigned<u128> : public std::type_identity<u128> {};
template <typename T> using make_unsigned_t = typename make_unsigned<T>::type;
struct noncopyable {
noncopyable() = default;
virtual ~noncopyable() = default;
noncopyable(const noncopyable &) = delete;
noncopyable &operator=(const noncopyable &) = delete;
};
class istream : public noncopyable {
private:
int base = 10;
bool unget = false, eof = false, fail = false;
char chr = '\0';
static bool isssign(char c) { return isspace(c) || c == '+' || c == '-'; }
int todigit(char c) {
if (::isdigit(c)) return c - '0';
if (isupper(c)) return c - 'A' + 10;
if (islower(c)) return c - 'a' + 10;
return base;
}
bool isdigit(char c) { return todigit(c) < base; }
virtual char vget() = 0;
public:
char get() {
if (!unget)
if ((chr = vget()) == EOF) eof = true;
unget = false;
return chr;
}
istream &get(char &c) {
c = get();
return *this;
}
explicit operator bool() { return !fail; }
bool operator!() { return fail; }
template <typename T, std::enable_if_t<is_integral_v<T>, int> = 0>
istream &operator>>(T &n) {
n = 0;
bool f = false;
while (isssign(get()) && !eof)
if (chr == '-' && is_integral_v<T>) f = !f;
if (eof) return fail = true, *this;
unget = true;
while (isdigit(get())) n = n * base + todigit(chr);
if (f) n = -n;
unget = true;
return *this;
}
template <typename T, std::enable_if_t<is_floating_v<T>, int> = 0>
istream &operator>>(T &n) {
n = 0;
bool f = false;
while (isssign(get()) && !eof)
if (chr == '-') f = !f;
if (eof) return fail = true, *this;
unget = true;
while (isdigit(get())) n = n * base + todigit(chr);
if (chr == '.') {
i64 pow = 1;
while (isdigit(get())) n += todigit(chr) / (T)(pow *= base);
}
if (f) n = -n;
unget = true;
return *this;
}
istream &operator>>(char &c) {
c = '\0';
while (isspace(get()) && !eof);
if (eof) return fail = true, *this;
c = chr;
return *this;
}
istream &operator>>(bool &f) {
i64 n;
*this >> n;
f = (bool)n;
return *this;
}
istream &operator>>(char *s) {
s[0] = '\0';
int len = 0;
while (isspace(get()) && !eof);
if (eof) return fail = true, *this;
unget = true;
while (isgraph(get())) s[len++] = chr;
unget = true, s[len] = '\0';
return *this;
}
istream &operator>>(std::string &s) {
s.clear();
while (isspace(get()) && !eof);
if (eof) return fail = true, *this;
unget = true;
while (isgraph(get())) s.push_back(chr);
unget = true;
return *this;
}
istream &operator>>(symbols::symbol a) {
switch (a) {
case symbols::bin: base = 2; break;
case symbols::oct: base = 8; break;
case symbols::dec: base = 10; break;
case symbols::hex: base = 16; break;
case symbols::ws:
while (isspace(get()) && !eof);
unget = true;
break;
default: base = 10;
}
return *this;
}
istream &operator>>(symbols::setbase a) {
base = std::max(std::min(a.base, 36), 2);
return *this;
}
istream &ignore(char end = '\n') {
while (get() != end && !eof);
if (eof) return fail = true, *this;
return *this;
}
istream &getline(char *s, char end = '\n') {
s[0] = '\0';
int len = 0;
if (eof) return fail = true, *this;
while (get() != end && !eof) s[len++] = chr;
if (s[len - 1] == '\r' && end == '\n') --len;
s[len] = '\0';
return *this;
}
istream &getline(std::string &s, char end = '\n') {
s.clear();
if (eof) return fail = true, *this;
while (get() != end && !eof) s.push_back(chr);
if (s.back() == '\r' && end == '\n') s.pop_back();
return *this;
}
istream &get(char *s, char end = '\n') {
getline(s, end);
unget = true;
return *this;
}
istream &get(std::string &s, char end = '\n') {
getline(s, end);
unget = true;
return *this;
}
};
class ostream : public noncopyable {
private:
int base = 10, precision = 6, width = 0;
i64 eps = 1e6;
bool adjust = true, boolalpha = false, showbase = false, showpoint = false,
showpos = false, kase = false, fixed = false;
char setfill = ' ';
static i64 qpow(i64 n, int m) {
i64 ret = 1;
for (int i = m; i; i >>= 1, n *= n)
if (i & 1) ret *= n;
return ret;
}
void fill(int n) {
if (width > n) vfill(setfill, width - n);
width = 0;
}
char toalpha(int n) {
if (n < 10) return n + '0';
return n - 10 + (kase ? 'A' : 'a');
}
virtual void vput(char) = 0;
virtual void vputs(const char *, int) = 0;
virtual void vfill(char, int) = 0;
virtual void vflush() = 0;
public:
void put(char c) { vput(c); }
template <typename T, std::enable_if_t<is_integral_v<T>, int> = 0>
ostream &operator<<(T n) {
static char buf[105];
char *p = buf + 100, *q = buf + 100;
bool f = n < 0;
if (f) n = -n;
make_unsigned_t<T> m = n;
if (!m) *p-- = '0';
while (m) *p-- = toalpha(m % base), m /= base;
if (showbase) switch (base) {
case 2: *p-- = kase ? 'B' : 'b', *p-- = '0'; break;
case 8: *p-- = '0'; break;
case 16: *p-- = kase ? 'X' : 'x', *p-- = '0'; break;
}
if (!f) {
if (showpos && is_signed_v<T>) *p-- = '+';
} else *p-- = '-';
if (adjust) fill(q - p);
vputs(p + 1, q - p);
if (!adjust) fill(q - p);
return *this;
}
template <typename T, std::enable_if_t<is_floating_v<T>, int> = 0>
ostream &operator<<(T n) {
static char buf1[105], buf2[105];
if (std::isinf(n)) {
if (n > 0) {
if (showpos) *this << (kase ? "+INF" : "+inf");
else *this << (kase ? "INF" : "inf");
} else *this << (kase ? "-INF" : "-inf");
return *this;
}
if (std::isnan(n)) return *this << (kase ? "NAN" : "nan");
char *p1 = buf1 + 100, *q1 = buf1 + 100, *p2 = buf2 + 100, *q2 = buf2 + 100;
bool f = n < 0;
if (f) n = -n;
i64 m1 = std::floor(n), m2 = std::round((n - m1) * eps);
int len = precision;
if (m2 >= eps) ++m1, m2 = 0;
if (!m1) *p1-- = '0';
while (m1) *p1-- = toalpha(m1 % base), m1 /= base;
while (len--) *p2-- = toalpha(m2 % base), m2 /= base;
if (showbase) switch (base) {
case 2: *p1-- = kase ? 'B' : 'b', *p1-- = '0'; break;
case 8: *p1-- = '0'; break;
case 16: *p1-- = kase ? 'X' : 'x', *p1-- = '0'; break;
}
if (!f) {
if (showpos) *p1-- = '+';
} else *p1-- = '-';
if (!fixed)
while (*q2 == '0' && p2 != q2) --q2;
if (showpoint || p2 != q2) *p2-- = '.';
if (adjust) fill((q1 - p1) + (q2 - p2));
vputs(p1 + 1, q1 - p1);
vputs(p2 + 1, q2 - p2);
if (!adjust) fill((q1 - p1) + (q2 - p2));
return *this;
}
ostream &operator<<(char c) {
if (adjust) fill(1);
vput(c);
if (!adjust) fill(1);
return *this;
}
ostream &operator<<(const char *s) {
int n = strlen(s);
if (adjust) fill(n);
vputs(s, n);
if (!adjust) fill(n);
return *this;
}
ostream &operator<<(const std::string &s) {
int n = s.size();
if (adjust) fill(n);
vputs(s.data(), n);
if (!adjust) fill(n);
return *this;
}
ostream &operator<<(std::string_view sv) {
int n = sv.size();
if (adjust) fill(n);
vputs(sv.data(), n);
if (!adjust) fill(n);
return *this;
}
ostream &operator<<(bool f) {
if (f) {
if (boolalpha) *this << (kase ? "TRUE" : "true");
else *this << '1';
} else {
if (boolalpha) *this << (kase ? "FALSE" : "false");
else *this << '0';
}
return *this;
}
ostream &operator<<(const void *p) {
int n = base;
bool f = showbase;
base = 16, showbase = true;
*this << (u64)p;
base = n, showbase = f;
return *this;
}
ostream &operator<<(std::nullptr_t) {
return *this << (kase ? "NULLPTR" : "nullptr");
}
ostream &operator<<(symbols::symbol a) {
switch (a) {
case symbols::endl: vput('\n'); break;
case symbols::ends: vput(' '); break;
case symbols::flush: vflush(); break;
case symbols::bin: eps = qpow(base = 2, precision); break;
case symbols::oct: eps = qpow(base = 8, precision); break;
case symbols::dec: eps = qpow(base = 10, precision); break;
case symbols::hex: eps = qpow(base = 16, precision); break;
case symbols::left: adjust = false; break;
case symbols::right: adjust = true; break;
case symbols::boolalpha: boolalpha = true; break;
case symbols::noboolalpha: boolalpha = false; break;
case symbols::showbase: showbase = true; break;
case symbols::noshowbase: showbase = false; break;
case symbols::showpoint: showpoint = true; break;
case symbols::noshowpoint: showpoint = false; break;
case symbols::showpos: showpos = true; break;
case symbols::noshowpos: showpos = false; break;
case symbols::uppercase: kase = true; break;
case symbols::lowercase: kase = false; break;
case symbols::fixed: fixed = true; break;
case symbols::defaultfloat: fixed = false; break;
default:
base = 10, precision = 6, width = 0, eps = 1e6;
adjust = true;
boolalpha = showbase = showpoint = showpos = kase = fixed = false;
setfill = ' ';
}
return *this;
}
ostream &operator<<(symbols::setbase a) {
base = std::max(std::min(a.base, 36), 2);
eps = qpow(base, precision);
return *this;
}
ostream &operator<<(symbols::setfill a) {
setfill = a.fill;
return *this;
}
ostream &operator<<(symbols::setprecision a) {
precision = std::max(a.precision, 0);
eps = qpow(base, precision);
return *this;
}
ostream &operator<<(symbols::setw a) {
width = std::max(a.width, 0);
return *this;
}
};
} // namespace interface
const int SIZ = 0xfffff;
class istream : public interface::istream {
private:
char buf[SIZ], *p = buf, *q = buf;
virtual char vget() final {
if (p == q) {
int len = fread(buf, 1, SIZ, stream);
if (!len) return EOF;
p = buf, q = buf + len;
}
return *p++;
}
protected:
FILE *stream = stdin;
public:
virtual ~istream() { fclose(stream); }
};
class ifstream : public istream {
public:
explicit ifstream(FILE *p) { istream::stream = p; }
explicit ifstream(const char *s) { istream::stream = fopen(s, "r"); }
};
class ostream : public interface::ostream {
private:
char buf[SIZ], *p = buf;
virtual void vput(char c) final {
if (p - buf >= SIZ) vflush();
*p++ = c;
}
virtual void vputs(const char *s, int n) final {
int used = p - buf, len = 0;
while (n - len + used >= SIZ) {
memcpy(buf + used, s + len, SIZ - used);
p = buf + SIZ;
vflush();
len += SIZ - used, used = 0;
}
memcpy(buf + used, s + len, n - len);
p = buf + used + n - len;
}
virtual void vfill(char c, int n) final {
int used = p - buf, len = 0;
while (n - len + used >= SIZ) {
memset(buf + used, c, SIZ - used);
p = buf + SIZ;
vflush();
len += SIZ - used, used = 0;
}
memset(buf + used, c, n - len);
p = buf + used + n - len;
}
virtual void vflush() final {
fwrite(buf, 1, p - buf, stream);
p = buf;
fflush(stream);
}
protected:
FILE *stream = stdout;
public:
virtual ~ostream() {
vflush();
fclose(stream);
}
};
class ofstream : public ostream {
public:
explicit ofstream(FILE *p) { ostream::stream = p; }
explicit ofstream(const char *s) { ostream::stream = fopen(s, "w"); }
};
static istream is;
static ostream os;
}; // namespace fastio

Java

来源:https://raw.githubusercontent.com/f-droid/fdroidclient/master/app/src/full/java/javax/jmdns/impl/FDroidServiceInfo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package javax.jmdns.impl;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.util.ByteWrangler;
/**
* The ServiceInfo class needs to be serialized in order to be sent as an Android broadcast.
* In order to make it Parcelable (or Serializable for that matter), there are some package-scope
* methods which needed to be used. Thus, this class is in the javax.jmdns.impl package so that
* it can access those methods. This is as an alternative to modifying the source code of JmDNS.
*/
public class FDroidServiceInfo extends ServiceInfoImpl implements Parcelable {
public FDroidServiceInfo(ServiceInfo info) {
super(info);
}
/**
* Return the fingerprint of the signing key, or {@code null} if it is not set.
*/
public String getFingerprint() {
// getPropertyString() will return "true" if the value is a zero-length byte array
// so we just do a custom version using getPropertyBytes()
byte[] data = getPropertyBytes("fingerprint");
if (data == null || data.length == 0) {
return null;
}
String fingerprint = ByteWrangler.readUTF(data, 0, data.length);
if (TextUtils.isEmpty(fingerprint)) {
return null;
}
return fingerprint;
}
public String getRepoAddress() {
return getURL(); // Automatically appends the "path" property if present, so no need to do it ourselves.
}
private static byte[] readBytes(Parcel in) {
byte[] bytes = new byte[in.readInt()];
in.readByteArray(bytes);
return bytes;
}
public FDroidServiceInfo(Parcel in) {
super(
in.readString(),
in.readString(),
in.readString(),
in.readInt(),
in.readInt(),
in.readInt(),
in.readByte() != 0,
readBytes(in));
int addressCount = in.readInt();
for (int i = 0; i < addressCount; i++) {
try {
addAddress((Inet4Address) Inet4Address.getByAddress(readBytes(in)));
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
addressCount = in.readInt();
for (int i = 0; i < addressCount; i++) {
try {
addAddress((Inet6Address) Inet6Address.getByAddress(readBytes(in)));
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(getType());
dest.writeString(getName());
dest.writeString(getSubtype());
dest.writeInt(getPort());
dest.writeInt(getWeight());
dest.writeInt(getPriority());
dest.writeByte(isPersistent() ? (byte) 1 : (byte) 0);
dest.writeInt(getTextBytes().length);
dest.writeByteArray(getTextBytes());
dest.writeInt(getInet4Addresses().length);
for (int i = 0; i < getInet4Addresses().length; i++) {
Inet4Address address = getInet4Addresses()[i];
dest.writeInt(address.getAddress().length);
dest.writeByteArray(address.getAddress());
}
dest.writeInt(getInet6Addresses().length);
for (int i = 0; i < getInet6Addresses().length; i++) {
Inet6Address address = getInet6Addresses()[i];
dest.writeInt(address.getAddress().length);
dest.writeByteArray(address.getAddress());
}
}
public static final Parcelable.Creator<FDroidServiceInfo> CREATOR = new Parcelable.Creator<FDroidServiceInfo>() {
public FDroidServiceInfo createFromParcel(Parcel source) {
return new FDroidServiceInfo(source);
}
public FDroidServiceInfo[] newArray(int size) {
return new FDroidServiceInfo[size];
}
};
}

Kotlin

来源:https://raw.githubusercontent.com/f-droid/fdroidclient/master/libs/database/src/main/java/org/fdroid/repo/RepoAdder.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
package org.fdroid.repo
import android.content.Context
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.UserManager
import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES
import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
import androidx.annotation.AnyThread
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat.getSystemService
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import mu.KotlinLogging
import org.fdroid.database.AppOverviewItem
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.MinimalApp
import org.fdroid.database.NewRepository
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.DownloaderFactory
import org.fdroid.download.HttpManager
import org.fdroid.download.HttpManager.Companion.isInvalidHttpUrl
import org.fdroid.download.NotFoundException
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.RepoUriBuilder
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_INDEX
import org.fdroid.repo.AddRepoError.ErrorType.IO_ERROR
import org.fdroid.repo.AddRepoError.ErrorType.IS_ARCHIVE_REPO
import org.fdroid.repo.AddRepoError.ErrorType.UNKNOWN_SOURCES_DISALLOWED
import java.io.IOException
import java.net.Proxy
import kotlin.coroutines.CoroutineContext
internal const val REPO_ID = 0L
public sealed class AddRepoState
public object None : AddRepoState()
public class Fetching(
public val repo: Repository?,
public val apps: List<MinimalApp>,
public val fetchResult: FetchResult?,
/**
* true if fetching is complete.
*/
public val done: Boolean = false,
) : AddRepoState() {
/**
* true if the repository can be added (be it as new [Repository] or new mirror).
*/
public val canAdd: Boolean = repo != null &&
(fetchResult != null && fetchResult !is FetchResult.IsExistingRepository)
override fun toString(): String {
return "Fetching(repo=${repo?.address}, apps=${apps.size}, fetchResult=$fetchResult, " +
"done=$done, canAdd=$canAdd)"
}
}
public object Adding : AddRepoState()
public class Added(
public val repo: Repository,
) : AddRepoState()
public data class AddRepoError(
public val errorType: ErrorType,
public val exception: Exception? = null,
) : AddRepoState() {
public enum class ErrorType {
UNKNOWN_SOURCES_DISALLOWED,
INVALID_FINGERPRINT,
IS_ARCHIVE_REPO,
INVALID_INDEX,
IO_ERROR,
}
}
public sealed class FetchResult {
public data class IsNewRepository(internal val addUrl: String) : FetchResult()
public data class IsNewMirror(
internal val existingRepoId: Long,
internal val newMirrorUrl: String,
) : FetchResult()
public object IsExistingRepository : FetchResult()
}
@OptIn(DelicateCoroutinesApi::class)
internal class RepoAdder(
private val context: Context,
private val db: FDroidDatabase,
private val tempFileProvider: TempFileProvider,
private val downloaderFactory: DownloaderFactory,
private val httpManager: HttpManager,
private val repoUriGetter: RepoUriGetter = RepoUriGetter,
private val repoUriBuilder: RepoUriBuilder = defaultRepoUriBuilder,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) {
private val log = KotlinLogging.logger {}
private val repositoryDao = db.getRepositoryDao() as RepositoryDaoInt
internal val addRepoState: MutableStateFlow<AddRepoState> = MutableStateFlow(None)
private var fetchJob: Job? = null
internal fun fetchRepository(url: String, proxy: Proxy?) {
fetchJob = GlobalScope.launch(coroutineContext) {
fetchRepositoryInt(url, proxy)
}
}
@WorkerThread
@VisibleForTesting
internal suspend fun fetchRepositoryInt(
url: String,
proxy: Proxy? = null,
) {
if (hasDisallowInstallUnknownSources(context)) {
addRepoState.value = AddRepoError(UNKNOWN_SOURCES_DISALLOWED)
return
}
// get repo url and fingerprint
val nUri = repoUriGetter.getUri(url)
log.info("Parsed URI: $nUri")
if (nUri.uri.scheme !in listOf("content", "file") &&
isInvalidHttpUrl(nUri.uri.toString())
) {
val e = IllegalArgumentException("Unsupported URI: ${nUri.uri}")
addRepoState.value = AddRepoError(INVALID_INDEX, e)
return
}
if (nUri.uri.lastPathSegment == "archive") {
addRepoState.value = AddRepoError(IS_ARCHIVE_REPO)
return
}
// some plumping to receive the repo preview
var receivedRepo: Repository? = null
val apps = ArrayList<AppOverviewItem>()
var fetchResult: FetchResult? = null
val receiver = object : RepoPreviewReceiver {
override fun onRepoReceived(repo: Repository) {
receivedRepo = repo
if (repo.address in knownRepos) {
val knownFingerprint = knownRepos[repo.address]
if (knownFingerprint != repo.fingerprint) throw SigningException(
"Known fingerprint different from given one: ${repo.fingerprint}"
)
}
fetchResult = getFetchResult(nUri.uri.toString(), repo)
addRepoState.value = Fetching(receivedRepo, apps.toList(), fetchResult)
}
override fun onAppReceived(app: AppOverviewItem) {
apps.add(app)
addRepoState.value = Fetching(receivedRepo, apps.toList(), fetchResult)
}
}
// set a state early, so the ui can show progress animation
addRepoState.value = Fetching(receivedRepo, apps, fetchResult)
// try fetching repo with v2 format first and fallback to v1
try {
fetchRepo(nUri.uri, nUri.fingerprint, proxy, nUri.username, nUri.password, receiver)
} catch (e: SigningException) {
log.error(e) { "Error verifying repo with given fingerprint." }
addRepoState.value = AddRepoError(INVALID_FINGERPRINT, e)
return
} catch (e: IOException) {
log.error(e) { "Error fetching repo." }
addRepoState.value = AddRepoError(IO_ERROR, e)
return
} catch (e: SerializationException) {
log.error(e) { "Error fetching repo." }
addRepoState.value = AddRepoError(INVALID_INDEX, e)
return
} catch (e: NotFoundException) { // v1 repos can also have 404
log.error(e) { "Error fetching repo." }
addRepoState.value = AddRepoError(INVALID_INDEX, e)
return
}
// set final result
val finalRepo = receivedRepo
if (finalRepo == null) {
addRepoState.value = AddRepoError(INVALID_INDEX)
} else {
addRepoState.value = Fetching(finalRepo, apps, fetchResult, done = true)
}
}
private suspend fun fetchRepo(
uri: Uri,
fingerprint: String?,
proxy: Proxy?,
username: String?,
password: String?,
receiver: RepoPreviewReceiver,
) {
try {
val repo =
getTempRepo(uri, IndexFormatVersion.TWO, username, password)
val repoFetcher = RepoV2Fetcher(
tempFileProvider, downloaderFactory, httpManager, repoUriBuilder, proxy
)
repoFetcher.fetchRepo(uri, repo, receiver, fingerprint)
} catch (e: NotFoundException) {
log.warn(e) { "Did not find v2 repo, trying v1 now." }
// try to fetch v1 repo
val repo =
getTempRepo(uri, IndexFormatVersion.ONE, username, password)
val repoFetcher = RepoV1Fetcher(tempFileProvider, downloaderFactory, repoUriBuilder)
repoFetcher.fetchRepo(uri, repo, receiver, fingerprint)
}
}
private fun getFetchResult(url: String, repo: Repository): FetchResult {
val cert = repo.certificate ?: error("Certificate was null")
val existingRepo = repositoryDao.getRepository(cert)
return if (existingRepo == null) {
FetchResult.IsNewRepository(url)
} else {
val existingMirror = if (existingRepo.address.trimEnd('/') == url) {
url
} else {
existingRepo.mirrors.find { it.url.trimEnd('/') == url }
?: existingRepo.userMirrors.find { it.trimEnd('/') == url }
}
if (existingMirror == null) {
FetchResult.IsNewMirror(existingRepo.repoId, url)
} else {
FetchResult.IsExistingRepository
}
}
}
@WorkerThread
internal fun addFetchedRepository(): Repository? {
// prevent double calls (e.g. caused by double tapping a UI button)
if (addRepoState.compareAndSet(Adding, Adding)) return null
// cancel fetch preview job, so it stops emitting new states
fetchJob?.cancel()
// get current state before changing it
val state = (addRepoState.value as? Fetching)
?: throw IllegalStateException("Unexpected state: ${addRepoState.value}")
addRepoState.value = Adding
val repo = state.repo ?: throw IllegalStateException("No repo: ${addRepoState.value}")
val fetchResult = state.fetchResult
?: throw IllegalStateException("No fetchResult: ${addRepoState.value}")
val modifiedRepo: Repository = when (fetchResult) {
is FetchResult.IsExistingRepository -> error("Unexpected result: $fetchResult")
is FetchResult.IsNewRepository -> {
// reset the timestamp of the actual repo,
// so a following repo update will pick this up
val newRepo = NewRepository(
name = repo.repository.name,
icon = repo.repository.icon ?: emptyMap(),
address = repo.address,
formatVersion = repo.formatVersion,
certificate = repo.certificate ?: error("Repo had no certificate"),
username = repo.username,
password = repo.password,
)
db.runInTransaction<Repository> {
val repoId = repositoryDao.insert(newRepo)
// add user mirror, if URL is not the repo address and not a known mirror
if (fetchResult.addUrl != repo.address.trimEnd('/') &&
repo.mirrors.find { fetchResult.addUrl == it.url.trimEnd('/') } == null
) {
val userMirrors = listOf(fetchResult.addUrl)
repositoryDao.updateUserMirrors(repoId, userMirrors)
}
repositoryDao.getRepository(repoId) ?: error("New repository not found in DB")
}
}
is FetchResult.IsNewMirror -> {
val repoId = fetchResult.existingRepoId
db.runInTransaction<Repository> {
val existingRepo = repositoryDao.getRepository(repoId)
?: error("No repo with $repoId")
val userMirrors = existingRepo.userMirrors.toMutableList().apply {
add(fetchResult.newMirrorUrl)
}
repositoryDao.updateUserMirrors(repoId, userMirrors)
existingRepo
}
}
}
addRepoState.value = Added(modifiedRepo)
return modifiedRepo
}
internal fun abortAddingRepo() {
addRepoState.value = None
fetchJob?.cancel()
}
@AnyThread
internal suspend fun addArchiveRepo(repo: Repository, proxy: Proxy? = null) =
withContext(coroutineContext) {
if (repo.isArchiveRepo) error { "Repo ${repo.address} is already an archive repo." }
val address = repo.address.replace(Regex("repo/?$"), "archive")
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
val receiver = object : RepoPreviewReceiver {
override fun onRepoReceived(archiveRepo: Repository) {
// reset the timestamp of the actual repo,
// so a following repo update will pick this up
val newRepo = NewRepository(
name = archiveRepo.repository.name,
icon = archiveRepo.repository.icon ?: emptyMap(),
address = archiveRepo.address,
formatVersion = archiveRepo.formatVersion,
certificate = archiveRepo.certificate ?: error("Repo had no certificate"),
username = archiveRepo.username,
password = archiveRepo.password,
)
db.runInTransaction {
val repoId = repositoryDao.insert(newRepo)
repositoryDao.setWeight(repoId, repo.weight - 1)
}
cancel("expected") // no need to continue downloading the entire repo
}
override fun onAppReceived(app: AppOverviewItem) {
// no-op
}
}
val uri = Uri.parse(address)
fetchRepo(uri, repo.fingerprint, proxy, repo.username, repo.password, receiver)
}
private fun hasDisallowInstallUnknownSources(context: Context): Boolean {
val userManager = getSystemService(context, UserManager::class.java)
?: error("No UserManager available.")
return if (SDK_INT < 29) userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES)
else userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES) ||
userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY)
}
private fun getTempRepo(
uri: Uri,
indexFormatVersion: IndexFormatVersion,
username: String?,
password: String?,
) = Repository(
repoId = REPO_ID,
address = uri.toString(),
timestamp = -1L,
formatVersion = indexFormatVersion,
certificate = null,
version = 0L,
weight = 0,
lastUpdated = -1L,
username = username,
password = password,
)
}
internal val defaultRepoUriBuilder = RepoUriBuilder { repo, pathElements ->
val builder = Uri.parse(repo.address).buildUpon()
pathElements.forEach { builder.appendEncodedPath(it) }
builder.build()
}

解释语言

Python

来源:https://raw.githubusercontent.com/yt-dlp/yt-dlp/master/yt_dlp/downloader/dash.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import time
import urllib.parse
from . import get_suitable_downloader
from .fragment import FragmentFD
from ..utils import update_url_query, urljoin

class DashSegmentsFD(FragmentFD):
"""
Download segments in a DASH manifest. External downloaders can take over
the fragment downloads by supporting the 'dash_frag_urls' protocol
"""
FD_NAME = 'dashsegments'
def real_download(self, filename, info_dict):
if 'http_dash_segments_generator' in info_dict['protocol'].split('+'):
real_downloader = None # No external FD can support --live-from-start
else:
if info_dict.get('is_live'):
self.report_error('Live DASH videos are not supported')
real_downloader = get_suitable_downloader(
info_dict, self.params, None, protocol='dash_frag_urls', to_stdout=(filename == '-'))
real_start = time.time()
requested_formats = [{**info_dict, **fmt} for fmt in info_dict.get('requested_formats', [])]
args = []
for fmt in requested_formats or [info_dict]:
try:
fragment_count = 1 if self.params.get('test') else len(fmt['fragments'])
except TypeError:
fragment_count = None
ctx = {
'filename': fmt.get('filepath') or filename,
'live': 'is_from_start' if fmt.get('is_from_start') else fmt.get('is_live'),
'total_frags': fragment_count,
}
if real_downloader:
self._prepare_external_frag_download(ctx)
else:
self._prepare_and_start_frag_download(ctx, fmt)
ctx['start'] = real_start
extra_query = None
extra_param_to_segment_url = info_dict.get('extra_param_to_segment_url')
if extra_param_to_segment_url:
extra_query = urllib.parse.parse_qs(extra_param_to_segment_url)
fragments_to_download = self._get_fragments(fmt, ctx, extra_query)
if real_downloader:
self.to_screen(
f'[{self.FD_NAME}] Fragment downloads will be delegated to {real_downloader.get_basename()}')
info_dict['fragments'] = list(fragments_to_download)
fd = real_downloader(self.ydl, self.params)
return fd.real_download(filename, info_dict)
args.append([ctx, fragments_to_download, fmt])
return self.download_and_append_fragments_multiple(*args, is_fatal=lambda idx: idx == 0)
def _resolve_fragments(self, fragments, ctx):
fragments = fragments(ctx) if callable(fragments) else fragments
return [next(iter(fragments))] if self.params.get('test') else fragments
def _get_fragments(self, fmt, ctx, extra_query):
fragment_base_url = fmt.get('fragment_base_url')
fragments = self._resolve_fragments(fmt['fragments'], ctx)
frag_index = 0
for i, fragment in enumerate(fragments):
frag_index += 1
if frag_index <= ctx['fragment_index']:
continue
fragment_url = fragment.get('url')
if not fragment_url:
assert fragment_base_url
fragment_url = urljoin(fragment_base_url, fragment['path'])
if extra_query:
fragment_url = update_url_query(fragment_url, extra_query)
yield {
'frag_index': frag_index,
'fragment_count': fragment.get('fragment_count'),
'index': i,
'url': fragment_url,
}

Shell

来源:https://raw.githubusercontent.com/f-droid/fdroidclient/master/create_ota.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/bin/bash
#
# Script to prepare an update.zip containing F-Droid

set -e

PROG_DIR=$(dirname $(realpath $0))

TMP_DIR=$(mktemp -d -t fdroidclient.tmp.XXXXXXXX)
trap "rm -rf $TMP_DIR" EXIT

function error() {
echo "*** ERROR: " $@
usage
}

function usage() {
cat << EOFU
Usage: $0 variant
where:
- variant is one of: debug, release, or binary
EOFU
exit 1
}

# Parse input
VARIANT="$1"
[[ -z "$VARIANT" ]] && error "Missing variant"

VERSIONCODE=$2

GPG="gpg --keyring $PROG_DIR/f-droid.org-signing-key.gpg --no-default-keyring --trust-model always"

GITVERSION=$(git describe --tags --always)

FDROID_APK=F-Droid.apk

# Collect files
mkdir -p $TMP_DIR/META-INF/com/google/android/
cp app/src/main/scripts/update-binary $TMP_DIR/META-INF/com/google/android/

if [ $VARIANT == "binary" ] ; then
if [ -z $VERSIONCODE ]; then
curl -L https://f-droid.org/$FDROID_APK > $TMP_DIR/$FDROID_APK
curl -L https://f-droid.org/${FDROID_APK}.asc > $TMP_DIR/${FDROID_APK}.asc
else
GITVERSION=$VERSIONCODE
DL_APK=org.fdroid.fdroid_${VERSIONCODE}.apk
curl -L https://f-droid.org/repo/$DL_APK > $TMP_DIR/$FDROID_APK
curl -L https://f-droid.org/repo/${DL_APK}.asc > $TMP_DIR/${FDROID_APK}.asc
fi
$GPG --verify $TMP_DIR/${FDROID_APK}.asc
rm $TMP_DIR/${FDROID_APK}.asc
else
cd $PROG_DIR
./gradlew assemble$(echo $VARIANT | tr 'dr' 'DR')
OUT_DIR=$PROG_DIR/app/build/outputs/apk
if [ $VARIANT == "debug" ]; then
cp $OUT_DIR/app-${VARIANT}.apk \
$TMP_DIR/$FDROID_APK
elif [ -f $OUT_DIR/app-${VARIANT}-signed.apk ]; then
cp $OUT_DIR/app-${VARIANT}-signed.apk \
$TMP_DIR/$FDROID_APK
else
cp $OUT_DIR/app-${VARIANT}-unsigned.apk \
$TMP_DIR/$FDROID_APK
fi
fi

# Make zip
if [ $VARIANT == "binary" ] ; then
ZIPBASE=F-DroidFromBinaries-${GITVERSION}
else
ZIPBASE=F-Droid-${GITVERSION}
fi
if [ $VARIANT == "debug" ]; then
ZIP=${ZIPBASE}-debug.zip
else
ZIP=${ZIPBASE}.zip
fi
OUT_DIR=$PROG_DIR/app/build/distributions
mkdir -p $OUT_DIR
[ -f $OUT_DIR/$ZIP ] && rm -f $OUT_DIR/$ZIP
pushd $TMP_DIR
zip -r $OUT_DIR/$ZIP .
popd

JavaScript

来源:https://raw.githubusercontent.com/jerryc127/hexo-theme-butterfly/master/source/js/search/local-search.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/**
* Refer to hexo-generator-searchdb
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
* Modified by hexo-theme-butterfly
*/
class LocalSearch {
constructor ({
path = '',
unescape = false,
top_n_per_article = 1
}) {
this.path = path
this.unescape = unescape
this.top_n_per_article = top_n_per_article
this.isfetched = false
this.datas = null
}
getIndexByWord (words, text, caseSensitive = false) {
const index = []
const included = new Set()
if (!caseSensitive) {
text = text.toLowerCase()
}
words.forEach(word => {
if (this.unescape) {
const div = document.createElement('div')
div.innerText = word
word = div.innerHTML
}
const wordLen = word.length
if (wordLen === 0) return
let startPosition = 0
let position = -1
if (!caseSensitive) {
word = word.toLowerCase()
}
while ((position = text.indexOf(word, startPosition)) > -1) {
index.push({ position, word })
included.add(word)
startPosition = position + wordLen
}
})
// Sort index by position of keyword
index.sort((left, right) => {
if (left.position !== right.position) {
return left.position - right.position
}
return right.word.length - left.word.length
})
return [index, included]
}
// Merge hits into slices
mergeIntoSlice (start, end, index) {
let item = index[0]
let { position, word } = item
const hits = []
const count = new Set()
while (position + word.length <= end && index.length !== 0) {
count.add(word)
hits.push({
position,
length: word.length
})
const wordEnd = position + word.length
// Move to next position of hit
index.shift()
while (index.length !== 0) {
item = index[0]
position = item.position
word = item.word
if (wordEnd > position) {
index.shift()
} else {
break
}
}
}
return {
hits,
start,
end,
count: count.size
}
}
// Highlight title and content
highlightKeyword (val, slice) {
let result = ''
let index = slice.start
for (const { position, length } of slice.hits) {
result += val.substring(index, position)
index = position + length
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
}
result += val.substring(index, slice.end)
return result
}
getResultItems (keywords) {
const resultItems = []
this.datas.forEach(({ title, content, url }) => {
// The number of different keywords included in the article.
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
// Show search results
const hitCount = indexOfTitle.length + indexOfContent.length
if (hitCount === 0) return
const slicesOfTitle = []
if (indexOfTitle.length !== 0) {
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
}
let slicesOfContent = []
while (indexOfContent.length !== 0) {
const item = indexOfContent[0]
const { position } = item
// Cut out 120 characters. The maxlength of .search-input is 80.
const start = Math.max(0, position - 20)
const end = Math.min(content.length, position + 100)
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
}
// Sort slices in content by included keywords' count and hits' count
slicesOfContent.sort((left, right) => {
if (left.count !== right.count) {
return right.count - left.count
} else if (left.hits.length !== right.hits.length) {
return right.hits.length - left.hits.length
}
return left.start - right.start
})
// Select top N slices in content
const upperBound = parseInt(this.top_n_per_article, 10)
if (upperBound >= 0) {
slicesOfContent = slicesOfContent.slice(0, upperBound)
}
let resultItem = ''
url = new URL(url, location.origin)
url.searchParams.append('highlight', keywords.join(' '))
if (slicesOfTitle.length !== 0) {
resultItem += `<div class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
} else {
resultItem += `<div class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
}
slicesOfContent.forEach(slice => {
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
})
resultItem += '</div>'
resultItems.push({
item: resultItem,
id: resultItems.length,
hitCount,
includedCount
})
})
return resultItems
}
fetchData () {
const isXml = !this.path.endsWith('json')
fetch(this.path)
.then(response => response.text())
.then(res => {
// Get the contents from search data
this.isfetched = true
this.datas = isXml
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
title: element.querySelector('title').textContent,
content: element.querySelector('content').textContent,
url: element.querySelector('url').textContent
}))
: JSON.parse(res)
// Only match articles with non-empty titles
this.datas = this.datas.filter(data => data.title).map(data => {
data.title = data.title.trim()
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
return data
})
// Remove loading animation
window.dispatchEvent(new Event('search:loaded'))
})
}
// Highlight by wrapping node in mark elements with the given class name
highlightText (node, slice, className) {
const val = node.nodeValue
let index = slice.start
const children = []
for (const { position, length } of slice.hits) {
const text = document.createTextNode(val.substring(index, position))
index = position + length
const mark = document.createElement('mark')
mark.className = className
mark.appendChild(document.createTextNode(val.substr(position, length)))
children.push(text, mark)
}
node.nodeValue = val.substring(index, slice.end)
children.forEach(element => {
node.parentNode.insertBefore(element, node)
})
}
// Highlight the search words provided in the url in the text
highlightSearchWords (body) {
const params = new URL(location.href).searchParams.get('highlight')
const keywords = params ? params.split(' ') : []
if (!keywords.length || !body) return
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
const allNodes = []
while (walk.nextNode()) {
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
}
allNodes.forEach(node => {
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
if (!indexOfNode.length) return
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
this.highlightText(node, slice, 'search-keyword')
})
}
}
window.addEventListener('load', () => {
// Search
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
const localSearch = new LocalSearch({
path,
top_n_per_article,
unescape
})
const input = document.querySelector('#local-search-input input')
const statsItem = document.getElementById('local-search-stats-wrap')
const $loadingStatus = document.getElementById('loading-status')
const isXml = !path.endsWith('json')
const inputEventFunction = () => {
if (!localSearch.isfetched) return
let searchText = input.value.trim().toLowerCase()
isXml && (searchText = searchText.replace(/</g, '&lt;').replace(/>/g, '&gt;'))
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
const keywords = searchText.split(/[-\s]+/)
const container = document.getElementById('local-search-results')
let resultItems = []
if (searchText.length > 0) {
// Perform local searching
resultItems = localSearch.getResultItems(keywords)
}
if (keywords.length === 1 && keywords[0] === '') {
container.textContent = ''
statsItem.textContent = ''
} else if (resultItems.length === 0) {
container.textContent = ''
const statsDiv = document.createElement('div')
statsDiv.className = 'search-result-stats'
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
statsItem.innerHTML = statsDiv.outerHTML
} else {
resultItems.sort((left, right) => {
if (left.includedCount !== right.includedCount) {
return right.includedCount - left.includedCount
} else if (left.hitCount !== right.hitCount) {
return right.hitCount - left.hitCount
}
return right.id - left.id
})
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
container.innerHTML = `<div class="search-result-list">${resultItems.map(result => result.item).join('')}</div>`
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
window.pjax && window.pjax.refresh(container)
}
$loadingStatus.textContent = ''
}
let loadFlag = false
const $searchMask = document.getElementById('search-mask')
const $searchDialog = document.querySelector('#local-search .search-dialog')
// fix safari
const fixSafariHeight = () => {
if (window.innerWidth < 768) {
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
}
}
const openSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = '100%'
bodyStyle.overflow = 'hidden'
btf.animateIn($searchMask, 'to_show 0.5s')
btf.animateIn($searchDialog, 'titleScale 0.5s')
setTimeout(() => { input.focus() }, 300)
if (!loadFlag) {
!localSearch.isfetched && localSearch.fetchData()
input.addEventListener('input', inputEventFunction)
loadFlag = true
}
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
fixSafariHeight()
window.addEventListener('resize', fixSafariHeight)
}
const closeSearch = () => {
const bodyStyle = document.body.style
bodyStyle.width = ''
bodyStyle.overflow = ''
btf.animateOut($searchDialog, 'search_close .5s')
btf.animateOut($searchMask, 'to_hide 0.5s')
window.removeEventListener('resize', fixSafariHeight)
}
const searchClickFn = () => {
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
}
const searchFnOnce = () => {
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
$searchMask.addEventListener('click', closeSearch)
if (GLOBAL_CONFIG.localSearch.preload) {
localSearch.fetchData()
}
localSearch.highlightSearchWords(document.getElementById('article-container'))
}
window.addEventListener('search:loaded', () => {
const $loadDataItem = document.getElementById('loading-database')
$loadDataItem.nextElementSibling.style.display = 'block'
$loadDataItem.remove()
})
searchClickFn()
searchFnOnce()
// pjax
window.addEventListener('pjax:complete', () => {
!btf.isHidden($searchMask) && closeSearch()
localSearch.highlightSearchWords(document.getElementById('article-container'))
searchClickFn()
})
})

数据与前端

XML

来源:https://raw.githubusercontent.com/f-droid/fdroidclient/master/app/lint.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- TODO bump our targetSdkVersion when we are ready for it -->
<issue id="ExpiredTargetSdkVersion" severity="ignore" />
<!-- TODO This should be handled as part of an overhaul of Bluetooth swap -->
<issue id="MissingPermission" severity="">
<ignore path="src/full/java/org/fdroid/fdroid/nearby/BluetoothManager.java" />
<ignore path="src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java" />
</issue>
<!-- Our translations are crowd-sourced -->
<issue id="MissingTranslation" severity="ignore" />
<issue id="ExtraTranslation" severity="warning" />
<!-- to make CI fail on errors until this is fixed
https://github.com/rtyley/spongycastle/issues/7 -->
<issue id="InvalidPackage" severity="warning" />
<issue id="ImpliedQuantity" severity="error" />
<issue id="DefaultLocale" severity="error" />
<issue id="SimpleDateFormat" severity="error" />
<issue id="NewApi" severity="error" />
<issue id="InlinedApi" severity="error" />
<!-- These are important to us, so promote from warning to error -->
<issue id="UnusedResources" severity="error">
<ignore path="src/main/res/drawable/category_**.png" />
<ignore path="src/main/res/values/dimens.xml" />
<ignore path="src/main/res/values/styles.xml" />
<ignore path="src/full/res/values/styles.xml" />
<!-- keep a single strings.xml for all build flavors -->
<ignore path="src/main/res/values**/strings.xml" />
</issue>
<issue id="AppCompatMethod" severity="error" />
<issue id="NestedScrolling" severity="error" />
<issue id="Typos" severity="error" />
<issue id="StringFormatCount" severity="error" />
<issue id="UnsafeProtectedBroadcastReceiver" severity="error" />
<issue id="GetInstance" severity="error" />
<issue id="PackageManagerGetSignatures" severity="error" />
<issue id="HardwareIds" severity="error" />
<issue id="TrustAllX509TrustManager" severity="error">
<!-- these come from included libraries -->
<ignore path="org/apache/commons/net/ftp/FTPSTrustManager.class" />
<ignore path="org/bouncycastle/est/jcajce/JcaJceUtils$1.class" />
<ignore path="org/bouncycastle/est/jcajce/JcaJceUtils$2.class" />
<ignore path="org/apache/commons/net/util/TrustManagerUtils$TrustManager.class" />
<ignore path="\*/bcpkix-jdk15to18-1.71.jar" />
<ignore path="\*/commons-net-3.6.jar" />
</issue>
<issue id="PluralsCandidate" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="RtlCompat" severity="error" />
<issue id="RtlEnabled" severity="error" />
<!-- both the correct and deprecated locales need to be present for
them to be recognized on all devices -->
<issue id="LocaleFolder" severity="error">
<ignore path="src/main/res/values-he" />
<ignore path="src/main/res/values-id" />
</issue>
<!-- Weblate doesn't handle these yet: https://github.com/WeblateOrg/weblate/issues/7520 -->
<issue id="MissingQuantity" severity="error">
<ignore path="src/main/res/values-cs" />
<ignore path="src/main/res/values-fr" />
<ignore path="src/main/res/values-lt" />
<ignore path="src/main/res/values-sk" />
</issue>
<issue id="SetWorldReadable" severity="error">
<ignore path="src/main/java/org/fdroid/fdroid/installer/ApkFileProvider.java" />
</issue>
<issue id="ProtectedPermissions" severity="error">
<ignore path="src/debug/AndroidManifest.xml" />
<ignore path="src/full/AndroidManifest.xml" />
</issue>
<!-- these should be fixed, but it'll be a chunk of work -->
<issue id="SetTextI18n" severity="error">
<ignore path="src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java" />
<ignore path="src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java" />
</issue>
</lint>

Stylus

来源:https://raw.githubusercontent.com/jerryc127/hexo-theme-butterfly/master/source/css/_layout/post.styl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
beautify()
headStyle(fontsize)
padding-left: unit(fontsize + 8, 'px')
&:before
font-size: unit(fontsize - 2, 'px')
&:hover
padding-left: unit(fontsize + 12, 'px')
h1,
h2,
h3,
h4,
h5,
h6
transition: all .2s ease-out
&:before
position: absolute
top: calc(50% - 7px)
color: $title-prefix-icon-color
content: $title-prefix-icon
left: 0
line-height: 1
transition: all .2s ease-out
@extend .fontawesomeIcon
&:hover
&:before
color: $light-blue
h1
headStyle(20)
h2
headStyle(18)
h3
headStyle(16)
h4
headStyle(14)
h5
headStyle(12)
h6
headStyle(12)
ol,
ul
p
margin: 0 0 8px
li
&::marker
color: $light-blue
font-weight: 600
font-size: 1.05em
&:hover
&::marker
color: var(--pseudo-hover)
ul > li
list-style-type: circle

hr
@extend .custom-hr
#article-container
word-wrap: break-word
overflow-wrap: break-word
if hexo-config('text_align_justify')
text-align: justify
a
color: $theme-link-color
&:hover
text-decoration: underline
img
display: block
margin: 0 auto 20px
max-width: 100%
transition: filter 375ms ease-in .2s
p
margin: 0 0 16px
iframe
margin: 0 0 20px
kbd
margin: 0 3px
padding: 3px 5px
border: 1px solid #b4b4b4
border-radius: 3px
background-color: #f8f8f8
box-shadow: 0 1px 3px rgba(0, 0, 0, .25), 0 2px 1px 0 rgba(255, 255, 255, .6) inset
color: #34495e
white-space: nowrap
font-weight: 600
font-size: .9em
font-family: Monaco, 'Ubuntu Mono', monospace
line-height: 1em
if hexo-config('anchor.click_to_scroll')
h1,
h2,
h3,
h4,
h5,
h6
width: fit-content
a:not(.headerlink)
position relative
z-index 10
a.headerlink
position: absolute
top: 0
right: 0
left 0
bottom: 0
width 100%
height: 100%
ol,
ul
ol,
ul
padding-left: 20px
li
margin: 4px 0
p
margin: 0 0 8px
> :last-child
margin-bottom: 0 !important
hr
margin: 20px 0
if hexo-config('beautify.enable')
if hexo-config('beautify.field') == 'site'
beautify()
else if hexo-config('beautify.field') == 'post'
&.post-content
beautify()
#post
.tag_share
&:after
display: block
clear: both
content: ''
.post-meta
&__tag-list
display: inline-block
&__tags
display: inline-block
margin: 8px 8px 8px 0
padding: 0 12px
width: fit-content
border: 1px solid $light-blue
border-radius: 12px
color: $light-blue
font-size: .85em
transition: all .2s ease-in-out
&:hover
background: $light-blue
color: var(--white)
.post_share
display: inline-block
float: right
margin: 8px 0 0
width: fit-content
.social-share
font-size: .85em
.social-share-icon
margin: 0 4px
width: w = 1.85em
height: w
font-size: 1.2em
line-height: w
.post-copyright
position: relative
margin: 40px 0 10px
padding: 10px 16px
border: 1px solid var(--light-grey)
transition: box-shadow .3s ease-in-out
&:before
@extend .fontawesomeIcon
position: absolute
top: 2px
right: 12px
color: $theme-color
content: '\f1f9'
font-size: 1.3em
&:hover
box-shadow: 0 0 8px 0 rgba(232, 237, 250, .6), 0 2px 4px 0 rgba(232, 237, 250, .5)
.post-copyright
&-meta
color: $light-blue
font-weight: bold
i
margin-right: 3px
&-info
padding-left: 6px
a
text-decoration: underline
word-break: break-word
&:hover
text-decoration: none
.post-outdate-notice
position: relative
margin: 0 0 20px
padding: .5em 1.2em
border-radius: 3px
background-color: $noticeOutdate-bg
color: $noticeOutdate-color
if hexo-config('noticeOutdate.style') == 'flat'
padding: .5em 1em .5em 2.6em
border-left: 5px solid $noticeOutdate-border
&:before
@extend .fontawesomeIcon
position: absolute
top: 50%
left: .9em
color: $noticeOutdate-border
content: '\f071'
transform: translateY(-50%)
.ads-wrap
margin: 40px 0

HTML

来源:https://raw.githubusercontent.com/h5bp/html5-boilerplate/main/src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link rel="stylesheet" href="css/style.css">
<meta name="description" content="">
<meta property="og:title" content="">
<meta property="og:type" content="">
<meta property="og:url" content="">
<meta property="og:image" content="">
<meta property="og:image:alt" content="">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="icon.png">
<link rel="manifest" href="site.webmanifest">
<meta name="theme-color" content="#fafafa">
</head>
<body>
<!-- Add your site or application content here -->
<p>Hello world! This is HTML5 Boilerplate.</p>
<script src="js/app.js"></script>
</body>
</html>

数学

单行公式

F=G=mg=1kg9.8N/kg=9.8NF=G=m_\textsf {物} g=1kg・9.8N/kg=9.8N

数学文字混杂

让我们尝试证明一个稍微复杂一些的等式:

k=0n(nk)=2n\sum_{k=0}^{n} \binom{n}{k} = 2^n

我们可以使用二项式定理证明这个等式。二项式定理表述为:

(a+b)n=k=0n(nk)ankbk(a + b)^n = \sum_{k=0}^{n} \binom{n}{k} a^{n-k} b^k

现在,令 a=1a = 1, b=1b = 1,然后代入:

(1+1)n=k=0n(nk)1nk1k(1 + 1)^n = \sum_{k=0}^{n} \binom{n}{k} 1^{n-k} 1^k

2n=k=0n(nk)2^n = \sum_{k=0}^{n} \binom{n}{k}

这证明了 k=0n(nk)=2n\sum_{k=0}^{n} \binom{n}{k} = 2^n

渲染器拓展语法测试

这是 被注释文本

上标示例

下标示例

脚注示例 [1]

Butterfly 拓展语法测试 [2]

Note

simple

默认 提示块标签

default 提示块标签

primary 提示块标签

success 提示块标签

info 提示块标签

warning 提示块标签

danger 提示块标签

modern

默认 提示块标签

default 提示块标签

primary 提示块标签

success 提示块标签

info 提示块标签

warning 提示块标签

danger 提示块标签

flat

默认 提示块标签

default 提示块标签

primary 提示块标签

success 提示块标签

info 提示块标签

warning 提示块标签

danger 提示块标签

disable

默认 提示块标签

default 提示块标签

primary 提示块标签

success 提示块标签

info 提示块标签

warning 提示块标签

danger 提示块标签

Tag Hide

哪个英文字母最酷? 因为西装裤 (C 装酷)

Mermaid[^5]

流程图

时序图

甘特图

类图

状态图

饼图

用户体验旅程图

C4 图

Tabs

This is Tab 1.

This is Tab 2.

This is Tab 3.

Button

Butterfly Butterfly Butterfly Butterfly Butterfly Butterfly Butterfly

label

臣亮言:先帝 创业未半,而中道崩殂 。今天下三分,益州疲敝 ,此诚危急存亡之秋 也!然侍衞之臣,不懈于内;忠志之士 ,忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气;不宜妄自菲薄,引喻失义,以塞忠谏之路也。

宫中、府中,俱为一体;陟罚臧否,不宜异同。若有作奸犯科 ,及为忠善者,宜付有司,论其刑赏,以昭陛下平明之治;不宜偏私,使内外异法也。

Timeline

2022

01-02

这是测试页面


  1. 脚注是指附在文章页面的最底端的,对某些东西加以说明的注文。 ↩︎

  2. 代码来自 Butterfly 安裝文檔 (三) 主題配置 - 1 | Butterfly,采用 CC-BY-NC-SA 协议授权 ↩︎

  3. 画作由 TysonTan 绘制,采用 CC-BY-SA 协议授权 ↩︎

  4. 代码来自 Mermaid ReadMe ↩︎